Merge branch 'helix-editor:master' into doc/tutor-split2

pull/9417/head
JR 5 months ago committed by GitHub
commit 6bce00ce3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,17 @@
# we use tokio_unstable to enable runtime::Handle::id so we can separate
# globals from multiple parallel tests. If that function ever does get removed
# its possible to replace (with some additional overhead and effort)
# Annoyingly build.rustflags doesn't work here because it gets overwritten
# if people have their own global target.<..> config (for example to enable mold)
# specifying flags this way is more robust as they get merged
# This still gets overwritten by RUST_FLAGS though, luckily it shouldn't be necessary
# to set those most of the time. If downstream does overwrite this its not a huge
# deal since it will only break tests anyway
[target."cfg(all())"]
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]
[alias]
xtask = "run --package xtask --"
integration-test = "test --features integration --profile integration --workspace --test integration"

@ -46,7 +46,7 @@ jobs:
shared-key: "build"
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}

435
Cargo.lock generated

@ -101,9 +101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bstr"
@ -145,9 +145,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.84"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
@ -171,14 +171,14 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.31"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.48.0",
"windows-targets 0.52.0",
]
[[package]]
@ -285,7 +285,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"crossterm_winapi",
"filedescriptor",
"futures-core",
@ -358,9 +358,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
[[package]]
name = "either"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
@ -424,9 +424,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fern"
@ -448,6 +448,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
[[package]]
name = "flate2"
version = "1.0.27"
@ -522,50 +534,57 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gix"
version = "0.57.1"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd025382892c7b500a9ce1582cd803f9c2ebfe44aff52e9c7f86feee7ced75e"
checksum = "31887c304d9a935f3e5494fb5d6a0106c34e965168ec0db9b457424eedd0c741"
dependencies = [
"gix-actor",
"gix-attributes",
"gix-command",
"gix-commitgraph",
"gix-config",
"gix-date",
"gix-diff",
"gix-discover",
"gix-features",
"gix-filter",
"gix-fs",
"gix-glob",
"gix-hash",
"gix-hashtable",
"gix-ignore",
"gix-index",
"gix-lock",
"gix-macros",
"gix-object",
"gix-odb",
"gix-pack",
"gix-path",
"gix-pathspec",
"gix-ref",
"gix-refspec",
"gix-revision",
"gix-revwalk",
"gix-sec",
"gix-submodule",
"gix-tempfile",
"gix-trace",
"gix-traverse",
"gix-url",
"gix-utils",
"gix-validate",
"gix-worktree",
"once_cell",
"parking_lot",
"smallvec",
"thiserror",
"unicode-normalization",
]
[[package]]
name = "gix-actor"
version = "0.29.1"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da27b5ab4ab5c75ff891dccd48409f8cc53c28a79480f1efdd33184b2dc1d958"
checksum = "0a7bb9fad6125c81372987c06469601d37e1a2d421511adb69971b9083517a8a"
dependencies = [
"bstr",
"btoi",
@ -575,6 +594,32 @@ dependencies = [
"winnow 0.5.28",
]
[[package]]
name = "gix-attributes"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214ee3792e504ee1ce206b36dcafa4f328ca313d1e2ac0b41433d68ef4e14260"
dependencies = [
"bstr",
"gix-glob",
"gix-path",
"gix-quote",
"gix-trace",
"kstring",
"smallvec",
"thiserror",
"unicode-bom",
]
[[package]]
name = "gix-bitmap"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b6cd0f246180034ddafac9b00a112f19178135b21eb031b3f79355891f7325"
dependencies = [
"thiserror",
]
[[package]]
name = "gix-chunk"
version = "0.4.7"
@ -584,11 +629,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "gix-command"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1ffc7db3fb50b7dae6ecd937a3527cb725f444614df2ad8988d81806f13f09"
dependencies = [
"bstr",
"gix-path",
"gix-trace",
"shell-words",
]
[[package]]
name = "gix-commitgraph"
version = "0.23.1"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a39c675fd737cb43a2120eddf1aa652c19d76b28d79783a198ac1b398ed9ce6"
checksum = "82dbd7fb959862e3df2583331f0ad032ac93533e8a52f1b0694bc517f5d292bc"
dependencies = [
"bstr",
"gix-chunk",
@ -600,9 +657,9 @@ dependencies = [
[[package]]
name = "gix-config"
version = "0.33.1"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "367304855b369cadcac4ee5fb5a3a20da9378dd7905106141070b79f85241079"
checksum = "e62bf2073b6ce3921ffa6d8326f645f30eec5fc4a8e8a4bc0fcb721a2f3f69dc"
dependencies = [
"bstr",
"gix-config-value",
@ -621,11 +678,11 @@ dependencies = [
[[package]]
name = "gix-config-value"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e0be46f4cf1f8f9e88d0e3eb7b29718aff23889563249f379119bd1ab6910e"
checksum = "5b8a1e7bfb37a46ed0b8468db37a6d8a0a61d56bdbe4603ae492cb322e5f3958"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"bstr",
"gix-path",
"libc",
@ -646,9 +703,9 @@ dependencies = [
[[package]]
name = "gix-diff"
version = "0.39.1"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6a0454f8c42d686f17e7f084057c717c082b7dbb8209729e4e8f26749eb93a"
checksum = "cbdcb5e49c4b9729dd1c361040ae5c3cd7c497b2260b18c954f62db3a63e98cf"
dependencies = [
"bstr",
"gix-hash",
@ -658,12 +715,13 @@ dependencies = [
[[package]]
name = "gix-discover"
version = "0.28.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d7b2896edc3d899d28a646ccc6df729827a6600e546570b2783466404a42d6"
checksum = "b4669218f3ec0cbbf8f16857b32200890f8ca585f36f5817242e4115fe4551af"
dependencies = [
"bstr",
"dunce",
"gix-fs",
"gix-hash",
"gix-path",
"gix-ref",
@ -673,14 +731,15 @@ dependencies = [
[[package]]
name = "gix-features"
version = "0.37.1"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a80f0fe688d654c2a741751578b11131071026d1934d03c1820d6d767525ce"
checksum = "184f7f7d4e45db0e2a362aeaf12c06c5e84817d0ef91d08e8e90170dad9f0b07"
dependencies = [
"crc32fast",
"flate2",
"gix-hash",
"gix-trace",
"gix-utils",
"libc",
"once_cell",
"prodash",
@ -689,22 +748,44 @@ dependencies = [
"walkdir",
]
[[package]]
name = "gix-filter"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9240862840fb740d209422937195e129e4ed3da49af212383260134bea8f6c1a"
dependencies = [
"bstr",
"encoding_rs",
"gix-attributes",
"gix-command",
"gix-hash",
"gix-object",
"gix-packetline-blocking",
"gix-path",
"gix-quote",
"gix-trace",
"gix-utils",
"smallvec",
"thiserror",
]
[[package]]
name = "gix-fs"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7555c23a005537434bbfcb8939694e18cad42602961d0de617f8477cc2adecdd"
checksum = "4436e883d5769f9fb18677b8712b49228357815f9e4104174a6fc2d8461a437b"
dependencies = [
"gix-features",
"gix-utils",
]
[[package]]
name = "gix-glob"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6232f18b262770e343dcdd461c0011c9b9ae27f0c805e115012aa2b902c1b8"
checksum = "4965a1d06d0ab84a29d4a67697a97352ab14ae1da821084e5afb1fd6d8191ca0"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"bstr",
"gix-features",
"gix-path",
@ -731,11 +812,49 @@ dependencies = [
"parking_lot",
]
[[package]]
name = "gix-ignore"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7069aaca4a05784c4cb44e392f0eaf627c6e57e05d3100c0e2386a37a682f0"
dependencies = [
"bstr",
"gix-glob",
"gix-path",
"gix-trace",
"unicode-bom",
]
[[package]]
name = "gix-index"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7152181ba8f0a3addc5075dd612cea31fc3e252b29c8be8c45f4892bf87426"
dependencies = [
"bitflags 2.4.2",
"bstr",
"btoi",
"filetime",
"gix-bitmap",
"gix-features",
"gix-fs",
"gix-hash",
"gix-lock",
"gix-object",
"gix-traverse",
"itoa",
"libc",
"memmap2",
"rustix",
"smallvec",
"thiserror",
]
[[package]]
name = "gix-lock"
version = "12.0.0"
version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cf112ddee94223c119a8534dad027740dc3aba3365ac5edeef8a7f6660c74db"
checksum = "651e46174dc5e7d18b7b809d31937b6de3681b1debd78618c99162cc30fcf3e1"
dependencies = [
"gix-tempfile",
"gix-utils",
@ -755,9 +874,9 @@ dependencies = [
[[package]]
name = "gix-object"
version = "0.40.1"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c89402e8faa41b49fde348665a8f38589e461036475af43b6b70615a6a313a2"
checksum = "693ce9d30741506cb082ef2d8b797415b48e032cce0ab23eff894c19a7e4777b"
dependencies = [
"bstr",
"btoi",
@ -774,13 +893,14 @@ dependencies = [
[[package]]
name = "gix-odb"
version = "0.56.1"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46ae6da873de41c6c2b73570e82c571b69df5154dcd8f46dfafc6687767c33b1"
checksum = "8ba2fa9e81f2461b78b4d81a807867667326c84cdab48e0aed7b73a593aa1be4"
dependencies = [
"arc-swap",
"gix-date",
"gix-features",
"gix-fs",
"gix-hash",
"gix-object",
"gix-pack",
@ -793,9 +913,9 @@ dependencies = [
[[package]]
name = "gix-pack"
version = "0.46.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782b4d42790a14072d5c400deda9851f5765f50fe72bca6dece0da1cd6f05a9a"
checksum = "8da5f3e78c96b76c4e6fe5e8e06b76221e4a0ee9a255aa935ed1fdf68988dfd8"
dependencies = [
"clru",
"gix-chunk",
@ -811,11 +931,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "gix-packetline-blocking"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7"
dependencies = [
"bstr",
"faster-hex",
"gix-trace",
"thiserror",
]
[[package]]
name = "gix-path"
version = "0.10.3"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dd0998ab245f33d40ca2267e58d542fe54185ebd1dc41923346cf28d179fb6"
checksum = "14a6282621aed1becc3f83d64099a564b3b9063f22783d9a87ea502a3e9f2e40"
dependencies = [
"bstr",
"gix-trace",
@ -824,6 +956,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "gix-pathspec"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cbd49750edb26b0a691e5246fc635fa554d344da825cd20fa9ee0da9c1b761f"
dependencies = [
"bitflags 2.4.2",
"bstr",
"gix-attributes",
"gix-config-value",
"gix-glob",
"gix-path",
"thiserror",
]
[[package]]
name = "gix-quote"
version = "0.4.10"
@ -837,9 +984,9 @@ dependencies = [
[[package]]
name = "gix-ref"
version = "0.40.1"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d9bd1984638d8f3511a2fcbe84fcedb8a5b5d64df677353620572383f42649"
checksum = "5818958994ad7879fa566f5441ebcc48f0926aa027b28948e6fbf6578894dc31"
dependencies = [
"gix-actor",
"gix-date",
@ -850,6 +997,7 @@ dependencies = [
"gix-object",
"gix-path",
"gix-tempfile",
"gix-utils",
"gix-validate",
"memmap2",
"thiserror",
@ -858,9 +1006,9 @@ dependencies = [
[[package]]
name = "gix-refspec"
version = "0.21.1"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be219df5092c1735abb2a53eccdf775e945eea6986ee1b6e7a5896dccc0be704"
checksum = "613aa4d93034c5791d13bdc635e530f4ddab1412ddfb4a8215f76213177b61c7"
dependencies = [
"bstr",
"gix-hash",
@ -872,9 +1020,9 @@ dependencies = [
[[package]]
name = "gix-revision"
version = "0.25.1"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa78e1df3633bc937d4db15f8dca2abdb1300ca971c0fabcf9fa97e38cf4cd9f"
checksum = "288f6549d7666db74dc3f169a9a333694fc28ecd2f5aa7b2c979c89eb556751a"
dependencies = [
"bstr",
"gix-date",
@ -888,9 +1036,9 @@ dependencies = [
[[package]]
name = "gix-revwalk"
version = "0.11.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702de5fe5c2bbdde80219f3a8b9723eb927466e7ecd187cfd1b45d986408e45f"
checksum = "5b9b4d91dfc5c14fee61a28c65113ded720403b65a0f46169c0460f731a5d03c"
dependencies = [
"gix-commitgraph",
"gix-date",
@ -903,21 +1051,36 @@ dependencies = [
[[package]]
name = "gix-sec"
version = "0.10.3"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78f6dce0c6683e2219e8169aac4b1c29e89540a8262fef7056b31d80d969408c"
checksum = "f8d9bf462feaf05f2121cba7399dbc6c34d88a9cad58fc1e95027791d6a3c6d2"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"gix-path",
"libc",
"windows 0.52.0",
"windows-sys 0.52.0",
]
[[package]]
name = "gix-submodule"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73182f6c1f5ed1ed94ba16581ac62593d5e29cd1c028b2af618f836283b8f8d4"
dependencies = [
"bstr",
"gix-config",
"gix-path",
"gix-pathspec",
"gix-refspec",
"gix-url",
"thiserror",
]
[[package]]
name = "gix-tempfile"
version = "12.0.0"
version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e76a494bd530e1a1309188ff971825a24f159c76c2db0bf71fa5dfb469a2c915"
checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30"
dependencies = [
"gix-fs",
"libc",
@ -928,15 +1091,15 @@ dependencies = [
[[package]]
name = "gix-trace"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8e1127ede0475b58f4fe9c0aaa0d9bb0bad2af90bbd93ccd307c8632b863d89"
checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1"
[[package]]
name = "gix-traverse"
version = "0.36.1"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb64213e52e1b726cb04581690c1e98b5910f983b977d5e9f2eb09f1a7fea6d2"
checksum = "bfc30c5b5e4e838683b59e1b0574ce6bc1c35916df9709aaab32bb7751daf08b"
dependencies = [
"gix-commitgraph",
"gix-date",
@ -950,9 +1113,9 @@ dependencies = [
[[package]]
name = "gix-url"
version = "0.26.1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0f17cceb7552a231d1fec690bc2740c346554e3be6f5d2c41dfa809594dc44"
checksum = "26f1981ecc700f4fd73ae62b9ca2da7c8816c8fd267f0185e3f8c21e967984ac"
dependencies = [
"bstr",
"gix-features",
@ -964,11 +1127,12 @@ dependencies = [
[[package]]
name = "gix-utils"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de6225e2de30b6e9bca2d9f1cc4731640fcef0fb3cabddceee366e7e85d3e94f"
checksum = "56e839f3d0798b296411263da6bee780a176ef8008a5dfc31287f7eda9266ab8"
dependencies = [
"fastrand",
"unicode-normalization",
]
[[package]]
@ -981,6 +1145,24 @@ dependencies = [
"thiserror",
]
[[package]]
name = "gix-worktree"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca36bb3dc54038c66507dc75c4d8edbee2d6d5cc45227b4eb508ad13dd60a006"
dependencies = [
"bstr",
"gix-attributes",
"gix-features",
"gix-fs",
"gix-glob",
"gix-hash",
"gix-ignore",
"gix-index",
"gix-object",
"gix-path",
]
[[package]]
name = "globset"
version = "0.4.14"
@ -1053,11 +1235,12 @@ version = "23.10.0"
dependencies = [
"ahash",
"arc-swap",
"bitflags 2.4.1",
"bitflags 2.4.2",
"chrono",
"dunce",
"encoding_rs",
"etcetera",
"globset",
"hashbrown 0.14.3",
"helix-loader",
"helix-stdx",
@ -1090,18 +1273,24 @@ dependencies = [
"anyhow",
"fern",
"helix-core",
"helix-stdx",
"log",
"serde",
"serde_json",
"thiserror",
"tokio",
"which",
]
[[package]]
name = "helix-event"
version = "23.10.0"
dependencies = [
"ahash",
"anyhow",
"futures-executor",
"hashbrown 0.14.3",
"log",
"once_cell",
"parking_lot",
"tokio",
]
@ -1123,7 +1312,6 @@ dependencies = [
"threadpool",
"toml",
"tree-sitter",
"which",
]
[[package]]
@ -1146,7 +1334,6 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"which",
]
[[package]]
@ -1159,7 +1346,9 @@ version = "23.10.0"
dependencies = [
"dunce",
"etcetera",
"ropey",
"tempfile",
"which",
]
[[package]]
@ -1198,18 +1387,18 @@ dependencies = [
"signal-hook-tokio",
"smallvec",
"tempfile",
"termini",
"tokio",
"tokio-stream",
"toml",
"url",
"which",
]
[[package]]
name = "helix-tui"
version = "23.10.0"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"cassowary",
"crossterm",
"helix-core",
@ -1243,7 +1432,7 @@ version = "23.10.0"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.4.1",
"bitflags 2.4.2",
"chardetng",
"clipboard-win",
"crossterm",
@ -1268,7 +1457,6 @@ dependencies = [
"tokio-stream",
"toml",
"url",
"which",
]
[[package]]
@ -1282,11 +1470,11 @@ dependencies = [
[[package]]
name = "home"
version = "0.5.5"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@ -1300,7 +1488,7 @@ dependencies = [
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows 0.48.0",
"windows",
]
[[package]]
@ -1399,11 +1587,20 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kstring"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
dependencies = [
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.152"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
@ -1644,11 +1841,11 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
[[package]]
name = "pulldown-cmark"
version = "0.9.3"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.2",
"memchr",
"unicase",
]
@ -1731,9 +1928,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.2"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
@ -1743,9 +1940,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
dependencies = [
"aho-corasick",
"memchr",
@ -1776,11 +1973,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.30"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.1",
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
@ -1816,18 +2013,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
@ -1836,9 +2033,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
@ -1871,6 +2068,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "signal-hook"
version = "0.3.17"
@ -1933,9 +2136,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.12.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smartstring"
@ -2000,13 +2203,12 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.9.0"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.52.0",
]
@ -2115,9 +2317,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.35.1"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
@ -2347,15 +2549,15 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "which"
version = "5.0.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14"
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2398,25 +2600,6 @@ dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.0",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

@ -51,7 +51,8 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
| `auto-format` | Enable automatic formatting on save | `true` |
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` |
| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |

@ -14,6 +14,7 @@
| cabal | | | | `haskell-language-server-wrapper` |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| cel | ✓ | | | |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | |
@ -21,10 +22,10 @@
| cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | | ✓ | `dart` |
| dart | ✓ | | ✓ | `dart` |
| dbml | ✓ | | | |
| devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
@ -97,7 +98,7 @@
| log | ✓ | | | |
| lpf | ✓ | | | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | | |
| make | ✓ | | | |
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | |
@ -123,6 +124,7 @@
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| pkl | ✓ | | ✓ | |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | |
@ -148,11 +150,12 @@
| scala | ✓ | ✓ | ✓ | `metals` |
| scheme | ✓ | | ✓ | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` |
| slint | ✓ | | ✓ | `slint-lsp` |
| smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` |
| sml | ✓ | | | |
| solidity | ✓ | | | `solc` |
| spicedb | ✓ | | | |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
@ -162,6 +165,7 @@
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| templ | ✓ | | | `templ` |
| tfvars | ✓ | | ✓ | `terraform-ls` |
@ -173,7 +177,7 @@
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| typst | ✓ | | | `typst-lsp` |
| ungrammar | ✓ | | | |
| unison | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |

@ -76,6 +76,15 @@ Releases are available in the `extra` repository:
```sh
sudo pacman -S helix
```
> 💡 When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
>
> For example:
> ```sh
> helix --health
> ```
> to check health
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
in the AUR, which builds the master branch.

@ -78,24 +78,26 @@ from the above section. `file-types` is a list of strings or tables, for
example:
```toml
file-types = ["Makefile", "toml", { suffix = ".git/config" }]
file-types = ["toml", { glob = "Makefile" }, { glob = ".git/config" }, { glob = ".github/workflows/*.yaml" } ]
```
When determining a language configuration to use, Helix searches the file-types
with the following priorities:
1. Exact match: if the filename of a file is an exact match of a string in a
`file-types` list, that language wins. In the example above, `"Makefile"`
will match against `Makefile` files.
2. Extension: if there are no exact matches, any `file-types` string that
matches the file extension of a given file wins. In the example above, the
`"toml"` matches files like `Cargo.toml` or `languages.toml`.
3. Suffix: if there are still no matches, any values in `suffix` tables
are checked against the full path of the given file. In the example above,
the `{ suffix = ".git/config" }` would match against any `config` files
in `.git` directories. Note: `/` is used as the directory separator but is
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.
1. Glob: values in `glob` tables are checked against the full path of the given
file. Globs are standard Unix-style path globs (e.g. the kind you use in Shell)
and can be used to match paths for a specific prefix, suffix, directory, etc.
In the above example, the `{ glob = "Makefile" }` config would match files
with the name `Makefile`, the `{ glob = ".git/config" }` config would match
`config` files in `.git` directories, and the `{ glob = ".github/workflows/*.yaml" }`
config would match any `yaml` files in `.github/workflow` directories. Note
that globs should always use the Unix path separator `/` even on Windows systems;
the matcher will automatically take the machine-specific separators into account.
If the glob isn't an absolute path or doesn't already start with a glob prefix,
`*/` will automatically be added to ensure it matches for any subdirectory.
2. Extension: if there are no glob matches, any `file-types` string that matches
the file extension of a given file wins. In the example above, the `"toml"`
config matches files like `Cargo.toml` or `languages.toml`.
## Language Server configuration
@ -120,13 +122,14 @@ languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT
These are the available options for a language server.
| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. |
A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

@ -20,7 +20,7 @@ helix-stdx = { path = "../helix-stdx" }
helix-loader = { path = "../helix-loader" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.12"
smallvec = "1.13"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
unicode-width = "0.1"
@ -52,6 +52,7 @@ textwrap = "0.16.0"
nucleo.workspace = true
parking_lot = "0.12"
globset = "0.4.14"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

@ -1,10 +1,45 @@
/// Syntax configuration loader based on built-in languages.toml.
pub fn default_syntax_loader() -> crate::syntax::Configuration {
use crate::syntax::{Configuration, Loader, LoaderError};
/// Language configuration based on built-in languages.toml.
pub fn default_lang_config() -> Configuration {
helix_loader::config::default_lang_config()
.try_into()
.expect("Could not serialize built-in languages.toml")
.expect("Could not deserialize built-in languages.toml")
}
/// Syntax configuration loader based on user configured languages.toml.
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
/// Language configuration loader based on built-in languages.toml.
pub fn default_lang_loader() -> Loader {
Loader::new(default_lang_config()).expect("Could not compile loader for default config")
}
#[derive(Debug)]
pub enum LanguageLoaderError {
DeserializeError(toml::de::Error),
LoaderError(LoaderError),
}
impl std::fmt::Display for LanguageLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DeserializeError(err) => write!(f, "Failed to parse language config: {err}"),
Self::LoaderError(err) => write!(f, "Failed to compile language config: {err}"),
}
}
}
impl std::error::Error for LanguageLoaderError {}
/// Language configuration based on user configured languages.toml.
pub fn user_lang_config() -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config()?.try_into()
}
/// Language configuration loader based on user configured languages.toml.
pub fn user_lang_loader() -> Result<Loader, LanguageLoaderError> {
let config: Configuration = helix_loader::config::user_lang_config()
.map_err(LanguageLoaderError::DeserializeError)?
.try_into()
.map_err(LanguageLoaderError::DeserializeError)?;
Loader::new(config).map_err(LanguageLoaderError::LoaderError)
}

@ -57,10 +57,10 @@ fn find_pair(
pos_: usize,
traverse_parents: bool,
) -> Option<usize> {
let tree = syntax.tree();
let pos = doc.char_to_byte(pos_);
let mut node = tree.root_node().descendant_for_byte_range(pos, pos + 1)?;
let root = syntax.tree_for_byte_range(pos, pos + 1).root_node();
let mut node = root.descendant_for_byte_range(pos, pos + 1)?;
loop {
if node.is_named() {
@ -118,9 +118,7 @@ fn find_pair(
};
node = parent;
}
let node = tree
.root_node()
.named_descendant_for_byte_range(pos, pos + 1)?;
let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
if node.child_count() != 0 {
return None;
}

@ -573,16 +573,11 @@ pub fn move_parent_node_end(
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
Some(node) => node,
None => {
log::debug!(

@ -10,6 +10,7 @@ use crate::{
use ahash::RandomState;
use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags;
use globset::GlobSet;
use hashbrown::raw::RawTable;
use slotmap::{DefaultKey as LayerId, HopSlotMap};
@ -82,12 +83,6 @@ pub struct Configuration {
pub language_server: HashMap<String, LanguageServerConfiguration>,
}
impl Default for Configuration {
fn default() -> Self {
crate::config::default_syntax_loader()
}
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@ -164,9 +159,11 @@ pub enum FileType {
/// The extension of the file, either the `Path::extension` or the full
/// filename if the file does not have an extension.
Extension(String),
/// The suffix of a file. This is compared to a given file's absolute
/// path, so it can be used to detect files based on their directories.
Suffix(String),
/// A Unix-style path glob. This is compared to the file's absolute path, so
/// it can be used to detect files based on their directories. If the glob
/// is not an absolute path and does not already start with a glob pattern,
/// a glob pattern will be prepended to it.
Glob(globset::Glob),
}
impl Serialize for FileType {
@ -178,9 +175,9 @@ impl Serialize for FileType {
match self {
FileType::Extension(extension) => serializer.serialize_str(extension),
FileType::Suffix(suffix) => {
FileType::Glob(glob) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?;
map.serialize_entry("glob", glob.glob())?;
map.end()
}
}
@ -213,9 +210,20 @@ impl<'de> Deserialize<'de> for FileType {
M: serde::de::MapAccess<'de>,
{
match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
})),
Some((key, mut glob)) if key == "glob" => {
// If the glob isn't an absolute path or already starts
// with a glob pattern, add a leading glob so we
// properly match relative paths.
if !glob.starts_with('/') && !glob.starts_with("*/") {
glob.insert_str(0, "*/");
}
globset::Glob::new(glob.as_str())
.map(FileType::Glob)
.map_err(|err| {
serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
})
}
Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}",
key
@ -263,7 +271,7 @@ impl Display for LanguageServerFeature {
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition",
GotoReference => "goto-reference",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
@ -358,6 +366,22 @@ where
serializer.end()
}
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
if patterns.is_empty() {
return Ok(None);
}
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
builder.add(glob);
}
builder.build().map(Some).map_err(serde::de::Error::custom)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
@ -371,6 +395,12 @@ pub struct LanguageServerConfiguration {
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(
default,
skip_serializing,
deserialize_with = "deserialize_required_root_patterns"
)]
pub required_root_patterns: Option<GlobSet>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -752,6 +782,47 @@ pub struct SoftWrap {
pub wrap_at_text_width: Option<bool>,
}
#[derive(Debug)]
struct FileTypeGlob {
glob: globset::Glob,
language_id: usize,
}
impl FileTypeGlob {
fn new(glob: globset::Glob, language_id: usize) -> Self {
Self { glob, language_id }
}
}
#[derive(Debug)]
struct FileTypeGlobMatcher {
matcher: globset::GlobSet,
file_types: Vec<FileTypeGlob>,
}
impl FileTypeGlobMatcher {
fn new(file_types: Vec<FileTypeGlob>) -> Result<Self, globset::Error> {
let mut builder = globset::GlobSetBuilder::new();
for file_type in &file_types {
builder.add(file_type.glob.clone());
}
Ok(Self {
matcher: builder.build()?,
file_types,
})
}
fn language_id_for_path(&self, path: &Path) -> Option<&usize> {
self.matcher
.matches(path)
.iter()
.filter_map(|idx| self.file_types.get(*idx))
.max_by_key(|file_type| file_type.glob.glob().len())
.map(|file_type| &file_type.language_id)
}
}
// Expose loader as Lazy<> global since it's always static?
#[derive(Debug)]
@ -759,7 +830,7 @@ pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_glob_matcher: FileTypeGlobMatcher,
language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
@ -767,66 +838,57 @@ pub struct Loader {
scopes: ArcSwap<Vec<String>>,
}
pub type LoaderError = globset::Error;
impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
scopes: ArcSwap::from_pointee(Vec::new()),
};
pub fn new(config: Configuration) -> Result<Self, LoaderError> {
let mut language_configs = Vec::new();
let mut language_config_ids_by_extension = HashMap::new();
let mut language_config_ids_by_shebang = HashMap::new();
let mut file_type_globs = Vec::new();
for config in config.language {
// get the next id
let language_id = loader.language_configs.len();
let language_id = language_configs.len();
for file_type in &config.file_types {
// entry().or_insert(Vec::new).push(language_id);
match file_type {
FileType::Extension(extension) => loader
.language_config_ids_by_extension
.insert(extension.clone(), language_id),
FileType::Suffix(suffix) => loader
.language_config_ids_by_suffix
.insert(suffix.clone(), language_id),
FileType::Extension(extension) => {
language_config_ids_by_extension.insert(extension.clone(), language_id);
}
FileType::Glob(glob) => {
file_type_globs.push(FileTypeGlob::new(glob.to_owned(), language_id));
}
};
}
for shebang in &config.shebangs {
loader
.language_config_ids_by_shebang
.insert(shebang.clone(), language_id);
language_config_ids_by_shebang.insert(shebang.clone(), language_id);
}
loader.language_configs.push(Arc::new(config));
language_configs.push(Arc::new(config));
}
loader
Ok(Self {
language_configs,
language_config_ids_by_extension,
language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?,
language_config_ids_by_shebang,
language_server_configs: config.language_server,
scopes: ArcSwap::from_pointee(Vec::new()),
})
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name
// or a suffix of the file name.
let configuration_id = path
.file_name()
.and_then(|n| n.to_str())
.and_then(|file_name| self.language_config_ids_by_extension.get(file_name))
let configuration_id = self
.language_config_ids_glob_matcher
.language_id_for_path(path)
.or_else(|| {
path.extension()
.and_then(|extension| extension.to_str())
.and_then(|extension| self.language_config_ids_by_extension.get(extension))
})
.or_else(|| {
self.language_config_ids_by_suffix
.iter()
.find_map(|(file_type, id)| {
if path.to_str()?.ends_with(file_type) {
Some(id)
} else {
None
}
})
});
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
@ -1338,7 +1400,7 @@ impl Syntax {
result
}
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree {
let mut container_id = self.root;
for (layer_id, layer) in self.layers.iter() {
@ -1349,8 +1411,17 @@ impl Syntax {
}
}
self.layers[container_id]
.tree()
self.layers[container_id].tree()
}
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.named_descendant_for_byte_range(start, end)
}
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.descendant_for_byte_range(start, end)
}
@ -2306,6 +2377,7 @@ impl<'a> Iterator for HighlightIter<'a> {
// highlighting patterns that are disabled for local variables.
if definition_highlight.is_some() || reference_highlight.is_some() {
while layer.config.non_local_variable_patterns[match_.pattern_index] {
match_.remove();
if let Some((next_match, next_capture_index)) = captures.peek() {
let next_capture = next_match.captures[*next_capture_index];
if next_capture.node == capture.node {
@ -2582,7 +2654,8 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
@ -2644,7 +2717,8 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
@ -2750,7 +2824,8 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();

@ -186,7 +186,7 @@ fn test_treesitter_indent(
lang_scope: &str,
ignored_lines: Vec<std::ops::Range<usize>>,
) {
let loader = Loader::new(indent_tests_config());
let loader = Loader::new(indent_tests_config()).unwrap();
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));

@ -13,6 +13,7 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" }
anyhow = "1.0"
@ -21,7 +22,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
which = "5.0.0"
[dev-dependencies]
fern = "0.6"

@ -113,7 +113,7 @@ impl Client {
id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
// Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
let cmd = helix_stdx::env::which(cmd)?;
let process = Command::new(cmd)
.args(args)

@ -19,6 +19,8 @@ pub enum Error {
#[error("server closed the stream")]
StreamClosed,
#[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type Result<T> = core::result::Result<T, Error>;

@ -12,5 +12,18 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
parking_lot = { version = "0.12", features = ["send_guard"] }
ahash = "0.8.3"
hashbrown = "0.14.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
# the event registry is essentially read only but must be an rwlock so we can
# setup new events on initialization, hardware-lock-elision hugely benefits this case
# as it essentially makes the lock entirely free as long as there is no writes
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
once_cell = "1.18"
anyhow = "1"
log = "0.4"
futures-executor = "0.3.28"
[features]
integration_test = []

@ -0,0 +1,19 @@
use std::future::Future;
pub use oneshot::channel as cancelation;
use tokio::sync::oneshot;
pub type CancelTx = oneshot::Sender<()>;
pub type CancelRx = oneshot::Receiver<()>;
pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
tokio::select! {
biased;
_ = cancel => {
None
}
res = future => {
Some(res)
}
}
}

@ -0,0 +1,67 @@
//! Utilities for declaring an async (usually debounced) hook
use std::time::Duration;
use futures_executor::block_on;
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
use tokio::time::Instant;
/// Async hooks provide a convenient framework for implementing (debounced)
/// async event handlers. Most synchronous event hooks will likely need to
/// debounce their events, coordinate multiple different hooks and potentially
/// track some state. `AsyncHooks` facilitate these use cases by running as
/// a background tokio task that waits for events (usually an enum) to be
/// sent through a channel.
pub trait AsyncHook: Sync + Send + 'static + Sized {
type Event: Sync + Send + 'static;
/// Called immediately whenever an event is received, this function can
/// consume the event immediately or debounce it. In case of debouncing,
/// it can either define a new debounce timeout or continue the current one
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
/// Called whenever the debounce timeline is reached
fn finish_debounce(&mut self);
fn spawn(self) -> mpsc::Sender<Self::Event> {
// the capacity doesn't matter too much here, unless the cpu is totally overwhelmed
// the cap will never be reached since we always immediately drain the channel
// so it should only be reached in case of total CPU overload.
// However, a bounded channel is much more efficient so it's nice to use here
let (tx, rx) = mpsc::channel(128);
tokio::spawn(run(self, rx));
tx
}
}
async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
let mut deadline = None;
loop {
let event = match deadline {
Some(deadline_) => {
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
match res {
Ok(event) => event,
Err(_) => {
hook.finish_debounce();
deadline = None;
continue;
}
}
}
None => rx.recv().await,
};
let Some(event) = event else {
break;
};
deadline = hook.handle_event(event, deadline);
}
}
pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
// block_on has some overhead and in practice the channel should basically
// never be full anyway so first try sending without blocking
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
let _ = block_on(tx.send_timeout(data, Duration::from_millis(10)));
}
}

@ -0,0 +1,91 @@
//! rust dynamic dispatch is extremely limited so we have to build our
//! own vtable implementation. Otherwise implementing the event system would not be possible.
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
//! (the various functions of the trait, drop, size and align). That makes dynamic
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
//! in the hook trait and don't need a drop implementation (event system is global anyway
//! and never dropped) so we can just store the entire vtable inline.
use anyhow::Result;
use std::ptr::{self, NonNull};
use crate::Event;
/// Opaque handle type that represents an erased type parameter.
///
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
/// until then we can use this.
///
/// Care should be taken that we don't use a concrete instance of this. It should only be used
/// through a reference, so we can maintain something else's lifetime.
struct Opaque(());
pub(crate) struct ErasedHook {
data: NonNull<Opaque>,
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
}
impl ErasedHook {
pub(crate) fn new_dynamic<H: Fn() -> Result<()> + 'static + Send + Sync>(
hook: H,
) -> ErasedHook {
unsafe fn call<F: Fn() -> Result<()> + 'static + Send + Sync>(
hook: NonNull<Opaque>,
_event: NonNull<Opaque>,
result: NonNull<Opaque>,
) {
let hook: NonNull<F> = hook.cast();
let result: NonNull<Result<()>> = result.cast();
let hook: &F = hook.as_ref();
let res = hook();
ptr::write(result.as_ptr(), res)
}
unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<H>,
}
}
}
pub(crate) fn new<E: Event, F: Fn(&mut E) -> Result<()>>(hook: F) -> ErasedHook {
unsafe fn call<E: Event, F: Fn(&mut E) -> Result<()>>(
hook: NonNull<Opaque>,
event: NonNull<Opaque>,
result: NonNull<Opaque>,
) {
let hook: NonNull<F> = hook.cast();
let mut event: NonNull<E> = event.cast();
let result: NonNull<Result<()>> = result.cast();
let hook: &F = hook.as_ref();
let res = hook(event.as_mut());
ptr::write(result.as_ptr(), res)
}
unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<E, F>,
}
}
}
pub(crate) unsafe fn call<E: Event>(&self, event: &mut E) -> Result<()> {
let mut res = Ok(());
unsafe {
(self.call)(
self.data,
NonNull::from(event).cast(),
NonNull::from(&mut res).cast(),
);
}
res
}
}
unsafe impl Sync for ErasedHook {}
unsafe impl Send for ErasedHook {}

@ -1,8 +1,203 @@
//! `helix-event` contains systems that allow (often async) communication between
//! different editor components without strongly coupling them. Currently this
//! crate only contains some smaller facilities but the intend is to add more
//! functionality in the future ( like a generic hook system)
//! different editor components without strongly coupling them. Specifically
//! it allows defining synchronous hooks that run when certain editor events
//! occur.
//!
//! The core of the event system are hook callbacks and the [`Event`] trait. A
//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>`
//! that gets called every time an appropriate event is dispatched. The implementation
//! details of the [`Event`] trait are considered private. The [`events`] macro is
//! provided which automatically declares event types. Similarly the `register_hook`
//! macro should be used to (safely) declare event hooks.
//!
//! Hooks run synchronously which can be advantageous since they can modify the
//! current editor state right away (for example to immediately hide the completion
//! popup). However, they can not contain their own state without locking since
//! they only receive immutable references. For handler that want to track state, do
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
//! Async hooks are based around a channels that receive events specific to
//! that `AsyncHook` (usually an enum). These events can be sent by synchronous
//! hooks. Due to some limitations around tokio channels the [`send_blocking`]
//! function exported in this crate should be used instead of the builtin
//! `blocking_send`.
//!
//! In addition to the core event system, this crate contains some message queues
//! that allow transfer of data back to the main event loop from async hooks and
//! hooks that may not have access to all application data (for example in helix-view).
//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
//! display status messages ([`status`]).
//!
//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
//! main loop (including access to the compositor). Ideally that queue will be moved
//! to helix-view in the future if we manage to detach the compositor from its rendering backend.
use anyhow::Result;
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
pub use debounce::{send_blocking, AsyncHook};
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
pub use registry::Event;
mod cancel;
mod debounce;
mod hook;
mod redraw;
mod registry;
#[doc(hidden)]
pub mod runtime;
pub mod status;
#[cfg(test)]
mod test;
pub fn register_event<E: Event + 'static>() {
registry::with_mut(|registry| registry.register_event::<E>())
}
/// Registers a hook that will be called when an event of type `E` is dispatched.
/// This function should usually not be used directly, use the [`register_hook`]
/// macro instead.
///
///
/// # Safety
///
/// `hook` must be totally generic over all lifetime parameters of `E`. For
/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to
/// express that kind of constraint for a generic type with the Rust type system
/// as of this writing.
pub unsafe fn register_hook_raw<E: Event>(
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
) {
registry::with_mut(|registry| registry.register_hook(hook))
}
/// Register a hook solely by event name
pub fn register_dynamic_hook(
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
id: &str,
) -> Result<()> {
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
}
pub fn dispatch(e: impl Event) {
registry::with(|registry| registry.dispatch(e));
}
/// Macro to declare events
///
/// # Examples
///
/// ``` no-compile
/// events! {
/// FileWrite(&Path)
/// ViewScrolled{ view: View, new_pos: ViewOffset }
/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet }
/// }
///
/// fn init() {
/// register_event::<FileWrite>();
/// register_event::<ViewScrolled>();
/// register_event::<DocumentChanged>();
/// }
///
/// fn save(path: &Path, content: &str){
/// std::fs::write(path, content);
/// dispatch(FileWrite(path));
/// }
/// ```
#[macro_export]
macro_rules! events {
($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name<$($lt),*> { $(pub $data: $data_ty),* }
unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> {
const ID: &'static str = stringify!($name);
const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*);
type Static = $crate::events!(@replace_lt $name, $('static, $lt),*);
}
$crate::events!{ $($rem)* }
};
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name { $(pub $data: $data_ty),* }
unsafe impl $crate::Event for $name {
const ID: &'static str = stringify!($name);
const LIFETIMES: usize = 0;
type Static = Self;
}
$crate::events!{ $($rem)* }
};
() => {};
(@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>};
(@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*};
}
/// Safely register statically typed event hooks
#[macro_export]
macro_rules! register_hook {
// Safety: this is safe because we fully control the type of the event here and
// ensure all lifetime arguments are fully generic and the correct number of lifetime arguments
// is present
(move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => {
let val = move |$event: &mut $event_ty<$($lt),*>| $body;
unsafe {
// Lifetimes are a bit of a pain. We want to allow events being
// non-static. Lifetimes don't actually exist at runtime so its
// fine to essentially transmute the lifetimes as long as we can
// prove soundness. The hook must therefore accept any combination
// of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok
// but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a
// mut Event<'a, 'a>) are not. To make this safe we use a macro to
// forbid the user from specifying lifetimes manually (all lifetimes
// specified are always function generics and passed to the event so
// lifetimes can't be used multiple times and using 'static causes a
// syntax error).
//
// There is one soundness hole tough: Type Aliases allow
// "accidentally" creating these problems. For example:
//
// type Event2 = Event<'static>.
// type Event2<'a> = Event<'a, a>.
//
// These cases can be caught by counting the number of lifetimes
// parameters at the parameter declaration site and then at the hook
// declaration site. By asserting the number of lifetime parameters
// are equal we can catch all bad type aliases under one assumption:
// There are no unused lifetime parameters. Introducing a static
// would reduce the number of arguments of the alias by one in the
// above example Event2 has zero lifetime arguments while the original
// event has one lifetime argument. Similar logic applies to using
// a lifetime argument multiple times. The ASSERT below performs a
// a compile time assertion to ensure exactly this property.
//
// With unused lifetime arguments it is still one way to cause unsound code:
//
// type Event2<'a, 'b> = Event<'a, 'a>;
//
// However, this case will always emit a compiler warning/cause CI
// failures so a user would have to introduce #[allow(unused)] which
// is easily caught in review (and a very theoretical case anyway).
// If we want to be pedantic we can simply compile helix with
// forbid(unused). All of this is just a safety net to prevent
// very theoretical misuse. This won't come up in real code (and is
// easily caught in review).
#[allow(unused)]
const ASSERT: () = {
if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){
panic!("invalid type alias");
}
};
$crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val);
}
};
(move |$event:ident: &mut $event_ty: ident| $body: expr) => {
let val = move |$event: &mut $event_ty| $body;
unsafe {
#[allow(unused)]
const ASSERT: () = {
if <$event_ty as $crate::Event>::LIFETIMES != 0{
panic!("invalid type alias");
}
};
$crate::register_hook_raw::<$event_ty>(val);
}
};
}

@ -5,16 +5,20 @@ use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify;
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor the render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchsonous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
use crate::runtime_local;
runtime_local! {
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor to render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchronous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
}
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;

@ -0,0 +1,131 @@
//! A global registry where events are registered and can be
//! subscribed to by registering hooks. The registry identifies event
//! types using their type name so multiple event with the same type name
//! may not be registered (will cause a panic to ensure soundness)
use std::any::TypeId;
use anyhow::{bail, Result};
use hashbrown::hash_map::Entry;
use hashbrown::HashMap;
use parking_lot::RwLock;
use crate::hook::ErasedHook;
use crate::runtime_local;
pub struct Registry {
events: HashMap<&'static str, TypeId, ahash::RandomState>,
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
}
impl Registry {
pub fn register_event<E: Event + 'static>(&mut self) {
let ty = TypeId::of::<E>();
assert_eq!(ty, TypeId::of::<E::Static>());
match self.events.entry(E::ID) {
Entry::Occupied(entry) => {
if entry.get() == &ty {
// don't warn during tests to avoid log spam
#[cfg(not(feature = "integration_test"))]
panic!("Event {} was registered multiple times", E::ID);
} else {
panic!("Multiple events with ID {} were registered", E::ID);
}
}
Entry::Vacant(ent) => {
ent.insert(ty);
self.handlers.insert(E::ID, Vec::new());
}
}
}
/// # Safety
///
/// `hook` must be totally generic over all lifetime parameters of `E`. For
/// example if `E` was a known type `Foo<'a, 'b> then the correct trait bound
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)` but there is no way to
/// express that kind of constraint for a generic type with the rust type system
/// right now.
pub unsafe fn register_hook<E: Event>(
&mut self,
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
) {
// ensure event type ids match so we can rely on them always matching
let id = E::ID;
let Some(&event_id) = self.events.get(id) else {
panic!("Tried to register handler for unknown event {id}");
};
assert!(
TypeId::of::<E::Static>() == event_id,
"Tried to register invalid hook for event {id}"
);
let hook = ErasedHook::new(hook);
self.handlers.get_mut(id).unwrap().push(hook);
}
pub fn register_dynamic_hook(
&mut self,
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
id: &str,
) -> Result<()> {
// ensure event type ids match so we can rely on them always matching
if self.events.get(id).is_none() {
bail!("Tried to register handler for unknown event {id}");
};
let hook = ErasedHook::new_dynamic(hook);
self.handlers.get_mut(id).unwrap().push(hook);
Ok(())
}
pub fn dispatch<E: Event>(&self, mut event: E) {
let Some(hooks) = self.handlers.get(E::ID) else {
log::error!("Dispatched unknown event {}", E::ID);
return;
};
let event_id = self.events[E::ID];
assert_eq!(
TypeId::of::<E::Static>(),
event_id,
"Tried to dispatch invalid event {}",
E::ID
);
for hook in hooks {
// safety: event type is the same
if let Err(err) = unsafe { hook.call(&mut event) } {
log::error!("{} hook failed: {err:#?}", E::ID);
crate::status::report_blocking(err);
}
}
}
}
runtime_local! {
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
// hardcoded random number is good enough here we don't care about DOS resistance
// and avoids the additional complexity of `Option<Registry>`
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
});
}
pub(crate) fn with<T>(f: impl FnOnce(&Registry) -> T) -> T {
f(&REGISTRY.read())
}
pub(crate) fn with_mut<T>(f: impl FnOnce(&mut Registry) -> T) -> T {
f(&mut REGISTRY.write())
}
/// # Safety
/// The number of specified lifetimes and the static type *must* be correct.
/// This is ensured automatically by the [`events`](crate::events)
/// macro.
pub unsafe trait Event: Sized {
/// Globally unique (case sensitive) string that identifies this type.
/// A good candidate is the events type name
const ID: &'static str;
const LIFETIMES: usize;
type Static: Event + 'static;
}

@ -0,0 +1,88 @@
//! The event system makes use of global to decouple different systems.
//! However, this can cause problems for the integration test system because
//! it runs multiple helix applications in parallel. Making the globals
//! thread-local does not work because a applications can/does have multiple
//! runtime threads. Instead this crate implements a similar notion to a thread
//! local but instead of being local to a single thread, the statics are local to
//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient.
//!
//! Therefore this function is only enabled during integration tests and behaves like
//! a normal static otherwise. I would prefer this module to be fully private and to only
//! export the macro but the macro still need to construct these internals so it's marked
//! `doc(hidden)` instead
use std::ops::Deref;
#[cfg(not(feature = "integration_test"))]
pub struct RuntimeLocal<T: 'static> {
/// inner API used in the macro, not part of public API
#[doc(hidden)]
pub __data: T,
}
#[cfg(not(feature = "integration_test"))]
impl<T> Deref for RuntimeLocal<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.__data
}
}
#[cfg(not(feature = "integration_test"))]
#[macro_export]
macro_rules! runtime_local {
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal {
__data: $init
};)*
};
}
#[cfg(feature = "integration_test")]
pub struct RuntimeLocal<T: 'static> {
data:
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
init: fn() -> T,
}
#[cfg(feature = "integration_test")]
impl<T> RuntimeLocal<T> {
/// inner API used in the macro, not part of public API
#[doc(hidden)]
pub const fn __new(init: fn() -> T) -> Self {
Self {
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
)),
init,
}
}
}
#[cfg(feature = "integration_test")]
impl<T> Deref for RuntimeLocal<T> {
type Target = T;
fn deref(&self) -> &T {
let id = tokio::runtime::Handle::current().id();
let guard = self.data.read();
match guard.get(&id) {
Some(res) => res,
None => {
drop(guard);
let data = Box::leak(Box::new((self.init)()));
let mut guard = self.data.write();
guard.insert(id, data);
data
}
}
}
}
#[cfg(feature = "integration_test")]
#[macro_export]
macro_rules! runtime_local {
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)*
};
}

@ -0,0 +1,68 @@
//! A queue of async messages/errors that will be shown in the editor
use std::borrow::Cow;
use std::time::Duration;
use crate::{runtime_local, send_blocking};
use once_cell::sync::OnceCell;
use tokio::sync::mpsc::{Receiver, Sender};
/// Describes the severity level of a [`StatusMessage`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub enum Severity {
Hint,
Info,
Warning,
Error,
}
pub struct StatusMessage {
pub severity: Severity,
pub message: Cow<'static, str>,
}
impl From<anyhow::Error> for StatusMessage {
fn from(err: anyhow::Error) -> Self {
StatusMessage {
severity: Severity::Error,
message: err.to_string().into(),
}
}
}
impl From<&'static str> for StatusMessage {
fn from(msg: &'static str) -> Self {
StatusMessage {
severity: Severity::Info,
message: msg.into(),
}
}
}
runtime_local! {
static MESSAGES: OnceCell<Sender<StatusMessage>> = OnceCell::new();
}
pub async fn report(msg: impl Into<StatusMessage>) {
// if the error channel overflows just ignore it
let _ = MESSAGES
.wait()
.send_timeout(msg.into(), Duration::from_millis(10))
.await;
}
pub fn report_blocking(msg: impl Into<StatusMessage>) {
let messages = MESSAGES.wait();
send_blocking(messages, msg.into())
}
/// Must be called once during editor startup exactly once
/// before any of the messages in this module can be used
///
/// # Panics
/// If called multiple times
pub fn setup() -> Receiver<StatusMessage> {
let (tx, rx) = tokio::sync::mpsc::channel(128);
let _ = MESSAGES.set(tx);
rx
}

@ -0,0 +1,90 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook};
#[test]
fn smoke_test() {
events! {
Event1 { content: String }
Event2 { content: usize }
}
register_event::<Event1>();
register_event::<Event2>();
// setup hooks
let res1: Arc<Mutex<String>> = Arc::default();
let acc = Arc::clone(&res1);
register_hook!(move |event: &mut Event1| {
acc.lock().push_str(&event.content);
Ok(())
});
let res2: Arc<AtomicUsize> = Arc::default();
let acc = Arc::clone(&res2);
register_hook!(move |event: &mut Event2| {
acc.fetch_add(event.content, Ordering::Relaxed);
Ok(())
});
// triggers events
let thread = std::thread::spawn(|| {
for i in 0..1000 {
dispatch(Event2 { content: i });
}
});
std::thread::sleep(Duration::from_millis(1));
dispatch(Event1 {
content: "foo".to_owned(),
});
dispatch(Event2 { content: 42 });
dispatch(Event1 {
content: "bar".to_owned(),
});
dispatch(Event1 {
content: "hello world".to_owned(),
});
thread.join().unwrap();
// check output
assert_eq!(&**res1.lock(), "foobarhello world");
assert_eq!(
res2.load(Ordering::Relaxed),
42 + (0..1000usize).sum::<usize>()
);
}
#[test]
fn dynamic() {
events! {
Event3 {}
Event4 { count: usize }
};
register_event::<Event3>();
register_event::<Event4>();
let count = Arc::new(AtomicUsize::new(0));
let count1 = count.clone();
let count2 = count.clone();
register_dynamic_hook(
move || {
count1.fetch_add(2, Ordering::Relaxed);
Ok(())
},
"Event3",
)
.unwrap();
register_dynamic_hook(
move || {
count2.fetch_add(3, Ordering::Relaxed);
Ok(())
},
"Event4",
)
.unwrap();
dispatch(Event3 {});
dispatch(Event4 { count: 0 });
dispatch(Event3 {});
assert_eq!(count.load(Ordering::Relaxed), 7)
}

@ -24,14 +24,13 @@ etcetera = "0.8"
tree-sitter.workspace = true
once_cell = "1.19"
log = "0.4"
which = "5.0.0"
# TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile = "3.9.0"
tempfile = "3.10.0"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result<Language> {
}
fn ensure_git_is_available() -> Result<()> {
match which::which("git") {
Ok(_cmd) => Ok(()),
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
}
helix_stdx::env::which("git")?;
Ok(())
}
pub fn fetch_grammars() -> Result<()> {

@ -27,7 +27,6 @@ lsp-types = { version = "0.95" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio = { version = "1.36", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14"
which = "5.0.0"
parking_lot = "0.12.1"

@ -1,4 +1,5 @@
use crate::{
file_operations::FileOperationsInterest,
find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
@ -9,20 +10,20 @@ use helix_loader::{self, VERSION_AND_GIT_HASH};
use helix_stdx::path;
use lsp::{
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
WorkspaceFoldersChangeEvent,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::future::Future;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
use std::{future::Future, sync::OnceLock};
use std::{path::Path, process::Stdio};
use tokio::{
io::{BufReader, BufWriter},
process::{Child, Command},
@ -51,6 +52,7 @@ pub struct Client {
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
pub(crate) file_operation_interest: OnceLock<FileOperationsInterest>,
config: Option<Value>,
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
@ -175,15 +177,14 @@ impl Client {
args: &[String],
config: Option<Value>,
server_environment: HashMap<String, String>,
root_markers: &[String],
manual_roots: &[PathBuf],
root_path: PathBuf,
root_uri: Option<lsp::Url>,
id: usize,
name: String,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
// Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
let cmd = helix_stdx::env::which(cmd)?;
let process = Command::new(cmd)
.envs(server_environment)
@ -204,22 +205,6 @@ impl Client {
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id, name.clone());
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::normalize(workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
);
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
let workspace_folders = root_uri
.clone()
@ -233,6 +218,7 @@ impl Client {
server_tx,
request_counter: AtomicU64::new(0),
capabilities: OnceCell::new(),
file_operation_interest: OnceLock::new(),
config,
req_timeout,
root_path,
@ -278,6 +264,11 @@ impl Client {
.expect("language server not yet initialized!")
}
pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest {
self.file_operation_interest
.get_or_init(|| FileOperationsInterest::new(self.capabilities()))
}
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
@ -717,27 +708,27 @@ impl Client {
})
}
pub fn prepare_file_rename(
pub fn will_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support willRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.will_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
let capabilities = self.file_operations_intests();
if !capabilities.will_rename.has_interest(old_path, is_dir) {
return None;
}
let url_from_path = |path| {
let url = if is_dir {
Url::from_directory_path(path)
} else {
Url::from_file_path(path)
};
Some(url.ok()?.to_string())
};
let files = vec![lsp::FileRename {
old_uri: old_uri.to_string(),
new_uri: new_uri.to_string(),
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
lsp::RenameFilesParams { files },
@ -751,27 +742,28 @@ impl Client {
})
}
pub fn did_file_rename(
pub fn did_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support DidRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.did_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
let capabilities = self.file_operations_intests();
if !capabilities.did_rename.has_interest(new_path, is_dir) {
return None;
}
let url_from_path = |path| {
let url = if is_dir {
Url::from_directory_path(path)
} else {
Url::from_file_path(path)
};
Some(url.ok()?.to_string())
};
let files = vec![lsp::FileRename {
old_uri: old_uri.to_string(),
new_uri: new_uri.to_string(),
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
}
@ -999,6 +991,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
context: lsp::CompletionContext,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
@ -1010,13 +1003,12 @@ impl Client {
text_document,
position,
},
context: Some(context),
// TODO: support these tokens by async receiving and updating the choice list
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
context: None,
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
};
Some(self.call::<lsp::request::Completion>(params))
@ -1063,7 +1055,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support signature help.
@ -1079,7 +1071,8 @@ impl Client {
// lsp::SignatureHelpContext
};
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
Some(async move { Ok(serde_json::from_value(res.await?)?) })
}
pub fn text_document_range_inlay_hints(

@ -0,0 +1,105 @@
use std::path::Path;
use globset::{GlobBuilder, GlobSet};
use crate::lsp;
#[derive(Default, Debug)]
pub(crate) struct FileOperationFilter {
dir_globs: GlobSet,
file_globs: GlobSet,
}
impl FileOperationFilter {
fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter {
let Some(cap) = capability else {
return FileOperationFilter::default();
};
let mut dir_globs = GlobSet::builder();
let mut file_globs = GlobSet::builder();
for filter in &cap.filters {
// TODO: support other url schemes
let is_non_file_schema = filter
.scheme
.as_ref()
.is_some_and(|schema| schema != "file");
if is_non_file_schema {
continue;
}
let ignore_case = filter
.pattern
.options
.as_ref()
.and_then(|opts| opts.ignore_case)
.unwrap_or(false);
let mut glob_builder = GlobBuilder::new(&filter.pattern.glob);
glob_builder.case_insensitive(!ignore_case);
let glob = match glob_builder.build() {
Ok(glob) => glob,
Err(err) => {
log::error!("invalid glob send by LS: {err}");
continue;
}
};
match filter.pattern.matches {
Some(lsp::FileOperationPatternKind::File) => {
file_globs.add(glob);
}
Some(lsp::FileOperationPatternKind::Folder) => {
dir_globs.add(glob);
}
None => {
file_globs.add(glob.clone());
dir_globs.add(glob);
}
};
}
let file_globs = file_globs.build().unwrap_or_else(|err| {
log::error!("invalid globs send by LS: {err}");
GlobSet::empty()
});
let dir_globs = dir_globs.build().unwrap_or_else(|err| {
log::error!("invalid globs send by LS: {err}");
GlobSet::empty()
});
FileOperationFilter {
dir_globs,
file_globs,
}
}
pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool {
if is_dir {
self.dir_globs.is_match(path)
} else {
self.file_globs.is_match(path)
}
}
}
#[derive(Default, Debug)]
pub(crate) struct FileOperationsInterest {
// TODO: support other notifications
// did_create: FileOperationFilter,
// will_create: FileOperationFilter,
pub did_rename: FileOperationFilter,
pub will_rename: FileOperationFilter,
// did_delete: FileOperationFilter,
// will_delete: FileOperationFilter,
}
impl FileOperationsInterest {
pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest {
let capabilities = capabilities
.workspace
.as_ref()
.and_then(|capabilities| capabilities.file_operations.as_ref());
let Some(capabilities) = capabilities else {
return FileOperationsInterest::default();
};
FileOperationsInterest {
did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()),
will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()),
}
}
}

@ -1,5 +1,6 @@
mod client;
pub mod file_event;
mod file_operations;
pub mod jsonrpc;
pub mod snippet;
mod transport;
@ -44,6 +45,8 @@ pub enum Error {
#[error("Unhandled")]
Unhandled,
#[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@ -677,7 +680,7 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Arc<Client>> {
) -> Result<Option<Arc<Client>>> {
let config = self
.syn_loader
.language_server_configs()
@ -685,7 +688,7 @@ impl Registry {
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
let id = self.counter;
self.counter += 1;
let NewClient(client, incoming) = start_client(
if let Some(NewClient(client, incoming)) = start_client(
id,
name,
ls_config,
@ -693,9 +696,12 @@ impl Registry {
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
)? {
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(Some(client))
} else {
Ok(None)
}
}
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
@ -720,8 +726,8 @@ impl Registry {
root_dirs,
enable_snippets,
) {
Ok(client) => client,
error => return Some(error),
Ok(client) => client?,
Err(error) => return Some(Err(error)),
};
let old_clients = self
.inner
@ -761,13 +767,13 @@ impl Registry {
root_dirs: &'a [PathBuf],
enable_snippets: bool,
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
language_config.language_servers.iter().map(
language_config.language_servers.iter().filter_map(
move |LanguageServerFeatures { name, .. }| {
if let Some(clients) = self.inner.get(name) {
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
return (name.to_owned(), Ok(client.clone()));
return Some((name.to_owned(), Ok(client.clone())));
}
}
match self.start_client(
@ -778,13 +784,14 @@ impl Registry {
enable_snippets,
) {
Ok(client) => {
let client = client?;
self.inner
.entry(name.to_owned())
.or_default()
.push(client.clone());
(name.clone(), Ok(client))
Some((name.clone(), Ok(client)))
}
Err(err) => (name.to_owned(), Err(err)),
Err(err) => Some((name.to_owned(), Err(err))),
}
},
)
@ -885,18 +892,45 @@ fn start_client(
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<NewClient> {
) -> Result<Option<NewClient>> {
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
let workspace = path::normalize(workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
&workspace,
workspace_is_cwd,
);
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
if let Some(globset) = &ls_config.required_root_patterns {
if !root_path
.read_dir()?
.flatten()
.map(|entry| entry.file_name())
.any(|entry| globset.is_match(entry))
{
return Ok(None);
}
}
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
ls_config.config.clone(),
ls_config.environment.clone(),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
root_path,
root_uri,
id,
name,
ls_config.timeout,
doc_path,
)?;
let client = Arc::new(client);
@ -935,7 +969,7 @@ fn start_client(
initialize_notify.notify_one();
});
Ok(NewClient(client, incoming))
Ok(Some(NewClient(client, incoming)))
}
/// Find an LSP workspace of a file using the following mechanism:

@ -14,6 +14,8 @@ homepage.workspace = true
[dependencies]
dunce = "1.0"
etcetera = "0.8"
ropey = { version = "1.6.1", default-features = false }
which = "6.0"
[dev-dependencies]
tempfile = "3.9"
tempfile = "3.10"

@ -1,4 +1,5 @@
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::RwLock,
};
@ -30,6 +31,37 @@ pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
Ok(())
}
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}
pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
which::which(binary_name).is_ok()
}
pub fn which<T: AsRef<OsStr>>(
binary_name: T,
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
which::which(binary_name.as_ref()).map_err(|err| ExecutableNotFoundError {
command: binary_name.as_ref().to_string_lossy().into_owned(),
inner: err,
})
}
#[derive(Debug)]
pub struct ExecutableNotFoundError {
command: String,
inner: which::Error,
}
impl std::fmt::Display for ExecutableNotFoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "command '{}' not found: {}", self.command, self.inner)
}
}
impl std::error::Error for ExecutableNotFoundError {}
#[cfg(test)]
mod tests {
use super::{current_working_dir, set_current_working_dir};

@ -1,2 +1,3 @@
pub mod env;
pub mod path;
pub mod rope;

@ -0,0 +1,26 @@
use ropey::RopeSlice;
pub trait RopeSliceExt: Sized {
fn ends_with(self, text: &str) -> bool;
fn starts_with(self, text: &str) -> bool;
}
impl RopeSliceExt for RopeSlice<'_> {
fn ends_with(self, text: &str) -> bool {
let len = self.len_bytes();
if len < text.len() {
return false;
}
self.get_byte_slice(len - text.len()..)
.map_or(false, |end| end == text)
}
fn starts_with(self, text: &str) -> bool {
let len = self.len_bytes();
if len < text.len() {
return false;
}
self.get_byte_slice(..len - text.len())
.map_or(false, |start| start == text)
}
}

@ -15,7 +15,7 @@ homepage.workspace = true
[features]
default = ["git"]
unicode-lines = ["helix-core/unicode-lines"]
integration = []
integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"]
[[bin]]
@ -35,8 +35,6 @@ helix-loader = { path = "../helix-loader" }
anyhow = "1"
once_cell = "1.19"
which = "5.0.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.27", features = ["event-stream"] }
@ -44,6 +42,7 @@ signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
arc-swap = { version = "1.6.0" }
termini = "1"
# Logging
fern = "0.6"
@ -54,7 +53,7 @@ log = "0.4"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false }
pulldown-cmark = { version = "0.10", default-features = false }
# file type detection
content_inspector = "0.2.4"
@ -74,7 +73,7 @@ grep-searcher = "0.1.13"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.152"
libc = "0.2.153"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
@ -83,6 +82,6 @@ crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
helix-loader = { path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.12"
smallvec = "1.13"
indoc = "2.0.4"
tempfile = "3.9.0"
tempfile = "3.10.0"

@ -6,4 +6,150 @@ fn main() {
build_grammars(Some(std::env::var("TARGET").unwrap()))
.expect("Failed to compile tree-sitter grammars");
}
#[cfg(windows)]
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
}
#[cfg(windows)]
mod windows_rc {
use std::io::prelude::Write;
use std::{env, io, path::Path, path::PathBuf, process};
pub(crate) fn link_icon_in_windows_exe(icon_path: &str) {
let rc_exe = find_rc_exe().expect("Windows SDK is to be installed along with MSVC");
let output = env::var("OUT_DIR").expect("Env var OUT_DIR should have been set by compiler");
let output_dir = PathBuf::from(output);
let rc_path = output_dir.join("resource.rc");
write_resource_file(&rc_path, icon_path).unwrap();
let resource_file = PathBuf::from(&output_dir).join("resource.lib");
compile_with_toolkit_msvc(rc_exe, resource_file, rc_path);
println!("cargo:rustc-link-search=native={}", output_dir.display());
println!("cargo:rustc-link-lib=dylib=resource");
}
fn compile_with_toolkit_msvc(rc_exe: PathBuf, output: PathBuf, input: PathBuf) {
let mut command = process::Command::new(rc_exe);
let command = command.arg(format!(
"/I{}",
env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR should have been set by Cargo")
));
let status = command
.arg(format!("/fo{}", output.display()))
.arg(format!("{}", input.display()))
.output()
.unwrap();
println!(
"RC Output:\n{}\n------",
String::from_utf8_lossy(&status.stdout)
);
println!(
"RC Error:\n{}\n------",
String::from_utf8_lossy(&status.stderr)
);
}
fn find_rc_exe() -> io::Result<PathBuf> {
let find_reg_key = process::Command::new("reg")
.arg("query")
.arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots")
.arg("/reg:32")
.arg("/v")
.arg("KitsRoot10")
.output();
match find_reg_key {
Err(find_reg_key) => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to run registry query: {}", find_reg_key),
))
}
Ok(find_reg_key) => {
if find_reg_key.status.code().unwrap() != 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
"Can not find Windows SDK",
));
} else {
let lines = String::from_utf8(find_reg_key.stdout)
.expect("Should be able to parse the output");
let mut lines: Vec<&str> = lines.lines().collect();
let mut rc_exe_paths: Vec<PathBuf> = Vec::new();
lines.reverse();
for line in lines {
if line.trim().starts_with("KitsRoot") {
let kit: String = line
.chars()
.skip(line.find("REG_SZ").unwrap() + 6)
.skip_while(|c| c.is_whitespace())
.collect();
let p = PathBuf::from(&kit);
let rc = if cfg!(target_arch = "x86_64") {
p.join(r"bin\x64\rc.exe")
} else {
p.join(r"bin\x86\rc.exe")
};
if rc.exists() {
println!("{:?}", rc);
rc_exe_paths.push(rc.to_owned());
}
if let Ok(bin) = p.join("bin").read_dir() {
for e in bin.filter_map(|e| e.ok()) {
let p = if cfg!(target_arch = "x86_64") {
e.path().join(r"x64\rc.exe")
} else {
e.path().join(r"x86\rc.exe")
};
if p.exists() {
println!("{:?}", p);
rc_exe_paths.push(p.to_owned());
}
}
}
}
}
if rc_exe_paths.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Can not find Windows SDK",
));
}
println!("{:?}", rc_exe_paths);
let rc_path = rc_exe_paths.pop().unwrap();
let rc_exe = if !rc_path.exists() {
if cfg!(target_arch = "x86_64") {
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x64\rc.exe")
} else {
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x86\rc.exe")
}
} else {
rc_path
};
println!("Selected RC path: '{}'", rc_exe.display());
Ok(rc_exe)
}
}
}
}
fn write_resource_file(rc_path: &Path, icon_path: &str) -> io::Result<()> {
let mut f = std::fs::File::create(rc_path)?;
writeln!(f, "{} ICON \"{}\"", 1, icon_path)?;
Ok(())
}
}

@ -1,6 +1,6 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{pos_at_coords, syntax, Selection};
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_range_to_range,
@ -21,9 +21,9 @@ use tui::backend::Backend;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::{Compositor, Event},
config::Config,
handlers,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
@ -96,11 +96,7 @@ fn setup_integration_logging() {
}
impl Application {
pub fn new(
args: Args,
config: Config,
syn_loader_conf: syntax::Configuration,
) -> Result<Self, Error> {
pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> {
#[cfg(feature = "integration")]
setup_integration_logging();
@ -126,7 +122,7 @@ impl Application {
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let syn_loader = std::sync::Arc::new(lang_loader);
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout(), &config.editor);
@ -138,6 +134,7 @@ impl Application {
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
let config = Arc::new(ArcSwap::from_pointee(config));
let handlers = handlers::setup(config.clone());
let mut editor = Editor::new(
area,
theme_loader.clone(),
@ -145,6 +142,7 @@ impl Application {
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
handlers,
);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@ -321,10 +319,21 @@ impl Application {
Some(event) = input_stream.next() => {
self.handle_terminal_events(event).await;
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
Some(callback) = self.jobs.callbacks.recv() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
self.render().await;
}
Some(msg) = self.jobs.status_messages.recv() => {
let severity = match msg.severity{
helix_event::status::Severity::Hint => Severity::Hint,
helix_event::status::Severity::Info => Severity::Info,
helix_event::status::Severity::Warning => Severity::Warning,
helix_event::status::Severity::Error => Severity::Error,
};
// TODO: show multiple status messages at once to avoid clobbering
self.editor.status_msg = Some((msg.message, severity));
helix_event::request_redraw();
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render().await;
@ -381,10 +390,8 @@ impl Application {
/// refresh language config after config change
fn refresh_language_config(&mut self) -> Result<(), Error> {
let syntax_config = helix_core::config::user_syntax_loader()
.map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?;
self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config));
let lang_loader = helix_core::config::user_lang_loader()?;
self.syn_loader = std::sync::Arc::new(lang_loader);
self.editor.syn_loader = self.syn_loader.clone();
for document in self.editor.documents.values_mut() {
document.detect_language(self.syn_loader.clone());
@ -559,26 +566,8 @@ impl Application {
let lines = doc_save_event.text.len_lines();
let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) {
doc.set_path(Some(&doc_save_event.path));
let loader = self.editor.syn_loader.clone();
// borrowing the same doc again to get around the borrow checker
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
self.editor.refresh_language_servers(id);
// and again a borrow checker workaround...
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
}
self.editor
.set_doc_path(doc_save_event.doc_id, &doc_save_event.path);
// TODO: fix being overwritten by lsp
self.editor.set_status(format!(
"'{}' written, {}L {}B",
@ -997,11 +986,9 @@ impl Application {
let language_server = language_server!();
if language_server.is_initialized() {
let offset_encoding = language_server.offset_encoding();
let res = apply_workspace_edit(
&mut self.editor,
offset_encoding,
&params.edit,
);
let res = self
.editor
.apply_workspace_edit(offset_encoding, &params.edit);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: res.is_ok(),

@ -5,7 +5,6 @@ pub(crate) mod typed;
pub use dap::*;
use helix_vcs::Hunk;
pub use lsp::*;
use tokio::sync::oneshot;
use tui::widgets::Row;
pub use typed::*;
@ -33,7 +32,7 @@ use helix_core::{
};
use helix_view::{
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, CompleteAction},
editor::Action,
info::Info,
input::KeyEvent,
keyboard::KeyCode,
@ -52,14 +51,10 @@ use crate::{
filter_picker_entry,
job::Callback,
keymap::ReverseKeymap,
ui::{
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
Popup, Prompt, PromptEvent,
},
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Jobs};
use futures_util::{stream::FuturesUnordered, TryStreamExt};
use std::{
collections::{HashMap, HashSet},
fmt,
@ -88,7 +83,7 @@ pub struct Context<'a> {
pub count: Option<NonZeroUsize>,
pub editor: &'a mut Editor,
pub callback: Option<crate::compositor::Callback>,
pub callback: Vec<crate::compositor::Callback>,
pub on_next_key_callback: Option<OnKeyCallback>,
pub jobs: &'a mut Jobs,
}
@ -96,16 +91,18 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) {
self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component)
}));
self.callback
.push(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component)
}));
}
/// Call `replace_or_push` on the Compositor
pub fn replace_or_push_layer<T: Component>(&mut self, id: &'static str, component: T) {
self.callback = Some(Box::new(move |compositor: &mut Compositor, _| {
compositor.replace_or_push(id, component);
}));
self.callback
.push(Box::new(move |compositor: &mut Compositor, _| {
compositor.replace_or_push(id, component);
}));
}
#[inline]
@ -2591,7 +2588,6 @@ fn delete_by_selection_insert_mode(
);
}
doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
fn delete_selection(cx: &mut Context) {
@ -2665,10 +2661,6 @@ fn insert_mode(cx: &mut Context) {
.transform(|range| Range::new(range.to(), range.from()));
doc.set_selection(view.id, selection);
// [TODO] temporary workaround until we're not using the idle timer to
// trigger auto completions any more
cx.editor.clear_idle_timer();
}
// inserts at the end of each selection
@ -2934,7 +2926,7 @@ pub fn command_palette(cx: &mut Context) {
let register = cx.register;
let count = cx.count;
cx.callback = Some(Box::new(
cx.callback.push(Box::new(
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
[&cx.editor.mode]
@ -2954,7 +2946,7 @@ pub fn command_palette(cx: &mut Context) {
register,
count,
editor: cx.editor,
callback: None,
callback: Vec::new(),
on_next_key_callback: None,
jobs: cx.jobs,
};
@ -2982,7 +2974,7 @@ pub fn command_palette(cx: &mut Context) {
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker
cx.callback = Some(Box::new(|compositor, cx| {
cx.callback.push(Box::new(|compositor, cx| {
if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker);
} else {
@ -3494,9 +3486,10 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
}
pub mod insert {
use crate::events::PostInsertChar;
use super::*;
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
pub type PostHook = fn(&mut Context, char);
/// Exclude the cursor in range.
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
@ -3510,88 +3503,6 @@ pub mod insert {
}
}
// It trigger completion when idle timer reaches deadline
// Only trigger completion if the word under cursor is longer than n characters
pub fn idle_completion(cx: &mut Context) {
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
use helix_core::chars::char_is_word;
let mut iter = text.chars_at(cursor);
iter.reverse();
for _ in 0..config.completion_trigger_len {
match iter.next() {
Some(c) if char_is_word(c) => {}
_ => return,
}
}
super::completion(cx);
}
fn language_server_completion(cx: &mut Context, ch: char) {
let config = cx.editor.config();
if !config.auto_completion {
return;
}
use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let trigger_completion = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.any(|ls| {
// TODO: what if trigger is multiple chars long
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
});
if trigger_completion {
cx.editor.clear_idle_timer();
super::completion(cx);
}
}
fn signature_help(cx: &mut Context, ch: char) {
use helix_lsp::lsp;
// if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor);
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.next()
else {
return;
};
let capabilities = language_server.capabilities();
if let lsp::ServerCapabilities {
signature_help_provider:
Some(lsp::SignatureHelpOptions {
trigger_characters: Some(triggers),
// TODO: retrigger_characters
..
}),
..
} = capabilities
{
// TODO: what if trigger is multiple chars long
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
// lsp doesn't tell us when to close the signature help, so we request
// the help information again after common close triggers which should
// return None, which in turn closes the popup.
let close_triggers = &[')', ';', '.'];
if is_trigger || close_triggers.contains(&ch) {
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
}
}
// The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
@ -3621,12 +3532,7 @@ pub mod insert {
doc.apply(&t, view.id);
}
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
// this could also generically look at Transaction, but it's a bit annoying to look at
// Operation instead of Change.
for hook in &[language_server_completion, signature_help] {
hook(cx, c);
}
helix_event::dispatch(PostInsertChar { c, cx });
}
pub fn smart_tab(cx: &mut Context) {
@ -3745,7 +3651,7 @@ pub mod insert {
(pos, pos, local_offs)
};
let new_range = if doc.restore_cursor {
let new_range = if range.cursor(text) > range.anchor {
// when appending, extend the range by local_offs
Range::new(
range.anchor + global_offs,
@ -3851,8 +3757,6 @@ pub mod insert {
});
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
pub fn delete_char_forward(cx: &mut Context) {
@ -4506,151 +4410,14 @@ fn remove_primary_selection(cx: &mut Context) {
}
pub fn completion(cx: &mut Context) {
use helix_lsp::{lsp, util::pos_to_lsp_pos};
let (view, doc) = current!(cx.editor);
let range = doc.selection(view.id).primary();
let text = doc.text().slice(..);
let cursor = range.cursor(text);
let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
{
savepoint.clone()
} else {
doc.savepoint(view)
};
let text = savepoint.text.clone();
let cursor = savepoint.cursor();
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let language_server_id = language_server.id();
let offset_encoding = language_server.offset_encoding();
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
let doc_id = doc.identifier();
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
async move {
let json = completion_request.await?;
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => Vec::new(),
}
.into_iter()
.map(|item| CompletionItem {
item,
language_server_id,
resolved: false,
})
.collect();
anyhow::Ok(items)
}
})
.collect();
// setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel();
// set completion_request so that this request can be canceled
// by setting completion_request, the old channel stored there is dropped
// and the associated request is automatically dropped
cx.editor.completion_request_handle = Some(tx);
let future = async move {
let items_future = async move {
let mut items = Vec::new();
// TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
items.append(&mut lsp_items);
}
anyhow::Ok(items)
};
tokio::select! {
biased;
_ = rx => {
Ok(Vec::new())
}
res = items_future => {
res
}
}
};
let trigger_offset = cursor;
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
// completion filtering. For example logger.te| should filter the initial suggestion list with "te".
use helix_core::chars;
let mut iter = text.chars_at(cursor);
iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset);
let trigger_doc = doc.id();
let trigger_view = view.id;
// FIXME: The commands Context can only have a single callback
// which means it gets overwritten when executing keybindings
// with multiple commands or macros. This would mean that completion
// might be incorrectly applied when repeating the insertmode action
//
// TODO: to solve this either make cx.callback a Vec of callbacks or
// alternatively move `last_insert` to `helix_view::Editor`
cx.callback = Some(Box::new(
move |compositor: &mut Compositor, _cx: &mut compositor::Context| {
let ui = compositor.find::<ui::EditorView>().unwrap();
ui.last_insert.1.push(InsertEvent::RequestCompletion);
},
));
cx.jobs.callback(async move {
let items = future.await?;
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
// Completions are completed asynchronously and therefore the user could
//switch document/view or leave insert mode. In all of thoise cases the
// completion should be discarded
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
return;
}
if items.is_empty() {
// editor.set_error("No completion available");
return;
}
let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap();
let completion_area = ui.set_completion(
editor,
savepoint,
items,
start_offset,
trigger_offset,
size,
);
let size = compositor.size();
let signature_help_area = compositor
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
.map(|signature_help| signature_help.area(size, editor));
// Delete the signature help popup if they intersect.
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b))
{
compositor.remove(SignatureHelp::ID);
}
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
cx.editor
.handlers
.trigger_completions(cursor, doc.id(), view.id);
}
// comments
@ -4829,10 +4596,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
);
doc.set_selection(view.id, selection);
// [TODO] temporary workaround until we're not using the idle timer to
// trigger auto completions any more
editor.clear_idle_timer();
}
};
@ -5820,7 +5583,7 @@ fn replay_macro(cx: &mut Context) {
cx.editor.macro_replaying.push(reg);
let count = cx.count();
cx.callback = Some(Box::new(move |compositor, cx| {
cx.callback.push(Box::new(move |compositor, cx| {
for _ in 0..count {
for &key in keys.iter() {
compositor.handle_event(&compositor::Event::Key(key), cx);

@ -1,4 +1,4 @@
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
use futures_util::{stream::FuturesUnordered, FutureExt};
use helix_lsp::{
block_on,
lsp::{
@ -8,21 +8,21 @@ use helix_lsp::{
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
Client, OffsetEncoding,
};
use serde_json::Value;
use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
use helix_stdx::path;
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
document::{DocumentInlayHints, DocumentInlayHintsId},
editor::Action,
graphics::Margin,
handlers::lsp::SignatureHelpInvoked,
theme::Style,
Document, View,
};
@ -30,10 +30,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
PromptEvent,
},
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
};
use std::{
@ -42,7 +39,6 @@ use std::{
fmt::Write,
future::Future,
path::PathBuf,
sync::Arc,
};
/// Gets the first language server that is attached to a document which supports a specific feature.
@ -730,8 +726,7 @@ pub fn code_action(cx: &mut Context) {
resolved_code_action.as_ref().unwrap_or(code_action);
if let Some(ref workspace_edit) = resolved_code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
@ -791,63 +786,6 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd:
});
}
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp;
use std::fs;
match op {
ResourceOp::Create(op) => {
let path = op.uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if ignore_if_exists && path.exists() {
Ok(())
} else {
// Create directory if it does not exist
if let Some(dir) = path.parent() {
if !dir.is_dir() {
fs::create_dir_all(dir)?;
}
}
fs::write(&path, [])
}
}
ResourceOp::Delete(op) => {
let path = op.uri.to_file_path().unwrap();
if path.is_dir() {
let recursive = op
.options
.as_ref()
.and_then(|options| options.recursive)
.unwrap_or(false);
if recursive {
fs::remove_dir_all(&path)
} else {
fs::remove_dir(&path)
}
} else if path.is_file() {
fs::remove_file(&path)
} else {
Ok(())
}
}
ResourceOp::Rename(op) => {
let from = op.old_uri.to_file_path().unwrap();
let to = op.new_uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if ignore_if_exists && to.exists() {
Ok(())
} else {
fs::rename(from, &to)
}
}
}
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
@ -875,142 +813,7 @@ impl ToString for ApplyEditErrorKind {
}
}
///TODO make this transactional (and set failureMode to transactional)
pub fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) -> Result<(), ApplyEditError> {
let mut apply_edits = |uri: &helix_lsp::Url,
version: Option<i32>,
text_edits: Vec<lsp::TextEdit>|
-> Result<(), ApplyEditErrorKind> {
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
editor.set_error(err);
return Err(ApplyEditErrorKind::UnknownURISchema);
}
};
let doc_id = match editor.open(&path, Action::Load) {
Ok(doc_id) => doc_id,
Err(err) => {
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
editor.set_error(err);
return Err(ApplyEditErrorKind::FileNotFound);
}
};
let doc = doc!(editor, &doc_id);
if let Some(version) = version {
if version != doc.version() {
let err = format!("outdated workspace edit for {path:?}");
log::error!("{err}, expected {} but got {version}", doc.version());
editor.set_error(err);
return Err(ApplyEditErrorKind::DocumentChanged);
}
}
// Need to determine a view for apply/append_changes_to_history
let view_id = editor.get_synced_view_id(doc_id);
let doc = doc_mut!(editor, &doc_id);
let transaction = helix_lsp::util::generate_transaction_from_edits(
doc.text(),
text_edits,
offset_encoding,
);
let view = view_mut!(editor, view_id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(())
};
if let Some(ref document_changes) = workspace_edit.document_changes {
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for (i, document_edit) in document_edits.iter().enumerate() {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
lsp::DocumentChanges::Operations(operations) => {
log::debug!("document changes - operations: {:?}", operations);
for (i, operation) in operations.iter().enumerate() {
match operation {
lsp::DocumentChangeOperation::Op(op) => {
apply_document_resource_op(op).map_err(|io| ApplyEditError {
kind: ApplyEditErrorKind::IoError(io),
failed_change_idx: i,
})?;
}
lsp::DocumentChangeOperation::Edit(document_edit) => {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
}
}
}
return Ok(());
}
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
for (i, (uri, text_edits)) in changes.iter().enumerate() {
let text_edits = text_edits.to_vec();
apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
Ok(())
}
/// Precondition: `locations` should be non-empty.
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
@ -1023,9 +826,7 @@ fn goto_impl(
[location] => {
jump_to_location(editor, location, offset_encoding, Action::Replace);
}
[] => {
editor.set_error("No definition found.");
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
@ -1067,7 +868,11 @@ where
future,
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
let items = to_locations(response);
goto_impl(editor, compositor, items, offset_encoding);
if items.is_empty() {
editor.set_error("No definition found.");
} else {
goto_impl(editor, compositor, items, offset_encoding);
}
},
);
}
@ -1127,151 +932,19 @@ pub fn goto_reference(cx: &mut Context) {
future,
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
let items = response.unwrap_or_default();
goto_impl(editor, compositor, items, offset_encoding);
if items.is_empty() {
editor.set_error("No references found.");
} else {
goto_impl(editor, compositor, items, offset_encoding);
}
},
);
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
}
pub fn signature_help(cx: &mut Context) {
signature_help_impl(cx, SignatureHelpInvoked::Manual)
}
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
let future = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.find_map(|language_server| {
let pos = doc.position(view.id, language_server.offset_encoding());
language_server.text_document_signature_help(doc.identifier(), pos, None)
});
let Some(future) = future else {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual {
cx.editor
.set_error("No configured language server supports signature-help");
}
return;
};
signature_help_impl_with_future(cx, future.boxed(), invoked);
}
pub fn signature_help_impl_with_future(
cx: &mut Context,
future: BoxFuture<'static, helix_lsp::Result<Value>>,
invoked: SignatureHelpInvoked,
) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| invoked == SignatureHelpInvoked::Manual)
{
return;
}
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
compositor.remove(SignatureHelp::ID);
return;
}
};
let doc = doc!(editor);
let language = doc.language_name().unwrap_or("");
let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);
let signature_doc = if config.lsp.display_signature_help_docs {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
})
} else {
None
};
contents.set_signature_doc(signature_doc);
let active_param_range = || -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response.active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
// LS sends offsets based on utf-16 based string representation
// but highlighting in helix is done using byte offset.
use helix_core::str_utils::char_to_byte_idx;
let from = char_to_byte_idx(&signature.label, *start as usize);
let to = char_to_byte_idx(&signature.label, *end as usize);
Some((from, to))
}
}
};
contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let mut popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
// Don't create a popup if it intersects the auto-complete menu.
let size = compositor.size();
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.as_mut()
.map(|completion| completion.area(size, editor))
.filter(|area| area.intersects(popup.area(size, editor)))
.is_some()
{
return;
}
compositor.replace_or_push(SignatureHelp::ID, popup);
},
);
cx.editor
.handlers
.trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
}
pub fn hover(cx: &mut Context) {
@ -1396,7 +1069,7 @@ pub fn rename_symbol(cx: &mut Context) {
match block_on(future) {
Ok(edits) => {
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
}
Err(err) => cx.editor.set_error(err.to_string()),
}

@ -8,7 +8,6 @@ use super::*;
use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
use helix_core::{encoding, line_ending, shellwords::Shellwords};
use helix_lsp::{OffsetEncoding, Url};
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use serde_json::Value;
@ -1546,10 +1545,7 @@ fn tree_sitter_highlight_name(
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let byte = text.char_to_byte(cursor);
let node = syntax
.tree()
.root_node()
.descendant_for_byte_range(byte, byte)?;
let node = syntax.descendant_for_byte_range(byte, byte)?;
// Query the same range as the one used in syntax highlighting.
let range = {
// Calculate viewport byte ranges:
@ -2407,67 +2403,14 @@ fn move_buffer(
ensure!(args.len() == 1, format!(":move takes one argument"));
let doc = doc!(cx.editor);
let new_path =
helix_stdx::path::canonicalize(&PathBuf::from(args.first().unwrap().to_string()));
let old_path = doc
.path()
.ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
let old_path_as_url = doc.url().unwrap();
let new_path_as_url = Url::from_file_path(&new_path).unwrap();
let edits: Vec<(
helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
OffsetEncoding,
String,
)> = doc
.language_servers()
.map(|lsp| {
(
lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
lsp.offset_encoding(),
lsp.name().to_owned(),
)
})
.filter(|(f, _, _)| f.is_some())
.map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
.collect();
for (lsp_reply, encoding, name) in edits {
match lsp_reply {
Ok(edit) => {
if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
log::error!(
":move command failed to apply edits from lsp {}: {:?}",
name,
e
);
};
}
Err(e) => {
log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
}
};
let new_path = args.first().unwrap().to_string();
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
bail!("Could not move file: {err}");
}
let doc = doc_mut!(cx.editor);
doc.set_path(Some(new_path.as_path()));
if let Err(e) = std::fs::rename(&old_path, &new_path) {
doc.set_path(Some(old_path.as_path()));
bail!("Could not move file: {}", e);
};
doc.language_servers().for_each(|lsp| {
lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
});
cx.editor
.language_servers
.file_event_handler
.file_changed(new_path);
Ok(())
}

@ -0,0 +1,20 @@
use helix_event::{events, register_event};
use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange};
use crate::commands;
use crate::keymap::MappableCommand;
events! {
OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> }
PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> }
PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> }
}
pub fn register() {
register_event::<OnModeSwitch>();
register_event::<PostInsertChar>();
register_event::<PostCommand>();
register_event::<DocumentDidChange>();
register_event::<SelectionDidChange>();
}

@ -0,0 +1,30 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use helix_event::AsyncHook;
use crate::config::Config;
use crate::events;
use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use completion::trigger_auto_completion;
pub use helix_view::handlers::lsp::SignatureHelpInvoked;
pub use helix_view::handlers::Handlers;
mod completion;
mod signature_help;
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
events::register();
let completions = CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn();
let handlers = Handlers {
completions,
signature_hints,
};
completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers);
handlers
}

@ -0,0 +1,465 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use arc_swap::ArcSwap;
use futures_util::stream::FuturesUnordered;
use helix_core::chars::char_is_word;
use helix_core::syntax::LanguageServerFeature;
use helix_event::{
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp;
use helix_lsp::util::pos_to_lsp_pos;
use helix_stdx::rope::RopeSliceExt;
use helix_view::document::{Mode, SavePoint};
use helix_view::handlers::lsp::CompletionEvent;
use helix_view::{DocumentId, Editor, ViewId};
use tokio::sync::mpsc::Sender;
use tokio::time::Instant;
use tokio_stream::StreamExt;
use crate::commands;
use crate::compositor::Compositor;
use crate::config::Config;
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
use crate::job::{dispatch, dispatch_blocking};
use crate::keymap::MappableCommand;
use crate::ui::editor::InsertEvent;
use crate::ui::lsp::SignatureHelp;
use crate::ui::{self, CompletionItem, Popup};
use super::Handlers;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum TriggerKind {
Auto,
TriggerChar,
Manual,
}
#[derive(Debug, Clone, Copy)]
struct Trigger {
pos: usize,
view: ViewId,
doc: DocumentId,
kind: TriggerKind,
}
#[derive(Debug)]
pub(super) struct CompletionHandler {
/// currently active trigger which will cause a
/// completion request after the timeout
trigger: Option<Trigger>,
/// A handle for currently active completion request.
/// This can be used to determine whether the current
/// request is still active (and new triggers should be
/// ignored) and can also be used to abort the current
/// request (by dropping the handle)
request: Option<CancelTx>,
config: Arc<ArcSwap<Config>>,
}
impl CompletionHandler {
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
Self {
config,
request: None,
trigger: None,
}
}
}
impl helix_event::AsyncHook for CompletionHandler {
type Event = CompletionEvent;
fn handle_event(
&mut self,
event: Self::Event,
_old_timeout: Option<Instant>,
) -> Option<Instant> {
match event {
CompletionEvent::AutoTrigger {
cursor: trigger_pos,
doc,
view,
} => {
// techically it shouldn't be possible to switch views/documents in insert mode
// but people may create weird keymaps/use the mouse so lets be extra careful
if self
.trigger
.as_ref()
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
{
self.trigger = Some(Trigger {
pos: trigger_pos,
view,
doc,
kind: TriggerKind::Auto,
});
}
}
CompletionEvent::TriggerChar { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests
self.request = None;
self.trigger = Some(Trigger {
pos: cursor,
view,
doc,
kind: TriggerKind::TriggerChar,
});
}
CompletionEvent::ManualTrigger { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests
self.request = None;
self.trigger = Some(Trigger {
pos: cursor,
view,
doc,
kind: TriggerKind::Manual,
});
// stop debouncing immediately and request the completion
self.finish_debounce();
return None;
}
CompletionEvent::Cancel => {
self.trigger = None;
self.request = None;
}
CompletionEvent::DeleteText { cursor } => {
// if we deleted the original trigger, abort the completion
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
self.trigger = None;
self.request = None;
}
}
}
self.trigger.map(|trigger| {
// if the current request was closed forget about it
// otherwise immediately restart the completion request
let cancel = self.request.take().map_or(false, |req| !req.is_closed());
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
self.config.load().editor.completion_timeout
} else {
// we want almost instant completions for trigger chars
// and restarting completion requests. The small timeout here mainly
// serves to better handle cases where the completion handler
// may fall behind (so multiple events in the channel) and macros
Duration::from_millis(5)
};
Instant::now() + timeout
})
}
fn finish_debounce(&mut self) {
let trigger = self.trigger.take().expect("debounce always has a trigger");
let (tx, rx) = cancelation();
self.request = Some(tx);
dispatch_blocking(move |editor, compositor| {
request_completion(trigger, rx, editor, compositor)
});
}
}
fn request_completion(
mut trigger: Trigger,
cancel: CancelRx,
editor: &mut Editor,
compositor: &mut Compositor,
) {
let (view, doc) = current!(editor);
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.is_some()
|| editor.mode != Mode::Insert
{
return;
}
let text = doc.text();
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
return;
}
// this looks odd... Why are we not using the trigger position from
// the `trigger` here? Won't that mean that the trigger char doesn't get
// send to the LS if we type fast enougn? Yes that is true but it's
// not actually a problem. The LSP will resolve the completion to the identifier
// anyway (in fact sending the later position is necessary to get the right results
// from LSPs that provide incomplete completion list). We rely on trigger offset
// and primary cursor matching for multi-cursor completions so this is definitely
// necessary from our side too.
trigger.pos = cursor;
let trigger_text = text.slice(..cursor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|ls| {
let language_server_id = ls.id();
let offset_encoding = ls.offset_encoding();
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
let doc_id = doc.identifier();
let context = if trigger.kind == TriggerKind::Manual {
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
trigger_character: None,
}
} else {
let trigger_char =
ls.capabilities()
.completion_provider
.as_ref()
.and_then(|provider| {
provider
.trigger_characters
.as_deref()?
.iter()
.find(|&trigger| trigger_text.ends_with(trigger))
});
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: trigger_char.cloned(),
}
};
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
async move {
let json = completion_response.await?;
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => Vec::new(),
}
.into_iter()
.map(|item| CompletionItem {
item,
language_server_id,
resolved: false,
})
.collect();
anyhow::Ok(items)
}
})
.collect();
let future = async move {
let mut items = Vec::new();
while let Some(lsp_items) = futures.next().await {
match lsp_items {
Ok(mut lsp_items) => items.append(&mut lsp_items),
Err(err) => {
log::debug!("completion request failed: {err:?}");
}
};
}
items
};
let savepoint = doc.savepoint(view);
let ui = compositor.find::<ui::EditorView>().unwrap();
ui.last_insert.1.push(InsertEvent::RequestCompletion);
tokio::spawn(async move {
let items = cancelable_future(future, cancel).await.unwrap_or_default();
if items.is_empty() {
return;
}
dispatch(move |editor, compositor| {
show_completion(editor, compositor, items, trigger, savepoint)
})
.await
});
}
fn show_completion(
editor: &mut Editor,
compositor: &mut Compositor,
items: Vec<CompletionItem>,
trigger: Trigger,
savepoint: Arc<SavePoint>,
) {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
// Completions are completed asynchronously and therefore the user could
//switch document/view or leave insert mode. In all of thoise cases the
// completion should be discarded
if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc {
return;
}
let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap();
if ui.completion.is_some() {
return;
}
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
let signature_help_area = compositor
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
.map(|signature_help| signature_help.area(size, editor));
// Delete the signature help popup if they intersect.
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) {
compositor.remove(SignatureHelp::ID);
}
}
pub fn trigger_auto_completion(
tx: &Sender<CompletionEvent>,
editor: &Editor,
trigger_char_only: bool,
) {
let config = editor.config.load();
if !config.auto_completion {
return;
}
let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor);
let mut text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
text = doc.text().slice(..cursor);
let is_trigger_char = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.any(|ls| {
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
});
if is_trigger_char {
send_blocking(
tx,
CompletionEvent::TriggerChar {
cursor,
doc: doc.id(),
view: view.id,
},
);
return;
}
let is_auto_trigger = !trigger_char_only
&& doc
.text()
.chars_at(cursor)
.reversed()
.take(config.completion_trigger_len as usize)
.all(char_is_word);
if is_auto_trigger {
send_blocking(
tx,
CompletionEvent::AutoTrigger {
cursor,
doc: doc.id(),
view: view.id,
},
);
}
}
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
cx.callback.push(Box::new(move |compositor, cx| {
let editor_view = compositor.find::<ui::EditorView>().unwrap();
if let Some(completion) = &mut editor_view.completion {
completion.update_filter(c);
if completion.is_empty() {
editor_view.clear_completion(cx.editor);
// clearing completions might mean we want to immediately rerequest them (usually
// this occurs if typing a trigger char)
if c.is_some() {
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
}
}
}
}))
}
fn clear_completions(cx: &mut commands::Context) {
cx.callback.push(Box::new(|compositor, cx| {
let editor_view = compositor.find::<ui::EditorView>().unwrap();
editor_view.clear_completion(cx.editor);
}))
}
fn completion_post_command_hook(
tx: &Sender<CompletionEvent>,
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
) -> anyhow::Result<()> {
if cx.editor.mode == Mode::Insert {
if cx.editor.last_completion.is_some() {
match command {
MappableCommand::Static {
name: "delete_word_forward" | "delete_char_forward" | "completion",
..
} => (),
MappableCommand::Static {
name: "delete_char_backward",
..
} => update_completions(cx, None),
_ => clear_completions(cx),
}
} else {
let event = match command {
MappableCommand::Static {
name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward",
..
} => {
let (view, doc) = current!(cx.editor);
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
CompletionEvent::DeleteText {
cursor: primary_cursor,
}
}
// hacks: some commands are handeled elsewhere and we don't want to
// cancel in that case
MappableCommand::Static {
name: "completion" | "insert_mode" | "append_mode",
..
} => return Ok(()),
_ => CompletionEvent::Cancel,
};
send_blocking(tx, event);
}
}
Ok(())
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.completions.clone();
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
let tx = handlers.completions.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
if event.old_mode == Mode::Insert {
send_blocking(&tx, CompletionEvent::Cancel);
clear_completions(event.cx);
} else if event.new_mode == Mode::Insert {
trigger_auto_completion(&tx, event.cx.editor, false)
}
Ok(())
});
let tx = handlers.completions.clone();
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
if event.cx.editor.last_completion.is_some() {
update_completions(event.cx, Some(event.c))
} else {
trigger_auto_completion(&tx, event.cx.editor, false);
}
Ok(())
});
}

@ -0,0 +1,335 @@
use std::sync::Arc;
use std::time::Duration;
use helix_core::syntax::LanguageServerFeature;
use helix_event::{
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp;
use helix_stdx::rope::RopeSliceExt;
use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange};
use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked};
use helix_view::Editor;
use tokio::sync::mpsc::Sender;
use tokio::time::Instant;
use crate::commands::Open;
use crate::compositor::Compositor;
use crate::events::{OnModeSwitch, PostInsertChar};
use crate::handlers::Handlers;
use crate::ui::lsp::SignatureHelp;
use crate::ui::Popup;
use crate::{job, ui};
#[derive(Debug)]
enum State {
Open,
Closed,
Pending { request: CancelTx },
}
/// debounce timeout in ms, value taken from VSCode
/// TODO: make this configurable?
const TIMEOUT: u64 = 120;
#[derive(Debug)]
pub(super) struct SignatureHelpHandler {
trigger: Option<SignatureHelpInvoked>,
state: State,
}
impl SignatureHelpHandler {
pub fn new() -> SignatureHelpHandler {
SignatureHelpHandler {
trigger: None,
state: State::Closed,
}
}
}
impl helix_event::AsyncHook for SignatureHelpHandler {
type Event = SignatureHelpEvent;
fn handle_event(
&mut self,
event: Self::Event,
timeout: Option<tokio::time::Instant>,
) -> Option<Instant> {
match event {
SignatureHelpEvent::Invoked => {
self.trigger = Some(SignatureHelpInvoked::Manual);
self.state = State::Closed;
self.finish_debounce();
return None;
}
SignatureHelpEvent::Trigger => {}
SignatureHelpEvent::ReTrigger => {
// don't retrigger if we aren't open/pending yet
if matches!(self.state, State::Closed) {
return timeout;
}
}
SignatureHelpEvent::Cancel => {
self.state = State::Closed;
return None;
}
SignatureHelpEvent::RequestComplete { open } => {
// don't cancel rerequest that was already triggered
if let State::Pending { request } = &self.state {
if !request.is_closed() {
return timeout;
}
}
self.state = if open { State::Open } else { State::Closed };
return timeout;
}
}
if self.trigger.is_none() {
self.trigger = Some(SignatureHelpInvoked::Automatic)
}
Some(Instant::now() + Duration::from_millis(TIMEOUT))
}
fn finish_debounce(&mut self) {
let invocation = self.trigger.take().unwrap();
let (tx, rx) = cancelation();
self.state = State::Pending { request: tx };
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx))
}
}
pub fn request_signature_help(
editor: &mut Editor,
invoked: SignatureHelpInvoked,
cancel: CancelRx,
) {
let (view, doc) = current!(editor);
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
let future = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.find_map(|language_server| {
let pos = doc.position(view.id, language_server.offset_encoding());
language_server.text_document_signature_help(doc.identifier(), pos, None)
});
let Some(future) = future else {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual {
editor
.set_error("No configured language server supports signature-help");
}
return;
};
tokio::spawn(async move {
match cancelable_future(future, cancel).await {
Some(Ok(res)) => {
job::dispatch(move |editor, compositor| {
show_signature_help(editor, compositor, invoked, res)
})
.await
}
Some(Err(err)) => log::error!("signature help request failed: {err}"),
None => (),
}
});
}
pub fn show_signature_help(
editor: &mut Editor,
compositor: &mut Compositor,
invoked: SignatureHelpInvoked,
response: Option<lsp::SignatureHelp>,
) {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| invoked == SignatureHelpInvoked::Manual)
{
return;
}
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
// For the most part this should not be needed as the request gets canceled automatically now
// but it's technically possible for the mode change to just preempt this callback so better safe than sorry
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
send_blocking(
&editor.handlers.signature_hints,
SignatureHelpEvent::RequestComplete { open: false },
);
compositor.remove(SignatureHelp::ID);
return;
}
};
send_blocking(
&editor.handlers.signature_hints,
SignatureHelpEvent::RequestComplete { open: true },
);
let doc = doc!(editor);
let language = doc.language_name().unwrap_or("");
let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);
let signature_doc = if config.lsp.display_signature_help_docs {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
})
} else {
None
};
contents.set_signature_doc(signature_doc);
let active_param_range = || -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response.active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
// LS sends offsets based on utf-16 based string representation
// but highlighting in helix is done using byte offset.
use helix_core::str_utils::char_to_byte_idx;
let from = char_to_byte_idx(&signature.label, *start as usize);
let to = char_to_byte_idx(&signature.label, *end as usize);
Some((from, to))
}
}
};
contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let mut popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
// Don't create a popup if it intersects the auto-complete menu.
let size = compositor.size();
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.as_mut()
.map(|completion| completion.area(size, editor))
.filter(|area| area.intersects(popup.area(size, editor)))
.is_some()
{
return;
}
compositor.replace_or_push(SignatureHelp::ID, popup);
}
fn signature_help_post_insert_char_hook(
tx: &Sender<SignatureHelpEvent>,
PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
) -> anyhow::Result<()> {
if !cx.editor.config().lsp.auto_signature_help {
return Ok(());
}
let (view, doc) = current!(cx.editor);
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.next()
else {
return Ok(());
};
let capabilities = language_server.capabilities();
if let lsp::ServerCapabilities {
signature_help_provider:
Some(lsp::SignatureHelpOptions {
trigger_characters: Some(triggers),
// TODO: retrigger_characters
..
}),
..
} = capabilities
{
let mut text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
text = text.slice(..cursor);
if triggers.iter().any(|trigger| text.ends_with(trigger)) {
send_blocking(tx, SignatureHelpEvent::Trigger)
}
}
Ok(())
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
match (event.old_mode, event.new_mode) {
(Mode::Insert, _) => {
send_blocking(&tx, SignatureHelpEvent::Cancel);
event.cx.callback.push(Box::new(|compositor, _| {
compositor.remove(SignatureHelp::ID);
}));
}
(_, Mode::Insert) => {
if event.cx.editor.config().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::Trigger);
}
}
_ => (),
}
Ok(())
});
let tx = handlers.signature_hints.clone();
register_hook!(
move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event)
);
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
if event.doc.config.load().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
}
Ok(())
});
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut SelectionDidChange<'_>| {
if event.doc.config.load().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
}
Ok(())
});
}

@ -2,7 +2,7 @@ use crossterm::{
style::{Color, Print, Stylize},
tty::IsTty,
};
use helix_core::config::{default_syntax_loader, user_syntax_loader};
use helix_core::config::{default_lang_config, user_lang_config};
use helix_loader::grammar::load_runtime_file;
use helix_view::clipboard::get_clipboard_provider;
use std::io::Write;
@ -128,7 +128,7 @@ pub fn languages_all() -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut syn_loader_conf = match user_syntax_loader() {
let mut syn_loader_conf = match user_lang_config() {
Ok(conf) => conf,
Err(err) => {
let stderr = std::io::stderr();
@ -141,7 +141,7 @@ pub fn languages_all() -> std::io::Result<()> {
err
)?;
writeln!(stderr, "{}", "Using default language config".yellow())?;
default_syntax_loader()
default_lang_config()
}
};
@ -182,7 +182,7 @@ pub fn languages_all() -> std::io::Result<()> {
.sort_unstable_by_key(|l| l.language_id.clone());
let check_binary = |cmd: Option<&str>| match cmd {
Some(cmd) => match which::which(cmd) {
Some(cmd) => match helix_stdx::env::which(cmd) {
Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
Err(_) => column(&format!("✘ {}", cmd), Color::Red),
},
@ -234,7 +234,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let syn_loader_conf = match user_syntax_loader() {
let syn_loader_conf = match user_lang_config() {
Ok(conf) => conf,
Err(err) => {
let stderr = std::io::stderr();
@ -247,7 +247,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
err
)?;
writeln!(stderr, "{}", "Using default language config".yellow())?;
default_syntax_loader()
default_lang_config()
}
};
@ -322,7 +322,7 @@ fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
writeln!(stdout)?;
for cmd in server_cmds {
let (path, icon) = match which::which(cmd) {
let (path, icon) = match helix_stdx::env::which(cmd) {
Ok(path) => (path.display().to_string().green(), "✓".green()),
Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "✘".red()),
};
@ -344,7 +344,7 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::R
writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?;
if let Some(cmd) = server_cmd {
let path = match which::which(&cmd) {
let path = match helix_stdx::env::which(&cmd) {
Ok(path) => path.display().to_string().green(),
Err(_) => format!("'{}' not found in $PATH", cmd).red(),
};

@ -1,13 +1,37 @@
use helix_event::status::StatusMessage;
use helix_event::{runtime_local, send_blocking};
use helix_view::Editor;
use once_cell::sync::OnceCell;
use crate::compositor::Compositor;
use futures_util::future::{BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt};
use tokio::sync::mpsc::{channel, Receiver, Sender};
pub type EditorCompositorCallback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
pub type EditorCallback = Box<dyn FnOnce(&mut Editor) + Send>;
runtime_local! {
static JOB_QUEUE: OnceCell<Sender<Callback>> = OnceCell::new();
}
pub async fn dispatch_callback(job: Callback) {
let _ = JOB_QUEUE.wait().send(job).await;
}
pub async fn dispatch(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
let _ = JOB_QUEUE
.wait()
.send(Callback::EditorCompositor(Box::new(job)))
.await;
}
pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
let jobs = JOB_QUEUE.wait();
send_blocking(jobs, Callback::EditorCompositor(Box::new(job)))
}
pub enum Callback {
EditorCompositor(EditorCompositorCallback),
Editor(EditorCallback),
@ -21,11 +45,11 @@ pub struct Job {
pub wait: bool,
}
#[derive(Default)]
pub struct Jobs {
pub futures: FuturesUnordered<JobFuture>,
/// These are the ones that need to complete before we exit.
/// jobs that need to complete before we exit.
pub wait_futures: FuturesUnordered<JobFuture>,
pub callbacks: Receiver<Callback>,
pub status_messages: Receiver<StatusMessage>,
}
impl Job {
@ -52,8 +76,16 @@ impl Job {
}
impl Jobs {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::default()
let (tx, rx) = channel(1024);
let _ = JOB_QUEUE.set(tx);
let status_messages = helix_event::status::setup();
Self {
wait_futures: FuturesUnordered::new(),
callbacks: rx,
status_messages,
}
}
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
@ -85,18 +117,17 @@ impl Jobs {
}
}
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
tokio::select! {
event = self.futures.next() => { event }
event = self.wait_futures.next() => { event }
}
}
pub fn add(&self, j: Job) {
if j.wait {
self.wait_futures.push(j.future);
} else {
self.futures.push(j.future);
tokio::spawn(async move {
match j.future.await {
Ok(Some(cb)) => dispatch_callback(cb).await,
Ok(None) => (),
Err(err) => helix_event::status::report(err).await,
}
});
}
}

@ -6,29 +6,46 @@ pub mod args;
pub mod commands;
pub mod compositor;
pub mod config;
pub mod events;
pub mod health;
pub mod job;
pub mod keymap;
pub mod ui;
use std::path::Path;
use futures_util::Future;
mod handlers;
use ignore::DirEntry;
use url::Url;
pub use keymap::macros::*;
#[cfg(not(windows))]
fn true_color() -> bool {
std::env::var("COLORTERM")
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
.unwrap_or(false)
}
#[cfg(windows)]
fn true_color() -> bool {
true
}
#[cfg(not(windows))]
fn true_color() -> bool {
if matches!(
std::env::var("COLORTERM").map(|v| matches!(v.as_str(), "truecolor" | "24bit")),
Ok(true)
) {
return true;
}
match termini::TermInfo::from_env() {
Ok(t) => {
t.extended_cap("RGB").is_some()
|| t.extended_cap("Tc").is_some()
|| (t.extended_cap("setrgbf").is_some() && t.extended_cap("setrgbb").is_some())
}
Err(_) => false,
}
}
/// Function used for filtering dir entries in the various file pickers.
fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool {
// We always want to ignore the .git directory, otherwise if

@ -145,18 +145,18 @@ FLAGS:
}
};
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| {
eprintln!("{}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
helix_core::config::default_syntax_loader()
helix_core::config::default_lang_loader()
});
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config, syn_loader_conf)
.context("unable to create new application")?;
let mut app =
Application::new(args, config, lang_loader).context("unable to create new application")?;
let exit_code = app.run(&mut EventStream::new()).await?;

@ -1,8 +1,12 @@
use crate::compositor::{Component, Context, Event, EventResult};
use crate::{
compositor::{Component, Context, Event, EventResult},
handlers::trigger_auto_completion,
};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
graphics::Margin,
handlers::lsp::SignatureHelpInvoked,
theme::{Modifier, Style},
ViewId,
};
@ -10,7 +14,7 @@ use tui::{buffer::Buffer as Surface, text::Span};
use std::{borrow::Cow, sync::Arc};
use helix_core::{Change, Transaction};
use helix_core::{chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
@ -95,10 +99,9 @@ pub struct CompletionItem {
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>,
start_offset: usize,
#[allow(dead_code)]
trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char
filter: String,
}
impl Completion {
@ -108,7 +111,6 @@ impl Completion {
editor: &Editor,
savepoint: Arc<SavePoint>,
mut items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
@ -246,7 +248,7 @@ impl Completion {
// (also without sending the transaction to the LS) *before any further transaction is applied*.
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
// is applied to).
if editor.last_completion.is_none() {
if matches!(editor.last_completion, Some(CompleteAction::Triggered)) {
editor.last_completion = Some(CompleteAction::Selected {
savepoint: doc.savepoint(view),
})
@ -324,8 +326,18 @@ impl Completion {
doc.apply(&transaction, view.id);
}
}
// we could have just inserted a trigger char (like a `crate::` completion for rust
// so we want to retrigger immediately when accepting a completion.
trigger_auto_completion(&editor.handlers.completions, editor, true);
}
};
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
if event != PromptEvent::Update {
editor
.handlers
.trigger_signature_help(SignatureHelpInvoked::Automatic, editor);
}
});
let margin = if editor.menu_border() {
@ -339,14 +351,30 @@ impl Completion {
.ignore_escape_key(true)
.margin(margin);
let (view, doc) = current_ref!(editor);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let offset = text
.chars_at(cursor)
.reversed()
.take_while(|ch| chars::char_is_word(*ch))
.count();
let start_offset = cursor.saturating_sub(offset);
let fragment = doc.text().slice(start_offset..cursor);
let mut completion = Self {
popup,
start_offset,
trigger_offset,
// TODO: expand nucleo api to allow moving straight to a Utf32String here
// and avoid allocation during matching
filter: String::from(fragment),
};
// need to recompute immediately in case start_offset != trigger_offset
completion.recompute_filter(editor);
completion
.popup
.contents_mut()
.score(&completion.filter, false);
completion
}
@ -366,39 +394,22 @@ impl Completion {
}
}
pub fn recompute_filter(&mut self, editor: &Editor) {
/// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter
/// this should be called whenever the user types or deletes a character in insert mode.
pub fn update_filter(&mut self, c: Option<char>) {
// recompute menu based on matches
let menu = self.popup.contents_mut();
let (view, doc) = current_ref!(editor);
// cx.hooks()
// cx.add_hook(enum type, ||)
// cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
// callback with editor & compositor
//
// trigger_hook sends event into channel, that's consumed in the global loop and
// triggers all registered callbacks
// TODO: hooks should get processed immediately so maybe do it after select!(), before
// looping?
let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.start_offset..cursor);
let text = Cow::from(fragment);
// TODO: logic is same as ui/picker
menu.score(&text);
} else {
// we backspaced before the start offset, clear the menu
// this will cause the editor to remove the completion popup
menu.clear();
match c {
Some(c) => self.filter.push(c),
None => {
self.filter.pop();
if self.filter.is_empty() {
menu.clear();
return;
}
}
}
}
pub fn update(&mut self, cx: &mut commands::Context) {
self.recompute_filter(cx.editor)
menu.score(&self.filter, c.is_some());
}
pub fn is_empty(&self) -> bool {

@ -1,7 +1,7 @@
use crate::{
commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
job::{self, Callback},
events::{OnModeSwitch, PostCommand},
key,
keymap::{KeymapResult, Keymaps},
ui::{
@ -33,8 +33,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span};
use super::document::LineDecoration;
use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
pub keymaps: Keymaps,
@ -835,35 +835,26 @@ impl EditorView {
let mut execute_command = |command: &commands::MappableCommand| {
command.execute(cxt);
helix_event::dispatch(PostCommand { command, cx: cxt });
let current_mode = cxt.editor.mode();
match (last_mode, current_mode) {
(Mode::Normal, Mode::Insert) => {
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
if current_mode != last_mode {
helix_event::dispatch(OnModeSwitch {
old_mode: last_mode,
new_mode: current_mode,
cx: cxt,
});
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
if current_mode == Mode::Insert {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = command.clone();
self.last_insert.1.clear();
commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.clear_completion(cxt.editor);
cxt.editor.completion_request_handle = None;
// TODO: Use an on_mode_change hook to remove signature help
cxt.jobs.callback(async {
let call: job::Callback =
Callback::EditorCompositor(Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID);
}));
Ok(call)
});
}
_ => (),
}
last_mode = current_mode;
};
@ -991,12 +982,10 @@ impl EditorView {
editor: &mut Editor,
savepoint: Arc<SavePoint>,
items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
size: Rect,
) -> Option<Rect> {
let mut completion =
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
if completion.is_empty() {
// skip if we got no completion results
@ -1004,7 +993,7 @@ impl EditorView {
}
let area = completion.area(size, editor);
editor.last_completion = None;
editor.last_completion = Some(CompleteAction::Triggered);
self.last_insert.1.push(InsertEvent::TriggerCompletion);
// TODO : propagate required size on resize to completion too
@ -1017,6 +1006,7 @@ impl EditorView {
self.completion = None;
if let Some(last_completion) = editor.last_completion.take() {
match last_completion {
CompleteAction::Triggered => (),
CompleteAction::Applied {
trigger_offset,
changes,
@ -1030,9 +1020,6 @@ impl EditorView {
}
}
}
// Clear any savepoints
editor.clear_idle_timer(); // don't retrigger
}
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
@ -1046,13 +1033,7 @@ impl EditorView {
};
}
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
return EventResult::Ignored(None);
}
crate::commands::insert::idle_completion(cx);
EventResult::Consumed(None)
EventResult::Ignored(None)
}
}
@ -1265,7 +1246,7 @@ impl Component for EditorView {
editor: context.editor,
count: None,
register: None,
callback: None,
callback: Vec::new(),
on_next_key_callback: None,
jobs: context.jobs,
};
@ -1338,12 +1319,6 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
self.clear_completion(cx.editor);
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
commands::signature_help_impl(
&mut cx,
commands::SignatureHelpInvoked::Automatic,
);
}
}
}
@ -1354,14 +1329,6 @@ impl Component for EditorView {
// record last_insert key
self.last_insert.1.push(InsertEvent::Key(key));
// lastly we recalculate completion
if let Some(completion) = &mut self.completion {
completion.update(&mut cx);
if completion.is_empty() {
self.clear_completion(cx.editor);
}
}
}
}
mode => self.command_mode(mode, &mut cx, key),
@ -1375,7 +1342,7 @@ impl Component for EditorView {
}
// appease borrowck
let callback = cx.callback.take();
let callbacks = take(&mut cx.callback);
// if the command consumed the last view, skip the render.
// on the next loop cycle the Application will then terminate.
@ -1394,6 +1361,16 @@ impl Component for EditorView {
if mode != Mode::Insert {
doc.append_changes_to_history(view);
}
let callback = if callbacks.is_empty() {
None
} else {
let callback: crate::compositor::Callback = Box::new(move |compositor, cx| {
for callback in callbacks {
callback(compositor, cx)
}
});
Some(callback)
};
EventResult::Consumed(callback)
}

@ -6,7 +6,7 @@ use tui::{
use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
@ -209,7 +209,7 @@ impl Markdown {
list_stack.push(list);
}
Event::End(Tag::List(_)) => {
Event::End(TagEnd::List(_)) => {
list_stack.pop();
// whenever top-level list closes, empty line
@ -249,7 +249,10 @@ impl Markdown {
Event::End(tag) => {
tags.pop();
match tag {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => {
TagEnd::Heading(_)
| TagEnd::Paragraph
| TagEnd::CodeBlock
| TagEnd::Item => {
push_line(&mut spans, &mut lines);
}
_ => (),
@ -257,7 +260,7 @@ impl Markdown {
// whenever heading, code block or paragraph closes, empty line
match tag {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => {
TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::CodeBlock => {
lines.push(Spans::default());
}
_ => (),
@ -279,7 +282,7 @@ impl Markdown {
lines.extend(tui_text.lines.into_iter());
} else {
let style = match tags.last() {
Some(Tag::Heading(level, ..)) => match level {
Some(Tag::Heading { level, .. }) => match level {
HeadingLevel::H1 => heading_styles[0],
HeadingLevel::H2 => heading_styles[1],
HeadingLevel::H3 => heading_styles[2],

@ -96,20 +96,34 @@ impl<T: Item> Menu<T> {
}
}
pub fn score(&mut self, pattern: &str) {
// reuse the matches allocation
self.matches.clear();
pub fn score(&mut self, pattern: &str, incremental: bool) {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
let mut buf = Vec::new();
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
let text = option.filter_text(&self.editor_data);
pattern
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
.map(|score| (i as u32, score as u32))
});
self.matches.extend(matches);
if incremental {
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index as usize];
let text = option.filter_text(&self.editor_data);
let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
match new_score {
Some(new_score) => {
*score = new_score as u32;
true
}
None => false,
}
})
} else {
self.matches.clear();
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
let text = option.filter_text(&self.editor_data);
pattern
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
.map(|score| (i as u32, score as u32))
});
self.matches.extend(matches);
}
self.matches
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
@ -413,6 +427,7 @@ impl<T: Item + 'static> Component for Menu<T> {
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else if !render_borders {
// Draw scroll track
cell.set_symbol(half_block);
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}

@ -303,6 +303,7 @@ impl<T: Component> Component for Popup<T> {
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else if !render_borders {
// Draw scroll track
cell.set_symbol(half_block);
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}

@ -393,7 +393,7 @@ impl Prompt {
height,
);
if !self.completion.is_empty() {
if completion_area.height > 0 && !self.completion.is_empty() {
let area = completion_area;
let background = theme.get("ui.menu");

@ -315,7 +315,7 @@ async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> {
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("#[l|]#et foo = 0;\n")
.with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into())))
.with_lang_loader(helpers::test_syntax_loader(Some(lang_conf.into())))
.build()?;
test_key_sequences(&mut app, vec![(Some(":w<ret>"), None)], false).await?;

@ -139,7 +139,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
let test_case = test_case.into();
let mut app = match app {
Some(app) => app,
None => Application::new(Args::default(), test_config(), test_syntax_conf(None))?,
None => Application::new(Args::default(), test_config(), test_syntax_loader(None))?,
};
let (view, doc) = helix_view::current!(app.editor);
@ -162,9 +162,9 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
.await
}
/// Generates language configs that merge in overrides, like a user language
/// Generates language config loader that merge in overrides, like a user language
/// config. The argument string must be a raw TOML document.
pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Configuration {
pub fn test_syntax_loader(overrides: Option<String>) -> helix_core::syntax::Loader {
let mut lang = helix_loader::config::default_lang_config();
if let Some(overrides) = overrides {
@ -172,7 +172,7 @@ pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Config
lang = helix_loader::merge_toml_values(lang, override_toml, 3);
}
lang.try_into().unwrap()
helix_core::syntax::Loader::new(lang.try_into().unwrap()).unwrap()
}
/// Use this for very simple test cases where there is one input
@ -271,7 +271,7 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
pub struct AppBuilder {
args: Args,
config: Config,
syn_conf: helix_core::syntax::Configuration,
syn_loader: helix_core::syntax::Loader,
input: Option<(String, Selection)>,
}
@ -280,7 +280,7 @@ impl Default for AppBuilder {
Self {
args: Args::default(),
config: test_config(),
syn_conf: test_syntax_conf(None),
syn_loader: test_syntax_loader(None),
input: None,
}
}
@ -314,8 +314,8 @@ impl AppBuilder {
self
}
pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self {
self.syn_conf = syn_conf;
pub fn with_lang_loader(mut self, syn_loader: helix_core::syntax::Loader) -> Self {
self.syn_loader = syn_loader;
self
}
@ -328,7 +328,7 @@ impl AppBuilder {
bail!("Having the directory {path:?} in args.files[0] is not yet supported for integration tests");
}
let mut app = Application::new(self.args, self.config, self.syn_conf)?;
let mut app = Application::new(self.args, self.config, self.syn_loader)?;
if let Some((text, selection)) = self.input {
let (view, doc) = helix_view::current!(app.editor);

@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
parking_lot = "0.12"
arc-swap = { version = "1.6.0" }
gix = { version = "0.57.1", default-features = false , optional = true }
gix = { version = "0.58.0", features = ["attributes"], default-features = false, optional = true }
imara-diff = "0.1.5"
anyhow = "1"
@ -29,4 +29,4 @@ log = "0.4"
git = ["gix"]
[dev-dependencies]
tempfile = "3.9"
tempfile = "3.10"

@ -1,5 +1,7 @@
use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap;
use gix::filter::plumbing::driver::apply::Delay;
use std::io::Read;
use std::path::Path;
use std::sync::Arc;
@ -76,29 +78,21 @@ impl DiffProvider for Git {
let file_oid = find_file_in_commit(&repo, &head, file)?;
let file_object = repo.find_object(file_oid)?;
let mut data = file_object.detach().data;
// convert LF to CRLF if configured to avoid showing every line as changed
if repo
.config_snapshot()
.boolean("core.autocrlf")
.unwrap_or(false)
{
let mut normalized_file = Vec::with_capacity(data.len());
let mut at_cr = false;
for &byte in &data {
if byte == b'\n' {
// if this is a LF instead of a CRLF (last byte was not a CR)
// insert a new CR to generate a CRLF
if !at_cr {
normalized_file.push(b'\r');
}
}
at_cr = byte == b'\r';
normalized_file.push(byte)
}
data = normalized_file
let data = file_object.detach().data;
// Get the actual data that git would make out of the git object.
// This will apply the user's git config or attributes like crlf conversions.
if let Some(work_dir) = repo.work_dir() {
let rela_path = file.strip_prefix(work_dir)?;
let rela_path = gix::path::try_into_bstr(rela_path)?;
let (mut pipeline, _) = repo.filter_pipeline(None)?;
let mut worktree_outcome =
pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?;
let mut buf = Vec::with_capacity(data.len());
worktree_outcome.read_to_end(&mut buf)?;
Ok(buf)
} else {
Ok(data)
}
Ok(data)
}
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {

@ -46,7 +46,6 @@ serde_json = "1.0"
toml = "0.7"
log = "~0.4"
which = "5.0.0"
parking_lot = "0.12.1"

@ -73,7 +73,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
#[cfg(target_os = "macos")]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
use crate::env::{binary_exists, env_var_is_set};
use helix_stdx::env::{binary_exists, env_var_is_set};
if env_var_is_set("TMUX") && binary_exists("tmux") {
command_provider! {
@ -98,7 +98,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
use crate::env::{binary_exists, env_var_is_set};
use helix_stdx::env::{binary_exists, env_var_is_set};
use provider::command::is_exit_success;
// TODO: support for user-defined provider, probably when we have plugin support by setting a
// variable?

@ -36,6 +36,7 @@ use helix_core::{
};
use crate::editor::Config;
use crate::events::{DocumentDidChange, SelectionDidChange};
use crate::{DocumentId, Editor, Theme, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
@ -114,19 +115,6 @@ pub struct SavePoint {
/// The view this savepoint is associated with
pub view: ViewId,
revert: Mutex<Transaction>,
pub text: Rope,
}
impl SavePoint {
pub fn cursor(&self) -> usize {
// we always create transactions with selections
self.revert
.lock()
.selection()
.unwrap()
.primary()
.cursor(self.text.slice(..))
}
}
pub struct Document {
@ -738,7 +726,12 @@ impl Document {
if let Some((fmt_cmd, fmt_args)) = self
.language_config()
.and_then(|c| c.formatter.as_ref())
.and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args)))
.and_then(|formatter| {
Some((
helix_stdx::env::which(&formatter.command).ok()?,
&formatter.args,
))
})
{
use std::process::Stdio;
let text = self.text().clone();
@ -1048,6 +1041,9 @@ impl Document {
self.encoding
}
/// sets the document path without sending events to various
/// observers (like LSP), in most cases `Editor::set_doc_path`
/// should be used instead
pub fn set_path(&mut self, path: Option<&Path>) {
let path = path.map(helix_stdx::path::canonicalize);
@ -1096,6 +1092,10 @@ impl Document {
// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
helix_event::dispatch(SelectionDidChange {
doc: self,
view: view_id,
})
}
/// Find the origin selection of the text in a document, i.e. where
@ -1149,6 +1149,14 @@ impl Document {
let success = transaction.changes().apply(&mut self.text);
if success {
if emit_lsp_notification {
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
old_text: &old_doc,
});
}
for selection in self.selections.values_mut() {
*selection = selection
.clone()
@ -1164,6 +1172,10 @@ impl Document {
view_id,
selection.clone().ensure_invariants(self.text.slice(..)),
);
helix_event::dispatch(SelectionDidChange {
doc: self,
view: view_id,
});
}
self.modified_since_accessed = true;
@ -1276,6 +1288,7 @@ impl Document {
}
if emit_lsp_notification {
// TODO: move to hook
// emit lsp notification
for language_server in self.language_servers() {
let notify = language_server.text_document_did_change(
@ -1386,7 +1399,6 @@ impl Document {
let savepoint = Arc::new(SavePoint {
view: view.id,
revert: Mutex::new(revert),
text: self.text.clone(),
});
self.savepoints.push(Arc::downgrade(&savepoint));
savepoint

@ -2,6 +2,7 @@ use crate::{
align_view,
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
graphics::{CursorKind, Rect},
handlers::Handlers,
info::Info,
input::KeyEvent,
register::Registers,
@ -22,7 +23,8 @@ use std::{
borrow::Cow,
cell::Cell,
collections::{BTreeMap, HashMap},
io::stdin,
fs,
io::{self, stdin},
num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
@ -30,10 +32,7 @@ use std::{
};
use tokio::{
sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
oneshot,
},
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
time::{sleep, Duration, Instant, Sleep},
};
@ -47,6 +46,7 @@ use helix_core::{
};
use helix_dap as dap;
use helix_lsp::lsp;
use helix_stdx::path::canonicalize;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
@ -243,12 +243,19 @@ pub struct Config {
/// Set a global text_width
pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger.
/// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
/// Used for various UI timeouts. Defaults to 250ms.
#[serde(
serialize_with = "serialize_duration_millis",
deserialize_with = "deserialize_duration_millis"
)]
pub idle_timeout: Duration,
/// Time in milliseconds after typing a word character before auto completions
/// are shown, set to 5 for instant. Defaults to 250ms.
#[serde(
serialize_with = "serialize_duration_millis",
deserialize_with = "deserialize_duration_millis"
)]
pub completion_timeout: Duration,
/// Whether to insert the completion suggestion on hover. Defaults to true.
pub preview_completion_insert: bool,
pub completion_trigger_len: u8,
@ -324,7 +331,7 @@ pub struct TerminalConfig {
#[cfg(windows)]
pub fn get_terminal_provider() -> Option<TerminalConfig> {
use crate::env::binary_exists;
use helix_stdx::env::binary_exists;
if binary_exists("wt") {
return Some(TerminalConfig {
@ -347,7 +354,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
#[cfg(not(any(windows, target_os = "wasm32")))]
pub fn get_terminal_provider() -> Option<TerminalConfig> {
use crate::env::{binary_exists, env_var_is_set};
use helix_stdx::env::{binary_exists, env_var_is_set};
if env_var_is_set("TMUX") && binary_exists("tmux") {
return Some(TerminalConfig {
@ -828,6 +835,7 @@ impl Default for Config {
auto_format: true,
auto_save: false,
idle_timeout: Duration::from_millis(250),
completion_timeout: Duration::from_millis(250),
preview_completion_insert: true,
completion_trigger_len: 2,
auto_info: true,
@ -952,14 +960,7 @@ pub struct Editor {
/// avoid calculating the cursor position multiple
/// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>,
/// When a new completion request is sent to the server old
/// unfinished request must be dropped. Each completion
/// request is associated with a channel that cancels
/// when the channel is dropped. That channel is stored
/// here. When a new completion request is sent this
/// field is set and any old requests are automatically
/// canceled as a result
pub completion_request_handle: Option<oneshot::Sender<()>>,
pub handlers: Handlers,
}
pub type Motion = Box<dyn Fn(&mut Editor)>;
@ -987,13 +988,16 @@ enum ThemeAction {
#[derive(Debug, Clone)]
pub enum CompleteAction {
Triggered,
/// A savepoint of the currently selected completion. The savepoint
/// MUST be restored before sending any event to the LSP
Selected {
savepoint: Arc<SavePoint>,
},
Applied {
trigger_offset: usize,
changes: Vec<Change>,
},
/// A savepoint of the currently selected completion. The savepoint
/// MUST be restored before sending any event to the LSP
Selected { savepoint: Arc<SavePoint> },
}
#[derive(Debug, Copy, Clone)]
@ -1027,6 +1031,7 @@ impl Editor {
theme_loader: Arc<theme::Loader>,
syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>,
handlers: Handlers,
) -> Self {
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
let conf = config.load();
@ -1071,7 +1076,7 @@ impl Editor {
config_events: unbounded_channel(),
needs_redraw: false,
cursor_cache: Cell::new(None),
completion_request_handle: None,
handlers,
}
}
@ -1144,7 +1149,7 @@ impl Editor {
#[inline]
pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
let error = error.into();
log::error!("editor error: {}", error);
log::debug!("editor error: {}", error);
self.status_msg = Some((error, Severity::Error));
}
@ -1212,6 +1217,90 @@ impl Editor {
self.launch_language_servers(doc_id)
}
/// moves/renames a path, invoking any event handlers (currently only lsp)
/// and calling `set_doc_path` if the file is open in the editor
pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> {
let new_path = canonicalize(new_path);
// sanity check
if old_path == new_path {
return Ok(());
}
let is_dir = old_path.is_dir();
let language_servers: Vec<_> = self
.language_servers
.iter_clients()
.filter(|client| client.is_initialized())
.cloned()
.collect();
for language_server in language_servers {
let Some(request) = language_server.will_rename(old_path, &new_path, is_dir) else {
continue;
};
let edit = match helix_lsp::block_on(request) {
Ok(edit) => edit,
Err(err) => {
log::error!("invalid willRename response: {err:?}");
continue;
}
};
if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) {
log::error!("failed to apply workspace edit: {err:?}")
}
}
fs::rename(old_path, &new_path)?;
if let Some(doc) = self.document_by_path(old_path) {
self.set_doc_path(doc.id(), &new_path);
}
let is_dir = new_path.is_dir();
for ls in self.language_servers.iter_clients() {
if let Some(notification) = ls.did_rename(old_path, &new_path, is_dir) {
tokio::spawn(notification);
};
}
self.language_servers
.file_event_handler
.file_changed(old_path.to_owned());
self.language_servers
.file_event_handler
.file_changed(new_path);
Ok(())
}
pub fn set_doc_path(&mut self, doc_id: DocumentId, path: &Path) {
let doc = doc_mut!(self, &doc_id);
let old_path = doc.path();
if let Some(old_path) = old_path {
// sanity check, should not occur but some callers (like an LSP) may
// create bogus calls
if old_path == path {
return;
}
// if we are open in LSPs send did_close notification
for language_server in doc.language_servers() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
}
// we need to clear the list of language servers here so that
// refresh_doc_language/refresh_language_servers doesn't resend
// text_document_did_close. Since we called `text_document_did_close`
// we have fully unregistered this document from its LS
doc.language_servers.clear();
doc.set_path(Some(path));
self.refresh_doc_language(doc_id)
}
pub fn refresh_doc_language(&mut self, doc_id: DocumentId) {
let loader = self.syn_loader.clone();
let doc = doc_mut!(self, &doc_id);
doc.detect_language(loader);
doc.detect_indent_and_line_ending();
self.refresh_language_servers(doc_id);
let doc = doc_mut!(self, &doc_id);
let diagnostics = Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, doc);
doc.replace_diagnostics(diagnostics, &[], None);
}
/// Launch a language server for a given document
fn launch_language_servers(&mut self, doc_id: DocumentId) {
if !self.config().lsp.enable {
@ -1235,19 +1324,26 @@ impl Editor {
.filter_map(|(lang, client)| match client {
Ok(client) => Some((lang, client)),
Err(err) => {
log::error!(
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
language.scope(),
lang,
err
);
if let helix_lsp::Error::ExecutableNotFound(err) = err {
// Silence by default since some language servers might just not be installed
log::debug!(
"Language server not found for `{}` {} {}", language.scope(), lang, err,
);
} else {
log::error!(
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
language.scope(),
lang,
err
);
}
None
}
})
.collect::<HashMap<_, _>>()
});
if language_servers.is_empty() {
if language_servers.is_empty() && doc.language_servers.is_empty() {
return;
}
@ -1894,10 +1990,12 @@ impl Editor {
if doc.restore_cursor {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, range.to()),
)
let mut head = range.to();
if range.head > range.anchor {
head = graphemes::prev_grapheme_boundary(text, head);
}
Range::new(range.from(), head)
});
doc.set_selection(view.id, selection);

@ -1,8 +0,0 @@
pub fn binary_exists(binary_name: &str) -> bool {
which::which(binary_name).is_ok()
}
#[cfg(not(windows))]
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}

@ -0,0 +1,9 @@
use helix_core::Rope;
use helix_event::events;
use crate::{Document, ViewId};
events! {
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
}

@ -0,0 +1,41 @@
use helix_event::send_blocking;
use tokio::sync::mpsc::Sender;
use crate::handlers::lsp::SignatureHelpInvoked;
use crate::{DocumentId, Editor, ViewId};
pub mod dap;
pub mod lsp;
pub struct Handlers {
// only public because most of the actual implementation is in helix-term right now :/
pub completions: Sender<lsp::CompletionEvent>,
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
}
impl Handlers {
/// Manually trigger completion (c-x)
pub fn trigger_completions(&self, trigger_pos: usize, doc: DocumentId, view: ViewId) {
send_blocking(
&self.completions,
lsp::CompletionEvent::ManualTrigger {
cursor: trigger_pos,
doc,
view,
},
);
}
pub fn trigger_signature_help(&self, invocation: SignatureHelpInvoked, editor: &Editor) {
let event = match invocation {
SignatureHelpInvoked::Automatic => {
if !editor.config().lsp.auto_signature_help {
return;
}
lsp::SignatureHelpEvent::Trigger
}
SignatureHelpInvoked::Manual => lsp::SignatureHelpEvent::Invoked,
};
send_blocking(&self.signature_hints, event)
}
}

@ -1 +1,270 @@
use crate::editor::Action;
use crate::Editor;
use crate::{DocumentId, ViewId};
use helix_lsp::util::generate_transaction_from_edits;
use helix_lsp::{lsp, OffsetEncoding};
pub enum CompletionEvent {
/// Auto completion was triggered by typing a word char
AutoTrigger {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// Auto completion was triggered by typing a trigger char
/// specified by the LSP
TriggerChar {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// A completion was manually requested (c-x)
ManualTrigger {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// Some text was deleted and the cursor is now at `pos`
DeleteText { cursor: usize },
/// Invalidate the current auto completion trigger
Cancel,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Automatic,
Manual,
}
pub enum SignatureHelpEvent {
Invoked,
Trigger,
ReTrigger,
Cancel,
RequestComplete { open: bool },
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
pub failed_change_idx: usize,
}
#[derive(Debug)]
pub enum ApplyEditErrorKind {
DocumentChanged,
FileNotFound,
UnknownURISchema,
IoError(std::io::Error),
// TODO: check edits before applying and propagate failure
// InvalidEdit,
}
impl ToString for ApplyEditErrorKind {
fn to_string(&self) -> String {
match self {
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(),
ApplyEditErrorKind::IoError(err) => err.to_string(),
}
}
}
impl Editor {
fn apply_text_edits(
&mut self,
uri: &helix_lsp::Url,
version: Option<i32>,
text_edits: Vec<lsp::TextEdit>,
offset_encoding: OffsetEncoding,
) -> Result<(), ApplyEditErrorKind> {
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
self.set_error(err);
return Err(ApplyEditErrorKind::UnknownURISchema);
}
};
let doc_id = match self.open(&path, Action::Load) {
Ok(doc_id) => doc_id,
Err(err) => {
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
self.set_error(err);
return Err(ApplyEditErrorKind::FileNotFound);
}
};
let doc = doc_mut!(self, &doc_id);
if let Some(version) = version {
if version != doc.version() {
let err = format!("outdated workspace edit for {path:?}");
log::error!("{err}, expected {} but got {version}", doc.version());
self.set_error(err);
return Err(ApplyEditErrorKind::DocumentChanged);
}
}
// Need to determine a view for apply/append_changes_to_history
let view_id = self.get_synced_view_id(doc_id);
let doc = doc_mut!(self, &doc_id);
let transaction = generate_transaction_from_edits(doc.text(), text_edits, offset_encoding);
let view = view_mut!(self, view_id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(())
}
// TODO make this transactional (and set failureMode to transactional)
pub fn apply_workspace_edit(
&mut self,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) -> Result<(), ApplyEditError> {
if let Some(ref document_changes) = workspace_edit.document_changes {
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for (i, document_edit) in document_edits.iter().enumerate() {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
self.apply_text_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
offset_encoding,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
lsp::DocumentChanges::Operations(operations) => {
log::debug!("document changes - operations: {:?}", operations);
for (i, operation) in operations.iter().enumerate() {
match operation {
lsp::DocumentChangeOperation::Op(op) => {
self.apply_document_resource_op(op).map_err(|io| {
ApplyEditError {
kind: ApplyEditErrorKind::IoError(io),
failed_change_idx: i,
}
})?;
}
lsp::DocumentChangeOperation::Edit(document_edit) => {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
self.apply_text_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
offset_encoding,
)
.map_err(|kind| {
ApplyEditError {
kind,
failed_change_idx: i,
}
})?;
}
}
}
}
}
return Ok(());
}
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
for (i, (uri, text_edits)) in changes.iter().enumerate() {
let text_edits = text_edits.to_vec();
self.apply_text_edits(uri, None, text_edits, offset_encoding)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
Ok(())
}
fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp;
use std::fs;
match op {
ResourceOp::Create(op) => {
let path = op.uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if !ignore_if_exists || !path.exists() {
// Create directory if it does not exist
if let Some(dir) = path.parent() {
if !dir.is_dir() {
fs::create_dir_all(dir)?;
}
}
fs::write(&path, [])?;
self.language_servers.file_event_handler.file_changed(path);
}
}
ResourceOp::Delete(op) => {
let path = op.uri.to_file_path().unwrap();
if path.is_dir() {
let recursive = op
.options
.as_ref()
.and_then(|options| options.recursive)
.unwrap_or(false);
if recursive {
fs::remove_dir_all(&path)?
} else {
fs::remove_dir(&path)?
}
self.language_servers.file_event_handler.file_changed(path);
} else if path.is_file() {
fs::remove_file(&path)?;
}
}
ResourceOp::Rename(op) => {
let from = op.old_uri.to_file_path().unwrap();
let to = op.new_uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if !ignore_if_exists || !to.exists() {
self.move_path(&from, &to)?;
}
}
}
Ok(())
}
}

@ -1,17 +1,14 @@
#[macro_use]
pub mod macros;
pub mod base64;
pub mod clipboard;
pub mod document;
pub mod editor;
pub mod env;
pub mod events;
pub mod graphics;
pub mod gutter;
pub mod handlers {
pub mod dap;
pub mod lsp;
}
pub mod base64;
pub mod handlers;
pub mod info;
pub mod input;
pub mod keyboard;

@ -253,7 +253,7 @@ source = { git = "https://github.com/FuelLabs/tree-sitter-sway", rev = "e491a005
name = "toml"
scope = "source.toml"
injection-regex = "toml"
file-types = ["toml", "poetry.lock", "Cargo.lock"]
file-types = ["toml", { glob = "poetry.lock" }, { glob = "Cargo.lock" }]
comment-token = "#"
language-servers = [ "taplo" ]
indent = { tab-width = 2, unit = " " }
@ -292,7 +292,7 @@ source = { git = "https://github.com/yusdacra/tree-sitter-protobuf", rev = "19c2
name = "elixir"
scope = "source.elixir"
injection-regex = "(elixir|ex)"
file-types = ["ex", "exs", "mix.lock"]
file-types = ["ex", "exs", { glob = "mix.lock" }]
shebangs = ["elixir"]
roots = ["mix.exs", "mix.lock"]
comment-token = "#"
@ -326,6 +326,29 @@ comment-token = "//"
language-servers = [ "mint" ]
indent = { tab-width = 2, unit = " " }
[[language]]
name = "janet"
scope = "source.janet"
injection-regex = "janet"
file-types = ["cgen", "janet", "jdn"]
shebangs = ["janet"]
roots = ["project.janet"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
formatter = { command = "janet-format" }
grammar = "janet-simple"
[language.auto-pairs]
'"' = '"'
'(' = ')'
'[' = ']'
'{' = '}'
"`" = "`"
[[grammar]]
name = "janet-simple"
source = { git = "https://github.com/sogaiu/tree-sitter-janet-simple", rev = "51271e260346878e1a1aa6c506ce6a797b7c25e2" }
[[language]]
name = "json"
scope = "source.json"
@ -338,20 +361,20 @@ file-types = [
"geojson",
"gltf",
"webmanifest",
"flake.lock",
".babelrc",
".bowerrc",
".jscrc",
{ glob = "flake.lock" },
{ glob = ".babelrc" },
{ glob = ".bowerrc" },
{ glob = ".jscrc" },
"js.map",
"ts.map",
"css.map",
".jslintrc",
{ glob = ".jslintrc" },
"jsonld",
".vuerc",
"composer.lock",
".watchmanconfig",
{ glob = ".vuerc" },
{ glob = "composer.lock" },
{ glob = ".watchmanconfig" },
"avsc",
".prettierrc"
{ glob = ".prettierrc" },
]
language-servers = [ "vscode-json-language-server" ]
auto-format = true
@ -416,7 +439,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
name = "cpp"
scope = "source.cpp"
injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { suffix = ".hpp.in" }, { suffix = ".h.in" }]
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { glob = ".hpp.in" }, { glob = ".h.in" }]
comment-token = "//"
language-servers = [ "clangd" ]
indent = { tab-width = 2, unit = " " }
@ -491,6 +514,30 @@ args = { processId = "{0}" }
name = "c-sharp"
source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "5b60f99545fea00a33bbfae5be956f684c4c69e2" }
[[language]]
name = "cel"
scope = "source.cel"
injection-regex = "cel"
file-types = ["cel"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "cel"
source = { git = "https://github.com/bufbuild/tree-sitter-cel", rev = "9f2b65da14c216df53933748e489db0f11121464" }
[[language]]
name = "spicedb"
scope = "source.zed"
injection-regex = "spicedb"
file-types = ["zed"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "spicedb"
source = { git = "https://github.com/jzelinskie/tree-sitter-spicedb", rev = "a4e4645651f86d6684c15dfa9931b7841dc52a66" }
[[language]]
name = "go"
scope = "source.go"
@ -548,7 +595,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "64457ea
name = "gomod"
scope = "source.gomod"
injection-regex = "gomod"
file-types = ["go.mod"]
file-types = [{ glob = "go.mod" }]
auto-format = true
comment-token = "//"
language-servers = [ "gopls" ]
@ -575,7 +622,7 @@ source = { git = "https://github.com/dannylongeuay/tree-sitter-go-template", rev
name = "gowork"
scope = "source.gowork"
injection-regex = "gowork"
file-types = ["go.work"]
file-types = [{ glob = "go.work" }]
auto-format = true
comment-token = "//"
language-servers = [ "gopls" ]
@ -590,7 +637,7 @@ name = "javascript"
scope = "source.js"
injection-regex = "(js|javascript)"
language-id = "javascript"
file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", "jakefile"]
file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = "jakefile" }]
shebangs = ["node"]
comment-token = "//"
language-servers = [ "typescript-language-server" ]
@ -693,7 +740,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53
name = "python"
scope = "source.python"
injection-regex = "python"
file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct", "rpy", "cpy", "ipy", "pyt", "SConscript"]
file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }]
shebangs = ["python"]
roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"]
comment-token = "#"
@ -746,38 +793,38 @@ injection-regex = "ruby"
file-types = [
"rb",
"rake",
"rakefile",
"irb",
"gemfile",
"gemspec",
"Rakefile",
"Gemfile",
"rabl",
"jbuilder",
"jb",
"Podfile",
"podspec",
"Vagrantfile",
"Brewfile",
"rjs",
"rbi",
"Guardfile",
"Capfile",
"Cheffile",
"Hobofile",
"Appraisals",
"Rantfile",
"Berksfile",
"Berksfile.lock",
"Thorfile",
"Puppetfile",
"Fastfile",
"Appfile",
"Deliverfile",
"Matchfile",
"Scanfile",
"Snapfile",
"Gymfile"
{ glob = "rakefile" },
{ glob = "gemfile" },
{ glob = "Rakefile" },
{ glob = "Gemfile" },
{ glob = "Podfile" },
{ glob = "Vagrantfile" },
{ glob = "Brewfile" },
{ glob = "Guardfile" },
{ glob = "Capfile" },
{ glob = "Cheffile" },
{ glob = "Hobofile" },
{ glob = "Appraisals" },
{ glob = "Rantfile" },
{ glob = "Berksfile" },
{ glob = "Berksfile.lock" },
{ glob = "Thorfile" },
{ glob = "Puppetfile" },
{ glob = "Fastfile" },
{ glob = "Appfile" },
{ glob = "Deliverfile" },
{ glob = "Matchfile" },
{ glob = "Scanfile" },
{ glob = "Snapfile" },
{ glob = "Gymfile" },
]
shebangs = ["ruby"]
comment-token = "#"
@ -796,42 +843,43 @@ file-types = [
"sh",
"bash",
"zsh",
".bash_login",
".bash_logout",
".bash_profile",
".bashrc",
".profile",
".zshenv",
"zshenv",
".zlogin",
"zlogin",
".zlogout",
"zlogout",
".zprofile",
"zprofile",
".zshrc",
"zshrc",
".zimrc",
"APKBUILD",
"PKGBUILD",
"eclass",
"ebuild",
"bazelrc",
".bash_aliases",
"Renviron",
".Renviron",
".xprofile",
".xsession",
".xsessionrc",
"zsh-theme",
"ksh",
"cshrc",
"tcshrc",
".yashrc",
".yash_profile",
".hushlogin",
"bashrc_Apple_Terminal",
"zshrc_Apple_Terminal"
"zshrc_Apple_Terminal",
{ glob = ".bash_history" },
{ glob = ".bash_login" },
{ glob = ".bash_logout" },
{ glob = ".bash_profile" },
{ glob = ".bashrc" },
{ glob = ".profile" },
{ glob = ".zshenv" },
{ glob = ".zlogin" },
{ glob = ".zlogout" },
{ glob = ".zprofile" },
{ glob = ".zshrc" },
{ glob = ".zimrc" },
{ glob = "APKBUILD" },
{ glob = "PKGBUILD" },
{ glob = ".bash_aliases" },
{ glob = ".Renviron" },
{ glob = ".xprofile" },
{ glob = ".xsession" },
{ glob = ".xsessionrc" },
{ glob = ".yashrc" },
{ glob = ".yash_profile" },
{ glob = ".hushlogin" },
]
shebangs = ["sh", "bash", "dash", "zsh"]
comment-token = "#"
@ -1192,7 +1240,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-tsq", rev = "48b5
[[language]]
name = "cmake"
scope = "source.cmake"
file-types = ["cmake", "CMakeLists.txt"]
file-types = ["cmake", { glob = "CMakeLists.txt" }]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-servers = [ "cmake-language-server" ]
@ -1205,7 +1253,7 @@ source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "6e51463ef30
[[language]]
name = "make"
scope = "source.make"
file-types = ["Makefile", "makefile", "make", "mk", "mak", "GNUmakefile", "OCamlMakefile"]
file-types = [{ glob = "Makefile" }, { glob = "makefile" }, "make", "mk", "mak", {glob = "GNUmakefile" }, { glob = "OCamlMakefile" }]
shebangs = ["make", "gmake"]
injection-regex = "(make|makefile|Makefile|mk)"
comment-token = "#"
@ -1248,7 +1296,7 @@ file-types = ["pod"]
[[grammar]]
name = "pod"
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "d466b84009a63986834498073ec05d58d727d55f" }
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "39da859947b94abdee43e431368e1ae975c0a424" }
[[language]]
name = "racket"
@ -1348,7 +1396,7 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd
name = "markdown"
scope = "source.md"
injection-regex = "md|markdown"
file-types = ["md", "markdown", "PULLREQ_EDITMSG", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook"]
file-types = ["md", "markdown", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }]
roots = [".marksman.toml"]
language-servers = [ "marksman" ]
indent = { tab-width = 2, unit = " " }
@ -1393,14 +1441,14 @@ language-servers = [ "metals" ]
[[grammar]]
name = "scala"
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "23d21310fe4ab4b3273e7a6810e781224a3e7fe1" }
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "7891815f42dca9ed6aeb464c2edc39d479ab965c" }
[[language]]
name = "dockerfile"
scope = "source.dockerfile"
injection-regex = "docker|dockerfile"
roots = ["Dockerfile", "Containerfile"]
file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
file-types = [{ glob = "Dockerfile*" }, { glob = "dockerfile*" }, { glob = "Containerfile*" }, { glob = "containerfile*" }]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-servers = [ "docker-langserver" ]
@ -1412,7 +1460,7 @@ source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev =
[[language]]
name = "git-commit"
scope = "git.commitmsg"
file-types = ["COMMIT_EDITMSG"]
file-types = [{ glob = "COMMIT_EDITMSG" }]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
rulers = [51, 73]
@ -1437,7 +1485,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-diff", rev = "fd7
[[language]]
name = "git-rebase"
scope = "source.gitrebase"
file-types = ["git-rebase-todo"]
file-types = [{ glob = "git-rebase-todo" }]
injection-regex = "git-rebase"
comment-token = "#"
indent = { tab-width = 2, unit = "y" }
@ -1450,7 +1498,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-rebase", rev
name = "regex"
scope = "source.regex"
injection-regex = "regex"
file-types = ["regex", ".Rbuildignore"]
file-types = ["regex", { glob = ".Rbuildignore" }]
[[grammar]]
name = "regex"
@ -1459,7 +1507,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf
[[language]]
name = "git-config"
scope = "source.gitconfig"
file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }]
file-types = [{ glob = ".gitmodules" }, { glob = ".gitconfig" }, { glob = ".git/config" }, { glob = ".config/git/config" }]
injection-regex = "git-config"
comment-token = "#"
indent = { tab-width = 4, unit = "\t" }
@ -1471,7 +1519,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev
[[language]]
name = "git-attributes"
scope = "source.gitattributes"
file-types = [".gitattributes"]
file-types = [{ glob = ".gitattributes" }]
injection-regex = "git-attributes"
comment-token = "#"
grammar = "gitattributes"
@ -1483,7 +1531,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
[[language]]
name = "git-ignore"
scope = "source.gitignore"
file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"]
file-types = [{ glob = ".gitignore" }, { glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = ".prettierignore" }, { glob = ".eslintignore" }, { glob = ".npmignore"}, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }]
injection-regex = "git-ignore"
comment-token = "#"
grammar = "gitignore"
@ -1548,7 +1596,7 @@ source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "
name = "erlang"
scope = "source.erlang"
injection-regex = "erl(ang)?"
file-types = ["erl", "hrl", "app", "rebar.config", "rebar.lock"]
file-types = ["erl", "hrl", "app", { glob = "rebar.config" }, { glob = "rebar.lock" }]
roots = ["rebar.config"]
shebangs = ["escript"]
comment-token = "%%"
@ -1674,7 +1722,7 @@ source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "322e4cc657
name = "r"
scope = "source.r"
injection-regex = "(r|R)"
file-types = ["r", "R", ".Rprofile", "Rprofile.site", ".RHistory"]
file-types = ["r", "R", { glob = ".Rprofile" }, { glob = "Rprofile.site" }, { glob = ".RHistory" }]
shebangs = ["r", "R"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
@ -1706,7 +1754,7 @@ language-servers = [ "sourcekit-lsp" ]
[[grammar]]
name = "swift"
source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "77c6312c8438f4dbaa0350cec92b3d6dd3d74a66" }
source = { git = "https://github.com/alex-pinkus/tree-sitter-swift", rev = "b1b66955d420d5cf5ff268ae552f0d6e43ff66e1" }
[[language]]
name = "erb"
@ -1808,7 +1856,7 @@ language-servers = [ "nu-lsp" ]
[[grammar]]
name = "nu"
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "98c11c491e3405c75affa1cf004097692da3dda2" }
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "358c4f509eb97f0148bbd25ad36acc729819b9c1" }
[[language]]
name = "vala"
@ -1889,7 +1937,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4
name = "meson"
scope = "source.meson"
injection-regex = "meson"
file-types = ["meson.build", "meson_options.txt"]
file-types = [{ glob = "meson.build" }, { glob = "meson_options.txt" }]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
@ -1900,7 +1948,7 @@ source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8
[[language]]
name = "sshclientconfig"
scope = "source.sshclientconfig"
file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }]
file-types = [{ glob = ".ssh/config" }, { glob = "/etc/ssh/ssh_config" }]
comment-token = "#"
[[grammar]]
@ -2021,7 +2069,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569
name = "starlark"
scope = "source.starlark"
injection-regex = "(starlark|bzl|bazel)"
file-types = ["bzl", "bazel", "BUILD", "star"]
file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUILD.*" }]
comment-token = "#"
indent = { tab-width = 4, unit = " " }
grammar = "python"
@ -2115,7 +2163,7 @@ language-servers = [ "slint-lsp" ]
[[grammar]]
name = "slint"
source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "00c8a2d3645766f68c0d0460086c0a994e5b0d85" }
source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "15618215b79b9db08f824a5c97a12d073dcc1c00" }
[[language]]
name = "task"
@ -2258,7 +2306,7 @@ grammar = "vhs"
[[grammar]]
name = "vhs"
source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d81f34c011c29ee86dd73b45a8ecc9f2e2bdaf" }
source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "9534865e614c95eb9418e5e73f061c32fa4d9540" }
[[language]]
name = "kdl"
@ -2389,7 +2437,7 @@ source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9a
[[language]]
name = "env"
scope = "source.env"
file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc", ".envrc.local", ".envrc.private"]
file-types = [{ glob = ".env" }, { glob = ".env.*" }, { glob = ".envrc" }, { glob = ".envrc.*" }]
injection-regex = "env"
comment-token = "#"
indent = { tab-width = 4, unit = "\t" }
@ -2417,7 +2465,7 @@ file-types = [
"volume",
"kube",
"network",
".editorconfig",
{ glob = ".editorconfig" },
"properties",
"cfg",
"directory"
@ -2545,7 +2593,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-pem", rev = "be67a433
[[language]]
name = "passwd"
scope = "source.passwd"
file-types = ["passwd"]
file-types = [{ glob = "passwd" }]
[[grammar]]
name = "passwd"
@ -2554,7 +2602,7 @@ source = { git = "https://github.com/ath3/tree-sitter-passwd", rev = "20239395ea
[[language]]
name = "hosts"
scope = "source.hosts"
file-types = ["hosts"]
file-types = [{ glob = "hosts" }]
comment-token = "#"
[[grammar]]
@ -2665,7 +2713,7 @@ language-servers = [ "cs" ]
[[grammar]]
name = "smithy"
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" }
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "8327eb84d55639ffbe08c9dc82da7fff72a1ad07" }
[[language]]
name = "vhdl"
@ -2762,7 +2810,7 @@ source = { git = "https://github.com/lefp/tree-sitter-opencl", rev = "8e1d24a570
[[language]]
name = "just"
scope = "source.just"
file-types = ["justfile", "Justfile", ".justfile", ".Justfile"]
file-types = [{ glob = "justfile" }, { glob = "Justfile" }, { glob = ".justfile" }, { glob = ".Justfile" }]
injection-regex = "just"
comment-token = "#"
indent = { tab-width = 4, unit = "\t" }
@ -2864,7 +2912,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "typst"
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "e35aa22395fdde82bbc4b5700c324ce346dfc9e5" }
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" }
[[language]]
name = "nunjucks"
@ -2916,12 +2964,12 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "unison"
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "98c4e8bc5c9f5989814a720457cf36963cf4043d" }
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505e2447fa876a87aee47ff3d70b9e141c744f" }
[[language]]
name = "todotxt"
scope = "text.todotxt"
file-types = [{ suffix = ".todo.txt" }, "todotxt"]
file-types = [{ glob = "todo.txt" }, { glob = "*.todo.txt" }, "todotxt"]
formatter = { command = "sort" }
auto-format = true
@ -3007,15 +3055,6 @@ file-types = ["log"]
name = "log"
source = { git = "https://github.com/Tudyx/tree-sitter-log", rev = "62cfe307e942af3417171243b599cc7deac5eab9" }
[[language]]
name = "janet"
scope = "source.janet"
injection-regex = "janet"
file-types = ["janet"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
grammar = "clojure"
[[language]]
name = "hocon"
scope = "source.conf"
@ -3027,3 +3066,33 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "hocon"
source = { git = "https://github.com/antosha417/tree-sitter-hocon", rev = "c390f10519ae69fdb03b3e5764f5592fb6924bcc" }
[[language]]
name = "tact"
scope = "source.tact"
injection-regex = "tact"
file-types = ["tact"]
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[language.auto-pairs]
'"' = '"'
'{' = '}'
'(' = ')'
'<' = '>'
[[grammar]]
name = "tact"
source = { git = "https://github.com/tact-lang/tree-sitter-tact", rev = "ec57ab29c86d632639726631fb2bb178d23e1c91" }
[[language]]
name = "pkl"
scope = "source.pkl"
injection-regex = "pkl"
file-types = ["pkl", "pcf"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "pkl"
source = { git = "https://github.com/apple/tree-sitter-pkl", rev = "c03f04a313b712f8ab00a2d862c10b37318699ae" }

@ -10,23 +10,37 @@
(variable_name) @variable.other.member
[
"if"
"then"
"else"
"elif"
"fi"
"case"
"in"
"esac"
] @keyword.control.conditional
[
"for"
"do"
"done"
"elif"
"else"
"esac"
"select"
"until"
"while"
] @keyword.control.repeat
[
"declare"
"typeset"
"export"
"fi"
"for"
"function"
"if"
"in"
"readonly"
"local"
"unset"
"while"
"then"
"unsetenv"
] @keyword
"function" @keyword.function
(comment) @comment
(function_definition name: (word) @function)

@ -0,0 +1,66 @@
; Operators
[
"-"
"!"
"*"
"/"
"&&"
"%"
"+"
"<"
"<="
"=="
">"
">="
"||"
] @operator
; Keywords
[
"in"
] @keyword
; Function calls
(call_expression
function: (identifier) @function)
(member_call_expression
function: (identifier) @function)
; Identifiers
(select_expression
operand: (identifier) @type)
(select_expression
operand: (select_expression
member: (identifier) @type))
(identifier) @variable.other.member
; Literals
[
(double_quote_string_literal)
(single_quoted_string_literal)
(triple_double_quote_string_literal)
(triple_single_quoted_string_literal)
] @string
[
(int_literal)
(uint_literal)
] @constant.numeric.integer
(float_literal) @constant.numeric.float
[
(true)
(false)
] @constant.builtin.boolean
(null) @constant.builtin
(comment) @comment

@ -0,0 +1,7 @@
[
(block)
] @indent
[
"}"
] @outdent

@ -0,0 +1,68 @@
(class_definition
body: (_) @class.inside) @class.around
(mixin_declaration
(class_body) @class.inside) @class.around
(extension_declaration
(extension_body) @class.inside) @class.around
(enum_declaration
body: (_) @class.inside) @class.around
(type_alias) @class.around
(_
(
[
(getter_signature)
(setter_signature)
(function_signature)
(method_signature)
(constructor_signature)
]
.
(function_body) @function.inside @function.around
) @function.around
)
(declaration
[
(constant_constructor_signature)
(constructor_signature)
(factory_constructor_signature)
(redirecting_factory_constructor_signature)
(getter_signature)
(setter_signature)
(operator_signature)
(function_signature)
]
) @function.around
(lambda_expression
body: (_) @function.inside
) @function.around
(function_expression
body: (_) @function.inside
) @function.around
[
(comment)
(documentation_comment)
] @comment.inside
(comment)+ @comment.around
(documentation_comment)+ @comment.around
(formal_parameter) @parameter.inside
(formal_parameter_list) @parameter.around
(expression_statement
((identifier) @_name (#any-of? @_name "test" "testWidgets"))
.
(selector (argument_part (arguments . (_) . (argument) @test.inside)))
) @test.around

@ -13,5 +13,7 @@
(typed_default_parameter)
] @parameter.inside @parameter.around)
(arguments (_expression) @parameter.inside @parameter.around)
(comment) @comment.inside
(comment)+ @comment.around

@ -183,9 +183,12 @@
[
(int_literal)
] @constant.numeric.integer
[
(float_literal)
(imaginary_literal)
] @constant.numeric.integer
] @constant.numeric.float
[
(true)
@ -197,4 +200,31 @@
(iota)
] @constant.builtin
; Comments
(comment) @comment
; Doc Comments
(source_file
.
(comment)+ @comment.block.documentation)
(source_file
(comment)+ @comment.block.documentation
.
(const_declaration))
(source_file
(comment)+ @comment.block.documentation
.
(function_declaration))
(source_file
(comment)+ @comment.block.documentation
.
(type_declaration))
(source_file
(comment)+ @comment.block.documentation
.
(var_declaration))

@ -1,2 +1,14 @@
((comment) @injection.content
(#set! injection.language "comment"))
(call_expression
(selector_expression) @_function
(#any-of? @_function "regexp.Match" "regexp.MatchReader" "regexp.MatchString" "regexp.Compile" "regexp.CompilePOSIX" "regexp.MustCompile" "regexp.MustCompilePOSIX")
(argument_list
.
[
(raw_string_literal)
(interpreted_string_literal)
] @injection.content
(#set! injection.language "regex")))

File diff suppressed because one or more lines are too long

@ -0,0 +1,8 @@
[
(define_directive)
(rule)
] @indent
[
"endef"
] @outdent

@ -2,7 +2,6 @@
;;; keywords
[
"def"
"def-env"
"alias"
"export-env"
"export"
@ -73,7 +72,6 @@
"tb" "tB" "Tb" "TB"
"pb" "pB" "Pb" "PB"
"eb" "eB" "Eb" "EB"
"zb" "zB" "Zb" "ZB"
"kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
"mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
@ -81,7 +79,6 @@
"tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
"pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
"eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
"zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB"
] @variable.parameter
)
(val_binary

@ -0,0 +1,179 @@
; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; https://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
; this definition is imprecise in that
; * any qualified or unqualified call to a method named "Regex" is considered a regex
; * string delimiters are considered part of the regex
; Operators
[
"??"
"@"
"="
"<"
">"
"!"
"=="
"!="
"<="
">="
"&&"
"||"
"+"
"-"
"**"
"*"
"/"
"~/"
"%"
"|>"
] @keyword.operator
[
"?"
"|"
"->"
] @operator.type
[
","
":"
"."
"?."
] @punctuation.delimiter
[
"("
")"
"]"
"{"
"}"
; "[" @punctuation.bracket TODO: FIGURE OUT HOW TO REFER TO CUSTOM TOKENS
] @punctuation.bracket
; Keywords
[
"abstract"
"amends"
"as"
"class"
"extends"
"external"
"function"
"hidden"
"import"
"import*"
"in"
"let"
"local"
"module"
"new"
"open"
"out"
"typealias"
"when"
] @keyword
[
"if"
"is"
"else"
] @keyword.control.conditional
[
"for"
] @keyword.control.repeat
(importExpr "import" @keyword.control.import)
(importGlobExpr "import*" @keyword.control.import)
"read" @function.builtin
"read?" @function.builtin
"read*" @function.builtin
"throw" @function.builtin
"trace" @function.builtin
(moduleExpr "module" @type.builtin)
"nothing" @type.builtin
"unknown" @type.builtin
(outerExpr) @variable.builtin
"super" @variable.builtin
(thisExpr) @variable.builtin
[
(falseLiteral)
(nullLiteral)
(trueLiteral)
] @constant.builtin
; Literals
(stringConstant) @string
(slStringLiteral) @string
(mlStringLiteral) @string
(escapeSequence) @constent.character.escape
(intLiteral) @constant.numeric.integer
(floatLiteral) @constant.numeric.float
(interpolationExpr
"\\(" @punctuation.special
")" @punctuation.special) @embedded
(interpolationExpr
"\\#(" @punctuation.special
")" @punctuation.special) @embedded
(interpolationExpr
"\\##(" @punctuation.special
")" @punctuation.special) @embedded
(lineComment) @comment
(blockComment) @comment
(docComment) @comment
; Identifiers
(classProperty (identifier) @variable.other.member)
(objectProperty (identifier) @variable.other.member)
(parameterList (typedIdentifier (identifier) @variable.parameter))
(objectBodyParameters (typedIdentifier (identifier) @variable.parameter))
(identifier) @variable
; Method definitions
(classMethod (methodHeader (identifier)) @function.method)
(objectMethod (methodHeader (identifier)) @function.method)
; Method calls
(methodCallExpr
(identifier) @function.method)
; Types
(clazz (identifier) @type)
(typeAlias (identifier) @type)
((identifier) @type
(match? @type "^[A-Z]"))
(typeArgumentList
"<" @punctuation.bracket
">" @punctuation.bracket)

@ -0,0 +1,23 @@
; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; https://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
; this definition is imprecise in that
; * any qualified or unqualified call to a method named "Regex" is considered a regex
; * string delimiters are considered part of the regex
[
(objectBody)
(classBody)
(ifExpr)
(mlStringLiteral) ; This isn't perfect; newlines are too indented but it's better than if omitted.
] @indent

@ -0,0 +1,30 @@
; Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; https://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
; this definition is imprecise in that
; * any qualified or unqualified call to a method named "Regex" is considered a regex
; * string delimiters are considered part of the regex
(
((methodCallExpr (identifier) @methodName (argumentList (slStringLiteral) @injection.content))
(#set! injection.language "regex"))
(eq? @methodName "Regex"))
((lineComment) @injection.content
(#set! injection.language "comment"))
((blockComment) @injection.content
(#set! injection.language "comment"))
((docComment) @injection.content
(#set! injection.language "markdown"))

@ -1,61 +1,97 @@
[(pod_directive)
(head_directive)
(over_directive)
(item_directive)
(back_directive)
(encoding_directive)
(cut_directive)] @tag
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head1")
; A highlight file for nvim-treesitter to use
[(pod_command)
(command)
(cut_command)] @keyword
(command_paragraph
(command) @keyword
(#eq? @keyword "=head1")
(content) @markup.heading.1)
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head2")
(command_paragraph
(command) @keyword
(#eq? @keyword "=head2")
(content) @markup.heading.2)
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head3")
(command_paragraph
(command) @keyword
(#eq? @keyword "=head3")
(content) @markup.heading.3)
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head4")
(command_paragraph
(command) @keyword
(#eq? @keyword "=head4")
(content) @markup.heading.4)
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head5")
(command_paragraph
(command) @keyword
(#eq? @keyword "=head5")
(content) @markup.heading.5)
(head_paragraph
(head_directive) @directive
(#eq? @directive "=head6")
(command_paragraph
(command) @keyword
(#eq? @keyword "=head6")
(content) @markup.heading.6)
(over_paragraph (content) @constant.numeric.integer)
(item_paragraph (content) @markup.list)
(encoding_paragraph (content) @string)
(command_paragraph
(command) @keyword
(#match? @keyword "^=over")
(content) @constant.numeric)
(command_paragraph
(command) @keyword
(#match? @keyword "^=item")
(content) @markup)
(command_paragraph
(command) @keyword
(#match? @keyword "^=encoding")
(content) @string.special)
(command_paragraph
(command) @keyword
(#not-match? @keyword "^=(head|over|item|encoding)")
(content) @string)
(verbatim_paragraph (content) @markup.raw)
(interior_sequence) @tag
(interior_sequence
(sequence_letter) @constant.character
["<" ">"] @punctuation.delimiter
)
(interior_sequence
(sequence_letter) @letter
(#eq? @letter "B")
(sequence_letter) @character
(#eq? @character "B")
(content) @markup.bold)
(interior_sequence
(sequence_letter) @letter
(#eq? @letter "C")
(content) @markup.raw)
(sequence_letter) @character
(#eq? @character "C")
(content) @markup.literal)
(interior_sequence
(sequence_letter) @letter
(#eq? @letter "F")
(content) @markup.italic)
(sequence_letter) @character
(#eq? @character "F")
(content) @markup.underline @string.special)
(interior_sequence
(sequence_letter) @letter
(#eq? @letter "I")
(content) @markup.italic)
(sequence_letter) @character
(#eq? @character "I")
(content) @markup.bold)
(interior_sequence
(sequence_letter) @letter
(#eq? @letter "L")
(sequence_letter) @character
(#eq? @character "L")
(content) @markup.link.url)
(interior_sequence
(sequence_letter) @character
(#eq? @character "X")
(content) @markup.reference)
(interior_sequence
(sequence_letter) @character
(#eq? @character "E")
(content) @string.special.escape)

@ -2,11 +2,29 @@
(#set! injection.language "comment"))
((macro_invocation
macro: (identifier) @_html (#eq? @_html "html")
macro:
[
(scoped_identifier
name: (_) @_macro_name)
(identifier) @_macro_name
]
(token_tree) @injection.content)
(#eq? @_macro_name "html")
(#set! injection.language "html")
(#set! injection.include-children))
((macro_invocation
macro:
[
(scoped_identifier
name: (_) @_macro_name)
(identifier) @_macro_name
]
(token_tree) @injection.content)
(#eq? @_macro_name "slint")
(#set! injection.language "slint")
(#set! injection.include-children))
((macro_invocation
(token_tree) @injection.content)
(#set! injection.language "rust")

@ -53,20 +53,13 @@
(var_declaration
name: (identifier) @variable)
; method definition
; function definitions/declarations
(class_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
(object_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
(trait_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
(function_declaration
name: (identifier) @function.method)
(function_definition
name: (identifier) @function.method)
; imports/exports
@ -263,7 +256,7 @@
"return" @keyword.control.return
(comment) @comment
[(comment) (block_comment)] @comment
;; `case` is a conditional keyword in case_block

@ -1,2 +1,16 @@
((comment) @injection.content
([(comment) (block_comment)] @injection.content
(#set! injection.language "comment"))
; TODO for some reason multiline string (triple quotes) interpolation works only if it contains interpolated value
; Matches these SQL interpolators:
; - Doobie: 'sql', 'fr'
; - Quill: 'sql', 'infix'
; - Slick: 'sql', 'sqlu'
(interpolated_string_expression
interpolator:
((identifier) @interpolator
(#any-of? @interpolator "fr" "infix" "sql" "sqlu"))
(interpolated_string) @injection.content
(#set! injection.language "sql"))

@ -1,12 +1,15 @@
; Function queries
(function_definition
body: (_) @function.inside) @function.around
body: (_) @function.inside) @function.around ; Does not include end marker
; Does not match block lambdas or Scala 3 braceless lambdas
(lambda_expression
(_) @function.inside) @function.around
; Scala 3 braceless lambda
(colon_argument
(_) @function.inside) @function.around
; Class queries
@ -32,6 +35,9 @@
(parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(class_parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(parameter_types
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
@ -51,8 +57,8 @@
; Comment queries
(comment) @comment.inside
(comment) @comment.around ; Does not match consecutive block comments
[(comment) (block_comment)] @comment.inside
[(comment) (block_comment)] @comment.around ; Does not match consecutive block comments
; Test queries

@ -1,122 +1,109 @@
(comment) @comment
; Different types:
(string_value) @string
(bool_value) @constant.builtin.boolean
; Constants
(escape_sequence) @constant.character.escape
(color_value) @constant
(identifier) @variable
[
(type_identifier)
(units)
]@type
(children_identifier)
(easing_kind_identifier)
] @constant.builtin
(array_literal
(identifier) @type)
[
(int_value)
(physical_length_value)
] @constant.numeric.integer
(function_identifier) @function
[
(image_macro)
(children_macro)
(radial_grad_macro)
(linear_grad_macro)
] @function.macro
(float_value)
(percent_value)
(length_value)
(duration_value)
(angle_value)
(relative_font_size_value)
] @constant.numeric.float
(call_expression
function: (identifier) @function)
(call_expression
function: (field_expression
field: (identifier) @function))
(purity) @keyword.storage.modifier
(vis) @keyword.control.import
(function_visibility) @keyword.storage.modifier
(transition_statement state: (identifier) @variable.other.member)
(state_expression state: (identifier) @variable.other.member)
(struct_block_definition field: (identifier) @variable.other.member)
(assign_property (identifier) @attribute)
(property_visibility) @keyword.storage.modifier
(comment) @comment
(builtin_type_identifier) @type.builtin
(string_literal) @string
(int_literal) @constant.numeric.integer
(float_literal) @constant.numeric.float
(reference_identifier) @variable.builtin
[
"in"
"in-out"
"for"
] @keyword.control.repeat
(type
[
(type_list)
(user_type_identifier)
(anon_struct_block)
]) @type
[
"import"
"export"
"from"
] @keyword.control.import
(user_type_identifier) @type
[
"if"
"else"
"when"
] @keyword.control.conditional
; Functions and callbacks
(argument) @variable.parameter
[
"struct"
"property"
] @keyword.storage.type
(function_call
name: (_) @function.call)
[
"global"
] @keyword.storage.modifier
; definitions
(callback
name: (_) @function)
(callback_alias
name: (_) @function)
[
"root"
"parent"
"duration"
"easing"
] @variable.builtin
(callback_event
name: (simple_identifier) @function.call)
(enum_definition
name: (_) @type.enum)
[
"callback"
"animate"
"states"
"out"
"transitions"
"component"
"inherits"
] @keyword
(function_definition
name: (_) @function)
[
"black"
"transparent"
"blue"
"ease"
"ease_in"
"ease-in"
"ease_in_out"
"ease-in-out"
"ease_out"
"ease-out"
"end"
"green"
"red"
"start"
"yellow"
"white"
"gray"
] @constant.builtin
(struct_definition
name: (_) @type)
(typed_identifier
type: (_) @type)
; Operators
(binary_expression
op: (_) @operator)
(unary_expression
op: (_) @operator)
[
"true"
"false"
] @constant.builtin.boolean
(comparison_operator)
(mult_prec_operator)
(add_prec_operator)
(unary_prec_operator)
(assignment_prec_operator)
] @operator
"@" @keyword
[
":="
"=>"
"->"
"<=>"
] @operator
; ; Punctuation
[
","
"."
";"
":"
"."
","
] @punctuation.delimiter
; ; Brackets
[
"("
")"
@ -126,46 +113,136 @@
"}"
] @punctuation.bracket
(define_property ["<" ">"] @punctuation.bracket)
(property
[
"<"
">"
] @punctuation.bracket)
[
"angle"
"bool"
"brush"
"color"
"duration"
"easing"
"float"
"image"
"int"
"length"
"percent"
"physical-length"
"physical_length"
"string"
] @type.builtin
; Properties, constants and variables
(component
id: (simple_identifier) @constant)
(property
name: (simple_identifier) @variable)
(binding_alias
name: (simple_identifier) @variable)
(binding
name: (simple_identifier) @variable)
(struct_block
(simple_identifier) @variable.other.member)
(anon_struct_block
(simple_identifier) @variable.other.member)
(property_assignment
property: (simple_identifier) @variable)
(states_definition
name: (simple_identifier) @variable)
(callback
name: (simple_identifier) @variable)
(typed_identifier
name: (_) @variable)
(simple_indexed_identifier
(simple_identifier) @variable)
(expression
(simple_identifier) @variable)
; Attributes
[
":="
"<=>"
"!"
"-"
"+"
"*"
"/"
"&&"
"||"
">"
"<"
">="
"<="
"="
":"
"+="
"-="
"*="
"/="
"?"
"=>" ] @operator
(ternary_expression [":" "?"] @keyword.control.conditional)
(linear_gradient_identifier)
(radial_gradient_identifier)
(radial_gradient_kind)
] @attribute
(image_call
"@image-url" @attribute)
(tr
"@tr" @attribute)
; Keywords
(animate_option_identifier) @keyword
(export) @keyword.control.import
(if_statement
"if" @keyword.control.conditional)
(if_expr
[
"if"
"else"
] @keyword.control.conditional)
(ternary_expression
[
"?"
":"
] @keyword.control.conditional)
(animate_statement
"animate" @keyword)
(callback
"callback" @keyword.function)
(component_definition
[
"component"
"inherits"
] @keyword.storage.type)
(enum_definition
"enum" @keyword.storage.type)
(for_loop
[
"for"
"in"
] @keyword.control.repeat)
(function_definition
"function" @keyword.function)
(global_definition
"global" @keyword.storage.type)
(imperative_block
"return" @keyword.control.return)
(import_statement
[
"import"
"from"
] @keyword.control.import)
(import_type
"as" @keyword.control.import)
(property
"property" @keyword.storage.type)
(states_definition
[
"states"
"when"
] @keyword)
(struct_definition
"struct" @keyword.storage.type)
(transitions_definition
[
"transitions"
"in"
"out"
] @keyword)

@ -1,12 +1,11 @@
[
(comp_body)
(state_statement)
(transition_statement)
(handler_body)
(consequence_body)
(global_single)
(anon_struct_block)
(assignment_block)
(block)
(enum_block)
(global_block)
(imperative_block)
(struct_block)
] @indent
[
"}"
] @outdent
"}" @outdent

@ -1,3 +1,6 @@
; locals.scm
(component_item) @local.scope
[
(component)
(component_definition)
(function_definition)
(imperative_block)
] @local.scope

@ -0,0 +1,35 @@
(function_definition
(imperative_block) @funtion.inside) @function.around
(callback_event
(imperative_block) @function.inside) @function.around
(property
(imperative_block) @function.inside) @function.around
(struct_definition
(struct_block) @class.inside) @class.around
(enum_definition
(enum_block) @class.inside) @class.around
(global_definition
(global_block) @class.inside) @class.around
(component_definition
(block) @class.inside) @class.around
(component_definition
(block) @class.inside) @class.around
(comment) @comment.around
(typed_identifier
name: (_) @parameter.inside) @parameter.around
(callback
arguments: (_) @parameter.inside)
(string_value
"\"" . (_) @text.inside . "\"") @text.around

@ -0,0 +1,47 @@
; highlights.scm
[
"definition"
"caveat"
"permission"
"relation"
"nil"
] @keyword
[
","
":"
] @punctuation.delimiter
[
"("
")"
"{"
"}"
] @punctuation.bracket
[
"|"
"+"
"-"
"&"
"#"
"->"
"="
] @operator
("with") @keyword.operator
[
"nil"
"*"
] @constant.builtin
(comment) @comment
(type_identifier) @type
(cel_type_identifier) @type
(cel_variable_identifier) @variable.parameter
(field_identifier) @variable.other.member
[
(func_identifier)
(method_identifier)
] @function.method

@ -0,0 +1,5 @@
((comment) @injection.content
(#set! injection.language "comment"))
((caveat_expr) @injection.content
(#set! injection.language "cel"))

@ -0,0 +1,4 @@
(object_definition
name: (type_identifier) @name) @definition.type
(type_identifier) @name @reference.type

@ -1,10 +1,10 @@
; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/8d2fd80e3322df51e3f70952e60d57f5d4077eb8/queries/highlights.scm
; Upstream: https://github.com/alex-pinkus/tree-sitter-swift/blob/1c586339fb00014b23d6933f2cc32b588a226f3b/queries/highlights.scm
(line_string_literal
["\\(" ")"] @punctuation.special)
["." ";" ":" "," ] @punctuation.delimiter
["(" ")" "[" "]" "{" "}"] @punctuation.bracket ; TODO: "\\(" ")" in interpolations should be @punctuation.special
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
; Identifiers
(attribute) @variable
@ -26,6 +26,7 @@
(function_declaration "init" @constructor)
(throws) @keyword
"async" @keyword
"await" @keyword
(where_keyword) @keyword
(parameter external_name: (simple_identifier) @variable.parameter)
(parameter name: (simple_identifier) @variable.parameter)
@ -48,6 +49,7 @@
"convenience"
"required"
"some"
"any"
] @keyword
[

@ -0,0 +1,298 @@
; See: https://docs.helix-editor.com/master/themes.html#syntax-highlighting
; -------------------------------------------------------------------------
; attribute
; ---------
[
"@name"
"@interface"
] @attribute
; comment.line
; ------------
((comment) @comment.line
(#match? @comment.line "^//"))
; comment.block
; -------------
(comment) @comment.block
; function.builtin
; ----------------
((identifier) @function.builtin
(#any-of? @function.builtin
"send" "sender" "require" "now"
"myBalance" "myAddress" "newAddress"
"contractAddress" "contractAddressExt"
"emit" "cell" "ton"
"beginString" "beginComment" "beginTailString" "beginStringFromBuilder" "beginCell" "emptyCell"
"randomInt" "random"
"checkSignature" "checkDataSignature" "sha256"
"min" "max" "abs" "pow"
"throw" "dump" "getConfigParam"
"nativeThrowWhen" "nativeThrowUnless" "nativeReserve"
"nativeRandomize" "nativeRandomizeLt" "nativePrepareRandom" "nativeRandom" "nativeRandomInterval")
(#is-not? local))
; function.method
; ---------------
(method_call_expression
name: (identifier) @function.method)
; function
; --------
(func_identifier) @function
(native_function
name: (identifier) @function)
(static_function
name: (identifier) @function)
(static_call_expression
name: (identifier) @function)
(init_function
"init" @function.method)
(receive_function
"receive" @function.method)
(bounced_function
"bounced" @function.method)
(external_function
"external" @function.method)
(function
name: (identifier) @function.method)
; keyword.control.conditional
; ---------------------------
[
"if" "else"
] @keyword.control.conditional
; keyword.control.repeat
; ----------------------
[
"while" "repeat" "do" "until"
] @keyword.control.repeat
; keyword.control.import
; ----------------------
"import" @keyword.control.import
; keyword.control.return
; ----------------------
"return" @keyword.control.return
; keyword.operator
; ----------------
"initOf" @keyword.operator
; keyword.directive
; -----------------
"primitive" @keyword.directive
; keyword.function
; ----------------
[
"fun"
"native"
] @keyword.function
; keyword.storage.type
; --------------------
[
"contract" "trait" "struct" "message" "with"
"const" "let"
] @keyword.storage.type
; keyword.storage.modifier
; ------------------------
[
"get" "mutates" "extends" "virtual" "override" "inline" "abstract"
] @keyword.storage.modifier
; keyword
; -------
[
"with"
; "public" ; -- not used, but declared in grammar.ohm
; "extend" ; -- not used, but declared in grammar.ohm
] @keyword
; constant.builtin.boolean
; ------------------------
(boolean) @constant.builtin.boolean
; constant.builtin
; ----------------
((identifier) @constant.builtin
(#any-of? @constant.builtin
"SendPayGasSeparately"
"SendIgnoreErrors"
"SendDestroyIfZero"
"SendRemainingValue"
"SendRemainingBalance")
(#is-not? local))
(null) @constant.builtin
; constant.numeric.integer
; ------------------------
(integer) @constant.numeric.integer
; constant
; --------
(constant
name: (identifier) @constant)
; string.special.path
; -------------------
(import_statement
library: (string) @string.special.path)
; string
; ------
(string) @string
; type.builtin
; ------------
(tlb_serialization
"as" @keyword
type: (identifier) @type.builtin
(#any-of? @type.builtin
"int8" "int16" "int32" "int64" "int128" "int256" "int257"
"uint8" "uint16" "uint32" "uint64" "uint128" "uint256"
"coins" "remaining" "bytes32" "bytes64"))
((type_identifier) @type.builtin
(#any-of? @type.builtin
"Address" "Bool" "Builder" "Cell" "Int" "Slice" "String" "StringBuilder"))
(map_type
"map" @type.builtin
"<" @punctuation.bracket
">" @punctuation.bracket)
(bounced_type
"bounced" @type.builtin
"<" @punctuation.bracket
">" @punctuation.bracket)
((identifier) @type.builtin
(#eq? @type.builtin "SendParameters")
(#is-not? local))
; type
; ----
(type_identifier) @type
; constructor
; -----------
(instance_expression
name: (identifier) @constructor)
(initOf
name: (identifier) @constructor)
; operator
; --------
[
"-" "-="
"+" "+="
"*" "*="
"/" "/="
"%" "%="
"=" "=="
"!" "!=" "!!"
"<" "<=" "<<"
">" ">=" ">>"
"&" "|"
"&&" "||"
] @operator
; punctuation.bracket
; -------------------
[
"(" ")"
"{" "}"
] @punctuation.bracket
; punctuation.delimiter
; ---------------------
[
";"
","
"."
":"
"?"
] @punctuation.delimiter
; variable.other.member
; ---------------------
(field
name: (identifier) @variable.other.member)
(contract_body
(constant
name: (identifier) @variable.other.member))
(trait_body
(constant
name: (identifier) @variable.other.member))
(field_access_expression
name: (identifier) @variable.other.member)
(lvalue (_) (_) @variable.other.member)
(instance_argument
name: (identifier) @variable.other.member)
; variable.parameter
; ------------------
(parameter
name: (identifier) @variable.parameter)
; variable.builtin
; ----------------
(self) @variable.builtin
; variable
; --------
(identifier) @variable

@ -0,0 +1,38 @@
; indent
; ------
[
; (..., ...)
(parameter_list)
(argument_list)
; {..., ...}
(instance_argument_list)
; {...; ...}
(message_body)
(struct_body)
(contract_body)
(trait_body)
(function_body)
(block_statement)
; misc.
(binary_expression)
(return_statement)
] @indent
; outdent
; -------
[
"}"
")"
">"
] @outdent
; indent.always
; outdent.always
; align
; extend
; extend.prevent-once

@ -0,0 +1,5 @@
; See: https://docs.helix-editor.com/guides/injection.html
((comment) @injection.content
(#set! injection.language "comment")
(#match? @injection.content "^//"))

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save