Merge remote-tracking branch 'origin/master' into goto_next_reference

pull/6465/head
Anthony Templeton 1 year ago
commit 276ba37f0b

@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install nix - name: Install nix
uses: cachix/install-nix-action@v20 uses: cachix/install-nix-action@v21
- name: Authenticate with Cachix - name: Authenticate with Cachix
uses: cachix/cachix-action@v12 uses: cachix/cachix-action@v12

@ -1,3 +1,120 @@
# 23.05 (2023-05-18)
23.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
Features:
- Add a config option to exclude declaration from LSP references request ([#6886](https://github.com/helix-editor/helix/pull/6886))
- Enable injecting languages based on their file extension and shebang ([#3970](https://github.com/helix-editor/helix/pull/3970))
- Sort the buffer picker by most recent access ([#2980](https://github.com/helix-editor/helix/pull/2980))
- Perform syntax highlighting in the picker asynchronously ([#7028](https://github.com/helix-editor/helix/pull/7028))
Commands:
- `:update` is now aliased as `:u` ([#6835](https://github.com/helix-editor/helix/pull/6835))
- Add `extend_to_first_nonwhitespace` which acts the same as `goto_first_nonwhitespace` but always extends ([#6837](https://github.com/helix-editor/helix/pull/6837))
- Add `:clear-register` for clearing the given register or all registers ([#5695](https://github.com/helix-editor/helix/pull/5695))
- Add `:write-buffer-close` and `:write-buffer-close!` ([#6947](https://github.com/helix-editor/helix/pull/6947))
Fixes:
- Normalize LSP workspace paths ([#6517](https://github.com/helix-editor/helix/pull/6517))
- Robustly handle invalid LSP ranges ([#6512](https://github.com/helix-editor/helix/pull/6512))
- Fix line number display for LSP goto pickers ([#6559](https://github.com/helix-editor/helix/pull/6559))
- Fix toggling of `soft-wrap.enable` option ([#6656](https://github.com/helix-editor/helix/pull/6656), [58e457a](https://github.com/helix-editor/helix/commit/58e457a), [#6742](https://github.com/helix-editor/helix/pull/6742))
- Handle `workspace/configuration` requests from stopped language servers ([#6693](https://github.com/helix-editor/helix/pull/6693))
- Fix possible crash from opening the jumplist picker ([#6672](https://github.com/helix-editor/helix/pull/6672))
- Fix theme preview returning to current theme on line and word deletions ([#6694](https://github.com/helix-editor/helix/pull/6694))
- Re-run crate build scripts on changes to revision and grammar repositories ([#6743](https://github.com/helix-editor/helix/pull/6743))
- Fix crash on opening from suspended state ([#6764](https://github.com/helix-editor/helix/pull/6764))
- Fix unwrap bug in DAP ([#6786](https://github.com/helix-editor/helix/pull/6786))
- Always build tree-sitter parsers with C++14 and C11 ([#6792](https://github.com/helix-editor/helix/pull/6792), [#6834](https://github.com/helix-editor/helix/pull/6834), [#6845](https://github.com/helix-editor/helix/pull/6845))
- Exit with a non-zero statuscode when tree-sitter parser builds fail ([#6795](https://github.com/helix-editor/helix/pull/6795))
- Flip symbol range in LSP goto commands ([#6794](https://github.com/helix-editor/helix/pull/6794))
- Fix runtime toggling of the `mouse` option ([#6675](https://github.com/helix-editor/helix/pull/6675))
- Fix panic in inlay hint computation when view anchor is out of bounds ([#6883](https://github.com/helix-editor/helix/pull/6883))
- Significantly improve performance of git discovery on slow file systems ([#6890](https://github.com/helix-editor/helix/pull/6890))
- Downgrade gix log level to info ([#6915](https://github.com/helix-editor/helix/pull/6915))
- Conserve BOM and properly support saving UTF16 files ([#6497](https://github.com/helix-editor/helix/pull/6497))
- Correctly handle completion re-request ([#6594](https://github.com/helix-editor/helix/pull/6594))
- Fix offset encoding in LSP `didChange` notifications ([#6921](https://github.com/helix-editor/helix/pull/6921))
- Change `gix` logging level to info ([#6915](https://github.com/helix-editor/helix/pull/6915))
- Improve error message when writes fail because parent directories do not exist ([#7014](https://github.com/helix-editor/helix/pull/7014))
- Replace DAP variables popup instead of pushing more popups ([#7034](https://github.com/helix-editor/helix/pull/7034))
- Disable tree-sitter for files after parsing for 500ms ([#7028](https://github.com/helix-editor/helix/pull/7028))
- Fix crash when deleting with multiple cursors ([#6024](https://github.com/helix-editor/helix/pull/6024))
- Fix selection sliding when deleting forwards in append mode ([#6024](https://github.com/helix-editor/helix/pull/6024))
- Fix completion on paths containing spaces ([#6779](https://github.com/helix-editor/helix/pull/6779))
Themes:
- Style inlay hints in `dracula` theme ([#6515](https://github.com/helix-editor/helix/pull/6515))
- Style inlay hints in `onedark` theme ([#6503](https://github.com/helix-editor/helix/pull/6503))
- Style inlay hints and the soft-wrap indicator in `varua` ([#6568](https://github.com/helix-editor/helix/pull/6568), [#6589](https://github.com/helix-editor/helix/pull/6589))
- Style inlay hints in `emacs` theme ([#6569](https://github.com/helix-editor/helix/pull/6569))
- Update `base16_transparent` and `dark_high_contrast` themes ([#6577](https://github.com/helix-editor/helix/pull/6577))
- Style inlay hints for `mellow` and `rasmus` themes ([#6583](https://github.com/helix-editor/helix/pull/6583))
- Dim pane divider for `base16_transparent` theme ([#6534](https://github.com/helix-editor/helix/pull/6534))
- Style inlay hints in `zenburn` theme ([#6593](https://github.com/helix-editor/helix/pull/6593))
- Style inlay hints in `boo_berry` theme ([#6625](https://github.com/helix-editor/helix/pull/6625))
- Add `ferra` theme ([#6619](https://github.com/helix-editor/helix/pull/6619), [#6776](https://github.com/helix-editor/helix/pull/6776))
- Style inlay hints in `nightfox` theme ([#6655](https://github.com/helix-editor/helix/pull/6655))
- Fix `ayu` theme family markup code block background ([#6538](https://github.com/helix-editor/helix/pull/6538))
- Improve whitespace and search match colors in `rose_pine` theme ([#6679](https://github.com/helix-editor/helix/pull/6679))
- Highlight selected items in `base16_transparent` theme ([#6716](https://github.com/helix-editor/helix/pull/6716))
- Adjust everforest to resemble original more closely ([#5866](https://github.com/helix-editor/helix/pull/5866))
- Refactor `dracula` theme ([#6552](https://github.com/helix-editor/helix/pull/6552), [#6767](https://github.com/helix-editor/helix/pull/6767), [#6855](https://github.com/helix-editor/helix/pull/6855), [#6987](https://github.com/helix-editor/helix/pull/6987))
- Style inlay hints in `darcula` theme ([#6732](https://github.com/helix-editor/helix/pull/6732))
- Style inlay hints in `kanagawa` theme ([#6773](https://github.com/helix-editor/helix/pull/6773))
- Improve `ayu_dark` theme ([#6622](https://github.com/helix-editor/helix/pull/6622))
- Refactor `noctis` theme multiple cursor highlighting ([96720e7](https://github.com/helix-editor/helix/commit/96720e7))
- Refactor `noctis` theme whitespace rendering and indent guides ([f2ccc03](https://github.com/helix-editor/helix/commit/f2ccc03))
- Add `amberwood` theme ([#6924](https://github.com/helix-editor/helix/pull/6924))
- Update `nightfox` theme ([#7061](https://github.com/helix-editor/helix/pull/7061))
Language support:
- R language server: use the `--no-echo` flag to silence output ([#6570](https://github.com/helix-editor/helix/pull/6570))
- Recognize CUDA files as C++ ([#6521](https://github.com/helix-editor/helix/pull/6521))
- Add support for Hurl ([#6450](https://github.com/helix-editor/helix/pull/6450))
- Add textobject queries for Julia ([#6588](https://github.com/helix-editor/helix/pull/6588))
- Update Ruby highlight queries ([#6587](https://github.com/helix-editor/helix/pull/6587))
- Add xsd to XML file-types ([#6631](https://github.com/helix-editor/helix/pull/6631))
- Support Robot Framework ([#6611](https://github.com/helix-editor/helix/pull/6611))
- Update Gleam tree-sitter parser ([#6641](https://github.com/helix-editor/helix/pull/6641))
- Update git-commit tree-sitter parser ([#6692](https://github.com/helix-editor/helix/pull/6692))
- Update Haskell tree-sitter parser ([#6317](https://github.com/helix-editor/helix/pull/6317))
- Add injection queries for Haskell quasiquotes ([#6474](https://github.com/helix-editor/helix/pull/6474))
- Highlight C/C++ escape sequences ([#6724](https://github.com/helix-editor/helix/pull/6724))
- Support Markdoc ([#6432](https://github.com/helix-editor/helix/pull/6432))
- Support OpenCL ([#6473](https://github.com/helix-editor/helix/pull/6473))
- Support DTD ([#6644](https://github.com/helix-editor/helix/pull/6644))
- Fix constant highlighting in Python queries ([#6751](https://github.com/helix-editor/helix/pull/6751))
- Support Just ([#6453](https://github.com/helix-editor/helix/pull/6453))
- Fix Go locals query for `var_spec` identifiers ([#6763](https://github.com/helix-editor/helix/pull/6763))
- Update Markdown tree-sitter parser ([#6785](https://github.com/helix-editor/helix/pull/6785))
- Fix Haskell workspace root for cabal projects ([#6828](https://github.com/helix-editor/helix/pull/6828))
- Avoid extra indentation in Go switches ([#6817](https://github.com/helix-editor/helix/pull/6817))
- Fix Go workspace roots ([#6884](https://github.com/helix-editor/helix/pull/6884))
- Set PerlNavigator as the default Perl language server ([#6860](https://github.com/helix-editor/helix/pull/6860))
- Highlight more sqlx macros in Rust ([#6793](https://github.com/helix-editor/helix/pull/6793))
- Switch Odin tree-sitter grammar ([#6766](https://github.com/helix-editor/helix/pull/6766))
- Recognize `poetry.lock` as TOML ([#6928](https://github.com/helix-editor/helix/pull/6928))
- Recognize Jupyter notebooks as JSON ([#6927](https://github.com/helix-editor/helix/pull/6927))
- Add language server configuration for Crystal ([#6948](https://github.com/helix-editor/helix/pull/6948))
- Add `build.gradle.kts` to Java and Scala roots ([#6970](https://github.com/helix-editor/helix/pull/6970))
- Recognize `sty` and `cls` files as latex ([#6986](https://github.com/helix-editor/helix/pull/6986))
- Update Dockerfile tree-sitter grammar ([#6895](https://github.com/helix-editor/helix/pull/6895))
- Add comment injections for Odin ([#7027](https://github.com/helix-editor/helix/pull/7027))
- Recognize `gml` as XML ([#7055](https://github.com/helix-editor/helix/pull/7055))
- Recognize `geojson` as JSON ([#7054](https://github.com/helix-editor/helix/pull/7054))
Packaging:
- Update the Nix flake dependencies, remove a deprecated option ([#6546](https://github.com/helix-editor/helix/pull/6546))
- Fix and re-enable aarch64-macos release binary builds ([#6504](https://github.com/helix-editor/helix/pull/6504))
- The git dependency on `tree-sitter` has been replaced with a regular crates.io dependency ([#6608](https://github.com/helix-editor/helix/pull/6608))
# 23.03 (2023-03-31) # 23.03 (2023-03-31)
23.03 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors. 23.03 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors.

421
Cargo.lock generated

@ -49,6 +49,18 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f263788a35611fba42eb41ff811c5d0360c58b97402570312a350736e2542e"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -60,9 +72,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.70" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
@ -84,9 +96,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.2.1" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
[[package]] [[package]]
name = "bstr" name = "bstr"
@ -158,12 +170,12 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.24" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"num-integer",
"num-traits", "num-traits",
"winapi", "winapi",
] ]
@ -289,47 +301,6 @@ dependencies = [
"syn 2.0.15", "syn 2.0.15",
] ]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.4" version = "1.0.4"
@ -393,12 +364,13 @@ dependencies = [
[[package]] [[package]]
name = "etcetera" name = "etcetera"
version = "0.7.1" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51822eedc6129d8c4d96cec86d56b785e983f943c9ce9fb892e0c2a99a7f47a0" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"home", "home",
"windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -449,9 +421,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
] ]
@ -514,9 +486,9 @@ dependencies = [
[[package]] [[package]]
name = "gix" name = "gix"
version = "0.43.1" version = "0.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c256ea71cc1967faaefdaad15f334146b7c806f12460dcafd3afed845c8c78dd" checksum = "6bf41b61f7df395284f7a579c0fa1a7e012c5aede655174d4e91299ef1cac643"
dependencies = [ dependencies = [
"gix-actor", "gix-actor",
"gix-attributes", "gix-attributes",
@ -526,9 +498,11 @@ dependencies = [
"gix-diff", "gix-diff",
"gix-discover", "gix-discover",
"gix-features", "gix-features",
"gix-fs",
"gix-glob", "gix-glob",
"gix-hash", "gix-hash",
"gix-hashtable", "gix-hashtable",
"gix-ignore",
"gix-index", "gix-index",
"gix-lock", "gix-lock",
"gix-mailmap", "gix-mailmap",
@ -544,6 +518,7 @@ dependencies = [
"gix-tempfile", "gix-tempfile",
"gix-traverse", "gix-traverse",
"gix-url", "gix-url",
"gix-utils",
"gix-validate", "gix-validate",
"gix-worktree", "gix-worktree",
"log", "log",
@ -556,9 +531,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-actor" name = "gix-actor"
version = "0.19.0" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc22b0cdc52237667c301dd7cdc6ead8f8f73c9f824e9942c8ebd6b764f6c0bf" checksum = "848efa0f1210cea8638f95691c82a46f98a74b9e3524f01d4955ebc25a8f84f3"
dependencies = [ dependencies = [
"bstr", "bstr",
"btoi", "btoi",
@ -570,24 +545,26 @@ dependencies = [
[[package]] [[package]]
name = "gix-attributes" name = "gix-attributes"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2231a25934a240d0a4b6f4478401c73ee81d8be52de0293eedbc172334abf3e1" checksum = "3015baa01ad2122fbcaab7863c857a603eb7b7ec12ac8141207c42c6439805e2"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-features",
"gix-glob", "gix-glob",
"gix-path", "gix-path",
"gix-quote", "gix-quote",
"kstring",
"log",
"smallvec",
"thiserror", "thiserror",
"unicode-bom", "unicode-bom",
] ]
[[package]] [[package]]
name = "gix-bitmap" name = "gix-bitmap"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "024bca0c7187517bda5ea24ab148c9ca8208dd0c3e2bea88cdb2008f91791a6d" checksum = "55a95f4942360766c3880bdb2b4b57f1ef73b190fc424755e7fdf480430af618"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
@ -612,9 +589,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-config" name = "gix-config"
version = "0.20.1" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbad5ce54a8fc997acc50febd89ec80fa6e97cb7f8d0654cb229936407489d8" checksum = "1d252a0eddb6df74600d3d8872dc9fe98835a7da43110411d705b682f49d4ac1"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-config-value", "gix-config-value",
@ -634,11 +611,11 @@ dependencies = [
[[package]] [[package]]
name = "gix-config-value" name = "gix-config-value"
version = "0.10.2" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d09154c0c8677e4da0ec35e896f56ee3e338e741b9599fae06075edd83a4081c" checksum = "786861e84a5793ad5f863d846de5eb064cd23b87e61ad708c8c402608202e7be"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"bstr", "bstr",
"gix-path", "gix-path",
"libc", "libc",
@ -647,9 +624,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-credentials" name = "gix-credentials"
version = "0.12.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "750b684197374518ea057e0a0594713e07683faa0a3f43c0f93d97f64130ad8d" checksum = "4874a4fc11ffa844a3c2b87a66957bda30a73b577ef1acf15ac34df5745de5ff"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-command", "gix-command",
@ -663,9 +640,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-date" name = "gix-date"
version = "0.4.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96271912ce39822501616f177dea7218784e6c63be90d5f36322ff3a722aae2" checksum = "99056f37270715f5c7584fd8b46899a2296af9cae92463bf58b8bd1f5a78e553"
dependencies = [ dependencies = [
"bstr", "bstr",
"itoa", "itoa",
@ -675,9 +652,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-diff" name = "gix-diff"
version = "0.28.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "103a0fa79b0d438f5ecb662502f052e530ace4fe1fe8e1c83c0c6da76d728e67" checksum = "644a0f2768bc42d7a69289ada80c9e15c589caefc6a315d2307202df83ed1186"
dependencies = [ dependencies = [
"gix-hash", "gix-hash",
"gix-object", "gix-object",
@ -687,9 +664,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-discover" name = "gix-discover"
version = "0.16.2" version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eba8ba458cb8f4a6c33409b0fe650b1258655175a7ffd1d24fafd3ed31d880b" checksum = "1a6b61363e63e7cdaa3e6f96acb0257ebdb3d8883e21eba5930c99f07f0a5fc0"
dependencies = [ dependencies = [
"bstr", "bstr",
"dunce", "dunce",
@ -702,9 +679,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-features" name = "gix-features"
version = "0.28.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b76f9a80f6dd7be66442ae86e1f534effad9546676a392acc95e269d0c21c22" checksum = "cf69b0f5c701cc3ae22d3204b671907668f6437ca88862d355eaf9bc47a4f897"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"flate2", "flate2",
@ -717,21 +694,32 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "gix-fs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b37a1832f691fdc09910bd267f9a2e413737c1f9ec68c6e31f9e802616278a9"
dependencies = [
"gix-features",
]
[[package]] [[package]]
name = "gix-glob" name = "gix-glob"
version = "0.5.5" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e43efd776bc543f46f0fd0ca3d920c37af71a764a16f2aebd89765e9ff2993" checksum = "c07c98204529ac3f24b34754540a852593d2a4c7349008df389240266627a72a"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"bstr", "bstr",
"gix-features",
"gix-path",
] ]
[[package]] [[package]]
name = "gix-hash" name = "gix-hash"
version = "0.10.4" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a258595457bc192d1f1c59d0d168a1e34e2be9b97a614e14995416185de41a7" checksum = "078eec3ac2808cc03f0bddd2704cb661da5c5dc33b41a9d7947b141d499c7c42"
dependencies = [ dependencies = [
"hex", "hex",
"thiserror", "thiserror",
@ -739,22 +727,34 @@ dependencies = [
[[package]] [[package]]
name = "gix-hashtable" name = "gix-hashtable"
version = "0.1.3" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e55e40dfd694884f0eb78796c5bddcf2f8b295dace47039099dd7e76534973" checksum = "afebb85691c6a085b114e01a27f4a61364519298c5826cb87a45c304802299bc"
dependencies = [ dependencies = [
"gix-hash", "gix-hash",
"hashbrown 0.13.2", "hashbrown 0.13.2",
"parking_lot", "parking_lot",
] ]
[[package]]
name = "gix-ignore"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba205b6df563e2906768bb22834c82eb46c5fdfcd86ba2c347270bc8309a05b2"
dependencies = [
"bstr",
"gix-glob",
"gix-path",
"unicode-bom",
]
[[package]] [[package]]
name = "gix-index" name = "gix-index"
version = "0.15.1" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "717ab601ece7921f59fe86849dbe27d44a46ebb883b5885732c4f30df4996177" checksum = "fa282756760f79c401d4f4f42588fbb4aa27bbb4b0830f3b4d3480c21a4ac5a7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"bstr", "bstr",
"btoi", "btoi",
"filetime", "filetime",
@ -783,9 +783,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-mailmap" name = "gix-mailmap"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b66aea5e52875cd4915f4957a6f4b75831a36981e2ec3f5fad9e370e444fe1a" checksum = "e8856cec3bdc3610c06970d28b6cb20a0c6621621cf9a8ec48cbd23f2630f362"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-actor", "gix-actor",
@ -794,9 +794,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-object" name = "gix-object"
version = "0.28.0" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df068db9180ee935fbb70504848369e270bdcb576b05c0faa8b9fd3b86fc017" checksum = "c9bb30ce0818d37096daa29efe361a4bc6dd0b51a5726598898be7e9a40a01e1"
dependencies = [ dependencies = [
"bstr", "bstr",
"btoi", "btoi",
@ -813,9 +813,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-odb" name = "gix-odb"
version = "0.43.1" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83af2e3e36005bfe010927f0dff41fb5acc3e3d89c6f1174135b3a34086bda2" checksum = "bca2f324aa67672b6d0f2c0fa93f96eb6a7029d260e4c1df5dce3c015f5e5add"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"gix-features", "gix-features",
@ -831,9 +831,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-pack" name = "gix-pack"
version = "0.33.2" version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9401911c7fe032ad7b31c6a6b5be59cb283d1d6c999417a8215056efe6d635f3" checksum = "164a515900a83257ae4aa80e741655bee7a2e39113fb535d7a5ac623b445ff20"
dependencies = [ dependencies = [
"clru", "clru",
"gix-chunk", "gix-chunk",
@ -853,24 +853,26 @@ dependencies = [
[[package]] [[package]]
name = "gix-path" name = "gix-path"
version = "0.7.3" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32370dce200bb951df013e03dff35b4233fc7a89458642b047629b91734a7e19" checksum = "4fc78f47095a0c15aea0e66103838f0748f4494bf7a9555dfe0f00425400396c"
dependencies = [ dependencies = [
"bstr", "bstr",
"home",
"once_cell",
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "gix-prompt" name = "gix-prompt"
version = "0.3.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3034d4d935aef2c7bf719aaa54b88c520e82413118d886ae880a31d5bdee57" checksum = "330d11fdf88fff3366c2491efde2f3e454958efe7d5ddf60272e8fb1d944bb01"
dependencies = [ dependencies = [
"gix-command", "gix-command",
"gix-config-value", "gix-config-value",
"nix",
"parking_lot", "parking_lot",
"rustix",
"thiserror", "thiserror",
] ]
@ -887,12 +889,13 @@ dependencies = [
[[package]] [[package]]
name = "gix-ref" name = "gix-ref"
version = "0.27.2" version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e909396ed3b176823991ccc391c276ae2a015e54edaafa3566d35123cfac9d" checksum = "1e03989e9d49954368e1b526578230fc7189d1634acdfbe79e9ba1de717e15d5"
dependencies = [ dependencies = [
"gix-actor", "gix-actor",
"gix-features", "gix-features",
"gix-fs",
"gix-hash", "gix-hash",
"gix-lock", "gix-lock",
"gix-object", "gix-object",
@ -906,9 +909,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-refspec" name = "gix-refspec"
version = "0.9.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aba332462bda2e8efeae4302b39a6ed01ad56ef772fd5b7ef197cf2798294d65" checksum = "0a6ea733820df67e4cd7797deb12727905824d8f5b7c59d943c456d314475892"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-hash", "gix-hash",
@ -920,9 +923,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-revision" name = "gix-revision"
version = "0.12.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6f6ff53f888858afc24bf12628446a14279ceec148df6194481f306f553ad2" checksum = "810f35e9afeccca999d5d348b239f9c162353127d2e13ff3240e31b919e35476"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-date", "gix-date",
@ -934,15 +937,14 @@ dependencies = [
[[package]] [[package]]
name = "gix-sec" name = "gix-sec"
version = "0.6.2" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ffa5bf0772f9b01de501c035b6b084cf9b8bb07dec41e3afc6a17336a65f47" checksum = "794520043d5a024dfeac335c6e520cb616f6963e30dab995892382e998c12897"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"dirs",
"gix-path", "gix-path",
"libc", "libc",
"windows 0.43.0", "windows",
] ]
[[package]] [[package]]
@ -961,9 +963,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-traverse" name = "gix-traverse"
version = "0.24.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd9a4a07bb22168dc79c60e1a6a41919d198187ca83d8a5940ad8d7122a45df3" checksum = "a5be1e807f288c33bb005075111886cceb43ed8a167b3182a0f62c186e2a0dd1"
dependencies = [ dependencies = [
"gix-hash", "gix-hash",
"gix-hashtable", "gix-hashtable",
@ -973,9 +975,9 @@ dependencies = [
[[package]] [[package]]
name = "gix-url" name = "gix-url"
version = "0.16.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a22b4b32ad14d68f7b7fb6458fa58d44b01797d94c1b8f4db2d9c7b3c366b5" checksum = "dfc77f89054297cc81491e31f1bab4027e554b5ef742a44bd7035db9a0f78b76"
dependencies = [ dependencies = [
"bstr", "bstr",
"gix-features", "gix-features",
@ -985,6 +987,15 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "gix-utils"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c10b69beac219acb8df673187a1f07dde2d74092f974fb3f9eb385aeb667c909"
dependencies = [
"fastrand",
]
[[package]] [[package]]
name = "gix-validate" name = "gix-validate"
version = "0.7.4" version = "0.7.4"
@ -997,15 +1008,18 @@ dependencies = [
[[package]] [[package]]
name = "gix-worktree" name = "gix-worktree"
version = "0.15.2" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ec9a000b4f24af706c3cc680c7cda235656cbe3216336522f5692773b8a301" checksum = "10bf56a1f5037d84293ea6cece61d9f27c4866b1e13c1c95f37cf56b7da7af25"
dependencies = [ dependencies = [
"bstr", "bstr",
"filetime",
"gix-attributes", "gix-attributes",
"gix-features", "gix-features",
"gix-fs",
"gix-glob", "gix-glob",
"gix-hash", "gix-hash",
"gix-ignore",
"gix-index", "gix-index",
"gix-object", "gix-object",
"gix-path", "gix-path",
@ -1079,8 +1093,15 @@ name = "hashbrown"
version = "0.13.2" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
dependencies = [ dependencies = [
"ahash 0.8.3", "ahash 0.8.3",
"allocator-api2",
] ]
[[package]] [[package]]
@ -1089,12 +1110,12 @@ version = "0.6.0"
dependencies = [ dependencies = [
"ahash 0.8.3", "ahash 0.8.3",
"arc-swap", "arc-swap",
"bitflags 2.2.1", "bitflags 2.3.1",
"chrono", "chrono",
"dunce", "dunce",
"encoding_rs", "encoding_rs",
"etcetera", "etcetera",
"hashbrown 0.13.2", "hashbrown 0.14.0",
"helix-loader", "helix-loader",
"imara-diff", "imara-diff",
"indoc", "indoc",
@ -1216,7 +1237,7 @@ dependencies = [
name = "helix-tui" name = "helix-tui"
version = "0.6.0" version = "0.6.0"
dependencies = [ dependencies = [
"bitflags 2.2.1", "bitflags 2.3.1",
"cassowary", "cassowary",
"crossterm", "crossterm",
"helix-core", "helix-core",
@ -1249,7 +1270,7 @@ version = "0.6.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
"bitflags 2.2.1", "bitflags 2.3.1",
"chardetng", "chardetng",
"clipboard-win", "clipboard-win",
"crossterm", "crossterm",
@ -1315,7 +1336,7 @@ dependencies = [
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows 0.48.0", "windows",
] ]
[[package]] [[package]]
@ -1330,9 +1351,9 @@ dependencies = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [ dependencies = [
"unicode-bidi", "unicode-bidi",
"unicode-normalization", "unicode-normalization",
@ -1426,6 +1447,15 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1434,9 +1464,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.142" version = "0.2.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -1459,9 +1489,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.1" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -1475,12 +1505,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.17" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
@ -1537,18 +1564,6 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"static_assertions",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1559,16 +1574,6 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
@ -1599,9 +1604,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.1" version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
@ -1628,9 +1633,9 @@ dependencies = [
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
@ -1661,9 +1666,9 @@ checksum = "9516b775656bc3e8985e19cd4b8c0c0de045095074e453d2c0a513b5f978392d"
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.9.2" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"memchr", "memchr",
@ -1724,26 +1729,15 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
] ]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall 0.2.16",
"thiserror",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.8.1" version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [ dependencies = [
"aho-corasick 1.0.1", "aho-corasick 1.0.1",
"memchr", "memchr",
"regex-syntax 0.7.1", "regex-syntax 0.7.2",
] ]
[[package]] [[package]]
@ -1760,9 +1754,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]] [[package]]
name = "ropey" name = "ropey"
@ -1776,9 +1770,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.11" version = "0.37.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"errno", "errno",
@ -1817,18 +1811,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.160" version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.160" version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1859,9 +1853,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -2029,11 +2023,11 @@ dependencies = [
[[package]] [[package]]
name = "termini" name = "termini"
version = "0.1.4" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c0f7ecb9c2a380d2686a747e4fc574043712326e8d39fbd220ab3bd29768a12" checksum = "2ad441d87dd98bc5eeb31cf2fb7e4839968763006b478efb38668a3bf9da0d59"
dependencies = [ dependencies = [
"dirs-next", "home",
] ]
[[package]] [[package]]
@ -2132,9 +2126,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.27.0" version = "1.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -2146,14 +2140,14 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.45.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.0.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2162,9 +2156,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.12" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@ -2173,9 +2167,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.7.3" version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -2185,18 +2179,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.8" version = "0.19.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2232,9 +2226,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]] [[package]]
name = "unicode-bom" name = "unicode-bom"
version = "1.1.4" version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32" checksum = "98e90c70c9f0d4d1ee6d0a7d04aa06cb9bbd53d8cfbdd62a0269a7c2eb640552"
[[package]] [[package]]
name = "unicode-general-category" name = "unicode-general-category"
@ -2281,9 +2275,9 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@ -2409,21 +2403,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.48.0" version = "0.48.0"
@ -2567,9 +2546,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.4.1" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

@ -1 +1 @@
23.03 23.05

@ -16,3 +16,4 @@
- [Adding languages](./guides/adding_languages.md) - [Adding languages](./guides/adding_languages.md)
- [Adding textobject queries](./guides/textobject.md) - [Adding textobject queries](./guides/textobject.md)
- [Adding indent queries](./guides/indent.md) - [Adding indent queries](./guides/indent.md)
- [Adding injection queries](./guides/injection.md)

@ -128,6 +128,7 @@ The following statusline elements can be configured:
| `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` | | `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` | | `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path. [^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix.

@ -7,10 +7,11 @@
| beancount | ✓ | | | | | beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` | | bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` | | bicep | ✓ | | | `bicep-langserver` |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` | | c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` | | c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | | | cabal | | | | |
| cairo | ✓ | | | | | cairo | ✓ | | | `cairo-language-server` |
| capnp | ✓ | | ✓ | | | capnp | ✓ | | ✓ | |
| clojure | ✓ | | | `clojure-lsp` | | clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
@ -18,7 +19,7 @@
| common-lisp | ✓ | | | `cl-lsp` | | common-lisp | ✓ | | | `cl-lsp` |
| cpon | ✓ | | ✓ | | | cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` | | cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | | | crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` | | css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` | | cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` | | d | ✓ | ✓ | ✓ | `serve-d` |
@ -40,6 +41,7 @@
| erlang | ✓ | ✓ | | `erlang_ls` | | erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | | | esdl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | | | fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | |
| fortran | ✓ | | ✓ | `fortls` | | fortran | ✓ | | ✓ | `fortls` |
| gdscript | ✓ | ✓ | ✓ | | | gdscript | ✓ | ✓ | ✓ | |
| git-attributes | ✓ | | | | | git-attributes | ✓ | | | |
@ -98,14 +100,14 @@
| nu | ✓ | | | | | nu | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | | `ols` | | odin | ✓ | | | `ols` |
| opencl | ✓ | ✓ | ✓ | `clangd` | | opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` | | openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | | | org | ✓ | | | |
| pascal | ✓ | ✓ | | `pasls` | | pascal | ✓ | ✓ | | `pasls` |
| passwd | ✓ | | | | | passwd | ✓ | | | |
| pem | ✓ | | | | | pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | | | perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` | | php | ✓ | ✓ | ✓ | `intelephense` |
| po | ✓ | ✓ | | | | po | ✓ | ✓ | | |
| ponylang | ✓ | ✓ | ✓ | | | ponylang | ✓ | ✓ | ✓ | |

@ -12,7 +12,9 @@
| `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. | | `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. | | `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) | | `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write some/path.txt) | | `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt) |
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |
| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. | | `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. | | `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) | | `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
@ -48,8 +50,8 @@
| `:reload-all` | Discard changes and reload all documents from the source files. | | `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. | | `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker | | `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc | | `:lsp-restart` | Restarts the language servers used by the current doc |
| `:lsp-stop` | Stops the Language Server that is in use by the current doc | | `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
@ -78,3 +80,4 @@
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
| `:run-shell-command`, `:sh` | Run a shell command | | `:run-shell-command`, `:sh` | Run a shell command |
| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |

@ -9,6 +9,7 @@ below.
necessary configuration for the new language. For more information on necessary configuration for the new language. For more information on
language configuration, refer to the language configuration, refer to the
[language configuration section](../languages.md) of the documentation. [language configuration section](../languages.md) of the documentation.
A new language server can be added by extending the `[language-server]` table in the same file.
2. If you are adding a new language or updating an existing language server 2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation. [Language Support](../lang-support.md) documentation.

@ -0,0 +1,57 @@
# Adding Injection Queries
Writing language injection queries allows one to highlight a specific node as a different language.
In addition to the [standard](upstream-docs) language injection options used by tree-sitter, there
are a few Helix specific extensions that allow for more control.
And example of a simple query that would highlight all strings as bash in Nix:
```scm
((string_expression (string_fragment) @injection.content)
(#set! injection.language "bash"))
```
## Capture Types
- `@injection.language` (standard):
The captured node may contain the language name used to highlight the node captured by
`@injection.content`.
- `@injection.content` (standard):
Marks the content to be highlighted as the language captured with `@injection.language` _et al_.
- `@injection.filename` (extension):
The captured node may contain a filename with a file-extension known to Helix,
highlighting `@injection.content` as that language. This uses the language extensions defined in
both the default languages.toml distributed with Helix, as well as user defined languages.
- `@injection.shebang` (extension):
The captured node may contain a shebang used to choose a language to highlight as. This also uses
the shebangs defined in the default and user `languages.toml`.
## Settings
- `injection.combined` (standard):
Indicates that all the matching nodes in the tree should have their content parsed as one
nested document.
- `injection.language` (standard):
Forces the captured content to be highlighted as the given language
- `injection.include-children` (standard):
Indicates that the content nodes entire text should be re-parsed, including the text of its child
nodes. By default, child nodes text will be excluded from the injected document.
- `injection.include-unnamed-children` (extension):
Same as `injection.include-children` but only for unnamed child nodes.
## Predicates
- `#eq?` (standard):
The first argument (a capture) must be equal to the second argument
(a capture or a string).
- `#match?` (standard):
The first argument (a capture) must match the regex given in the
second argument (a string).
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

@ -8,6 +8,7 @@
- [Fedora/RHEL](#fedorarhel) - [Fedora/RHEL](#fedorarhel)
- [Arch Linux community](#arch-linux-community) - [Arch Linux community](#arch-linux-community)
- [NixOS](#nixos) - [NixOS](#nixos)
- [Flatpak](#flatpak)
- [AppImage](#appimage) - [AppImage](#appimage)
- [macOS](#macos) - [macOS](#macos)
- [Homebrew Core](#homebrew-core) - [Homebrew Core](#homebrew-core)
@ -18,6 +19,9 @@
- [MSYS2](#msys2) - [MSYS2](#msys2)
- [Building from source](#building-from-source) - [Building from source](#building-from-source)
- [Configuring Helix's runtime files](#configuring-helixs-runtime-files) - [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
- [Linux and macOS](#linux-and-macos)
- [Windows](#windows)
- [Multiple runtime directories](#multiple-runtime-directories)
- [Validating the installation](#validating-the-installation) - [Validating the installation](#validating-the-installation)
- [Configure the desktop shortcut](#configure-the-desktop-shortcut) - [Configure the desktop shortcut](#configure-the-desktop-shortcut)
<!--toc:end--> <!--toc:end-->
@ -78,7 +82,10 @@ in the AUR, which builds the master branch.
### NixOS ### NixOS
Helix is available as a [flake](https://nixos.wiki/wiki/Flakes) in the project Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
the unstable channel usually carries the latest release.
Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
root. Use `nix develop` to spin up a reproducible development shell. Outputs are root. Use `nix develop` to spin up a reproducible development shell. Outputs are
cached for each push to master using [Cachix](https://www.cachix.org/). The cached for each push to master using [Cachix](https://www.cachix.org/). The
flake is configured to automatically make use of this cache assuming the user flake is configured to automatically make use of this cache assuming the user
@ -88,6 +95,15 @@ If you are using a version of Nix without flakes enabled,
[install Cachix CLI](https://docs.cachix.org/installation) and use [install Cachix CLI](https://docs.cachix.org/installation) and use
`cachix use helix` to configure Nix to use cached outputs when possible. `cachix use helix` to configure Nix to use cached outputs when possible.
### Flatpak
Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
```sh
flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix
```
### AppImage ### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format. Install Helix using the Linux [AppImage](https://appimage.org/) format.
@ -188,9 +204,11 @@ HELIX_RUNTIME=/home/user-name/src/helix/runtime
Or, create a symlink in `~/.config/helix` that links to the source code directory: Or, create a symlink in `~/.config/helix` that links to the source code directory:
```sh ```sh
ln -s $PWD/runtime ~/.config/helix/runtime ln -Ts $PWD/runtime ~/.config/helix/runtime
``` ```
If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
#### Windows #### Windows
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for

@ -15,7 +15,7 @@
- [Popup](#popup) - [Popup](#popup)
- [Unimpaired](#unimpaired) - [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode) - [Insert mode](#insert-mode)
- [Select / extend mode](#select-extend-mode) - [Select / extend mode](#select--extend-mode)
- [Picker](#picker) - [Picker](#picker)
- [Prompt](#prompt) - [Prompt](#prompt)
@ -32,8 +32,8 @@
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` | | `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_line_down` | | `j`, `Down` | Move down | `move_visual_line_down` |
| `k`, `Up` | Move up | `move_line_up` | | `k`, `Up` | Move up | `move_visual_line_up` |
| `l`, `Right` | Move right | `move_char_right` | | `l`, `Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` | | `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` | | `b` | Move previous word start | `move_prev_word_start` |
@ -111,7 +111,8 @@
| `s` | Select all regex matches inside selections | `select_regex` | | `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into sub selections on regex matches | `split_selection` | | `S` | Split selection into sub selections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` | | `Alt-minus` | Merge selections | `merge_selections` |
| `Alt-_` | Merge consecutive selections | `merge_consecutive_selections` |
| `&` | Align selection in columns | `align_selections` | | `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` | | `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
@ -218,6 +219,8 @@ Jumps to various locations.
| `n` | Go to next buffer | `goto_next_buffer` | | `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` | | `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` |
#### Match mode #### Match mode

@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
```toml ```toml
# in <config_dir>/helix/languages.toml # in <config_dir>/helix/languages.toml
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]] [[language]]
name = "rust" name = "rust"
auto-format = false auto-format = false
@ -41,8 +44,8 @@ injection-regex = "mylang"
file-types = ["mylang", "myl"] file-types = ["mylang", "myl"]
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] } formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
``` ```
These configuration keys are available: These configuration keys are available:
@ -50,6 +53,7 @@ These configuration keys are available:
| Key | Description | | Key | Description |
| ---- | ----------- | | ---- | ----------- |
| `name` | The name of the language | | `name` | The name of the language |
| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages | | `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | | `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. | | `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@ -59,8 +63,7 @@ These configuration keys are available:
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) | | `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| `comment-token` | The token to use as a comment-token | | `comment-token` | The token to use as a comment-token |
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) | | `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
| `language-server` | The Language Server to run. See the Language Server configuration section below. | | `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
| `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
@ -92,31 +95,102 @@ with the following priorities:
replaced at runtime with the appropriate path separator for the operating replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows. system, so this rule would match against `.git\config` files on Windows.
### Language Server configuration ## Language Server configuration
Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
The `language-server` field takes the following keys: For example:
| Key | Description | ```toml
| --- | ----------- | [language-server.mylang-lsp]
| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` | command = "mylang-lsp"
| `args` | A list of arguments to pass to the language server binary | args = ["--stdio"]
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | config = { provideFormatter = true }
| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer | environment = { "ENV1" = "value1", "ENV2" = "value2" }
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
[language-server.efm-lsp-prettier]
command = "efm-langserver"
[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
The top-level `config` field is used to configure the LSP initialization options. A `format` These are the available options for a language server.
sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook). | 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" }` |
A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
For example with typescript: For example with typescript:
```toml ```toml
[[language]] [language-server.typescript-language-server]
name = "typescript"
auto-format = true
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix. # pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } } config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
``` ```
### Configuring Language Servers for a language
The `language-servers` attribute in a language tells helix which language servers are used for this language.
They have to be defined in the `[language-server]` table as described in the previous section.
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers.
For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default)
The language configuration for typescript could look like this:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```
or equivalent:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```
Each requested LSP feature is prioritized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
If no `except-features` or `only-features` is given all features for the language server are enabled.
If a language server itself doesn't support a feature the next language server array entry will be tried (and so on).
The list of supported features is:
- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`
## Tree-sitter grammar configuration ## Tree-sitter grammar configuration
The source for a language's tree-sitter grammar is specified in a `[[grammar]]` The source for a language's tree-sitter grammar is specified in a `[[grammar]]`

@ -296,7 +296,7 @@ These scopes are used for theming the editor interface:
| `ui.window` | Borderlines separating splits | | `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands | | `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. | | `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | | | `ui.text.focus` | The currently selected line in the picker |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) | | `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |

@ -96,13 +96,13 @@ function or block of code.
| `(`, `[`, `'`, etc. | Specified surround pairs | | `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair | | `m` | The closest surround pair |
| `f` | Function | | `f` | Function |
| `c` | Class | | `t` | Type (or Class) |
| `a` | Argument/parameter | | `a` | Argument/parameter |
| `o` | Comment | | `c` | Comment |
| `t` | Test | | `T` | Test |
| `g` | Change | | `g` | Change |
> 💡 `f`, `c`, etc. need a tree-sitter grammar active for the current > 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only document and a special tree-sitter query file to work properly. [Only
some grammars][lang-support] currently have the query file implemented. some grammars][lang-support] currently have the query file implemented.
Contributions are welcome! Contributions are welcome!
@ -112,7 +112,7 @@ Contributions are welcome!
Navigating between functions, classes, parameters, and other elements is Navigating between functions, classes, parameters, and other elements is
possible using tree-sitter and textobject queries. For possible using tree-sitter and textobject queries. For
example to move to the next function use `]f`, to move to previous example to move to the next function use `]f`, to move to previous
class use `[c`, and so on. type use `[t`, and so on.
![Tree-sitter-nav-demo][tree-sitter-nav-demo] ![Tree-sitter-nav-demo][tree-sitter-nav-demo]

@ -36,6 +36,9 @@
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="23.05" date="2023-05-18">
<url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
</release>
<release version="23.03" date="2023-03-31"> <release version="23.03" date="2023-03-31">
<url>https://helix-editor.com/news/release-23-03-highlights/</url> <url>https://helix-editor.com/news/release-23-03-highlights/</url>
</release> </release>

@ -3,15 +3,16 @@
"crane": { "crane": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1670900067, "lastModified": 1681175776,
"narHash": "sha256-VXVa+KBfukhmWizaiGiHRVX/fuk66P8dgSFfkVN4/MY=", "narHash": "sha256-7SsUy9114fryHAZ8p1L6G6YSu7jjz55FddEwa2U8XZc=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "59b31b41a589c0a65e4a1f86b0e5eac68081468b", "rev": "445a3d222947632b5593112bb817850e8a9cf737",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "ipetkov", "owner": "ipetkov",
"ref": "v0.12.1",
"repo": "crane", "repo": "crane",
"type": "github" "type": "github"
} }
@ -62,11 +63,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1680258209, "lastModified": 1683212002,
"narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=", "narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "dream2nix", "repo": "dream2nix",
"rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35", "rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -94,11 +95,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1680172861, "lastModified": 1680698112,
"narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=", "narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=",
"owner": "davhau", "owner": "davhau",
"repo": "drv-parts", "repo": "drv-parts",
"rev": "ced8a52f62b0a94244713df2225c05c85b416110", "rev": "e8c2ec1157dc1edb002989669a0dbd935f430201",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -124,12 +125,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1681202837,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -141,11 +145,11 @@
"mk-naked-shell": { "mk-naked-shell": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1676572903, "lastModified": 1681286841,
"narHash": "sha256-oQoDHHUTxNVSURfkFcYLuAK+btjs30T4rbEUtCUyKy8=", "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "mk-naked-shell", "repo": "mk-naked-shell",
"rev": "aeca9f8aa592f5e8f71f407d081cb26fd30c5a57", "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -167,11 +171,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1680329418, "lastModified": 1683699050,
"narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=", "narHash": "sha256-UWKQpzVcSshB+sU2O8CCHjOSTQrNS7Kk9V3+UeBsJpg=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540", "rev": "ed27173cd1b223f598343ea3c15aacb1d140feac",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -182,11 +186,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1680213900, "lastModified": 1683408522,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2", "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -199,11 +203,11 @@
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"dir": "lib", "dir": "lib",
"lastModified": 1678375444, "lastModified": 1682879489,
"narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=", "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e", "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -237,11 +241,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1679737941, "lastModified": 1683560683,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -255,11 +259,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1679737941, "lastModified": 1683560683,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -284,11 +288,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1680315536, "lastModified": 1683771545,
"narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=", "narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "5c8c151bdd639074a0051325c16df1a64ee23497", "rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -296,6 +300,21 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

@ -64,7 +64,7 @@
}; };
in in
inp.parts.lib.mkFlake {inputs = inp;} { inp.parts.lib.mkFlake {inputs = inp;} {
imports = [inp.nci.flakeModule]; imports = [inp.nci.flakeModule inp.parts.flakeModules.easyOverlay];
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"x86_64-darwin" "x86_64-darwin"
@ -146,6 +146,10 @@
packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {}; packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
packages.default = config.packages.helix; packages.default = config.packages.helix;
overlayAttrs = {
inherit (config.packages) helix;
};
devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: { devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: {
nativeBuildInputs = nativeBuildInputs =
(old.nativeBuildInputs or []) (old.nativeBuildInputs or [])

@ -26,12 +26,12 @@ unicode-general-category = "0.6"
# slab = "0.4.2" # slab = "0.4.2"
slotmap = "1.0" slotmap = "1.0"
tree-sitter = "0.20" tree-sitter = "0.20"
once_cell = "1.17" once_cell = "1.18"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
bitflags = "2.2" bitflags = "2.3"
ahash = "0.8.3" ahash = "0.8.3"
hashbrown = { version = "0.13.2", features = ["raw"] } hashbrown = { version = "0.14.0", features = ["raw"] }
dunce = "1.0" dunce = "1.0"
log = "0.4" log = "0.4"
@ -45,7 +45,7 @@ encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.7" etcetera = "0.8"
textwrap = "0.16.0" textwrap = "0.16.0"
[dev-dependencies] [dev-dependencies]

@ -43,6 +43,7 @@ pub struct Diagnostic {
pub message: String, pub message: String,
pub severity: Option<Severity>, pub severity: Option<Severity>,
pub code: Option<NumberOrString>, pub code: Option<NumberOrString>,
pub language_server_id: usize,
pub tags: Vec<DiagnosticTag>, pub tags: Vec<DiagnosticTag>,
pub source: Option<String>, pub source: Option<String>,
pub data: Option<serde_json::Value>, pub data: Option<serde_json::Value>,

@ -67,4 +67,4 @@ pub use syntax::Syntax;
pub use diagnostic::Diagnostic; pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING}; pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction}; pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};

@ -2,6 +2,9 @@ use tree_sitter::Node;
use crate::{Rope, Syntax}; use crate::{Rope, Syntax};
const MAX_PLAINTEXT_SCAN: usize = 10000;
// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
const PAIRS: &[(char, char)] = &[ const PAIRS: &[(char, char)] = &[
('(', ')'), ('(', ')'),
('{', '}'), ('{', '}'),
@ -11,15 +14,15 @@ const PAIRS: &[(char, char)] = &[
('\"', '\"'), ('\"', '\"'),
]; ];
// limit matching pairs to only ( ) { } [ ] < > ' ' " " /// Returns the position of the matching bracket under cursor.
///
// Returns the position of the matching bracket under cursor. /// If the cursor is on the opening bracket, the position of
// /// the closing bracket is returned. If the cursor on the closing
// If the cursor is one the opening bracket, the position of /// bracket, the position of the opening bracket is returned.
// the closing bracket is returned. If the cursor in the closing ///
// bracket, the position of the opening bracket is returned. /// If the cursor is not on a bracket, `None` is returned.
// ///
// If the cursor is not on a bracket, `None` is returned. /// If no matching bracket is found, `None` is returned.
#[must_use] #[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
@ -70,10 +73,77 @@ fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) ->
} }
} }
/// Returns the position of the matching bracket under cursor.
/// This function works on plain text and ignores tree-sitter grammar.
/// The search is limited to `MAX_PLAINTEXT_SCAN` characters
///
/// If the cursor is on the opening bracket, the position of
/// the closing bracket is returned. If the cursor on the closing
/// bracket, the position of the opening bracket is returned.
///
/// If the cursor is not on a bracket, `None` is returned.
///
/// If no matching bracket is found, `None` is returned.
#[must_use]
pub fn find_matching_bracket_current_line_plaintext(
doc: &Rope,
cursor_pos: usize,
) -> Option<usize> {
// Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.char(cursor_pos);
if !is_valid_bracket(bracket) {
return None;
}
// Determine the direction of the matching.
let is_fwd = is_forward_bracket(bracket);
let chars_iter = if is_fwd {
doc.chars_at(cursor_pos + 1)
} else {
doc.chars_at(cursor_pos).reversed()
};
let mut open_cnt = 1;
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
if candidate == bracket {
open_cnt += 1;
} else if is_valid_pair(
doc,
if is_fwd {
cursor_pos
} else {
cursor_pos - i - 1
},
if is_fwd {
cursor_pos + i + 1
} else {
cursor_pos
},
) {
// Return when all pending brackets have been closed.
if open_cnt == 1 {
return Some(if is_fwd {
cursor_pos + i + 1
} else {
cursor_pos - i - 1
});
}
open_cnt -= 1;
}
}
None
}
fn is_valid_bracket(c: char) -> bool { fn is_valid_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, r)| *l == c || *r == c) PAIRS.iter().any(|(l, r)| *l == c || *r == c)
} }
fn is_forward_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, _)| *l == c)
}
fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool { fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
} }
@ -90,3 +160,36 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
Some((start_byte, end_byte)) Some((start_byte, end_byte))
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| {
let input = &Rope::from(input);
let actual = find_matching_bracket_current_line_plaintext(input, pos);
assert_eq!(expected, actual.unwrap());
let actual = find_matching_bracket_current_line_plaintext(input, expected);
assert_eq!(pos, actual.unwrap(), "expected symmetrical behaviour");
};
assert("(hello)", 0, 6);
assert("((hello))", 0, 8);
assert("((hello))", 1, 7);
assert("(((hello)))", 2, 8);
assert("key: ${value}", 6, 12);
assert("key: ${value} # (some comment)", 16, 29);
assert("(paren (paren {bracket}))", 0, 24);
assert("(paren (paren {bracket}))", 7, 23);
assert("(paren (paren {bracket}))", 14, 22);
assert("(prev line\n ) (middle) ( \n next line)", 0, 12);
assert("(prev line\n ) (middle) ( \n next line)", 14, 21);
assert("(prev line\n ) (middle) ( \n next line)", 23, 36);
}
}

@ -78,4 +78,12 @@ impl Registers {
pub fn inner(&self) -> &HashMap<char, Register> { pub fn inner(&self) -> &HashMap<char, Register> {
&self.inner &self.inner
} }
pub fn clear(&mut self) {
self.inner.clear();
}
pub fn remove(&mut self, name: char) -> Option<Register> {
self.inner.remove(&name)
}
} }

@ -522,7 +522,14 @@ impl Selection {
self self
} }
// Merges all ranges that are consecutive /// Replaces ranges with one spanning from first to last range.
pub fn merge_ranges(self) -> Self {
let first = self.ranges.first().unwrap();
let last = self.ranges.last().unwrap();
Selection::new(smallvec![first.merge(*last)], 0)
}
/// Merges all ranges that are consecutive.
pub fn merge_consecutive_ranges(mut self) -> Self { pub fn merge_consecutive_ranges(mut self) -> Self {
let mut primary = self.ranges[self.primary_index]; let mut primary = self.ranges[self.primary_index];

@ -397,15 +397,10 @@ mod test {
let selections: SmallVec<[Range; 1]> = spec let selections: SmallVec<[Range; 1]> = spec
.match_indices('^') .match_indices('^')
.into_iter()
.map(|(i, _)| Range::point(i)) .map(|(i, _)| Range::point(i))
.collect(); .collect();
let expectations: Vec<usize> = spec let expectations: Vec<usize> = spec.match_indices('_').map(|(i, _)| i).collect();
.match_indices('_')
.into_iter()
.map(|(i, _)| i)
.collect();
(rope, Selection::new(selections, 0), expectations) (rope, Selection::new(selections, 0), expectations)
} }

@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
collections::{HashMap, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt, fmt::{self, Display},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
mem::{replace, transmute}, mem::{replace, transmute},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -26,7 +26,7 @@ use std::{
}; };
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize}; use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file}; use helix_loader::grammar::{get_language, load_runtime_file};
@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
} }
impl Default for Configuration { impl Default for Configuration {
@ -75,7 +78,10 @@ impl Default for Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[serde(rename = "name")]
pub language_id: String, // c-sharp, rust pub language_id: String, // c-sharp, rust, tsx
#[serde(rename = "language-id")]
// see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
pub scope: String, // source.rust pub scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc> pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)] #[serde(default)]
@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
pub text_width: Option<usize>, pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>, pub soft_wrap: Option<SoftWrap>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default)] #[serde(default)]
pub auto_format: bool, pub auto_format: bool,
@ -107,8 +110,13 @@ pub struct LanguageConfiguration {
#[serde(skip)] #[serde(skip)]
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>, pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
pub language_server: Option<LanguageServerConfiguration>, default,
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_lang_features",
deserialize_with = "deserialize_lang_features"
)]
pub language_servers: Vec<LanguageServerFeatures>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>, pub indent: Option<IndentationConfiguration>,
@ -187,9 +195,12 @@ impl<'de> Deserialize<'de> for FileType {
M: serde::de::MapAccess<'de>, M: serde::de::MapAccess<'de>,
{ {
match map.next_entry::<String, String>()? { match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix( Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
suffix.replace('/', &std::path::MAIN_SEPARATOR.to_string()), // FIXME: use `suffix.replace('/', std::path::MAIN_SEPARATOR_STR)`
)), // if MSRV is updated to 1.68
let mut separator = [0; 1];
suffix.replace('/', std::path::MAIN_SEPARATOR.encode_utf8(&mut separator))
})),
Some((key, _value)) => Err(serde::de::Error::custom(format!( Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}", "unknown key in `file-types` list: {}",
key key
@ -205,6 +216,133 @@ impl<'de> Deserialize<'de> for FileType {
} }
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature {
Format,
GotoDeclaration,
GotoDefinition,
GotoTypeDefinition,
GotoReference,
GotoImplementation,
// Goto, use bitflags, combining previous Goto members?
SignatureHelp,
Hover,
DocumentHighlight,
Completion,
CodeAction,
WorkspaceCommand,
DocumentSymbols,
WorkspaceSymbols,
// Symbols, use bitflags, see above?
Diagnostics,
RenameSymbol,
InlayHints,
}
impl Display for LanguageServerFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use LanguageServerFeature::*;
let feature = match self {
Format => "format",
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
DocumentHighlight => "document-highlight",
Completion => "completion",
CodeAction => "code-action",
WorkspaceCommand => "workspace-command",
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
};
write!(f, "{feature}",)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
enum LanguageServerFeatureConfiguration {
#[serde(rename_all = "kebab-case")]
Features {
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
only_features: HashSet<LanguageServerFeature>,
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
except_features: HashSet<LanguageServerFeature>,
name: String,
},
Simple(String),
}
#[derive(Debug, Default)]
pub struct LanguageServerFeatures {
pub name: String,
pub only: HashSet<LanguageServerFeature>,
pub excluded: HashSet<LanguageServerFeature>,
}
impl LanguageServerFeatures {
pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
(self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
}
}
fn deserialize_lang_features<'de, D>(
deserializer: D,
) -> Result<Vec<LanguageServerFeatures>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
let res = raw
.into_iter()
.map(|config| match config {
LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
name,
..Default::default()
},
LanguageServerFeatureConfiguration::Features {
only_features,
except_features,
name,
} => LanguageServerFeatures {
name,
only: only_features,
excluded: except_features,
},
})
.collect();
Ok(res)
}
fn serialize_lang_features<S>(
map: &Vec<LanguageServerFeatures>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut serializer = serializer.serialize_seq(Some(map.len()))?;
for features in map {
let features = if features.only.is_empty() && features.excluded.is_empty() {
LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
} else {
LanguageServerFeatureConfiguration::Features {
only_features: features.only.clone(),
except_features: features.excluded.clone(),
name: features.name.to_owned(),
}
};
serializer.serialize_element(&features)?;
}
serializer.end()
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration { pub struct LanguageServerConfiguration {
@ -214,9 +352,10 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>, pub environment: HashMap<String, String>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]
pub timeout: u64, pub timeout: u64,
pub language_id: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -591,6 +730,8 @@ pub struct Loader {
language_config_ids_by_suffix: HashMap<String, usize>, language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>, language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
scopes: ArcSwap<Vec<String>>, scopes: ArcSwap<Vec<String>>,
} }
@ -598,6 +739,7 @@ impl Loader {
pub fn new(config: Configuration) -> Self { pub fn new(config: Configuration) -> Self {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(), language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(), language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(), language_config_ids_by_shebang: HashMap::new(),
@ -662,9 +804,8 @@ impl Loader {
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0)); let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| { static SHEBANG_REGEX: Lazy<Regex> =
Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap() Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap());
});
let configuration_id = SHEBANG_REGEX let configuration_id = SHEBANG_REGEX
.captures(&line) .captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1])); .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
@ -686,15 +827,14 @@ impl Loader {
.cloned() .cloned()
} }
pub fn language_configuration_for_injection_string( /// Unlike language_config_for_language_id, which only returns Some for an exact id, this
&self, /// function will perform a regex match on the given string to find the closest language match.
string: &str, pub fn language_config_for_name(&self, name: &str) -> Option<Arc<LanguageConfiguration>> {
) -> Option<Arc<LanguageConfiguration>> {
let mut best_match_length = 0; let mut best_match_length = 0;
let mut best_match_position = None; let mut best_match_position = None;
for (i, configuration) in self.language_configs.iter().enumerate() { for (i, configuration) in self.language_configs.iter().enumerate() {
if let Some(injection_regex) = &configuration.injection_regex { if let Some(injection_regex) = &configuration.injection_regex {
if let Some(mat) = injection_regex.find(string) { if let Some(mat) = injection_regex.find(name) {
let length = mat.end() - mat.start(); let length = mat.end() - mat.start();
if length > best_match_length { if length > best_match_length {
best_match_position = Some(i); best_match_position = Some(i);
@ -704,17 +844,30 @@ impl Loader {
} }
} }
if let Some(i) = best_match_position { best_match_position.map(|i| self.language_configs[i].clone())
let configuration = &self.language_configs[i]; }
return Some(configuration.clone());
pub fn language_configuration_for_injection_string(
&self,
capture: &InjectionLanguageMarker,
) -> Option<Arc<LanguageConfiguration>> {
match capture {
InjectionLanguageMarker::Name(string) => self.language_config_for_name(string),
InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file),
InjectionLanguageMarker::Shebang(shebang) => {
self.language_config_for_language_id(shebang)
}
} }
None
} }
pub fn language_configs(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> { pub fn language_configs(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter() self.language_configs.iter()
} }
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
&self.language_server_configs
}
pub fn set_scopes(&self, scopes: Vec<String>) { pub fn set_scopes(&self, scopes: Vec<String>) {
self.scopes.store(Arc::new(scopes)); self.scopes.store(Arc::new(scopes));
@ -758,7 +911,11 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
} }
impl Syntax { impl Syntax {
pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self { pub fn new(
source: &Rope,
config: Arc<HighlightConfiguration>,
loader: Arc<Loader>,
) -> Option<Self> {
let root_layer = LanguageLayer { let root_layer = LanguageLayer {
tree: None, tree: None,
config, config,
@ -783,11 +940,13 @@ impl Syntax {
loader, loader,
}; };
syntax let res = syntax.update(source, source, &ChangeSet::new(source));
.update(source, source, &ChangeSet::new(source))
.unwrap();
syntax if res.is_err() {
log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
return None;
}
Some(syntax)
} }
pub fn update( pub fn update(
@ -800,7 +959,7 @@ impl Syntax {
queue.push_back(self.root); queue.push_back(self.root);
let scopes = self.loader.scopes.load(); let scopes = self.loader.scopes.load();
let injection_callback = |language: &str| { let injection_callback = |language: &InjectionLanguageMarker| {
self.loader self.loader
.language_configuration_for_injection_string(language) .language_configuration_for_injection_string(language)
.and_then(|language_config| language_config.highlight_config(&scopes)) .and_then(|language_config| language_config.highlight_config(&scopes))
@ -915,6 +1074,7 @@ impl Syntax {
PARSER.with(|ts_parser| { PARSER.with(|ts_parser| {
let ts_parser = &mut ts_parser.borrow_mut(); let ts_parser = &mut ts_parser.borrow_mut();
ts_parser.parser.set_timeout_micros(1000 * 500); // half a second is pretty generours
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
// TODO: might need to set cursor range // TODO: might need to set cursor range
cursor.set_byte_range(0..usize::MAX); cursor.set_byte_range(0..usize::MAX);
@ -961,12 +1121,9 @@ impl Syntax {
); );
let mut injections = Vec::new(); let mut injections = Vec::new();
for mat in matches { for mat in matches {
let (language_name, content_node, included_children) = injection_for_match( let (injection_capture, content_node, included_children) = layer
&layer.config, .config
&layer.config.injections_query, .injection_for_match(&layer.config.injections_query, &mat, source_slice);
&mat,
source_slice,
);
// Explicitly remove this match so that none of its other captures will remain // Explicitly remove this match so that none of its other captures will remain
// in the stream of captures. // in the stream of captures.
@ -974,9 +1131,10 @@ impl Syntax {
// If a language is found with the given name, then add a new language layer // If a language is found with the given name, then add a new language layer
// to the highlighted document. // to the highlighted document.
if let (Some(language_name), Some(content_node)) = (language_name, content_node) if let (Some(injection_capture), Some(content_node)) =
(injection_capture, content_node)
{ {
if let Some(config) = (injection_callback)(&language_name) { if let Some(config) = (injection_callback)(&injection_capture) {
let ranges = let ranges =
intersect_ranges(&layer.ranges, &[content_node], included_children); intersect_ranges(&layer.ranges, &[content_node], included_children);
@ -1001,14 +1159,11 @@ impl Syntax {
); );
for mat in matches { for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index]; let entry = &mut injections_by_pattern_index[mat.pattern_index];
let (language_name, content_node, included_children) = injection_for_match( let (injection_capture, content_node, included_children) = layer
&layer.config, .config
combined_injections_query, .injection_for_match(combined_injections_query, &mat, source_slice);
&mat, if injection_capture.is_some() {
source_slice, entry.0 = injection_capture;
);
if language_name.is_some() {
entry.0 = language_name;
} }
if let Some(content_node) = content_node { if let Some(content_node) = content_node {
entry.1.push(content_node); entry.1.push(content_node);
@ -1395,6 +1550,8 @@ pub struct HighlightConfiguration {
non_local_variable_patterns: Vec<bool>, non_local_variable_patterns: Vec<bool>,
injection_content_capture_index: Option<u32>, injection_content_capture_index: Option<u32>,
injection_language_capture_index: Option<u32>, injection_language_capture_index: Option<u32>,
injection_filename_capture_index: Option<u32>,
injection_shebang_capture_index: Option<u32>,
local_scope_capture_index: Option<u32>, local_scope_capture_index: Option<u32>,
local_def_capture_index: Option<u32>, local_def_capture_index: Option<u32>,
local_def_value_capture_index: Option<u32>, local_def_value_capture_index: Option<u32>,
@ -1538,6 +1695,8 @@ impl HighlightConfiguration {
// Store the numeric ids for all of the special captures. // Store the numeric ids for all of the special captures.
let mut injection_content_capture_index = None; let mut injection_content_capture_index = None;
let mut injection_language_capture_index = None; let mut injection_language_capture_index = None;
let mut injection_filename_capture_index = None;
let mut injection_shebang_capture_index = None;
let mut local_def_capture_index = None; let mut local_def_capture_index = None;
let mut local_def_value_capture_index = None; let mut local_def_value_capture_index = None;
let mut local_ref_capture_index = None; let mut local_ref_capture_index = None;
@ -1558,6 +1717,8 @@ impl HighlightConfiguration {
match name.as_str() { match name.as_str() {
"injection.content" => injection_content_capture_index = i, "injection.content" => injection_content_capture_index = i,
"injection.language" => injection_language_capture_index = i, "injection.language" => injection_language_capture_index = i,
"injection.filename" => injection_filename_capture_index = i,
"injection.shebang" => injection_shebang_capture_index = i,
_ => {} _ => {}
} }
} }
@ -1573,6 +1734,8 @@ impl HighlightConfiguration {
non_local_variable_patterns, non_local_variable_patterns,
injection_content_capture_index, injection_content_capture_index,
injection_language_capture_index, injection_language_capture_index,
injection_filename_capture_index,
injection_shebang_capture_index,
local_scope_capture_index, local_scope_capture_index,
local_def_capture_index, local_def_capture_index,
local_def_value_capture_index, local_def_value_capture_index,
@ -1631,6 +1794,90 @@ impl HighlightConfiguration {
self.highlight_indices.store(Arc::new(indices)); self.highlight_indices.store(Arc::new(indices));
} }
fn injection_pair<'a>(
&self,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (Option<InjectionLanguageMarker<'a>>, Option<Node<'a>>) {
let mut injection_capture = None;
let mut content_node = None;
for capture in query_match.captures {
let index = Some(capture.index);
if index == self.injection_language_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
injection_capture = Some(InjectionLanguageMarker::Name(name));
} else if index == self.injection_filename_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
let path = Path::new(name.as_ref()).to_path_buf();
injection_capture = Some(InjectionLanguageMarker::Filename(path.into()));
} else if index == self.injection_shebang_capture_index {
let node_slice = source.byte_slice(capture.node.byte_range());
// some languages allow space and newlines before the actual string content
// so a shebang could be on either the first or second line
let lines = if let Ok(end) = node_slice.try_line_to_byte(2) {
node_slice.byte_slice(..end)
} else {
node_slice
};
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(SHEBANG).unwrap());
injection_capture = SHEBANG_REGEX
.captures(&Cow::from(lines))
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_owned()))
} else if index == self.injection_content_capture_index {
content_node = Some(capture.node);
}
}
(injection_capture, content_node)
}
fn injection_for_match<'a>(
&self,
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (
Option<InjectionLanguageMarker<'a>>,
Option<Node<'a>>,
IncludedChildren,
) {
let (mut injection_capture, content_node) = self.injection_pair(query_match, source);
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
// captured node, it can also be hard-coded via a `#set!` predicate
// that sets the injection.language key.
"injection.language" if injection_capture.is_none() => {
injection_capture = prop
.value
.as_ref()
.map(|s| InjectionLanguageMarker::Name(s.as_ref().into()));
}
// By default, injections do not include the *children* of an
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => {
included_children = IncludedChildren::Unnamed
}
_ => {}
}
}
(injection_capture, content_node, included_children)
}
} }
impl<'a> HighlightIterLayer<'a> { impl<'a> HighlightIterLayer<'a> {
@ -2042,56 +2289,15 @@ impl<'a> Iterator for HighlightIter<'a> {
} }
} }
fn injection_for_match<'a>( #[derive(Debug, Clone)]
config: &HighlightConfiguration, pub enum InjectionLanguageMarker<'a> {
query: &'a Query, Name(Cow<'a, str>),
query_match: &QueryMatch<'a, 'a>, Filename(Cow<'a, Path>),
source: RopeSlice<'a>, Shebang(String),
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, IncludedChildren) {
let content_capture_index = config.injection_content_capture_index;
let language_capture_index = config.injection_language_capture_index;
let mut language_name = None;
let mut content_node = None;
for capture in query_match.captures {
let index = Some(capture.index);
if index == language_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
language_name = Some(name);
} else if index == content_capture_index {
content_node = Some(capture.node);
}
}
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
// captured node, it can also be hard-coded via a `#set!` predicate
// that sets the injection.language key.
"injection.language" => {
if language_name.is_none() {
language_name = prop.value.as_ref().map(|s| s.as_ref().into())
}
}
// By default, injections do not include the *children* of an
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => included_children = IncludedChildren::Unnamed,
_ => {}
}
}
(language_name, content_node, included_children)
} }
const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";
pub struct Merge<I> { pub struct Merge<I> {
iter: I, iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>, spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
@ -2307,7 +2513,10 @@ mod test {
"#, "#,
); );
let loader = Loader::new(Configuration { language: vec![] }); let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap(); let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap(); let query = Query::new(language, query_str).unwrap();
@ -2315,7 +2524,7 @@ mod test {
let mut cursor = QueryCursor::new(); let mut cursor = QueryCursor::new();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax.tree().root_node(); let root = syntax.tree().root_node();
let mut test = |capture, range| { let mut test = |capture, range| {
@ -2366,7 +2575,10 @@ mod test {
.map(String::from) .map(String::from)
.collect(); .collect();
let loader = Loader::new(Configuration { language: vec![] }); let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap(); let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new( let config = HighlightConfiguration::new(
@ -2386,7 +2598,7 @@ mod test {
fn main() {} fn main() {}
", ",
); );
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
let tree = syntax.tree(); let tree = syntax.tree();
let root = tree.root_node(); let root = tree.root_node();
assert_eq!(root.kind(), "source_file"); assert_eq!(root.kind(), "source_file");
@ -2469,11 +2681,14 @@ mod test {
) { ) {
let source = Rope::from_str(source); let source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] }); let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language(language_name).unwrap(); let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax let root = syntax
.tree() .tree()

@ -5,6 +5,7 @@ use std::borrow::Cow;
/// (from, to, replacement) /// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>); pub type Change = (usize, usize, Option<Tendril>);
pub type Deletion = (usize, usize);
// TODO: pub(crate) // TODO: pub(crate)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -534,6 +535,46 @@ impl Transaction {
Self::from(changeset) Self::from(changeset)
} }
/// Generate a transaction from a set of potentially overlapping deletions
/// by merging overlapping deletions together.
pub fn delete<I>(doc: &Rope, deletions: I) -> Self
where
I: Iterator<Item = Deletion>,
{
let len = doc.len_chars();
let (lower, upper) = deletions.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
let mut last = 0;
for (mut from, to) in deletions {
if last > to {
continue;
}
if last > from {
from = last
}
debug_assert!(
from <= to,
"Edit end must end before it starts (should {from} <= {to})"
);
// Retain from last "to" to current "from"
changeset.retain(from - last);
changeset.delete(to - from);
last = to;
}
changeset.retain(len - last);
Self::from(changeset)
}
pub fn insert_at_eof(mut self, text: Tendril) -> Transaction {
self.changes.insert(text);
self
}
/// Generate a transaction with a change per selection range. /// Generate a transaction with a change per selection range.
pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where where
@ -580,6 +621,16 @@ impl Transaction {
) )
} }
/// Generate a transaction with a deletion per selection range.
/// Compared to using `change_by_selection` directly these ranges may overlap.
/// In that case they are merged
pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where
F: FnMut(&Range) -> Deletion,
{
Self::delete(doc, selection.iter().map(f))
}
/// Insert text at each selection head. /// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self { pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| { Self::change_by_selection(doc, selection, |range| {

@ -72,7 +72,7 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
let language_config = loader.language_config_for_scope(lang_scope).unwrap(); let language_config = loader.language_config_for_scope(lang_scope).unwrap();
let highlight_config = language_config.highlight_config(&[]).unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap();
let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)).unwrap();
let indent_query = language_config.indent_query().unwrap(); let indent_query = language_config.indent_query().unwrap();
let text = doc.slice(..); let text = doc.slice(..);

@ -17,9 +17,9 @@ path = "src/main.rs"
anyhow = "1" anyhow = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.7" toml = "0.7"
etcetera = "0.7" etcetera = "0.8"
tree-sitter = "0.20" tree-sitter = "0.20"
once_cell = "1.17" once_cell = "1.18"
log = "0.4" log = "0.4"
# TODO: these two should be on !wasm32 only # TODO: these two should be on !wasm32 only

@ -209,6 +209,24 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
} }
} }
/// Finds the current workspace folder.
/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
///
/// This function starts searching the FS upward from the CWD
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
}
}
(current_dir, true)
}
#[cfg(test)] #[cfg(test)]
mod merge_toml_tests { mod merge_toml_tests {
use std::str; use std::str;
@ -281,21 +299,3 @@ mod merge_toml_tests {
) )
} }
} }
/// Finds the current workspace folder.
/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
///
/// This function starts searching the FS upward from the CWD
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
}
}
(current_dir, true)
}

@ -24,7 +24,7 @@ lsp-types = { version = "0.94" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.28", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12" tokio-stream = "0.1.14"
which = "4.4" which = "4.4"
parking_lot = "0.12.1" parking_lot = "0.12.1"

@ -4,7 +4,7 @@ use crate::{
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{find_workspace, path, ChangeSet, Rope}; use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{ use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
id: usize, id: usize,
name: String,
_process: Child, _process: Child,
server_tx: UnboundedSender<Payload>, server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64, request_counter: AtomicU64,
@ -166,8 +167,7 @@ impl Client {
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity, clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub fn start( pub fn start(
cmd: &str, cmd: &str,
args: &[String], args: &[String],
@ -176,6 +176,7 @@ impl Client {
root_markers: &[String], root_markers: &[String],
manual_roots: &[PathBuf], manual_roots: &[PathBuf],
id: usize, id: usize,
name: String,
req_timeout: u64, req_timeout: u64,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@ -200,7 +201,7 @@ impl Client {
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (server_rx, server_tx, initialize_notify) = let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id); Transport::start(reader, writer, stderr, id, name.clone());
let (workspace, workspace_is_cwd) = find_workspace(); let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace); let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace( let root = find_lsp_workspace(
@ -225,6 +226,7 @@ impl Client {
let client = Self { let client = Self {
id, id,
name,
_process: process, _process: process,
server_tx, server_tx,
request_counter: AtomicU64::new(0), request_counter: AtomicU64::new(0),
@ -240,6 +242,10 @@ impl Client {
Ok((client, server_rx, initialize_notify)) Ok((client, server_rx, initialize_notify))
} }
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> usize { pub fn id(&self) -> usize {
self.id self.id
} }
@ -270,6 +276,87 @@ impl Client {
.expect("language server not yet initialized!") .expect("language server not yet initialized!")
} }
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
let capabilities = self.capabilities();
use lsp::*;
match feature {
LanguageServerFeature::Format => matches!(
capabilities.document_formatting_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoDeclaration => matches!(
capabilities.declaration_provider,
Some(
DeclarationCapability::Simple(true)
| DeclarationCapability::RegistrationOptions(_)
| DeclarationCapability::Options(_),
)
),
LanguageServerFeature::GotoDefinition => matches!(
capabilities.definition_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoTypeDefinition => matches!(
capabilities.type_definition_provider,
Some(
TypeDefinitionProviderCapability::Simple(true)
| TypeDefinitionProviderCapability::Options(_),
)
),
LanguageServerFeature::GotoReference => matches!(
capabilities.references_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoImplementation => matches!(
capabilities.implementation_provider,
Some(
ImplementationProviderCapability::Simple(true)
| ImplementationProviderCapability::Options(_),
)
),
LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
LanguageServerFeature::Hover => matches!(
capabilities.hover_provider,
Some(HoverProviderCapability::Simple(true) | HoverProviderCapability::Options(_),)
),
LanguageServerFeature::DocumentHighlight => matches!(
capabilities.document_highlight_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
LanguageServerFeature::CodeAction => matches!(
capabilities.code_action_provider,
Some(
CodeActionProviderCapability::Simple(true)
| CodeActionProviderCapability::Options(_),
)
),
LanguageServerFeature::WorkspaceCommand => {
capabilities.execute_command_provider.is_some()
}
LanguageServerFeature::DocumentSymbols => matches!(
capabilities.document_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::WorkspaceSymbols => matches!(
capabilities.workspace_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
),
LanguageServerFeature::InlayHints => matches!(
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
}
}
pub fn offset_encoding(&self) -> OffsetEncoding { pub fn offset_encoding(&self) -> OffsetEncoding {
self.capabilities() self.capabilities()
.position_encoding .position_encoding
@ -645,7 +732,11 @@ impl Client {
// Calculation is therefore a bunch trickier. // Calculation is therefore a bunch trickier.
use helix_core::RopeSlice; use helix_core::RopeSlice;
fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position { fn traverse(
pos: lsp::Position,
text: RopeSlice,
offset_encoding: OffsetEncoding,
) -> lsp::Position {
let lsp::Position { let lsp::Position {
mut line, mut line,
mut character, mut character,
@ -662,7 +753,11 @@ impl Client {
line += 1; line += 1;
character = 0; character = 0;
} else { } else {
character += ch.len_utf16() as u32; character += match offset_encoding {
OffsetEncoding::Utf8 => ch.len_utf8() as u32,
OffsetEncoding::Utf16 => ch.len_utf16() as u32,
OffsetEncoding::Utf32 => 1,
};
} }
} }
lsp::Position { line, character } lsp::Position { line, character }
@ -683,7 +778,7 @@ impl Client {
} }
Delete(_) => { Delete(_) => {
let start = pos_to_lsp_pos(new_text, new_pos, offset_encoding); let start = pos_to_lsp_pos(new_text, new_pos, offset_encoding);
let end = traverse(start, old_text.slice(old_pos..old_end)); let end = traverse(start, old_text.slice(old_pos..old_end), offset_encoding);
// deletion // deletion
changes.push(lsp::TextDocumentContentChangeEvent { changes.push(lsp::TextDocumentContentChangeEvent {
@ -700,7 +795,8 @@ impl Client {
// a subsequent delete means a replace, consume it // a subsequent delete means a replace, consume it
let end = if let Some(Delete(len)) = iter.peek() { let end = if let Some(Delete(len)) = iter.peek() {
old_end = old_pos + len; old_end = old_pos + len;
let end = traverse(start, old_text.slice(old_pos..old_end)); let end =
traverse(start, old_text.slice(old_pos..old_end), offset_encoding);
iter.next(); iter.next();
@ -1167,6 +1263,7 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
include_declaration: bool,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> { ) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap(); let capabilities = self.capabilities.get().unwrap();
@ -1183,7 +1280,7 @@ impl Client {
position, position,
}, },
context: lsp::ReferenceContext { context: lsp::ReferenceContext {
include_declaration: true, include_declaration,
}, },
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams { partial_result_params: lsp::PartialResultParams {
@ -1285,21 +1382,13 @@ impl Client {
Some(self.call::<lsp::request::CodeActionRequest>(params)) Some(self.call::<lsp::request::CodeActionRequest>(params))
} }
pub fn supports_rename(&self) -> bool {
let capabilities = self.capabilities.get().unwrap();
matches!(
capabilities.rename_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
}
pub fn rename_symbol( pub fn rename_symbol(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
new_name: String, new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> { ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
if !self.supports_rename() { if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None; return None;
} }

@ -12,24 +12,21 @@ pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_core::{ use helix_core::{
path, path,
syntax::{LanguageConfiguration, LanguageServerConfiguration}, syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
}; };
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::Arc,
atomic::{AtomicUsize, Ordering},
Arc,
},
}; };
use thiserror::Error; use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String; pub type LanguageServerName = String;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -49,7 +46,7 @@ pub enum Error {
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OffsetEncoding { pub enum OffsetEncoding {
/// UTF-8 code units aka bytes /// UTF-8 code units aka bytes
Utf8, Utf8,
@ -624,23 +621,18 @@ impl Notification {
#[derive(Debug)] #[derive(Debug)]
pub struct Registry { pub struct Registry {
inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>, inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
syn_loader: Arc<helix_core::syntax::Loader>,
counter: AtomicUsize, counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>, pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
} }
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry { impl Registry {
pub fn new() -> Self { pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
Self { Self {
inner: HashMap::new(), inner: HashMap::new(),
counter: AtomicUsize::new(0), syn_loader,
counter: 0,
incoming: SelectAll::new(), incoming: SelectAll::new(),
} }
} }
@ -649,65 +641,92 @@ impl Registry {
self.inner self.inner
.values() .values()
.flatten() .flatten()
.find(|(client_id, _)| client_id == &id) .find(|client| client.id() == id)
.map(|(_, client)| client.as_ref()) .map(|client| &**client)
} }
pub fn remove_by_id(&mut self, id: usize) { pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, clients| { self.inner.retain(|_, language_servers| {
clients.retain(|&(client_id, _)| client_id != id); language_servers.retain(|ls| id != ls.id());
!clients.is_empty() !language_servers.is_empty()
}) });
} }
fn start_client(
&mut self,
name: String,
ls_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Arc<Client>> {
let config = self
.syn_loader
.language_server_configs()
.get(&name)
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
let id = self.counter;
self.counter += 1;
let NewClient(client, incoming) = start_client(
id,
name,
ls_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
}
/// 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,
/// as it could be that language servers of these documents were stopped by this method.
/// See helix_view::editor::Editor::refresh_language_servers
pub fn restart( pub fn restart(
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<Vec<Arc<Client>>> {
let config = match &language_config.language_server { language_config
Some(config) => config, .language_servers
None => return Ok(None), .iter()
}; .filter_map(|LanguageServerFeatures { name, .. }| {
if self.inner.contains_key(name) {
let scope = language_config.scope.clone(); let client = match self.start_client(
name.clone(),
match self.inner.entry(scope) { language_config,
Entry::Vacant(_) => Ok(None), doc_path,
Entry::Occupied(mut entry) => { root_dirs,
// initialize a new client enable_snippets,
let id = self.counter.fetch_add(1, Ordering::Relaxed); ) {
Ok(client) => client,
let NewClientResult(client, incoming) = start_client( error => return Some(error),
id, };
language_config, let old_clients = self
config, .inner
doc_path, .insert(name.clone(), vec![client.clone()])
root_dirs, .unwrap();
enable_snippets,
)?; for old_client in old_clients {
self.incoming.push(UnboundedReceiverStream::new(incoming)); tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
let old_clients = entry.insert(vec![(id, client.clone())]); });
}
for (_, old_client) in old_clients {
tokio::spawn(async move { Some(Ok(client))
let _ = old_client.force_shutdown().await; } else {
}); None
} }
})
Ok(Some(client)) .collect()
}
}
} }
pub fn stop(&mut self, language_config: &LanguageConfiguration) { pub fn stop(&mut self, name: &str) {
let scope = language_config.scope.clone(); if let Some(clients) = self.inner.remove(name) {
for client in clients {
if let Some(clients) = self.inner.remove(&scope) {
for (_, client) in clients {
tokio::spawn(async move { tokio::spawn(async move {
let _ = client.force_shutdown().await; let _ = client.force_shutdown().await;
}); });
@ -721,37 +740,34 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
let config = match &language_config.language_server { language_config
Some(config) => config, .language_servers
None => return Ok(None), .iter()
}; .map(|LanguageServerFeatures { name, .. }| {
if let Some(clients) = self.inner.get(name) {
let clients = self.inner.entry(language_config.scope.clone()).or_default(); if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
// check if we already have a client for this documents root that we can reuse client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| { }) {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) return Ok((name.to_owned(), client.clone()));
}) { }
return Ok(Some(client.1.clone())); }
} let client = self.start_client(
// initialize a new client name.clone(),
let id = self.counter.fetch_add(1, Ordering::Relaxed); language_config,
doc_path,
let NewClientResult(client, incoming) = start_client( root_dirs,
id, enable_snippets,
language_config, )?;
config, let clients = self.inner.entry(name.clone()).or_default();
doc_path, clients.push(client.clone());
root_dirs, Ok((name.clone(), client))
enable_snippets, })
)?; .collect()
clients.push((id, client.clone()));
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(Some(client))
} }
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> { pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().flatten().map(|(_, client)| client) self.inner.values().flatten()
} }
} }
@ -833,26 +849,28 @@ impl LspProgressMap {
} }
} }
struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>); struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense. /// it is only called when it makes sense.
fn start_client( fn start_client(
id: usize, id: usize,
name: String,
config: &LanguageConfiguration, config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration, ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<NewClientResult> { ) -> Result<NewClient> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, &ls_config.command,
&ls_config.args, &ls_config.args,
config.config.clone(), ls_config.config.clone(),
ls_config.environment.clone(), ls_config.environment.clone(),
&config.roots, &config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id, id,
name,
ls_config.timeout, ls_config.timeout,
doc_path, doc_path,
)?; )?;
@ -886,7 +904,7 @@ fn start_client(
initialize_notify.notify_one(); initialize_notify.notify_one();
}); });
Ok(NewClientResult(client, incoming)) Ok(NewClient(client, incoming))
} }
/// Find an LSP workspace of a file using the following mechanism: /// Find an LSP workspace of a file using the following mechanism:

@ -38,6 +38,7 @@ enum ServerMessage {
#[derive(Debug)] #[derive(Debug)]
pub struct Transport { pub struct Transport {
id: usize, id: usize,
name: String,
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>, pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
} }
@ -47,6 +48,7 @@ impl Transport {
server_stdin: BufWriter<ChildStdin>, server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>, server_stderr: BufReader<ChildStderr>,
id: usize, id: usize,
name: String,
) -> ( ) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>, UnboundedSender<Payload>,
@ -58,6 +60,7 @@ impl Transport {
let transport = Self { let transport = Self {
id, id,
name,
pending_requests: Mutex::new(HashMap::default()), pending_requests: Mutex::new(HashMap::default()),
}; };
@ -83,6 +86,7 @@ impl Transport {
async fn recv_server_message( async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send), reader: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String, buffer: &mut String,
language_server_name: &str,
) -> Result<ServerMessage> { ) -> Result<ServerMessage> {
let mut content_length = None; let mut content_length = None;
loop { loop {
@ -124,7 +128,7 @@ impl Transport {
reader.read_exact(&mut content).await?; reader.read_exact(&mut content).await?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
info!("<- {}", msg); info!("{language_server_name} <- {msg}");
// try parsing as output (server response) or call (server request) // try parsing as output (server response) or call (server request)
let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg); let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@ -135,12 +139,13 @@ impl Transport {
async fn recv_server_error( async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send), err: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String, buffer: &mut String,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
buffer.truncate(0); buffer.truncate(0);
if err.read_line(buffer).await? == 0 { if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed); return Err(Error::StreamClosed);
}; };
error!("err <- {:?}", buffer); error!("{language_server_name} err <- {buffer:?}");
Ok(()) Ok(())
} }
@ -162,15 +167,17 @@ impl Transport {
Payload::Notification(value) => serde_json::to_string(&value)?, Payload::Notification(value) => serde_json::to_string(&value)?,
Payload::Response(error) => serde_json::to_string(&error)?, Payload::Response(error) => serde_json::to_string(&error)?,
}; };
self.send_string_to_server(server_stdin, json).await self.send_string_to_server(server_stdin, json, &self.name)
.await
} }
async fn send_string_to_server( async fn send_string_to_server(
&self, &self,
server_stdin: &mut BufWriter<ChildStdin>, server_stdin: &mut BufWriter<ChildStdin>,
request: String, request: String,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
info!("-> {}", request); info!("{language_server_name} -> {request}");
// send the headers // send the headers
server_stdin server_stdin
@ -189,9 +196,13 @@ impl Transport {
&self, &self,
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>, client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
msg: ServerMessage, msg: ServerMessage,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
match msg { match msg {
ServerMessage::Output(output) => self.process_request_response(output).await?, ServerMessage::Output(output) => {
self.process_request_response(output, language_server_name)
.await?
}
ServerMessage::Call(call) => { ServerMessage::Call(call) => {
client_tx client_tx
.send((self.id, call)) .send((self.id, call))
@ -202,14 +213,18 @@ impl Transport {
Ok(()) Ok(())
} }
async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> { async fn process_request_response(
&self,
output: jsonrpc::Output,
language_server_name: &str,
) -> Result<()> {
let (id, result) = match output { let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => { jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result); info!("{language_server_name} <- {}", result);
(id, Ok(result)) (id, Ok(result))
} }
jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => { jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
error!("<- {}", error); error!("{language_server_name} <- {error}");
(id, Err(error.into())) (id, Err(error.into()))
} }
}; };
@ -240,12 +255,17 @@ impl Transport {
) { ) {
let mut recv_buffer = String::new(); let mut recv_buffer = String::new();
loop { loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
.await
{
Ok(msg) => { Ok(msg) => {
match transport.process_server_message(&client_tx, msg).await { match transport
.process_server_message(&client_tx, msg, &transport.name)
.await
{
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
}; };
@ -270,7 +290,7 @@ impl Transport {
params: jsonrpc::Params::None, params: jsonrpc::Params::None,
})); }));
match transport match transport
.process_server_message(&client_tx, notification) .process_server_message(&client_tx, notification, &transport.name)
.await .await
{ {
Ok(_) => {} Ok(_) => {}
@ -281,20 +301,22 @@ impl Transport {
break; break;
} }
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
} }
} }
} }
async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) { async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
let mut recv_buffer = String::new(); let mut recv_buffer = String::new();
loop { loop {
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await { match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
.await
{
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
} }
@ -348,10 +370,11 @@ impl Transport {
method: lsp_types::notification::Initialized::METHOD.to_string(), method: lsp_types::notification::Initialized::METHOD.to_string(),
params: jsonrpc::Params::None, params: jsonrpc::Params::None,
})); }));
match transport.process_server_message(&client_tx, notification).await { let language_server_name = &transport.name;
match transport.process_server_message(&client_tx, notification, language_server_name).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{language_server_name} err: <- {err:?}");
} }
} }
@ -361,7 +384,7 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await { match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{language_server_name} err: <- {err:?}");
} }
} }
} }
@ -380,7 +403,7 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await { match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
} }
} }
} }

@ -31,7 +31,7 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1" anyhow = "1"
once_cell = "1.17" once_cell = "1.18"
which = "4.4" which = "4.4"
@ -68,7 +68,7 @@ grep-searcher = "0.1.11"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.142" libc = "0.2.145"
[build-dependencies] [build-dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }

@ -30,6 +30,7 @@ use crate::{
use log::{debug, error, warn}; use log::{debug, error, warn};
use std::{ use std::{
collections::btree_map::Entry,
io::{stdin, stdout}, io::{stdin, stdout},
path::Path, path::Path,
sync::Arc, sync::Arc,
@ -230,8 +231,14 @@ impl Application {
#[cfg(windows)] #[cfg(windows)]
let signals = futures_util::stream::empty(); let signals = futures_util::stream::empty();
#[cfg(not(windows))] #[cfg(not(windows))]
let signals = Signals::new([signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1]) let signals = Signals::new([
.context("build signal handler")?; signal::SIGTSTP,
signal::SIGCONT,
signal::SIGUSR1,
signal::SIGTERM,
signal::SIGINT,
])
.context("build signal handler")?;
let app = Self { let app = Self {
compositor, compositor,
@ -317,7 +324,9 @@ impl Application {
biased; biased;
Some(signal) = self.signals.next() => { Some(signal) = self.signals.next() => {
self.handle_signals(signal).await; if !self.handle_signals(signal).await {
return false;
};
} }
Some(event) = input_stream.next() => { Some(event) = input_stream.next() => {
self.handle_terminal_events(event).await; self.handle_terminal_events(event).await;
@ -441,10 +450,12 @@ impl Application {
#[cfg(windows)] #[cfg(windows)]
// no signal handling available on windows // no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) {} pub async fn handle_signals(&mut self, _signal: ()) -> bool {
true
}
#[cfg(not(windows))] #[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) { pub async fn handle_signals(&mut self, signal: i32) -> bool {
match signal { match signal {
signal::SIGTSTP => { signal::SIGTSTP => {
self.restore_term().unwrap(); self.restore_term().unwrap();
@ -498,8 +509,14 @@ impl Application {
self.refresh_config(); self.refresh_config();
self.render().await; self.render().await;
} }
signal::SIGTERM | signal::SIGINT => {
self.restore_term().unwrap();
return false;
}
_ => unreachable!(), _ => unreachable!(),
} }
true
} }
pub async fn handle_idle_timeout(&mut self) { pub async fn handle_idle_timeout(&mut self) {
@ -564,7 +581,7 @@ impl Application {
let doc = doc_mut!(self.editor, &doc_save_event.doc_id); let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id(); let id = doc.id();
doc.detect_language(loader); doc.detect_language(loader);
let _ = self.editor.refresh_language_server(id); self.editor.refresh_language_servers(id);
} }
// TODO: fix being overwritten by lsp // TODO: fix being overwritten by lsp
@ -662,6 +679,18 @@ impl Application {
) { ) {
use helix_lsp::{Call, MethodCall, Notification}; use helix_lsp::{Call, MethodCall, Notification};
macro_rules! language_server {
() => {
match self.editor.language_server_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
}
};
}
match call { match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) { let notification = match Notification::parse(&method, params) {
@ -677,14 +706,7 @@ impl Application {
match notification { match notification {
Notification::Initialized => { Notification::Initialized => {
let language_server = let language_server = language_server!();
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
// Trigger a workspace/didChangeConfiguration notification after initialization. // Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's // This might not be required by the spec but Neovim does this as well, so it's
@ -693,9 +715,10 @@ impl Application {
tokio::spawn(language_server.did_change_configuration(config.clone())); tokio::spawn(language_server.did_change_configuration(config.clone()));
} }
let docs = self.editor.documents().filter(|doc| { let docs = self
doc.language_server().map(|server| server.id()) == Some(server_id) .editor
}); .documents()
.filter(|doc| doc.supports_language_server(server_id));
// trigger textDocument/didOpen for docs that are already open // trigger textDocument/didOpen for docs that are already open
for doc in docs { for doc in docs {
@ -715,7 +738,7 @@ impl Application {
)); ));
} }
} }
Notification::PublishDiagnostics(mut params) => { Notification::PublishDiagnostics(params) => {
let path = match params.uri.to_file_path() { let path = match params.uri.to_file_path() {
Ok(path) => path, Ok(path) => path,
Err(_) => { Err(_) => {
@ -723,6 +746,7 @@ impl Application {
return; return;
} }
}; };
let offset_encoding = language_server!().offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| { let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version { if let Some(version) = params.version {
if version != doc.version() { if version != doc.version() {
@ -745,18 +769,11 @@ impl Application {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*}; use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity; use lsp::DiagnosticSeverity;
let language_server = if let Some(language_server) = doc.language_server() {
language_server
} else {
log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
return None;
};
// TODO: convert inside server // TODO: convert inside server
let start = if let Some(start) = lsp_pos_to_pos( let start = if let Some(start) = lsp_pos_to_pos(
text, text,
diagnostic.range.start, diagnostic.range.start,
language_server.offset_encoding(), offset_encoding,
) { ) {
start start
} else { } else {
@ -764,11 +781,9 @@ impl Application {
return None; return None;
}; };
let end = if let Some(end) = lsp_pos_to_pos( let end = if let Some(end) =
text, lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
diagnostic.range.end, {
language_server.offset_encoding(),
) {
end end
} else { } else {
log::warn!("lsp position out of bounds - {:?}", diagnostic); log::warn!("lsp position out of bounds - {:?}", diagnostic);
@ -807,14 +822,19 @@ impl Application {
None => None, None => None,
}; };
let tags = if let Some(ref tags) = diagnostic.tags { let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags.iter().filter_map(|tag| { let new_tags = tags
match *tag { .iter()
lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), .filter_map(|tag| match *tag {
lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), lsp::DiagnosticTag::DEPRECATED => {
_ => None Some(DiagnosticTag::Deprecated)
} }
}).collect(); lsp::DiagnosticTag::UNNECESSARY => {
Some(DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();
new_tags new_tags
} else { } else {
@ -830,25 +850,40 @@ impl Application {
tags, tags,
source: diagnostic.source.clone(), source: diagnostic.source.clone(),
data: diagnostic.data.clone(), data: diagnostic.data.clone(),
language_server_id: server_id,
}) })
}) })
.collect(); .collect();
doc.set_diagnostics(diagnostics); doc.replace_diagnostics(diagnostics, server_id);
} }
// Sort diagnostics first by severity and then by line numbers. let mut diagnostics = params
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
.diagnostics .diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start)); .into_iter()
.map(|d| (d, server_id))
.collect();
// Insert the original lsp::Diagnostics here because we may have no open document // Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position. // for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand. // When using them later in the diagnostics picker, we calculate them on-demand.
self.editor match self.editor.diagnostics.entry(params.uri) {
.diagnostics Entry::Occupied(o) => {
.insert(params.uri, params.diagnostics); let current_diagnostics = o.into_mut();
// there may entries of other language servers, which is why we can't overwrite the whole entry
current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
current_diagnostics.append(&mut diagnostics);
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
current_diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
}
Entry::Vacant(v) => {
diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
v.insert(diagnostics);
}
};
} }
Notification::ShowMessage(params) => { Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params); log::warn!("unhandled window/showMessage: {:?}", params);
@ -945,24 +980,18 @@ impl Application {
Notification::Exit => { Notification::Exit => {
self.editor.set_status("Language server exited"); self.editor.set_status("Language server exited");
// Clear any diagnostics for documents with this server open. // LSPs may produce diagnostics for files that haven't been opened in helix,
let urls: Vec<_> = self // we need to clear those and remove the entries from the list if this leads to
.editor // an empty diagnostic list for said files
.documents_mut() for diags in self.editor.diagnostics.values_mut() {
.filter_map(|doc| { diags.retain(|(_, lsp_id)| *lsp_id != server_id);
if doc.language_server().map(|server| server.id()) }
== Some(server_id)
{
doc.set_diagnostics(Vec::new());
doc.url()
} else {
None
}
})
.collect();
for url in urls { self.editor.diagnostics.retain(|_, diags| !diags.is_empty());
self.editor.diagnostics.remove(&url);
// Clear any diagnostics for documents with this server open.
for doc in self.editor.documents_mut() {
doc.clear_diagnostics(server_id);
} }
// Remove the language server from the registry. // Remove the language server from the registry.
@ -1029,31 +1058,21 @@ impl Application {
})) }))
} }
Ok(MethodCall::WorkspaceFolders) => { Ok(MethodCall::WorkspaceFolders) => {
let language_server = Ok(json!(&*language_server!().workspace_folders().await))
self.editor.language_servers.get_by_id(server_id).unwrap();
Ok(json!(&*language_server.workspace_folders().await))
} }
Ok(MethodCall::WorkspaceConfiguration(params)) => { Ok(MethodCall::WorkspaceConfiguration(params)) => {
let language_server = language_server!();
let result: Vec<_> = params let result: Vec<_> = params
.items .items
.iter() .iter()
.map(|item| { .map(|item| {
let mut config = match &item.scope_uri { let mut config = language_server.config()?;
Some(scope) => {
let path = scope.to_file_path().ok()?;
let doc = self.editor.document_by_path(path)?;
doc.language_config()?.config.as_ref()?
}
None => self
.editor
.language_servers
.get_by_id(server_id)?
.config()?,
};
if let Some(section) = item.section.as_ref() { if let Some(section) = item.section.as_ref() {
for part in section.split('.') { // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
config = config.get(part)?; if !section.is_empty() {
for part in section.split('.') {
config = config.get(part)?;
}
} }
} }
Some(config) Some(config)
@ -1074,15 +1093,7 @@ impl Application {
} }
}; };
let language_server = match self.editor.language_servers.get_by_id(server_id) { tokio::spawn(language_server!().reply(id, reply));
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
tokio::spawn(language_server.reply(id, reply));
} }
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
} }

@ -23,17 +23,18 @@ use helix_core::{
regex::{self, Regex, RegexBuilder}, regex::{self, Regex, RegexBuilder},
search::{self, CharMatcher}, search::{self, CharMatcher},
selection, shellwords, surround, selection, shellwords, surround,
syntax::LanguageServerFeature,
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
textobject, textobject,
tree_sitter::Node, tree_sitter::Node,
unicode::width::UnicodeWidthChar, unicode::width::UnicodeWidthChar,
visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
Selection, SmallVec, Tendril, Transaction, RopeSlice, Selection, SmallVec, Tendril, Transaction,
}; };
use helix_view::{ use helix_view::{
clipboard::ClipboardType, clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion}, editor::{Action, CompleteAction, Motion},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::KeyCode, keyboard::KeyCode,
@ -54,13 +55,13 @@ use crate::{
job::Callback, job::Callback,
keymap::ReverseKeymap, keymap::ReverseKeymap,
ui::{ ui::{
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
Popup, Prompt, PromptEvent, FilePicker, Picker, Popup, Prompt, PromptEvent,
}, },
}; };
use crate::job::{self, Jobs}; use crate::job::{self, Jobs};
use futures_util::StreamExt; use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize}; use std::{collections::HashSet, num::NonZeroUsize};
@ -97,6 +98,13 @@ impl<'a> Context<'a> {
})); }));
} }
/// 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);
}));
}
#[inline] #[inline]
pub fn on_next_key( pub fn on_next_key(
&mut self, &mut self,
@ -267,6 +275,7 @@ impl MappableCommand {
select_regex, "Select all regex matches inside selections", select_regex, "Select all regex matches inside selections",
split_selection, "Split selections on regex matches", split_selection, "Split selections on regex matches",
split_selection_on_newline, "Split selection on newlines", split_selection_on_newline, "Split selection on newlines",
merge_selections, "Merge selections",
merge_consecutive_selections, "Merge consecutive selections", merge_consecutive_selections, "Merge consecutive selections",
search, "Search for regex pattern", search, "Search for regex pattern",
rsearch, "Reverse search for regex pattern", rsearch, "Reverse search for regex pattern",
@ -790,54 +799,50 @@ fn extend_to_line_start(cx: &mut Context) {
} }
fn kill_to_line_start(cx: &mut Context) { fn kill_to_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor); delete_by_selection_insert_mode(
let text = doc.text().slice(..); cx,
move |text, range| {
let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text);
let line = range.cursor_line(text); let first_char = text.line_to_char(line);
let first_char = text.line_to_char(line); let anchor = range.cursor(text);
let anchor = range.cursor(text); let head = if anchor == first_char && line != 0 {
let head = if anchor == first_char && line != 0 { // select until previous line
// select until previous line line_end_char_index(&text, line - 1)
line_end_char_index(&text, line - 1) } else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
} else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) { if first_char + pos < anchor {
if first_char + pos < anchor { // select until first non-blank in line if cursor is after it
// select until first non-blank in line if cursor is after it first_char + pos
first_char + pos } else {
// select until start of line
first_char
}
} else { } else {
// select until start of line // select until start of line
first_char first_char
} };
} else { (head, anchor)
// select until start of line },
first_char Direction::Backward,
}; );
Range::new(head, anchor)
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
fn kill_to_line_end(cx: &mut Context) { fn kill_to_line_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor); delete_by_selection_insert_mode(
let text = doc.text().slice(..); cx,
|text, range| {
let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text);
let line = range.cursor_line(text); let line_end_pos = line_end_char_index(&text, line);
let line_end_pos = line_end_char_index(&text, line); let pos = range.cursor(text);
let pos = range.cursor(text);
let mut new_range = range.put_cursor(text, line_end_pos, true);
// don't want to remove the line separator itself if the cursor doesn't reach the end of line.
if pos != line_end_pos {
new_range.head = line_end_pos;
}
new_range
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); // if the cursor is on the newline char delete that
if pos == line_end_pos {
(pos, text.line_to_char(line + 1))
} else {
(pos, line_end_pos)
}
},
Direction::Forward,
);
} }
fn goto_first_nonwhitespace(cx: &mut Context) { fn goto_first_nonwhitespace(cx: &mut Context) {
@ -1263,7 +1268,7 @@ where
find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| { cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
find_char_impl(editor, &search_fn, inclusive, true, ch, 1); find_char_impl(editor, &search_fn, inclusive, extend, ch, 1);
}))); })));
}) })
} }
@ -1735,6 +1740,12 @@ fn split_selection_on_newline(cx: &mut Context) {
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
} }
fn merge_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id).clone().merge_ranges();
doc.set_selection(view.id, selection);
}
fn merge_consecutive_selections(cx: &mut Context) { fn merge_consecutive_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id).clone().merge_consecutive_ranges(); let selection = doc.selection(view.id).clone().merge_consecutive_ranges();
@ -2310,9 +2321,8 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
}; };
// then delete // then delete
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let transaction =
(range.from(), range.to(), None) Transaction::delete_by_selection(doc.text(), selection, |range| (range.from(), range.to()));
});
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
match op { match op {
@ -2327,11 +2337,49 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
} }
#[inline] #[inline]
fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: &Selection) { fn delete_by_selection_insert_mode(
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { cx: &mut Context,
(range.from(), range.to(), None) mut f: impl FnMut(RopeSlice, &Range) -> Deletion,
}); direction: Direction,
) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let mut selection = SmallVec::new();
let mut insert_newline = false;
let text_len = text.len_chars();
let mut transaction =
Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| {
let (start, end) = f(text, range);
if direction == Direction::Forward {
let mut range = *range;
if range.head > range.anchor {
insert_newline |= end == text_len;
// move the cursor to the right so that the selection
// doesn't shrink when deleting forward (so the text appears to
// move to left)
// += 1 is enough here as the range is normalized to grapheme boundaries
// later anyway
range.head += 1;
}
selection.push(range);
}
(start, end)
});
// in case we delete the last character and the cursor would be moved to the EOF char
// insert a newline, just like when entering append mode
if insert_newline {
transaction = transaction.insert_at_eof(doc.line_ending.as_str().into());
}
if direction == Direction::Forward {
doc.set_selection(
view.id,
Selection::new(selection, doc.selection(view.id).primary_index()),
);
}
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
fn delete_selection(cx: &mut Context) { fn delete_selection(cx: &mut Context) {
@ -2475,6 +2523,7 @@ fn buffer_picker(cx: &mut Context) {
path: Option<PathBuf>, path: Option<PathBuf>,
is_modified: bool, is_modified: bool,
is_current: bool, is_current: bool,
focused_at: std::time::Instant,
} }
impl ui::menu::Item for BufferMeta { impl ui::menu::Item for BufferMeta {
@ -2507,14 +2556,21 @@ fn buffer_picker(cx: &mut Context) {
path: doc.path().cloned(), path: doc.path().cloned(),
is_modified: doc.is_modified(), is_modified: doc.is_modified(),
is_current: doc.id() == current, is_current: doc.id() == current,
focused_at: doc.focused_at,
}; };
let mut items = cx
.editor
.documents
.values()
.map(|doc| new_meta(doc))
.collect::<Vec<BufferMeta>>();
// mru
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
let picker = FilePicker::new( let picker = FilePicker::new(
cx.editor items,
.documents
.values()
.map(|doc| new_meta(doc))
.collect(),
(), (),
|cx, meta, action| { |cx, meta, action| {
cx.editor.switch(meta.id, action); cx.editor.switch(meta.id, action);
@ -2983,7 +3039,7 @@ fn exit_select_mode(cx: &mut Context) {
fn goto_first_diag(cx: &mut Context) { fn goto_first_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().first() { let selection = match doc.shown_diagnostics().next() {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
@ -2992,7 +3048,7 @@ fn goto_first_diag(cx: &mut Context) {
fn goto_last_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().last() { let selection = match doc.shown_diagnostics().last() {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
@ -3008,10 +3064,9 @@ fn goto_next_diag(cx: &mut Context) {
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diag = doc let diag = doc
.diagnostics() .shown_diagnostics()
.iter()
.find(|diag| diag.range.start > cursor_pos) .find(|diag| diag.range.start > cursor_pos)
.or_else(|| doc.diagnostics().first()); .or_else(|| doc.shown_diagnostics().next());
let selection = match diag { let selection = match diag {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
@ -3029,11 +3084,10 @@ fn goto_prev_diag(cx: &mut Context) {
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diag = doc let diag = doc
.diagnostics() .shown_diagnostics()
.iter()
.rev() .rev()
.find(|diag| diag.range.start < cursor_pos) .find(|diag| diag.range.start < cursor_pos)
.or_else(|| doc.diagnostics().last()); .or_else(|| doc.shown_diagnostics().last());
let selection = match diag { let selection = match diag {
// NOTE: the selection is reversed because we're jumping to the // NOTE: the selection is reversed because we're jumping to the
@ -3188,23 +3242,19 @@ pub mod insert {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches completion char, trigger completion // if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() { let trigger_completion = doc
Some(language_server) => language_server, .language_servers_with_feature(LanguageServerFeature::Completion)
None => return, .any(|ls| {
}; // TODO: what if trigger is multiple chars long
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
let capabilities = language_server.capabilities(); trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
});
if let Some(lsp::CompletionOptions { if trigger_completion {
trigger_characters: Some(triggers), cx.editor.clear_idle_timer();
.. super::completion(cx);
}) = &capabilities.completion_provider
{
// TODO: what if trigger is multiple chars long
if triggers.iter().any(|trigger| trigger.contains(ch)) {
cx.editor.clear_idle_timer();
super::completion(cx);
}
} }
} }
@ -3212,12 +3262,12 @@ pub mod insert {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches signature_help char, trigger // if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
// The language_server!() macro is not used here since it will // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
// print an "LSP not active for current buffer" message on let Some(language_server) = doc
// every keypress. .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
let language_server = match doc.language_server() { .next()
Some(language_server) => language_server, else {
None => return, return;
}; };
let capabilities = language_server.capabilities(); let capabilities = language_server.capabilities();
@ -3409,10 +3459,10 @@ pub mod insert {
let auto_pairs = doc.auto_pairs(cx.editor); let auto_pairs = doc.auto_pairs(cx.editor);
let transaction = let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| {
let pos = range.cursor(text); let pos = range.cursor(text);
if pos == 0 { if pos == 0 {
return (pos, pos, None); return (pos, pos);
} }
let line_start_pos = text.line_to_char(range.cursor_line(text)); let line_start_pos = text.line_to_char(range.cursor_line(text));
// consider to delete by indent level if all characters before `pos` are indent units. // consider to delete by indent level if all characters before `pos` are indent units.
@ -3420,11 +3470,7 @@ pub mod insert {
if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') { if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
if text.get_char(pos.saturating_sub(1)) == Some('\t') { if text.get_char(pos.saturating_sub(1)) == Some('\t') {
// fast path, delete one char // fast path, delete one char
( (graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos)
graphemes::nth_prev_grapheme_boundary(text, pos, 1),
pos,
None,
)
} else { } else {
let width: usize = fragment let width: usize = fragment
.chars() .chars()
@ -3451,7 +3497,7 @@ pub mod insert {
_ => break, _ => break,
} }
} }
(start, pos, None) // delete! (start, pos) // delete!
} }
} else { } else {
match ( match (
@ -3469,17 +3515,12 @@ pub mod insert {
( (
graphemes::nth_prev_grapheme_boundary(text, pos, count), graphemes::nth_prev_grapheme_boundary(text, pos, count),
graphemes::nth_next_grapheme_boundary(text, pos, count), graphemes::nth_next_grapheme_boundary(text, pos, count),
None,
) )
} }
_ => _ =>
// delete 1 char // delete 1 char
{ {
( (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos)
graphemes::nth_prev_grapheme_boundary(text, pos, count),
pos,
None,
)
} }
} }
} }
@ -3492,50 +3533,40 @@ pub mod insert {
pub fn delete_char_forward(cx: &mut Context) { pub fn delete_char_forward(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); delete_by_selection_insert_mode(
let text = doc.text().slice(..); cx,
let transaction = |text, range| {
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let pos = range.cursor(text); let pos = range.cursor(text);
( (pos, graphemes::nth_next_grapheme_boundary(text, pos, count))
pos, },
graphemes::nth_next_grapheme_boundary(text, pos, count), Direction::Forward,
None, )
)
});
doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
pub fn delete_word_backward(cx: &mut Context) { pub fn delete_word_backward(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); delete_by_selection_insert_mode(
let text = doc.text().slice(..); cx,
|text, range| {
let selection = doc.selection(view.id).clone().transform(|range| { let anchor = movement::move_prev_word_start(text, *range, count).from();
let anchor = movement::move_prev_word_start(text, range, count).from(); let next = Range::new(anchor, range.cursor(text));
let next = Range::new(anchor, range.cursor(text)); let range = exclude_cursor(text, next, *range);
exclude_cursor(text, next, range) (range.from(), range.to())
}); },
delete_selection_insert_mode(doc, view, &selection); Direction::Backward,
);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
pub fn delete_word_forward(cx: &mut Context) { pub fn delete_word_forward(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); delete_by_selection_insert_mode(
let text = doc.text().slice(..); cx,
|text, range| {
let selection = doc.selection(view.id).clone().transform(|range| { let head = movement::move_next_word_end(text, *range, count).to();
let head = movement::move_next_word_end(text, range, count).to(); (range.cursor(text), head)
Range::new(range.cursor(text), head) },
}); Direction::Forward,
);
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
} }
@ -4019,55 +4050,60 @@ fn format_selections(cx: &mut Context) {
use helix_lsp::{lsp, util::range_to_lsp_range}; use helix_lsp::{lsp, util::range_to_lsp_range};
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id;
// via lsp if available // via lsp if available
// TODO: else via tree-sitter indentation calculations // TODO: else via tree-sitter indentation calculations
let language_server = match doc.language_server() { if doc.selection(view_id).len() != 1 {
Some(language_server) => language_server, cx.editor
None => return, .set_error("format_selections only supports a single selection for now");
return;
}
// TODO extra LanguageServerFeature::FormatSelections?
// maybe such that LanguageServerFeature::Format contains it as well
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::Format)
.find(|ls| {
matches!(
ls.capabilities().document_range_formatting_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
})
else {
cx.editor
.set_error("No configured language server does not support range formatting");
return;
}; };
let offset_encoding = language_server.offset_encoding();
let ranges: Vec<lsp::Range> = doc let ranges: Vec<lsp::Range> = doc
.selection(view.id) .selection(view_id)
.iter() .iter()
.map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding())) .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
.collect(); .collect();
if ranges.len() != 1 {
cx.editor
.set_error("format_selections only supports a single selection for now");
return;
}
// TODO: handle fails // TODO: handle fails
// TODO: concurrent map over all ranges // TODO: concurrent map over all ranges
let range = ranges[0]; let range = ranges[0];
let request = match language_server.text_document_range_formatting( let future = language_server
doc.identifier(), .text_document_range_formatting(
range, doc.identifier(),
lsp::FormattingOptions::default(), range,
None, lsp::FormattingOptions::default(),
) { None,
Some(future) => future, )
None => { .unwrap();
cx.editor
.set_error("Language server does not support range formatting");
return;
}
};
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default(); let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
let transaction = helix_lsp::util::generate_transaction_from_edits( let transaction =
doc.text(), helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
edits,
language_server.offset_encoding(),
);
doc.apply(&transaction, view.id); doc.apply(&transaction, view_id);
} }
fn join_selections_impl(cx: &mut Context, select_space: bool) { fn join_selections_impl(cx: &mut Context, select_space: bool) {
@ -4197,21 +4233,53 @@ pub fn completion(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let language_server = match doc.language_server() { let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
Some(language_server) => language_server, {
None => return, savepoint.clone()
} else {
doc.savepoint(view)
}; };
let offset_encoding = language_server.offset_encoding(); let text = savepoint.text.clone();
let text = doc.text().slice(..); let cursor = savepoint.cursor();
let cursor = doc.selection(view.id).primary().cursor(text);
let mut seen_language_servers = HashSet::new();
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
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();
let future = match language_server.completion(doc.identifier(), pos, None) { anyhow::Ok(items)
Some(future) => future, }
None => return, })
}; .collect();
// setup a channel that allows the request to be canceled // setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -4220,12 +4288,20 @@ pub fn completion(cx: &mut Context) {
// and the associated request is automatically dropped // and the associated request is automatically dropped
cx.editor.completion_request_handle = Some(tx); cx.editor.completion_request_handle = Some(tx);
let future = async move { 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! { tokio::select! {
biased; biased;
_ = rx => { _ = rx => {
Ok(serde_json::Value::Null) Ok(Vec::new())
} }
res = future => { res = items_future => {
res res
} }
} }
@ -4241,7 +4317,6 @@ pub fn completion(cx: &mut Context) {
iter.reverse(); iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset); let start_offset = cursor.saturating_sub(offset);
let savepoint = doc.savepoint(view);
let trigger_doc = doc.id(); let trigger_doc = doc.id();
let trigger_view = view.id; let trigger_view = view.id;
@ -4260,9 +4335,9 @@ pub fn completion(cx: &mut Context) {
}, },
)); ));
cx.callback( cx.jobs.callback(async move {
future, let items = future.await?;
move |editor, compositor, response: Option<lsp::CompletionResponse>| { let call = move |editor: &mut Editor, compositor: &mut Compositor| {
let (view, doc) = current_ref!(editor); let (view, doc) = current_ref!(editor);
// check if the completion request is stale. // check if the completion request is stale.
// //
@ -4273,16 +4348,6 @@ pub fn completion(cx: &mut Context) {
return; return;
} }
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(),
};
if items.is_empty() { if items.is_empty() {
// editor.set_error("No completion available"); // editor.set_error("No completion available");
return; return;
@ -4293,7 +4358,6 @@ pub fn completion(cx: &mut Context) {
editor, editor,
savepoint, savepoint,
items, items,
offset_encoding,
start_offset, start_offset,
trigger_offset, trigger_offset,
size, size,
@ -4307,8 +4371,9 @@ pub fn completion(cx: &mut Context) {
{ {
compositor.remove(SignatureHelp::ID); compositor.remove(SignatureHelp::ID);
} }
}, };
); Ok(Callback::EditorCompositor(Box::new(call)))
});
} }
// comments // comments
@ -4465,20 +4530,23 @@ fn select_prev_sibling(cx: &mut Context) {
fn match_brackets(cx: &mut Context) { fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let is_select = cx.editor.mode == Mode::Select;
let text = doc.text();
let text_slice = text.slice(..);
if let Some(syntax) = doc.syntax() { let selection = doc.selection(view.id).clone().transform(|range| {
let text = doc.text().slice(..); let pos = range.cursor(text_slice);
let selection = doc.selection(view.id).clone().transform(|range| { if let Some(matched_pos) = doc.syntax().map_or_else(
if let Some(pos) = || match_brackets::find_matching_bracket_current_line_plaintext(text, pos),
match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.cursor(text)) |syntax| match_brackets::find_matching_bracket_fuzzy(syntax, text, pos),
{ ) {
range.put_cursor(text, pos, cx.editor.mode == Mode::Select) range.put_cursor(text_slice, matched_pos, is_select)
} else { } else {
range range
} }
}); });
doc.set_selection(view.id, selection);
} doc.set_selection(view.id, selection);
} }
// //
@ -5113,9 +5181,10 @@ async fn shell_impl_async(
let output = if let Some(mut stdin) = process.stdin.take() { let output = if let Some(mut stdin) = process.stdin.take() {
let input_task = tokio::spawn(async move { let input_task = tokio::spawn(async move {
if let Some(input) = input { if let Some(input) = input {
helix_view::document::to_writer(&mut stdin, encoding::UTF_8, &input).await?; helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
.await?;
} }
Ok::<_, anyhow::Error>(()) anyhow::Ok(())
}); });
let (output, _) = tokio::join! { let (output, _) = tokio::join! {
process.wait_with_output(), process.wait_with_output(),

@ -580,7 +580,7 @@ pub fn dap_variables(cx: &mut Context) {
let contents = Text::from(tui::text::Text::from(variables)); let contents = Text::from(tui::text::Text::from(variables));
let popup = Popup::new("dap-variables", contents); let popup = Popup::new("dap-variables", contents);
cx.push_layer(Box::new(popup)); cx.replace_or_push_layer("dap-variables", popup);
} }
pub fn dap_terminate(cx: &mut Context) { pub fn dap_terminate(cx: &mut Context) {

File diff suppressed because it is too large Load Diff

@ -8,7 +8,6 @@ use super::*;
use helix_core::{encoding, shellwords::Shellwords}; use helix_core::{encoding, shellwords::Shellwords};
use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent}; use helix_view::editor::{Action, CloseError, ConfigEvent};
use serde_json::Value;
use ui::completers::{self, Completer}; use ui::completers::{self, Completer};
#[derive(Clone)] #[derive(Clone)]
@ -382,6 +381,36 @@ fn force_write(
write_impl(cx, args.first(), true) write_impl(cx, args.first(), true)
} }
fn write_buffer_close(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_impl(cx, args.first(), false)?;
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_write_buffer_close(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_impl(cx, args.first(), true)?;
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn new_file( fn new_file(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -1299,26 +1328,22 @@ fn lsp_workspace_command(
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return Ok(()); return Ok(());
} }
let doc = doc!(cx.editor);
let (_, doc) = current!(cx.editor); let Some((language_server_id, options)) = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
let language_server = match doc.language_server() { .find_map(|ls| {
Some(language_server) => language_server, ls.capabilities()
None => { .execute_command_provider
cx.editor .as_ref()
.set_status("Language server not active for current buffer"); .map(|options| (ls.id(), options))
return Ok(()); })
} else {
cx.editor.set_status(
"No active language servers for this document support workspace commands",
);
return Ok(());
}; };
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
cx.editor
.set_status("Workspace commands are not supported for this language server");
return Ok(());
}
};
if args.is_empty() { if args.is_empty() {
let commands = options let commands = options
.commands .commands
@ -1332,8 +1357,8 @@ fn lsp_workspace_command(
let callback = async move { let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new( let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| { move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| { let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone()); execute_lsp_command(cx.editor, language_server_id, command.clone());
}); });
compositor.push(Box::new(overlaid(picker))) compositor.push(Box::new(overlaid(picker)))
}, },
@ -1346,6 +1371,7 @@ fn lsp_workspace_command(
if options.commands.iter().any(|c| c == &command) { if options.commands.iter().any(|c| c == &command) {
execute_lsp_command( execute_lsp_command(
cx.editor, cx.editor,
language_server_id,
helix_lsp::lsp::Command { helix_lsp::lsp::Command {
title: command.clone(), title: command.clone(),
arguments: None, arguments: None,
@ -1377,7 +1403,6 @@ fn lsp_restart(
.language_config() .language_config()
.context("LSP not defined for the current document")?; .context("LSP not defined for the current document")?;
let scope = config.scope.clone();
cx.editor.language_servers.restart( cx.editor.language_servers.restart(
config, config,
doc.path(), doc.path(),
@ -1390,13 +1415,22 @@ fn lsp_restart(
.editor .editor
.documents() .documents()
.filter_map(|doc| match doc.language_config() { .filter_map(|doc| match doc.language_config() {
Some(config) if config.scope.eq(&scope) => Some(doc.id()), Some(config)
if config.language_servers.iter().any(|ls| {
config
.language_servers
.iter()
.any(|restarted_ls| restarted_ls.name == ls.name)
}) =>
{
Some(doc.id())
}
_ => None, _ => None,
}) })
.collect(); .collect();
for document_id in document_ids_to_refresh { for document_id in document_ids_to_refresh {
cx.editor.refresh_language_server(document_id); cx.editor.refresh_language_servers(document_id);
} }
Ok(()) Ok(())
@ -1411,22 +1445,18 @@ fn lsp_stop(
return Ok(()); return Ok(());
} }
let doc = doc!(cx.editor); let ls_shutdown_names = doc!(cx.editor)
.language_servers()
let ls_id = doc .map(|ls| ls.name().to_string())
.language_server() .collect::<Vec<_>>();
.map(|ls| ls.id())
.context("LSP not running for the current document")?;
let config = doc for ls_name in &ls_shutdown_names {
.language_config() cx.editor.language_servers.stop(ls_name);
.context("LSP not defined for the current document")?;
cx.editor.language_servers.stop(config);
for doc in cx.editor.documents_mut() { for doc in cx.editor.documents_mut() {
if doc.language_server().map_or(false, |ls| ls.id() == ls_id) { if let Some(client) = doc.remove_language_server_by_name(ls_name) {
doc.set_language_server(None); doc.clear_diagnostics(client.id());
doc.set_diagnostics(Default::default()); }
} }
} }
@ -1759,8 +1789,8 @@ fn toggle_option(
return Ok(()); return Ok(());
} }
if args.len() != 1 { if args.is_empty() {
anyhow::bail!("Bad arguments. Usage: `:toggle key`"); anyhow::bail!("Bad arguments. Usage: `:toggle key [values]?`");
} }
let key = &args[0].to_lowercase(); let key = &args[0].to_lowercase();
@ -1770,22 +1800,48 @@ fn toggle_option(
let pointer = format!("/{}", key.replace('.', "/")); let pointer = format!("/{}", key.replace('.', "/"));
let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
let Value::Bool(old_value) = *value else { *value = match value.as_bool() {
anyhow::bail!("Key `{}` is not toggle-able", key) Some(value) => {
ensure!(
args.len() == 1,
"Bad arguments. For boolean configurations use: `:toggle key`"
);
serde_json::Value::Bool(!value)
}
None => {
ensure!(
args.len() > 2,
"Bad arguments. For non-boolean configurations use: `:toggle key val1 val2 ...`",
);
ensure!(
value.is_string(),
"Bad configuration. Cannot cycle non-string configurations"
);
let value = value
.as_str()
.expect("programming error: should have been ensured before");
serde_json::Value::String(
args[1..]
.iter()
.skip_while(|e| *e != value)
.nth(1)
.unwrap_or_else(|| &args[1])
.to_string(),
)
}
}; };
let new_value = !old_value; let status = format!("'{key}' is now set to {value}");
*value = Value::Bool(new_value); let config = serde_json::from_value(config)
// This unwrap should never fail because we only replace one boolean value .map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?;
// with another, maintaining a valid json config
let config = serde_json::from_value(config).unwrap();
cx.editor cx.editor
.config_events .config_events
.0 .0
.send(ConfigEvent::Update(config))?; .send(ConfigEvent::Update(config))?;
cx.editor cx.editor.set_status(status);
.set_status(format!("Option `{}` is now set to `{}`", key, new_value));
Ok(()) Ok(())
} }
@ -1820,7 +1876,7 @@ fn language(
doc.detect_indent_and_line_ending(); doc.detect_indent_and_line_ending();
let id = doc.id(); let id = doc.id();
cx.editor.refresh_language_server(id); cx.editor.refresh_language_servers(id);
Ok(()) Ok(())
} }
@ -2167,6 +2223,38 @@ fn reset_diff_change(
Ok(()) Ok(())
} }
fn clear_register(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
ensure!(args.len() <= 1, ":clear-register takes at most 1 argument");
if args.is_empty() {
cx.editor.registers.clear();
cx.editor.set_status("All registers cleared");
return Ok(());
}
ensure!(
args[0].chars().count() == 1,
format!("Invalid register {}", args[0])
);
let register = args[0].chars().next().unwrap_or_default();
match cx.editor.registers.remove(register) {
Some(_) => cx
.editor
.set_status(format!("Register {} cleared", register)),
None => cx
.editor
.set_error(format!("Register {} not found", register)),
}
Ok(())
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "quit", name: "quit",
@ -2255,10 +2343,24 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "write!", name: "write!",
aliases: &["w!"], aliases: &["w!"],
doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write some/path.txt)", doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
fun: force_write, fun: force_write,
signature: CommandSignature::positional(&[completers::filename]), signature: CommandSignature::positional(&[completers::filename]),
}, },
TypableCommand {
name: "write-buffer-close",
aliases: &["wbc"],
doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
fun: write_buffer_close,
signature: CommandSignature::positional(&[completers::filename]),
},
TypableCommand {
name: "write-buffer-close!",
aliases: &["wbc!"],
doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
fun: force_write_buffer_close,
signature: CommandSignature::positional(&[completers::filename]),
},
TypableCommand { TypableCommand {
name: "new", name: "new",
aliases: &["n"], aliases: &["n"],
@ -2512,14 +2614,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "lsp-restart", name: "lsp-restart",
aliases: &[], aliases: &[],
doc: "Restarts the Language Server that is in use by the current doc", doc: "Restarts the language servers used by the current doc",
fun: lsp_restart, fun: lsp_restart,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
TypableCommand { TypableCommand {
name: "lsp-stop", name: "lsp-stop",
aliases: &[], aliases: &[],
doc: "Stops the Language Server that is in use by the current doc", doc: "Stops the language servers that are used by the current doc",
fun: lsp_stop, fun: lsp_stop,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
@ -2720,6 +2822,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: reset_diff_change, fun: reset_diff_change,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
TypableCommand {
name: "clear-register",
aliases: &[],
doc: "Clear given register. If no argument is provided, clear all registers.",
fun: clear_register,
signature: CommandSignature::none(),
},
]; ];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> = pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
@ -2764,13 +2873,10 @@ pub(super) fn command_mode(cx: &mut Context) {
} else { } else {
// Otherwise, use the command's completer and the last shellword // Otherwise, use the command's completer and the last shellword
// as completion input. // as completion input.
let (part, part_len) = if words.len() == 1 || shellwords.ends_with_whitespace() { let (word, word_len) = if words.len() == 1 || shellwords.ends_with_whitespace() {
(&Cow::Borrowed(""), 0) (&Cow::Borrowed(""), 0)
} else { } else {
( (words.last().unwrap(), words.last().unwrap().len())
words.last().unwrap(),
shellwords.parts().last().unwrap().len(),
)
}; };
let argument_number = argument_number_of(&shellwords); let argument_number = argument_number_of(&shellwords);
@ -2779,13 +2885,13 @@ pub(super) fn command_mode(cx: &mut Context) {
.get(&words[0] as &str) .get(&words[0] as &str)
.map(|tc| tc.completer_for_argument_number(argument_number)) .map(|tc| tc.completer_for_argument_number(argument_number))
{ {
completer(editor, part) completer(editor, word)
.into_iter() .into_iter()
.map(|(range, file)| { .map(|(range, file)| {
let file = shellwords::escape(file); let file = shellwords::escape(file);
// offset ranges to input // offset ranges to input
let offset = input.len() - part_len; let offset = input.len() - word_len;
let range = (range.start + offset)..; let range = (range.start + offset)..;
(range, file) (range, file)
}) })

@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> {
for lang in &syn_loader_conf.language { for lang in &syn_loader_conf.language {
column(&lang.language_id, Color::Reset); column(&lang.language_id, Color::Reset);
let lsp = lang // TODO multiple language servers (check binary for each supported language server, not just the first)
.language_server
.as_ref() let lsp = lang.language_servers.first().and_then(|ls| {
.map(|lsp| lsp.command.to_string()); syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| config.command.clone())
});
check_binary(lsp); check_binary(lsp);
let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@ -264,11 +268,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
} }
}; };
// TODO multiple language servers
probe_protocol( probe_protocol(
"language server", "language server",
lang.language_server lang.language_servers.first().and_then(|ls| {
.as_ref() syn_loader_conf
.map(|lsp| lsp.command.to_string()), .language_server
.get(&ls.name)
.map(|config| config.command.clone())
}),
)?; )?;
probe_protocol( probe_protocol(

@ -79,6 +79,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"s" => select_regex, "s" => select_regex,
"A-s" => split_selection_on_newline, "A-s" => split_selection_on_newline,
"A-minus" => merge_selections,
"A-_" => merge_consecutive_selections, "A-_" => merge_consecutive_selections,
"S" => split_selection, "S" => split_selection,
";" => collapse_selection, ";" => collapse_selection,

@ -15,8 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
use crate::commands; use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::{lsp, util}; use helix_lsp::{lsp, util, OffsetEncoding};
use lsp::CompletionItem;
impl menu::Item for CompletionItem { impl menu::Item for CompletionItem {
type Data = (); type Data = ();
@ -26,28 +25,30 @@ impl menu::Item for CompletionItem {
#[inline] #[inline]
fn filter_text(&self, _data: &Self::Data) -> Cow<str> { fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
self.filter_text self.item
.filter_text
.as_ref() .as_ref()
.unwrap_or(&self.label) .unwrap_or(&self.item.label)
.as_str() .as_str()
.into() .into()
} }
fn format(&self, _data: &Self::Data) -> menu::Row { fn format(&self, _data: &Self::Data) -> menu::Row {
let deprecated = self.deprecated.unwrap_or_default() let deprecated = self.item.deprecated.unwrap_or_default()
|| self.tags.as_ref().map_or(false, |tags| { || self.item.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED) tags.contains(&lsp::CompletionItemTag::DEPRECATED)
}); });
menu::Row::new(vec![ menu::Row::new(vec![
menu::Cell::from(Span::styled( menu::Cell::from(Span::styled(
self.label.as_str(), self.item.label.as_str(),
if deprecated { if deprecated {
Style::default().add_modifier(Modifier::CROSSED_OUT) Style::default().add_modifier(Modifier::CROSSED_OUT)
} else { } else {
Style::default() Style::default()
}, },
)), )),
menu::Cell::from(match self.kind { menu::Cell::from(match self.item.kind {
Some(lsp::CompletionItemKind::TEXT) => "text", Some(lsp::CompletionItemKind::TEXT) => "text",
Some(lsp::CompletionItemKind::METHOD) => "method", Some(lsp::CompletionItemKind::METHOD) => "method",
Some(lsp::CompletionItemKind::FUNCTION) => "function", Some(lsp::CompletionItemKind::FUNCTION) => "function",
@ -79,15 +80,17 @@ impl menu::Item for CompletionItem {
} }
None => "", None => "",
}), }),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
]) ])
} }
} }
#[derive(Debug, PartialEq, Default, Clone)]
pub struct CompletionItem {
pub item: lsp::CompletionItem,
pub language_server_id: usize,
pub resolved: bool,
}
/// Wraps a Menu. /// Wraps a Menu.
pub struct Completion { pub struct Completion {
popup: Popup<Menu<CompletionItem>>, popup: Popup<Menu<CompletionItem>>,
@ -104,21 +107,20 @@ impl Completion {
editor: &Editor, editor: &Editor,
savepoint: Arc<SavePoint>, savepoint: Arc<SavePoint>,
mut items: Vec<CompletionItem>, mut items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
let replace_mode = editor.config().completion_replace; let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server) // Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false)); items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
// Then create the menu // Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn item_to_transaction(
doc: &Document, doc: &Document,
view_id: ViewId, view_id: ViewId,
item: &CompletionItem, item: &lsp::CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: OffsetEncoding,
trigger_offset: usize, trigger_offset: usize,
include_placeholder: bool, include_placeholder: bool,
replace_mode: bool, replace_mode: bool,
@ -209,77 +211,107 @@ impl Completion {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
// if more text was entered, remove it macro_rules! language_server {
doc.restore(view, &savepoint); ($item:expr) => {
match editor
.language_servers
.get_by_id($item.language_server_id)
{
Some(ls) => ls,
None => {
editor.set_error("completions are outdated");
// TODO close the completion menu somehow,
// currently there is no trivial way to access the EditorView to close the completion menu
return;
}
}
};
}
match event { match event {
PromptEvent::Abort => { PromptEvent::Abort => {}
editor.last_completion = None;
}
PromptEvent::Update => { PromptEvent::Update => {
// Update creates "ghost" transactions which are not sent to the
// lsp server to avoid messing up re-requesting completions. Once a
// completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything
// is typed. The only way to avoid that is to explicitly abort the completion
// with c-c. This will remove the "ghost" transaction.
//
// The ghost transaction is modeled with a transaction that is not sent to the LS.
// (apply_temporary) and a savepoint. It's extremely important this savepoint is restored
// (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() {
editor.last_completion = Some(CompleteAction::Selected {
savepoint: doc.savepoint(view),
})
}
// if more text was entered, remove it
doc.restore(view, &savepoint, false);
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
item, &item.item,
offset_encoding, language_server!(item).offset_encoding(),
trigger_offset, trigger_offset,
true, true,
replace_mode, replace_mode,
); );
doc.apply_temporary(&transaction, view.id);
// initialize a savepoint
doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction {
trigger_offset,
changes: completion_changes(&transaction, trigger_offset),
});
} }
PromptEvent::Validate => { PromptEvent::Validate => {
if let Some(CompleteAction::Selected { savepoint }) =
editor.last_completion.take()
{
doc.restore(view, &savepoint, false);
}
// always present here // always present here
let item = item.unwrap(); let mut item = item.unwrap().clone();
let language_server = language_server!(item);
let offset_encoding = language_server.offset_encoding();
let language_server = editor
.language_servers
.get_by_id(item.language_server_id)
.unwrap();
// resolve item if not yet resolved
if !item.resolved {
if let Some(resolved) =
Self::resolve_completion_item(language_server, item.item.clone())
{
item.item = resolved;
}
};
// if more text was entered, remove it
doc.restore(view, &savepoint, true);
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
item, &item.item,
offset_encoding, offset_encoding,
trigger_offset, trigger_offset,
false, false,
replace_mode, replace_mode,
); );
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction { editor.last_completion = Some(CompleteAction::Applied {
trigger_offset, trigger_offset,
changes: completion_changes(&transaction, trigger_offset), changes: completion_changes(&transaction, trigger_offset),
}); });
// apply additional edits, mostly used to auto import unqualified types // TODO: add additional _edits to completion_changes?
let resolved_item = if item if let Some(additional_edits) = item.item.additional_text_edits {
.additional_text_edits
.as_ref()
.map(|edits| !edits.is_empty())
.unwrap_or(false)
{
None
} else {
Self::resolve_completion_item(doc, item.clone())
};
if let Some(additional_edits) = resolved_item
.as_ref()
.and_then(|item| item.additional_text_edits.as_ref())
.or(item.additional_text_edits.as_ref())
{
if !additional_edits.is_empty() { if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits( let transaction = util::generate_transaction_from_edits(
doc.text(), doc.text(),
additional_edits.clone(), additional_edits,
offset_encoding, // TODO: should probably transcode in Client offset_encoding, // TODO: should probably transcode in Client
); );
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
@ -304,11 +336,9 @@ impl Completion {
} }
fn resolve_completion_item( fn resolve_completion_item(
doc: &Document, language_server: &helix_lsp::Client,
completion_item: lsp::CompletionItem, completion_item: lsp::CompletionItem,
) -> Option<CompletionItem> { ) -> Option<lsp::CompletionItem> {
let language_server = doc.language_server()?;
let future = language_server.resolve_completion_item(completion_item)?; let future = language_server.resolve_completion_item(completion_item)?;
let response = helix_lsp::block_on(future); let response = helix_lsp::block_on(future);
match response { match response {
@ -359,7 +389,7 @@ impl Completion {
self.popup.contents().is_empty() self.popup.contents().is_empty()
} }
fn replace_item(&mut self, old_item: lsp::CompletionItem, new_item: lsp::CompletionItem) { fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
self.popup.contents_mut().replace_option(old_item, new_item); self.popup.contents_mut().replace_option(old_item, new_item);
} }
@ -375,20 +405,14 @@ impl Completion {
// > The returned completion item should have the documentation property filled in. // > The returned completion item should have the documentation property filled in.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
let current_item = match self.popup.contents().selection() { let current_item = match self.popup.contents().selection() {
Some(item) if item.documentation.is_none() => item.clone(), Some(item) if !item.resolved => item.clone(),
_ => return false, _ => return false,
}; };
let language_server = match doc!(cx.editor).language_server() { let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
Some(language_server) => language_server,
None => return false,
};
// This method should not block the compositor so we handle the response asynchronously. // This method should not block the compositor so we handle the response asynchronously.
let future = match language_server.resolve_completion_item(current_item.clone()) { let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
Some(future) => future,
None => return false,
};
cx.callback( cx.callback(
future, future,
@ -403,6 +427,12 @@ impl Completion {
.unwrap() .unwrap()
.completion .completion
{ {
let resolved_item = CompletionItem {
item: resolved_item,
language_server_id: current_item.language_server_id,
resolved: true,
};
completion.replace_item(current_item, resolved_item); completion.replace_item(current_item, resolved_item);
} }
}, },
@ -457,25 +487,25 @@ impl Component for Completion {
Markdown::new(md, cx.editor.syn_loader.clone()) Markdown::new(md, cx.editor.syn_loader.clone())
}; };
let mut markdown_doc = match &option.documentation { let mut markdown_doc = match &option.item.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText, kind: lsp::MarkupKind::PlainText,
value: contents, value: contents,
})) => { })) => {
// TODO: convert to wrapped text // TODO: convert to wrapped text
markdowned(language, option.detail.as_deref(), Some(contents)) markdowned(language, option.item.detail.as_deref(), Some(contents))
} }
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown, kind: lsp::MarkupKind::Markdown,
value: contents, value: contents,
})) => { })) => {
// TODO: set language based on doc scope // TODO: set language based on doc scope
markdowned(language, option.detail.as_deref(), Some(contents)) markdowned(language, option.item.detail.as_deref(), Some(contents))
} }
None if option.detail.is_some() => { None if option.item.detail.is_some() => {
// TODO: set language based on doc scope // TODO: set language based on doc scope
markdowned(language, option.detail.as_deref(), None) markdowned(language, option.item.detail.as_deref(), None)
} }
None => return, None => return,
}; };

@ -19,7 +19,7 @@ use helix_core::{
syntax::{self, HighlightEvent}, syntax::{self, HighlightEvent},
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr, unicode::width::UnicodeWidthStr,
visual_offset_from_block, Position, Range, Selection, Transaction, visual_offset_from_block, Change, Position, Range, Selection, Transaction,
}; };
use helix_view::{ use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span}; use tui::{buffer::Buffer as Surface, text::Span};
use super::statusline; use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp}; use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView { pub struct EditorView {
@ -48,7 +48,10 @@ pub struct EditorView {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InsertEvent { pub enum InsertEvent {
Key(KeyEvent), Key(KeyEvent),
CompletionApply(CompleteAction), CompletionApply {
trigger_offset: usize,
changes: Vec<Change>,
},
TriggerCompletion, TriggerCompletion,
RequestCompletion, RequestCompletion,
} }
@ -103,7 +106,7 @@ impl EditorView {
// Set DAP highlights, if needed. // Set DAP highlights, if needed.
if let Some(frame) = editor.current_stack_frame() { if let Some(frame) = editor.current_stack_frame() {
let dap_line = frame.line.saturating_sub(1) as usize; let dap_line = frame.line.saturating_sub(1);
let style = theme.get("ui.highlight.frameline"); let style = theme.get("ui.highlight.frameline");
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
if pos.doc_line != dap_line { if pos.doc_line != dap_line {
@ -647,7 +650,7 @@ impl EditorView {
.primary() .primary()
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| { let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
}); });
@ -813,7 +816,7 @@ impl EditorView {
} }
(Mode::Insert, Mode::Normal) => { (Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion // if exiting insert mode, remove completion
self.completion = None; self.clear_completion(cxt.editor);
cxt.editor.completion_request_handle = None; cxt.editor.completion_request_handle = None;
// TODO: Use an on_mode_change hook to remove signature help // TODO: Use an on_mode_change hook to remove signature help
@ -891,22 +894,25 @@ impl EditorView {
for key in self.last_insert.1.clone() { for key in self.last_insert.1.clone() {
match key { match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key), InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::CompletionApply(compl) => { InsertEvent::CompletionApply {
trigger_offset,
changes,
} => {
let (view, doc) = current!(cxt.editor); let (view, doc) = current!(cxt.editor);
if let Some(last_savepoint) = last_savepoint.as_deref() { if let Some(last_savepoint) = last_savepoint.as_deref() {
doc.restore(view, last_savepoint); doc.restore(view, last_savepoint, true);
} }
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text); let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position = let shift_position =
|pos: usize| -> usize { pos + cursor - compl.trigger_offset }; |pos: usize| -> usize { pos + cursor - trigger_offset };
let tx = Transaction::change( let tx = Transaction::change(
doc.text(), doc.text(),
compl.changes.iter().cloned().map(|(start, end, t)| { changes.iter().cloned().map(|(start, end, t)| {
(shift_position(start), shift_position(end), t) (shift_position(start), shift_position(end), t)
}), }),
); );
@ -947,20 +953,13 @@ impl EditorView {
&mut self, &mut self,
editor: &mut Editor, editor: &mut Editor,
savepoint: Arc<SavePoint>, savepoint: Arc<SavePoint>,
items: Vec<helix_lsp::lsp::CompletionItem>, items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
size: Rect, size: Rect,
) -> Option<Rect> { ) -> Option<Rect> {
let mut completion = Completion::new( let mut completion =
editor, Completion::new(editor, savepoint, items, start_offset, trigger_offset);
savepoint,
items,
offset_encoding,
start_offset,
trigger_offset,
);
if completion.is_empty() { if completion.is_empty() {
// skip if we got no completion results // skip if we got no completion results
@ -979,6 +978,21 @@ impl EditorView {
pub fn clear_completion(&mut self, editor: &mut Editor) { pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None; self.completion = None;
if let Some(last_completion) = editor.last_completion.take() {
match last_completion {
CompleteAction::Applied {
trigger_offset,
changes,
} => self.last_insert.1.push(InsertEvent::CompletionApply {
trigger_offset,
changes,
}),
CompleteAction::Selected { savepoint } => {
let (view, doc) = current!(editor);
doc.restore(view, &savepoint, false);
}
}
}
// Clear any savepoints // Clear any savepoints
editor.clear_idle_timer(); // don't retrigger editor.clear_idle_timer(); // don't retrigger
@ -1265,12 +1279,22 @@ impl Component for EditorView {
jobs: cx.jobs, jobs: cx.jobs,
scroll: None, scroll: None,
}; };
completion.handle_event(event, &mut cx)
};
if let EventResult::Consumed(callback) = res { if let EventResult::Consumed(callback) =
consumed = true; completion.handle_event(event, &mut cx)
{
consumed = true;
Some(callback)
} else if let EventResult::Consumed(callback) =
completion.handle_event(&Event::Key(key!(Enter)), &mut cx)
{
Some(callback)
} else {
None
}
};
if let Some(callback) = res {
if callback.is_some() { if callback.is_some() {
// assume close_fn // assume close_fn
self.clear_completion(cx.editor); self.clear_completion(cx.editor);
@ -1286,10 +1310,6 @@ impl Component for EditorView {
// if completion didn't take the event, we pass it onto commands // if completion didn't take the event, we pass it onto commands
if !consumed { if !consumed {
if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::CompletionApply(compl));
}
self.insert_mode(&mut cx, key); self.insert_mode(&mut cx, key);
// record last_insert key // record last_insert key

@ -9,7 +9,7 @@ use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use helix_core::{ use helix_core::{
syntax::{self, HighlightEvent, Syntax}, syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
Rope, Rope,
}; };
use helix_view::{ use helix_view::{
@ -47,9 +47,11 @@ pub fn highlighted_code_block<'a>(
let rope = Rope::from(text.as_ref()); let rope = Rope::from(text.as_ref());
let syntax = config_loader let syntax = config_loader
.language_configuration_for_injection_string(language) .language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes())) .and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config, Arc::clone(&config_loader))); .and_then(|config| Syntax::new(&rope, config, Arc::clone(&config_loader)));
let syntax = match syntax { let syntax = match syntax {
Some(s) => s, Some(s) => s,

@ -17,7 +17,7 @@ mod text;
use crate::compositor::{Component, Compositor}; use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry; use crate::filter_picker_entry;
use crate::job::{self, Callback}; use crate::job::{self, Callback};
pub use completion::Completion; pub use completion::{Completion, CompletionItem};
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
@ -238,6 +238,7 @@ pub mod completers {
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme; use helix_view::theme;
use helix_view::{editor::Config, Editor}; use helix_view::{editor::Config, Editor};
@ -393,20 +394,11 @@ pub mod completers {
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default(); let matcher = Matcher::default();
let (_, doc) = current_ref!(editor); let Some(options) = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
let language_server = match doc.language_server() { .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
Some(language_server) => language_server, else {
None => { return vec![];
return vec![];
}
};
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
return vec![];
}
}; };
let mut matches: Vec<_> = options let mut matches: Vec<_> = options

@ -1,7 +1,9 @@
use crate::{ use crate::{
alt, alt,
compositor::{Component, Compositor, Context, Event, EventResult}, compositor::{self, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift, ctrl,
job::Callback,
key, shift,
ui::{ ui::{
self, self,
document::{render_document, LineDecoration, LinePos, TextRenderer}, document::{render_document, LineDecoration, LinePos, TextRenderer},
@ -9,7 +11,7 @@ use crate::{
EditorView, EditorView,
}, },
}; };
use futures_util::future::BoxFuture; use futures_util::{future::BoxFuture, FutureExt};
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
layout::Constraint, layout::Constraint,
@ -26,7 +28,7 @@ use std::{collections::HashMap, io::Read, path::PathBuf};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::{ use helix_core::{
movement::Direction, text_annotations::TextAnnotations, movement::Direction, text_annotations::TextAnnotations,
unicode::segmentation::UnicodeSegmentation, Position, unicode::segmentation::UnicodeSegmentation, Position, Syntax,
}; };
use helix_view::{ use helix_view::{
editor::Action, editor::Action,
@ -122,7 +124,7 @@ impl Preview<'_, '_> {
} }
} }
impl<T: Item> FilePicker<T> { impl<T: Item + 'static> FilePicker<T> {
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
editor_data: T::Data, editor_data: T::Data,
@ -208,29 +210,64 @@ impl<T: Item> FilePicker<T> {
} }
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult { fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
let Some((current_file, _)) = self.current_file(cx.editor) else {
return EventResult::Consumed(None)
};
// Try to find a document in the cache // Try to find a document in the cache
let doc = self let doc = match &current_file {
.current_file(cx.editor) PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
.and_then(|(path, _range)| match path { PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
PathOrId::Id(doc_id) => Some(doc_mut!(cx.editor, &doc_id)), Some(CachedPreview::Document(ref mut doc)) => doc,
PathOrId::Path(path) => match self.preview_cache.get_mut(&path) { _ => return EventResult::Consumed(None),
Some(CachedPreview::Document(doc)) => Some(doc), },
_ => None, };
},
}); let mut callback: Option<compositor::Callback> = None;
// Then attempt to highlight it if it has no language set // Then attempt to highlight it if it has no language set
if let Some(doc) = doc { if doc.language_config().is_none() {
if doc.language_config().is_none() { if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = cx.editor.syn_loader.clone(); let loader = cx.editor.syn_loader.clone();
doc.detect_language(loader); let job = tokio::task::spawn_blocking(move || {
let syntax = language_config
.highlight_config(&loader.scopes())
.and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
let Some(syntax) = syntax else {
log::info!("highlighting picker item failed");
return
};
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
log::info!("picker closed before syntax highlighting finished");
return
};
// Try to find a document in the cache
let doc = match current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return,
},
};
doc.syntax = Some(syntax);
};
Callback::EditorCompositor(Box::new(callback))
});
let tmp: compositor::Callback = Box::new(move |_, ctx| {
ctx.jobs
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
});
callback = Some(Box::new(tmp))
} }
// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future
} }
EventResult::Consumed(None) // QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future
EventResult::Consumed(callback)
} }
} }
@ -373,6 +410,10 @@ impl<T: Item + 'static> Component for FilePicker<T> {
self.picker.required_size((picker_width, height))?; self.picker.required_size((picker_width, height))?;
Some((width, height)) Some((width, height))
} }
fn id(&self) -> Option<&'static str> {
Some("file-picker")
}
} }
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
@ -945,17 +986,16 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
cx.jobs.callback(async move { cx.jobs.callback(async move {
let new_options = new_options.await?; let new_options = new_options.await?;
let callback = let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
crate::job::Callback::EditorCompositor(Box::new(move |editor, compositor| { // Wrapping of pickers in overlay is done outside the picker code,
// Wrapping of pickers in overlay is done outside the picker code, // so this is fragile and will break if wrapped in some other widget.
// so this is fragile and will break if wrapped in some other widget. let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) { Some(overlay) => &mut overlay.content.file_picker.picker,
Some(overlay) => &mut overlay.content.file_picker.picker, None => return,
None => return, };
}; picker.set_options(new_options);
picker.set_options(new_options); editor.reset_idle_timer();
editor.reset_idle_timer(); }));
}));
anyhow::Ok(callback) anyhow::Ok(callback)
}); });
EventResult::Consumed(None) EventResult::Consumed(None)

@ -197,15 +197,15 @@ where
); );
} }
// TODO think about handling multiple language servers
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F) fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{ {
let language_server = context.doc.language_servers().next();
write( write(
context, context,
context language_server
.doc
.language_server()
.and_then(|srv| { .and_then(|srv| {
context context
.spinners .spinners
@ -225,8 +225,7 @@ where
{ {
let (warnings, errors) = context let (warnings, errors) = context
.doc .doc
.diagnostics() .shown_diagnostics()
.iter()
.fold((0, 0), |mut counts, diag| { .fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
match diag.severity { match diag.severity {
@ -266,7 +265,7 @@ where
.diagnostics .diagnostics
.values() .values()
.flatten() .flatten()
.fold((0, 0), |mut counts, diag| { .fold((0, 0), |mut counts, (diag, _)| {
match diag.severity { match diag.severity {
Some(DiagnosticSeverity::WARNING) => counts.0 += 1, Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
@ -276,7 +275,7 @@ where
}); });
if warnings > 0 || errors > 0 { if warnings > 0 || errors > 0 {
write(context, format!(" {} ", "W"), None); write(context, " W ".into(), None);
} }
if warnings > 0 { if warnings > 0 {

@ -12,15 +12,13 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
#[lo|]#rem #[lo|]#rem
ipsum ipsum
dolor dolor
"}) "}),
.as_str(),
"CC", "CC",
platform_line(indoc! {"\ platform_line(indoc! {"\
#(lo|)#rem #(lo|)#rem
#(ip|)#sum #(ip|)#sum
#[do|]#lor #[do|]#lor
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -30,15 +28,13 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
#[|lo]#rem #[|lo]#rem
ipsum ipsum
dolor dolor
"}) "}),
.as_str(),
"CC", "CC",
platform_line(indoc! {"\ platform_line(indoc! {"\
#(|lo)#rem #(|lo)#rem
#(|ip)#sum #(|ip)#sum
#[|do]#lor #[|do]#lor
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -47,14 +43,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\ platform_line(indoc! {"\
test test
#[testitem|]# #[testitem|]#
"}) "}),
.as_str(),
"<A-C>", "<A-C>",
platform_line(indoc! {"\ platform_line(indoc! {"\
test test
#[testitem|]# #[testitem|]#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -63,14 +57,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\ platform_line(indoc! {"\
test test
#[test|]# #[test|]#
"}) "}),
.as_str(),
"<A-C>", "<A-C>",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[test|]# #[test|]#
#(test|)# #(test|)#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -79,14 +71,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\ platform_line(indoc! {"\
#[testitem|]# #[testitem|]#
test test
"}) "}),
.as_str(),
"C", "C",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[testitem|]# #[testitem|]#
test test
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -95,14 +85,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\ platform_line(indoc! {"\
#[test|]# #[test|]#
test test
"}) "}),
.as_str(),
"C", "C",
platform_line(indoc! {"\ platform_line(indoc! {"\
#(test|)# #(test|)#
#[test|]# #[test|]#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
Ok(()) Ok(())
@ -174,15 +162,13 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> {
#[|lorem]# #[|lorem]#
#(|ipsum)# #(|ipsum)#
#(|dolor)# #(|dolor)#
"}) "}),
.as_str(),
"yp", "yp",
platform_line(indoc! {"\ platform_line(indoc! {"\
lorem#[|lorem]# lorem#[|lorem]#
ipsum#(|ipsum)# ipsum#(|ipsum)#
dolor#(|dolor)# dolor#(|dolor)#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -197,8 +183,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]# #[|lorem]#
#(|ipsum)# #(|ipsum)#
#(|dolor)# #(|dolor)#
"}) "}),
.as_str(),
"|echo foo<ret>", "|echo foo<ret>",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[|foo\n]# #[|foo\n]#
@ -207,8 +192,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#(|foo\n)# #(|foo\n)#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -218,8 +202,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]# #[|lorem]#
#(|ipsum)# #(|ipsum)#
#(|dolor)# #(|dolor)#
"}) "}),
.as_str(),
"!echo foo<ret>", "!echo foo<ret>",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[|foo\n]# #[|foo\n]#
@ -228,8 +211,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
ipsum ipsum
#(|foo\n)# #(|foo\n)#
dolor dolor
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -239,8 +221,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]# #[|lorem]#
#(|ipsum)# #(|ipsum)#
#(|dolor)# #(|dolor)#
"}) "}),
.as_str(),
"<A-!>echo foo<ret>", "<A-!>echo foo<ret>",
platform_line(indoc! {"\ platform_line(indoc! {"\
lorem#[|foo\n]# lorem#[|foo\n]#
@ -249,8 +230,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
dolor#(|foo\n)# dolor#(|foo\n)#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -294,16 +274,14 @@ async fn test_extend_line() -> anyhow::Result<()> {
ipsum ipsum
dolor dolor
"}) "}),
.as_str(),
"x2x", "x2x",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[lorem #[lorem
ipsum ipsum
dolor\n|]# dolor\n|]#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -313,15 +291,13 @@ async fn test_extend_line() -> anyhow::Result<()> {
#[l|]#orem #[l|]#orem
ipsum ipsum
"}) "}),
.as_str(),
"2x", "2x",
platform_line(indoc! {"\ platform_line(indoc! {"\
#[lorem #[lorem
ipsum\n|]# ipsum\n|]#
"}) "}),
.as_str(),
)) ))
.await?; .await?;
@ -385,3 +361,68 @@ async fn test_character_info() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("#(x|)# #[x|]#"),
"c<space><backspace><esc>",
platform_line("#[\n|]#"),
))
.await?;
test((
platform_line("#( |)##( |)#a#( |)#axx#[x|]#a"),
"li<backspace><esc>",
platform_line("#(a|)##(|a)#xx#[|a]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#ba#(r|)#"),
"a<C-w><esc>",
platform_line("#[\n|]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_forward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#b#(|ar)#"),
"i<A-d><esc>",
platform_line("fo#[\n|]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_forward() -> anyhow::Result<()> {
test((
platform_line(indoc! {"\
#[abc|]#def
#(abc|)#ef
#(abc|)#f
#(abc|)#
"}),
"a<del><esc>",
platform_line(indoc! {"\
#[abc|]#ef
#(abc|)#f
#(abc|)#
#(abc|)#
"}),
))
.await?;
Ok(())
}

@ -407,3 +407,41 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_write_utf_bom_file() -> anyhow::Result<()> {
// "ABC" with utf8 bom
const UTF8_FILE: [u8; 6] = [0xef, 0xbb, 0xbf, b'A', b'B', b'C'];
// "ABC" in UTF16 with bom
const UTF16LE_FILE: [u8; 8] = [0xff, 0xfe, b'A', 0x00, b'B', 0x00, b'C', 0x00];
const UTF16BE_FILE: [u8; 8] = [0xfe, 0xff, 0x00, b'A', 0x00, b'B', 0x00, b'C'];
edit_file_with_content(&UTF8_FILE).await?;
edit_file_with_content(&UTF16LE_FILE).await?;
edit_file_with_content(&UTF16BE_FILE).await?;
Ok(())
}
async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
file.as_file_mut().write_all(&file_content)?;
helpers::test_key_sequence(
&mut helpers::AppBuilder::new().build()?,
Some(&format!(":o {}<ret>:x<ret>", file.path().to_string_lossy())),
None,
true,
)
.await?;
file.rewind()?;
let mut new_file_content: Vec<u8> = Vec::new();
file.read_to_end(&mut new_file_content)?;
assert_eq!(file_content, new_file_content);
Ok(())
}

@ -16,13 +16,13 @@ include = ["src/**/*", "README.md"]
default = ["crossterm"] default = ["crossterm"]
[dependencies] [dependencies]
bitflags = "2.2" bitflags = "2.3"
cassowary = "0.3" cassowary = "0.3"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
crossterm = { version = "0.26", optional = true } crossterm = { version = "0.26", optional = true }
termini = "0.1" termini = "1.0"
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
once_cell = "1.17" once_cell = "1.18"
log = "~0.4" log = "~0.4"
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }

@ -442,7 +442,7 @@ impl Buffer {
let mut x_offset = x as usize; let mut x_offset = x as usize;
let max_offset = min(self.area.right(), width.saturating_add(x)); let max_offset = min(self.area.right(), width.saturating_add(x));
let mut start_index = self.index_of(x, y); let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y); let mut index = self.index_of(max_offset, y);
let content_width = spans.width(); let content_width = spans.width();
let truncated = content_width > width as usize; let truncated = content_width > width as usize;

@ -17,7 +17,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
parking_lot = "0.12" parking_lot = "0.12"
arc-swap = { version = "1.6.0" } arc-swap = { version = "1.6.0" }
gix = { version = "0.43.0", default-features = false , optional = true } gix = { version = "0.44.1", default-features = false , optional = true }
imara-diff = "0.1.5" imara-diff = "0.1.5"
anyhow = "1" anyhow = "1"

@ -20,8 +20,8 @@ use super::{MAX_DIFF_BYTES, MAX_DIFF_LINES};
/// A cache that stores the `lines` of a rope as a vector. /// A cache that stores the `lines` of a rope as a vector.
/// It allows safely reusing the allocation of the vec when updating the rope /// It allows safely reusing the allocation of the vec when updating the rope
pub(crate) struct InternedRopeLines { pub(crate) struct InternedRopeLines {
diff_base: Rope, diff_base: Box<Rope>,
doc: Rope, doc: Box<Rope>,
num_tokens_diff_base: u32, num_tokens_diff_base: u32,
interned: InternedInput<RopeSlice<'static>>, interned: InternedInput<RopeSlice<'static>>,
} }
@ -34,8 +34,8 @@ impl InternedRopeLines {
after: Vec::with_capacity(doc.len_lines()), after: Vec::with_capacity(doc.len_lines()),
interner: Interner::new(diff_base.len_lines() + doc.len_lines()), interner: Interner::new(diff_base.len_lines() + doc.len_lines()),
}, },
diff_base, diff_base: Box::new(diff_base),
doc, doc: Box::new(doc),
// will be populated by update_diff_base_impl // will be populated by update_diff_base_impl
num_tokens_diff_base: 0, num_tokens_diff_base: 0,
}; };
@ -44,19 +44,19 @@ impl InternedRopeLines {
} }
pub fn doc(&self) -> Rope { pub fn doc(&self) -> Rope {
self.doc.clone() Rope::clone(&*self.doc)
} }
pub fn diff_base(&self) -> Rope { pub fn diff_base(&self) -> Rope {
self.diff_base.clone() Rope::clone(&*self.diff_base)
} }
/// Updates the `diff_base` and optionally the document if `doc` is not None /// Updates the `diff_base` and optionally the document if `doc` is not None
pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option<Rope>) { pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option<Rope>) {
self.interned.clear(); self.interned.clear();
self.diff_base = diff_base; self.diff_base = Box::new(diff_base);
if let Some(doc) = doc { if let Some(doc) = doc {
self.doc = doc self.doc = Box::new(doc)
} }
if !self.is_too_large() { if !self.is_too_large() {
self.update_diff_base_impl(); self.update_diff_base_impl();
@ -74,7 +74,7 @@ impl InternedRopeLines {
.interner .interner
.erase_tokens_after(self.num_tokens_diff_base.into()); .erase_tokens_after(self.num_tokens_diff_base.into());
self.doc = doc; self.doc = Box::new(doc);
if self.is_too_large() { if self.is_too_large() {
self.interned.after.clear(); self.interned.after.clear();
} else { } else {

@ -23,7 +23,7 @@ impl Git {
// This path depends on the install location of git and therefore requires some overhead to lookup // This path depends on the install location of git and therefore requires some overhead to lookup
// This is basically only used on windows and has some overhead hence it's disabled on other platforms. // This is basically only used on windows and has some overhead hence it's disabled on other platforms.
// `gitoxide` doesn't use this as default // `gitoxide` doesn't use this as default
let config = gix::permissions::Config { let config = gix::open::permissions::Config {
system: true, system: true,
git: true, git: true,
user: true, user: true,
@ -32,19 +32,24 @@ impl Git {
git_binary: cfg!(windows), git_binary: cfg!(windows),
}; };
// change options for config permissions without touching anything else // change options for config permissions without touching anything else
git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(gix::Permissions { git_open_opts_map.reduced = git_open_opts_map
.reduced
.permissions(gix::open::Permissions {
config,
..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced)
});
git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions {
config, config,
..gix::Permissions::default_for_level(gix::sec::Trust::Reduced) ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full)
});
git_open_opts_map.full = git_open_opts_map.full.permissions(gix::Permissions {
config,
..gix::Permissions::default_for_level(gix::sec::Trust::Full)
}); });
let mut open_options = gix::discover::upwards::Options::default(); let open_options = gix::discover::upwards::Options {
if let Some(ceiling_dir) = ceiling_dir { ceiling_dirs: ceiling_dir
open_options.ceiling_dirs = vec![ceiling_dir.to_owned()]; .map(|dir| vec![dir.to_owned()])
} .unwrap_or_default(),
dot_git_only: true,
..Default::default()
};
let res = ThreadSafeRepository::discover_with_environment_overrides_opts( let res = ThreadSafeRepository::discover_with_environment_overrides_opts(
path, path,

@ -46,8 +46,8 @@ impl DiffProviderRegistry {
.find_map(|provider| match provider.get_diff_base(file) { .find_map(|provider| match provider.get_diff_base(file) {
Ok(res) => Some(res), Ok(res) => Some(res),
Err(err) => { Err(err) => {
log::error!("{err:#?}"); log::info!("{err:#?}");
log::error!("failed to open diff base for {}", file.display()); log::info!("failed to open diff base for {}", file.display());
None None
} }
}) })
@ -59,8 +59,8 @@ impl DiffProviderRegistry {
.find_map(|provider| match provider.get_current_head_name(file) { .find_map(|provider| match provider.get_current_head_name(file) {
Ok(res) => Some(res), Ok(res) => Some(res),
Err(err) => { Err(err) => {
log::error!("{err:#?}"); log::info!("{err:#?}");
log::error!("failed to obtain current head name for {}", file.display()); log::info!("failed to obtain current head name for {}", file.display());
None None
} }
}) })

@ -14,7 +14,7 @@ default = []
term = ["crossterm"] term = ["crossterm"]
[dependencies] [dependencies]
bitflags = "2.2" bitflags = "2.3"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
@ -24,7 +24,7 @@ crossterm = { version = "0.26", optional = true }
helix-vcs = { version = "0.6", path = "../helix-vcs" } helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits # Conversion traits
once_cell = "1.17" once_cell = "1.18"
url = "2" url = "2"
arc-swap = { version = "1.6.0" } arc-swap = { version = "1.6.0" }

@ -68,7 +68,7 @@ macro_rules! command_provider {
#[cfg(windows)] #[cfg(windows)]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
Box::new(provider::WindowsProvider::default()) Box::<provider::WindowsProvider>::default()
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

@ -5,7 +5,8 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt; use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs; use helix_core::auto_pairs::AutoPairs;
use helix_core::doc_formatter::TextFormat; use helix_core::doc_formatter::TextFormat;
use helix_core::syntax::Highlight; use helix_core::encoding::Encoding;
use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_core::Range; use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry}; use helix_vcs::{DiffHandle, DiffProviderRegistry};
@ -113,6 +114,19 @@ pub struct SavePoint {
/// The view this savepoint is associated with /// The view this savepoint is associated with
pub view: ViewId, pub view: ViewId,
revert: Mutex<Transaction>, 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 { pub struct Document {
@ -130,6 +144,7 @@ pub struct Document {
path: Option<PathBuf>, path: Option<PathBuf>,
encoding: &'static encoding::Encoding, encoding: &'static encoding::Encoding,
has_bom: bool,
pub restore_cursor: bool, pub restore_cursor: bool,
@ -139,9 +154,9 @@ pub struct Document {
/// The document's default line ending. /// The document's default line ending.
pub line_ending: LineEnding, pub line_ending: LineEnding,
syntax: Option<Syntax>, pub syntax: Option<Syntax>,
/// Corresponding language scope name. Usually `source.<lang>`. /// Corresponding language scope name. Usually `source.<lang>`.
pub(crate) language: Option<Arc<LanguageConfiguration>>, pub language: Option<Arc<LanguageConfiguration>>,
/// Pending changes since last history commit. /// Pending changes since last history commit.
changes: ChangeSet, changes: ChangeSet,
@ -164,11 +179,14 @@ pub struct Document {
version: i32, // should be usize? version: i32, // should be usize?
pub(crate) modified_since_accessed: bool, pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>, pub(crate) diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>, pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
diff_handle: Option<DiffHandle>, diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
// when document was used for most-recent-used buffer picker
pub focused_at: std::time::Instant,
} }
/// Inlay hints for a single `(Document, View)` combo. /// Inlay hints for a single `(Document, View)` combo.
@ -274,16 +292,104 @@ impl fmt::Debug for DocumentInlayHintsId {
} }
} }
enum Encoder {
Utf16Be,
Utf16Le,
EncodingRs(encoding::Encoder),
}
impl Encoder {
fn from_encoding(encoding: &'static encoding::Encoding) -> Self {
if encoding == encoding::UTF_16BE {
Self::Utf16Be
} else if encoding == encoding::UTF_16LE {
Self::Utf16Le
} else {
Self::EncodingRs(encoding.new_encoder())
}
}
fn encode_from_utf8(
&mut self,
src: &str,
dst: &mut [u8],
is_empty: bool,
) -> (encoding::CoderResult, usize, usize) {
if src.is_empty() {
return (encoding::CoderResult::InputEmpty, 0, 0);
}
let mut write_to_buf = |convert: fn(u16) -> [u8; 2]| {
let to_write = src.char_indices().map(|(indice, char)| {
let mut encoded: [u16; 2] = [0, 0];
(
indice,
char.encode_utf16(&mut encoded)
.iter_mut()
.flat_map(|char| convert(*char))
.collect::<Vec<u8>>(),
)
});
let mut total_written = 0usize;
for (indice, utf16_bytes) in to_write {
let character_size = utf16_bytes.len();
if dst.len() <= (total_written + character_size) {
return (encoding::CoderResult::OutputFull, indice, total_written);
}
for character in utf16_bytes {
dst[total_written] = character;
total_written += 1;
}
}
(encoding::CoderResult::InputEmpty, src.len(), total_written)
};
match self {
Self::Utf16Be => write_to_buf(u16::to_be_bytes),
Self::Utf16Le => write_to_buf(u16::to_le_bytes),
Self::EncodingRs(encoder) => {
let (code_result, read, written, ..) = encoder.encode_from_utf8(src, dst, is_empty);
(code_result, read, written)
}
}
}
}
// Apply BOM if encoding permit it, return the number of bytes written at the start of buf
fn apply_bom(encoding: &'static encoding::Encoding, buf: &mut [u8; BUF_SIZE]) -> usize {
if encoding == encoding::UTF_8 {
buf[0] = 0xef;
buf[1] = 0xbb;
buf[2] = 0xbf;
3
} else if encoding == encoding::UTF_16BE {
buf[0] = 0xfe;
buf[1] = 0xff;
2
} else if encoding == encoding::UTF_16LE {
buf[0] = 0xff;
buf[1] = 0xfe;
2
} else {
0
}
}
// The documentation and implementation of this function should be up-to-date with // The documentation and implementation of this function should be up-to-date with
// its sibling function, `to_writer()`. // its sibling function, `to_writer()`.
// //
/// Decodes a stream of bytes into UTF-8, returning a `Rope` and the /// Decodes a stream of bytes into UTF-8, returning a `Rope` and the
/// encoding it was decoded as. The optional `encoding` parameter can /// encoding it was decoded as with BOM information. The optional `encoding`
/// be used to override encoding auto-detection. /// parameter can be used to override encoding auto-detection.
pub fn from_reader<R: std::io::Read + ?Sized>( pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R, reader: &mut R,
encoding: Option<&'static encoding::Encoding>, encoding: Option<&'static Encoding>,
) -> Result<(Rope, &'static encoding::Encoding), Error> { ) -> Result<(Rope, &'static Encoding, bool), Error> {
// These two buffers are 8192 bytes in size each and are used as // These two buffers are 8192 bytes in size each and are used as
// intermediaries during the decoding process. Text read into `buf` // intermediaries during the decoding process. Text read into `buf`
// from `reader` is decoded into `buf_out` as UTF-8. Once either // from `reader` is decoded into `buf_out` as UTF-8. Once either
@ -293,25 +399,32 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
let mut buf_out = [0u8; BUF_SIZE]; let mut buf_out = [0u8; BUF_SIZE];
let mut builder = RopeBuilder::new(); let mut builder = RopeBuilder::new();
// By default, the encoding of the text is auto-detected via the // By default, the encoding of the text is auto-detected by
// `chardetng` crate which requires sample data from the reader. // `encoding_rs` for_bom, and if it fails, from `chardetng`
// crate which requires sample data from the reader.
// As a manual override to this auto-detection is possible, the // As a manual override to this auto-detection is possible, the
// same data is read into `buf` to ensure symmetry in the upcoming // same data is read into `buf` to ensure symmetry in the upcoming
// loop. // loop.
let (encoding, mut decoder, mut slice, mut is_empty) = { let (encoding, has_bom, mut decoder, mut slice, mut is_empty) = {
let read = reader.read(&mut buf)?; let read = reader.read(&mut buf)?;
let is_empty = read == 0; let is_empty = read == 0;
let encoding = encoding.unwrap_or_else(|| { let (encoding, has_bom) = encoding
let mut encoding_detector = chardetng::EncodingDetector::new(); .map(|encoding| (encoding, false))
encoding_detector.feed(&buf, is_empty); .or_else(|| {
encoding_detector.guess(None, true) encoding::Encoding::for_bom(&buf).map(|(encoding, _bom_size)| (encoding, true))
}); })
.unwrap_or_else(|| {
let mut encoding_detector = chardetng::EncodingDetector::new();
encoding_detector.feed(&buf, is_empty);
(encoding_detector.guess(None, true), false)
});
let decoder = encoding.new_decoder(); let decoder = encoding.new_decoder();
// If the amount of bytes read from the reader is less than // If the amount of bytes read from the reader is less than
// `buf.len()`, it is undesirable to read the bytes afterwards. // `buf.len()`, it is undesirable to read the bytes afterwards.
let slice = &buf[..read]; let slice = &buf[..read];
(encoding, decoder, slice, is_empty) (encoding, has_bom, decoder, slice, is_empty)
}; };
// `RopeBuilder::append()` expects a `&str`, so this is the "real" // `RopeBuilder::append()` expects a `&str`, so this is the "real"
@ -379,7 +492,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
is_empty = read == 0; is_empty = read == 0;
} }
let rope = builder.finish(); let rope = builder.finish();
Ok((rope, encoding)) Ok((rope, encoding, has_bom))
} }
// The documentation and implementation of this function should be up-to-date with // The documentation and implementation of this function should be up-to-date with
@ -390,7 +503,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
/// replacement characters may appear in the encoded text. /// replacement characters may appear in the encoded text.
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
writer: &'a mut W, writer: &'a mut W,
encoding: &'static encoding::Encoding, encoding_with_bom_info: (&'static Encoding, bool),
rope: &'a Rope, rope: &'a Rope,
) -> Result<(), Error> { ) -> Result<(), Error> {
// Text inside a `Rope` is stored as non-contiguous blocks of data called // Text inside a `Rope` is stored as non-contiguous blocks of data called
@ -399,13 +512,22 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
// determined by filtering the iterator to remove all empty chunks and then // determined by filtering the iterator to remove all empty chunks and then
// appending an empty chunk to it. This is valuable for detecting when all // appending an empty chunk to it. This is valuable for detecting when all
// chunks in the `Rope` have been iterated over in the subsequent loop. // chunks in the `Rope` have been iterated over in the subsequent loop.
let (encoding, has_bom) = encoding_with_bom_info;
let iter = rope let iter = rope
.chunks() .chunks()
.filter(|c| !c.is_empty()) .filter(|c| !c.is_empty())
.chain(std::iter::once("")); .chain(std::iter::once(""));
let mut buf = [0u8; BUF_SIZE]; let mut buf = [0u8; BUF_SIZE];
let mut encoder = encoding.new_encoder();
let mut total_written = 0usize; let mut total_written = if has_bom {
apply_bom(encoding, &mut buf)
} else {
0
};
let mut encoder = Encoder::from_encoding(encoding);
for chunk in iter { for chunk in iter {
let is_empty = chunk.is_empty(); let is_empty = chunk.is_empty();
let mut total_read = 0usize; let mut total_read = 0usize;
@ -446,6 +568,7 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
break; break;
} }
} }
Ok(()) Ok(())
} }
@ -457,16 +580,16 @@ where
*mut_ref = f(mem::take(mut_ref)); *mut_ref = f(mem::take(mut_ref));
} }
use helix_lsp::lsp; use helix_lsp::{lsp, Client, LanguageServerName};
use url::Url; use url::Url;
impl Document { impl Document {
pub fn from( pub fn from(
text: Rope, text: Rope,
encoding: Option<&'static encoding::Encoding>, encoding_with_bom_info: Option<(&'static Encoding, bool)>,
config: Arc<dyn DynAccess<Config>>, config: Arc<dyn DynAccess<Config>>,
) -> Self { ) -> Self {
let encoding = encoding.unwrap_or(encoding::UTF_8); let (encoding, has_bom) = encoding_with_bom_info.unwrap_or((encoding::UTF_8, false));
let changes = ChangeSet::new(&text); let changes = ChangeSet::new(&text);
let old_state = None; let old_state = None;
@ -474,6 +597,7 @@ impl Document {
id: DocumentId::default(), id: DocumentId::default(),
path: None, path: None,
encoding, encoding,
has_bom,
text, text,
selections: HashMap::default(), selections: HashMap::default(),
inlay_hints: HashMap::default(), inlay_hints: HashMap::default(),
@ -492,10 +616,11 @@ impl Document {
last_saved_time: SystemTime::now(), last_saved_time: SystemTime::now(),
last_saved_revision: 0, last_saved_revision: 0,
modified_since_accessed: false, modified_since_accessed: false,
language_server: None, language_servers: HashMap::new(),
diff_handle: None, diff_handle: None,
config, config,
version_control_head: None, version_control_head: None,
focused_at: std::time::Instant::now(),
} }
} }
pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self { pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self {
@ -507,21 +632,21 @@ impl Document {
/// overwritten with the `encoding` parameter. /// overwritten with the `encoding` parameter.
pub fn open( pub fn open(
path: &Path, path: &Path,
encoding: Option<&'static encoding::Encoding>, encoding: Option<&'static Encoding>,
config_loader: Option<Arc<syntax::Loader>>, config_loader: Option<Arc<syntax::Loader>>,
config: Arc<dyn DynAccess<Config>>, config: Arc<dyn DynAccess<Config>>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty). // Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding) = if path.exists() { let (rope, encoding, has_bom) = if path.exists() {
let mut file = let mut file =
std::fs::File::open(path).context(format!("unable to open {:?}", path))?; std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)? from_reader(&mut file, encoding)?
} else { } else {
let encoding = encoding.unwrap_or(encoding::UTF_8); let encoding = encoding.unwrap_or(encoding::UTF_8);
(Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding) (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding, false)
}; };
let mut doc = Self::from(rope, Some(encoding), config); let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
// set the path and try detecting the language // set the path and try detecting the language
doc.set_path(Some(path))?; doc.set_path(Some(path))?;
@ -572,7 +697,7 @@ impl Document {
})?; })?;
{ {
let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?; let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?;
to_writer(&mut stdin, encoding::UTF_8, &text) to_writer(&mut stdin, (encoding::UTF_8, false), &text)
.await .await
.map_err(|_| FormatterError::BrokenStdin)?; .map_err(|_| FormatterError::BrokenStdin)?;
} }
@ -605,10 +730,12 @@ impl Document {
return Some(formatting_future.boxed()); return Some(formatting_future.boxed());
}; };
let language_server = self.language_server()?;
let text = self.text.clone(); let text = self.text.clone();
// finds first language server that supports formatting and then formats
let language_server = self
.language_servers_with_feature(LanguageServerFeature::Format)
.next()?;
let offset_encoding = language_server.offset_encoding(); let offset_encoding = language_server.offset_encoding();
let request = language_server.text_document_formatting( let request = language_server.text_document_formatting(
self.identifier(), self.identifier(),
lsp::FormattingOptions { lsp::FormattingOptions {
@ -672,20 +799,18 @@ impl Document {
if self.path.is_none() { if self.path.is_none() {
bail!("Can't save with no path set!"); bail!("Can't save with no path set!");
} }
self.path.as_ref().unwrap().clone() self.path.as_ref().unwrap().clone()
} }
}; };
let identifier = self.path().map(|_| self.identifier()); let identifier = self.path().map(|_| self.identifier());
let language_server = self.language_server.clone(); let language_servers = self.language_servers.clone();
// mark changes up to now as saved // mark changes up to now as saved
let current_rev = self.get_current_revision(); let current_rev = self.get_current_revision();
let doc_id = self.id(); let doc_id = self.id();
let encoding = self.encoding; let encoding_with_bom_info = (self.encoding, self.has_bom);
let last_saved_time = self.last_saved_time; let last_saved_time = self.last_saved_time;
// We encode the file according to the `Document`'s encoding. // We encode the file according to the `Document`'s encoding.
@ -697,7 +822,7 @@ impl Document {
if force { if force {
std::fs::DirBuilder::new().recursive(true).create(parent)?; std::fs::DirBuilder::new().recursive(true).create(parent)?;
} else { } else {
bail!("can't save file, parent directory does not exist"); bail!("can't save file, parent directory does not exist (use :w! to create it)");
} }
} }
} }
@ -714,7 +839,7 @@ impl Document {
} }
let mut file = File::create(&path).await?; let mut file = File::create(&path).await?;
to_writer(&mut file, encoding, &text).await?; to_writer(&mut file, encoding_with_bom_info, &text).await?;
let event = DocumentSavedEvent { let event = DocumentSavedEvent {
revision: current_rev, revision: current_rev,
@ -723,14 +848,13 @@ impl Document {
text: text.clone(), text: text.clone(),
}; };
if let Some(language_server) = language_server { for (_, language_server) in language_servers {
if !language_server.is_initialized() { if !language_server.is_initialized() {
return Ok(event); return Ok(event);
} }
if let Some(identifier) = &identifier {
if let Some(identifier) = identifier {
if let Some(notification) = if let Some(notification) =
language_server.text_document_did_save(identifier, &text) language_server.text_document_did_save(identifier.clone(), &text)
{ {
notification.await?; notification.await?;
} }
@ -745,12 +869,20 @@ impl Document {
/// Detect the programming language based on the file type. /// Detect the programming language based on the file type.
pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) { pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) {
if let Some(path) = &self.path { self.set_language(
let language_config = config_loader self.detect_language_config(&config_loader),
.language_config_for_file_name(path) Some(config_loader),
.or_else(|| config_loader.language_config_for_shebang(self.text())); );
self.set_language(language_config, Some(config_loader)); }
}
/// Detect the programming language based on the file type.
pub fn detect_language_config(
&self,
config_loader: &syntax::Loader,
) -> Option<Arc<helix_core::syntax::LanguageConfiguration>> {
config_loader
.language_config_for_file_name(self.path.as_ref()?)
.or_else(|| config_loader.language_config_for_shebang(self.text()))
} }
/// Detect the indentation used in the file, or otherwise defaults to the language indentation /// Detect the indentation used in the file, or otherwise defaults to the language indentation
@ -772,7 +904,7 @@ impl Document {
provider_registry: &DiffProviderRegistry, provider_registry: &DiffProviderRegistry,
redraw_handle: RedrawHandle, redraw_handle: RedrawHandle,
) -> Result<(), Error> { ) -> Result<(), Error> {
let encoding = &self.encoding; let encoding = self.encoding;
let path = self let path = self
.path() .path()
.filter(|path| path.exists()) .filter(|path| path.exists())
@ -806,13 +938,16 @@ impl Document {
/// Sets the [`Document`]'s encoding with the encoding correspondent to `label`. /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> { pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
self.encoding = encoding::Encoding::for_label(label.as_bytes()) let encoding =
.ok_or_else(|| anyhow!("unknown encoding"))?; Encoding::for_label(label.as_bytes()).ok_or_else(|| anyhow!("unknown encoding"))?;
self.encoding = encoding;
Ok(()) Ok(())
} }
/// Returns the [`Document`]'s current encoding. /// Returns the [`Document`]'s current encoding.
pub fn encoding(&self) -> &'static encoding::Encoding { pub fn encoding(&self) -> &'static Encoding {
self.encoding self.encoding
} }
@ -837,8 +972,7 @@ impl Document {
) { ) {
if let (Some(language_config), Some(loader)) = (language_config, loader) { if let (Some(language_config), Some(loader)) = (language_config, loader) {
if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) { if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
let syntax = Syntax::new(&self.text, highlight_config, loader); self.syntax = Syntax::new(&self.text, highlight_config, loader);
self.syntax = Some(syntax);
} }
self.language = Some(language_config); self.language = Some(language_config);
@ -870,11 +1004,6 @@ impl Document {
Ok(()) Ok(())
} }
/// Set the LSP.
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
self.language_server = language_server;
}
/// Select text within the [`Document`]. /// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
// TODO: use a transaction? // TODO: use a transaction?
@ -908,6 +1037,11 @@ impl Document {
} }
} }
/// Mark document as recent used for MRU sorting
pub fn mark_as_focused(&mut self) {
self.focused_at = std::time::Instant::now();
}
/// Remove a view's selection and inlay hints from this document. /// Remove a view's selection and inlay hints from this document.
pub fn remove_view(&mut self, view_id: ViewId) { pub fn remove_view(&mut self, view_id: ViewId) {
self.selections.remove(&view_id); self.selections.remove(&view_id);
@ -915,7 +1049,12 @@ impl Document {
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text. /// Apply a [`Transaction`] to the [`Document`] to change its text.
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { fn apply_impl(
&mut self,
transaction: &Transaction,
view_id: ViewId,
emit_lsp_notification: bool,
) -> bool {
use helix_core::Assoc; use helix_core::Assoc;
let old_doc = self.text().clone(); let old_doc = self.text().clone();
@ -968,9 +1107,11 @@ impl Document {
// update tree-sitter syntax tree // update tree-sitter syntax tree
if let Some(syntax) = &mut self.syntax { if let Some(syntax) = &mut self.syntax {
// TODO: no unwrap // TODO: no unwrap
syntax let res = syntax.update(&old_doc, &self.text, transaction.changes());
.update(&old_doc, &self.text, transaction.changes()) if res.is_err() {
.unwrap(); log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
self.syntax = None;
}
} }
let changes = transaction.changes(); let changes = transaction.changes();
@ -1011,25 +1152,31 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints); apply_inlay_hint_changes(padding_after_inlay_hints);
} }
// emit lsp notification if emit_lsp_notification {
if let Some(language_server) = self.language_server() { // emit lsp notification
let notify = language_server.text_document_did_change( for language_server in self.language_servers() {
self.versioned_identifier(), let notify = language_server.text_document_did_change(
&old_doc, self.versioned_identifier(),
self.text(), &old_doc,
changes, self.text(),
); changes,
);
if let Some(notify) = notify { if let Some(notify) = notify {
tokio::spawn(notify); tokio::spawn(notify);
}
} }
} }
} }
success success
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text. fn apply_inner(
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { &mut self,
transaction: &Transaction,
view_id: ViewId,
emit_lsp_notification: bool,
) -> bool {
// store the state just before any changes are made. This allows us to undo to the // store the state just before any changes are made. This allows us to undo to the
// state just before a transaction was applied. // state just before a transaction was applied.
if self.changes.is_empty() && !transaction.changes().is_empty() { if self.changes.is_empty() && !transaction.changes().is_empty() {
@ -1039,7 +1186,7 @@ impl Document {
}); });
} }
let success = self.apply_impl(transaction, view_id); let success = self.apply_impl(transaction, view_id, emit_lsp_notification);
if !transaction.changes().is_empty() { if !transaction.changes().is_empty() {
// Compose this transaction with the previous one // Compose this transaction with the previous one
@ -1049,12 +1196,23 @@ impl Document {
} }
success success
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text.
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
self.apply_inner(transaction, view_id, true)
}
/// Apply a [`Transaction`] to the [`Document`] to change its text
/// without notifying the language servers. This is useful for temporary transactions
/// that must not influence the server.
pub fn apply_temporary(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
self.apply_inner(transaction, view_id, false)
}
fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool { fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool {
let mut history = self.history.take(); let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() }; let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn { let success = if let Some(txn) = txn {
self.apply_impl(txn, view.id) self.apply_impl(txn, view.id, true)
} else { } else {
false false
}; };
@ -1086,15 +1244,32 @@ impl Document {
/// the state it had when this function was called. /// the state it had when this function was called.
pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> { pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone()); let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
// check if there is already an existing (identical) savepoint around
if let Some(savepoint) = self
.savepoints
.iter()
.rev()
.find_map(|savepoint| savepoint.upgrade())
{
let transaction = savepoint.revert.lock();
if savepoint.view == view.id
&& transaction.changes().is_empty()
&& transaction.selection() == revert.selection()
{
drop(transaction);
return savepoint;
}
}
let savepoint = Arc::new(SavePoint { let savepoint = Arc::new(SavePoint {
view: view.id, view: view.id,
revert: Mutex::new(revert), revert: Mutex::new(revert),
text: self.text.clone(),
}); });
self.savepoints.push(Arc::downgrade(&savepoint)); self.savepoints.push(Arc::downgrade(&savepoint));
savepoint savepoint
} }
pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint) { pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint, emit_lsp_notification: bool) {
assert_eq!( assert_eq!(
savepoint.view, view.id, savepoint.view, view.id,
"Savepoint must not be used with a different view!" "Savepoint must not be used with a different view!"
@ -1109,7 +1284,7 @@ impl Document {
let savepoint_ref = self.savepoints.remove(savepoint_idx); let savepoint_ref = self.savepoints.remove(savepoint_idx);
let mut revert = savepoint.revert.lock(); let mut revert = savepoint.revert.lock();
self.apply(&revert, view.id); self.apply_inner(&revert, view.id, emit_lsp_notification);
*revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone()); *revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
self.savepoints.push(savepoint_ref) self.savepoints.push(savepoint_ref)
} }
@ -1122,7 +1297,7 @@ impl Document {
}; };
let mut success = false; let mut success = false;
for txn in txns { for txn in txns {
if self.apply_impl(&txn, view.id) { if self.apply_impl(&txn, view.id, true) {
success = true; success = true;
} }
} }
@ -1235,18 +1410,13 @@ impl Document {
.map(|language| language.language_id.as_str()) .map(|language| language.language_id.as_str())
} }
/// Language ID for the document. Either the `language-id` from the /// Language ID for the document. Either the `language-id`,
/// `language-server` configuration, or the document language if no /// or the document language name if no `language-id` has been specified.
/// `language-id` has been specified.
pub fn language_id(&self) -> Option<&str> { pub fn language_id(&self) -> Option<&str> {
let language_config = self.language.as_deref()?; self.language_config()?
.language_server_language_id
language_config
.language_server
.as_ref()?
.language_id
.as_deref() .as_deref()
.or(Some(language_config.language_id.as_str())) .or_else(|| self.language_name())
} }
/// Corresponding [`LanguageConfiguration`]. /// Corresponding [`LanguageConfiguration`].
@ -1259,10 +1429,45 @@ impl Document {
self.version self.version
} }
/// Language server if it has been initialized. /// maintains the order as configured in the language_servers TOML array
pub fn language_server(&self) -> Option<&helix_lsp::Client> { pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
let server = self.language_server.as_deref()?; self.language_config().into_iter().flat_map(move |config| {
server.is_initialized().then_some(server) config.language_servers.iter().filter_map(move |features| {
let ls = &**self.language_servers.get(&features.name)?;
if ls.is_initialized() {
Some(ls)
} else {
None
}
})
})
}
pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> {
self.language_servers.remove(name)
}
pub fn language_servers_with_feature(
&self,
feature: LanguageServerFeature,
) -> impl Iterator<Item = &helix_lsp::Client> {
self.language_config().into_iter().flat_map(move |config| {
config.language_servers.iter().filter_map(move |features| {
let ls = &**self.language_servers.get(&features.name)?;
if ls.is_initialized()
&& ls.supports_feature(feature)
&& features.has_feature(feature)
{
Some(ls)
} else {
None
}
})
})
}
pub fn supports_language_server(&self, id: usize) -> bool {
self.language_servers().any(|l| l.id() == id)
} }
pub fn diff_handle(&self) -> Option<&DiffHandle> { pub fn diff_handle(&self) -> Option<&DiffHandle> {
@ -1271,7 +1476,7 @@ impl Document {
/// Intialize/updates the differ for this document with a new base. /// Intialize/updates the differ for this document with a new base.
pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) { pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
if let Ok((diff_base, _)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) { if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
if let Some(differ) = &self.diff_handle { if let Some(differ) = &self.diff_handle {
differ.update_diff_base(diff_base); differ.update_diff_base(diff_base);
return; return;
@ -1385,12 +1590,29 @@ impl Document {
&self.diagnostics &self.diagnostics
} }
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator {
self.diagnostics = diagnostics; self.diagnostics.iter().filter(|d| {
self.language_servers_with_feature(LanguageServerFeature::Diagnostics)
.any(|ls| ls.id() == d.language_server_id)
})
}
pub fn replace_diagnostics(
&mut self,
mut diagnostics: Vec<Diagnostic>,
language_server_id: usize,
) {
self.clear_diagnostics(language_server_id);
self.diagnostics.append(&mut diagnostics);
self.diagnostics self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range); .sort_unstable_by_key(|diagnostic| diagnostic.range);
} }
pub fn clear_diagnostics(&mut self, language_server_id: usize) {
self.diagnostics
.retain(|d| d.language_server_id != language_server_id);
}
/// Get the document's auto pairs. If the document has a recognized /// Get the document's auto pairs. If the document has a recognized
/// language config with auto pairs configured, returns that; /// language config with auto pairs configured, returns that;
/// otherwise, falls back to the global auto pairs config. If the global /// otherwise, falls back to the global auto pairs config. If the global
@ -1715,7 +1937,7 @@ mod test {
assert!(ref_path.exists()); assert!(ref_path.exists());
let mut file = std::fs::File::open(path).unwrap(); let mut file = std::fs::File::open(path).unwrap();
let text = from_reader(&mut file, Some(encoding)) let text = from_reader(&mut file, Some(encoding.into()))
.unwrap() .unwrap()
.0 .0
.to_string(); .to_string();
@ -1741,7 +1963,7 @@ mod test {
let text = Rope::from_str(&std::fs::read_to_string(path).unwrap()); let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap(); helix_lsp::block_on(to_writer(&mut buf, (encoding, false), &text)).unwrap();
let expectation = std::fs::read(ref_path).unwrap(); let expectation = std::fs::read(ref_path).unwrap();
assert_eq!(buf, expectation); assert_eq!(buf, expectation);

@ -1,7 +1,7 @@
use crate::{ use crate::{
align_view, align_view,
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -354,6 +354,8 @@ pub struct LspConfig {
pub display_inlay_hints: bool, pub display_inlay_hints: bool,
/// Whether to enable snippet support /// Whether to enable snippet support
pub snippets: bool, pub snippets: bool,
/// Whether to include declaration in the goto reference query
pub goto_reference_include_declaration: bool,
} }
impl Default for LspConfig { impl Default for LspConfig {
@ -365,6 +367,7 @@ impl Default for LspConfig {
display_signature_help_docs: true, display_signature_help_docs: true,
display_inlay_hints: false, display_inlay_hints: false,
snippets: true, snippets: true,
goto_reference_include_declaration: true,
} }
} }
} }
@ -815,7 +818,7 @@ pub struct Editor {
pub macro_recording: Option<(char, Vec<KeyEvent>)>, pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>, pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
pub diff_providers: DiffProviderRegistry, pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>, pub debugger: Option<dap::Client>,
@ -871,7 +874,7 @@ pub struct Editor {
/// times during rendering and should not be set by other functions. /// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>, pub cursor_cache: Cell<Option<Option<Position>>>,
/// When a new completion request is sent to the server old /// When a new completion request is sent to the server old
/// unifinished request must be dropped. Each completion /// unfinished request must be dropped. Each completion
/// request is associated with a channel that cancels /// request is associated with a channel that cancels
/// when the channel is dropped. That channel is stored /// when the channel is dropped. That channel is stored
/// here. When a new completion request is sent this /// here. When a new completion request is sent this
@ -903,9 +906,14 @@ enum ThemeAction {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CompleteAction { pub enum CompleteAction {
pub trigger_offset: usize, Applied {
pub changes: Vec<Change>, 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)] #[derive(Debug, Copy, Clone)]
@ -933,6 +941,7 @@ impl Editor {
syn_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>, config: Arc<dyn DynAccess<Config>>,
) -> Self { ) -> Self {
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
let conf = config.load(); let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into(); let auto_pairs = (&conf.auto_pairs).into();
@ -952,7 +961,7 @@ impl Editor {
macro_recording: None, macro_recording: None,
macro_replaying: Vec::new(), macro_replaying: Vec::new(),
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(), language_servers,
diagnostics: BTreeMap::new(), diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(), diff_providers: DiffProviderRegistry::default(),
debugger: None, debugger: None,
@ -1084,60 +1093,75 @@ impl Editor {
self._refresh(); self._refresh();
} }
#[inline]
pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
self.language_servers.get_by_id(language_server_id)
}
/// Refreshes the language server for a given document /// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_server(doc_id) self.launch_language_servers(doc_id)
} }
/// Launch a language server for a given document /// Launch a language server for a given document
fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> { fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
if !self.config().lsp.enable { if !self.config().lsp.enable {
return None; return None;
} }
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it
let doc = self.document(doc_id)?; let doc = self.documents.get_mut(&doc_id)?;
let doc_url = doc.url()?;
let (lang, path) = (doc.language.clone(), doc.path().cloned()); let (lang, path) = (doc.language.clone(), doc.path().cloned());
let config = doc.config.load(); let config = doc.config.load();
let root_dirs = &config.workspace_lsp_roots; let root_dirs = &config.workspace_lsp_roots;
// try to find a language server based on the language name // try to find language servers based on the language name
let language_server = lang.as_ref().and_then(|language| { let language_servers = lang.as_ref().and_then(|language| {
self.language_servers self.language_servers
.get(language, path.as_ref(), root_dirs, config.lsp.snippets) .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}", "Failed to initialize the language servers for `{}` {{ {} }}",
language.scope(), language.scope(),
e e
) )
}) })
.ok() .ok()
.flatten()
}); });
let doc = self.document_mut(doc_id)?; if let Some(language_servers) = language_servers {
let doc_url = doc.url()?; let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
if let Some(language_server) = language_server { // only spawn new language servers if the servers aren't the same
// only spawn a new lang server if the servers aren't the same
if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); let doc_language_servers_not_in_registry =
doc.language_servers.iter().filter(|(name, doc_ls)| {
language_servers
.get(*name)
.map_or(true, |ls| ls.id() != doc_ls.id())
});
for (_, language_server) in doc_language_servers_not_in_registry {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
doc.language_servers
.get(*name)
.map_or(true, |doc_ls| ls.id() != doc_ls.id())
});
for (_, language_server) in language_servers_not_in_doc {
// TODO: this now races with on_init code if the init happens too quickly // TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open( tokio::spawn(language_server.text_document_did_open(
doc_url, doc_url.clone(),
doc.version(), doc.version(),
doc.text(), doc.text(),
language_id, language_id.clone(),
)); ));
doc.set_language_server(Some(language_server));
} }
doc.language_servers = language_servers;
} }
Some(()) Some(())
} }
@ -1173,6 +1197,7 @@ impl Editor {
let doc = doc_mut!(self, &doc_id); let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view.id); doc.ensure_view_init(view.id);
view.sync_changes(doc); view.sync_changes(doc);
doc.mark_as_focused();
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
} }
@ -1243,6 +1268,7 @@ impl Editor {
let view_id = view!(self).id; let view_id = view!(self).id;
let doc = doc_mut!(self, &id); let doc = doc_mut!(self, &id);
doc.ensure_view_init(view_id); doc.ensure_view_init(view_id);
doc.mark_as_focused();
return; return;
} }
Action::HorizontalSplit | Action::VerticalSplit => { Action::HorizontalSplit | Action::VerticalSplit => {
@ -1264,6 +1290,7 @@ impl Editor {
// initialize selection for view // initialize selection for view
let doc = doc_mut!(self, &id); let doc = doc_mut!(self, &id);
doc.ensure_view_init(view_id); doc.ensure_view_init(view_id);
doc.mark_as_focused();
} }
} }
@ -1299,10 +1326,10 @@ impl Editor {
} }
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> { pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?; let (rope, encoding, has_bom) = crate::document::from_reader(&mut stdin(), None)?;
Ok(self.new_file_from_document( Ok(self.new_file_from_document(
action, action,
Document::from(rope, Some(encoding), self.config.clone()), Document::from(rope, Some((encoding, has_bom)), self.config.clone()),
)) ))
} }
@ -1327,7 +1354,7 @@ impl Editor {
doc.set_version_control_head(self.diff_providers.get_current_head_name(&path)); doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
let id = self.new_document(doc); let id = self.new_document(doc);
let _ = self.launch_language_server(id); let _ = self.launch_language_servers(id);
id id
}; };
@ -1357,7 +1384,7 @@ impl Editor {
// This will also disallow any follow-up writes // This will also disallow any follow-up writes
self.saves.remove(&doc_id); self.saves.remove(&doc_id);
if let Some(language_server) = doc.language_server() { for language_server in doc.language_servers() {
// TODO: track error // TODO: track error
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
@ -1414,6 +1441,7 @@ impl Editor {
let view_id = self.tree.insert(view); let view_id = self.tree.insert(view);
let doc = doc_mut!(self, &doc_id); let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view_id); doc.ensure_view_init(view_id);
doc.mark_as_focused();
} }
self._refresh(); self._refresh();
@ -1468,6 +1496,10 @@ impl Editor {
view.sync_changes(doc); view.sync_changes(doc);
} }
} }
let view = view!(self, view_id);
let doc = doc_mut!(self, &view.doc);
doc.mark_as_focused();
} }
pub fn focus_next(&mut self) { pub fn focus_next(&mut self) {

@ -1,5 +1,7 @@
use std::fmt::Write; use std::fmt::Write;
use helix_core::syntax::LanguageServerFeature;
use crate::{ use crate::{
editor::GutterType, editor::GutterType,
graphics::{Style, UnderlineStyle}, graphics::{Style, UnderlineStyle},
@ -55,7 +57,7 @@ pub fn diagnostic<'doc>(
let error = theme.get("error"); let error = theme.get("error");
let info = theme.get("info"); let info = theme.get("info");
let hint = theme.get("hint"); let hint = theme.get("hint");
let diagnostics = doc.diagnostics(); let diagnostics = &doc.diagnostics;
Box::new( Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
@ -63,28 +65,24 @@ pub fn diagnostic<'doc>(
return None; return None;
} }
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line);
let after = diagnostics[index..].iter().take_while(|d| d.line == line); let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..]
.iter()
let before = diagnostics[..index] .take_while(|d| {
.iter() d.line == line
.rev() && doc
.take_while(|d| d.line == line); .language_servers_with_feature(LanguageServerFeature::Diagnostics)
.any(|ls| ls.id() == d.language_server_id)
let diagnostics_on_line = after.chain(before); });
diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
// This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search. write!(out, "●").ok();
let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap(); match d.severity {
write!(out, "●").unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error, Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning, Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info, Some(Severity::Info) => info,
Some(Severity::Hint) => hint, Some(Severity::Hint) => hint,
}); }
} })
None
}, },
) )
} }

@ -728,12 +728,11 @@ mod test {
tree.focus = l0; tree.focus = l0;
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(DocumentId::default(), GutterConfig::default());
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
let l2 = tree.focus;
// Tree in test // Tree in test
// | L0 | L2 | | // | L0 | L2 | |
// | L1 | R0 | // | L1 | R0 |
tree.focus = l2; let l2 = tree.focus;
assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left)); assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down)); assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right)); assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));

File diff suppressed because it is too large Load Diff

@ -0,0 +1,56 @@
(object_id) @attribute
(string) @string
(escape_sequence) @constant.character.escape
(comment) @comment
(constant) @constant.builtin
(boolean) @constant.builtin.boolean
(template) @keyword
(using) @keyword.control.import
(decorator) @attribute
(property_definition (property_name) @variable.other.member)
(object) @type
(signal_binding (signal_name) @function.builtin)
(signal_binding (function (identifier)) @function)
(signal_binding "swapped" @keyword)
(styles_list "styles" @function.macro)
(layout_definition "layout" @function.macro)
(gettext_string "_" @function.builtin)
(menu_definition "menu" @keyword)
(menu_section "section" @keyword)
(menu_item "item" @function.macro)
(template_definition (template_name_qualifier) @keyword.storage.type)
(import_statement (gobject_library) @namespace)
(import_statement (version_number) @constant.numeric.float)
(float) @constant.numeric.float
(number) @constant.numeric
[
";"
"."
","
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket

@ -1,15 +0,0 @@
(comment) @comment
[
"cabal-version"
(field_name)
] @type
(section_name) @type
[
(section_type)
"if"
"elseif"
"else"
] @keyword

@ -1,94 +1 @@
(ERROR) @error ; inherits: rust
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]+$"))
((identifier_def) @constant
(#match? @constant "^[A-Z][A-Z\\d_]+$"))
((identifier) @namespace
(#match? @namespace "^[A-Z]"))
((identifier_def) @namespace
(#match? @namespace "^[A-Z]"))
(identifier "." @punctuation)
(function_call (identifier) @function)
(func (identifier_def) @function)
(string) @string
(atom_short_string) @string
(code_element_directive) @keyword.directive
"return" @keyword
(number) @constant.numeric
(atom_hex_number) @constant.numeric
(comment) @comment
"*" @special
(type) @type
[
"felt"
; "codeoffset"
] @type.builtin
[
"if"
"else"
"assert"
"with"
"with_attr"
] @keyword.control
[
"from"
"import"
"func"
"namespace"
] @keyword ; keyword.declaration
[
"let"
"const"
"local"
"struct"
"alloc_locals"
"tempvar"
] @keyword
(decorator) @attribute
[
"="
"+"
"-"
"*"
"/"
; "%"
; "!"
; ">"
; "<"
; "\\"
; "&"
; "?"
; "^"
; "~"
"=="
"!="
"new"
] @operator
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
","
":"
] @punctuation.delimiter

@ -1,5 +1,3 @@
((hint) @injection.content ([(line_comment) (block_comment)] @injection.content
(#set! injection.language "python"))
((comment) @injection.content
(#set! injection.language "comment")) (#set! injection.language "comment"))

@ -48,4 +48,7 @@
((variable) @constant ((variable) @constant
(#match? @constant "^[A-Z][A-Z_0-9]*$")) (#match? @constant "^[A-Z][A-Z_0-9]*$"))
[
(param)
(mount_param)
] @constant

@ -0,0 +1,8 @@
([(start_definition)(end_definition)] @keyword)
([(lparen) (rparen)] @punctuation.bracket)
((stack_effect_sep) @punctuation)
((number) @constant)
((word) @function)
((comment) @comment)
([(core)] @type)
([(operator)] @operator)

@ -13,8 +13,6 @@
(marker_annotation (marker_annotation
name: (identifier) @attribute) name: (identifier) @attribute)
"@" @operator
; Types ; Types
(interface_declaration (interface_declaration
@ -48,6 +46,9 @@
(void_type) (void_type)
] @type.builtin ] @type.builtin
(type_arguments
(wildcard "?" @type.builtin))
; Variables ; Variables
((identifier) @constant ((identifier) @constant
@ -87,6 +88,84 @@
(line_comment) @comment (line_comment) @comment
(block_comment) @comment (block_comment) @comment
; Punctuation
[
"::"
"."
";"
","
] @punctuation.delimiter
[
"@"
"..."
] @punctuation.special
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(type_arguments
[
"<"
">"
] @punctuation.bracket)
(type_parameters
[
"<"
">"
] @punctuation.bracket)
; Operators
[
"="
">"
"<"
"!"
"~"
"?"
":"
"->"
"=="
">="
"<="
"!="
"&&"
"||"
"++"
"--"
"+"
"-"
"*"
"/"
"&"
"|"
"^"
"%"
"<<"
">>"
">>>"
"+="
"-="
"*="
"/="
"&="
"|="
"^="
"%="
"<<="
">>="
">>>="
] @operator
; Keywords ; Keywords
[ [

@ -1,5 +1,9 @@
; From nvim-treesitter/nvim-treesitter ; From nvim-treesitter/nvim-treesitter
(fenced_code_block
(code_fence_content) @injection.shebang @injection.content
(#set! injection.include-unnamed-children))
(fenced_code_block (fenced_code_block
(info_string (info_string
(language) @injection.language) (language) @injection.language)

@ -20,7 +20,7 @@
) )
(record_operand (atom (ident) @variable)) (record_operand (atom (ident) @variable))
(let_expr (let_in_block
"let" @keyword "let" @keyword
"rec"? @keyword "rec"? @keyword
pat: (pattern pat: (pattern
@ -53,7 +53,7 @@
(interpolation_end) @punctuation.bracket (interpolation_end) @punctuation.bracket
["forall" "default" "doc"] @keyword ["forall" "default" "doc"] @keyword
["if" "then" "else" "switch"] @keyword.control.conditional ["if" "then" "else" "match"] @keyword.control.conditional
"import" @keyword.control.import "import" @keyword.control.import
(infix_expr (infix_expr

@ -1,7 +1,7 @@
[ [
(fun_expr) (fun_expr)
(let_expr) (let_expr)
(switch_expr) (match_expr)
(ite_expr) (ite_expr)
(uni_record) (uni_record)

@ -0,0 +1,3 @@
(annot_atom doc: (static_string)
@injection.content
(#set! injection.language "markdown"))

@ -47,8 +47,10 @@
(float_expression) @constant.numeric.float (float_expression) @constant.numeric.float
(escape_sequence) @constant.character.escape (escape_sequence) @constant.character.escape
(dollar_escape) @constant.character.escape
(function_expression (function_expression
"@"? @punctuation.delimiter
universal: (identifier) @variable.parameter universal: (identifier) @variable.parameter
"@"? @punctuation.delimiter "@"? @punctuation.delimiter
) )
@ -82,7 +84,8 @@
(binding (binding
attrpath: (attrpath attr: (identifier)) @variable.other.member) attrpath: (attrpath attr: (identifier)) @variable.other.member)
(inherit_from attrs: (inherited_attrs attr: (identifier) @variable)) (inherit_from attrs: (inherited_attrs attr: (identifier) @variable.other.member))
(inherited_attrs attr: (identifier) @variable)
(has_attr_expression (has_attr_expression
expression: (_) expression: (_)

@ -10,9 +10,11 @@
; such as those of stdenv.mkDerivation. ; such as those of stdenv.mkDerivation.
((binding ((binding
attrpath: (attrpath (identifier) @_path) attrpath: (attrpath (identifier) @_path)
expression: (indented_string_expression expression: [
(string_fragment) @injection.content)) (indented_string_expression (string_fragment) @injection.content)
(#match? @_path "(^\\w*Phase|(pre|post)\\w*|(.*\\.)?\\w*([sS]cript|[hH]ook)|(.*\\.)?startup)$") (binary_expression (indented_string_expression (string_fragment) @injection.content))
])
(#match? @_path "(^\\w*Phase|command|(pre|post)\\w*|(.*\\.)?\\w*([sS]cript|[hH]ook)|(.*\\.)?startup)$")
(#set! injection.language "bash") (#set! injection.language "bash")
(#set! injection.combined)) (#set! injection.combined))
@ -150,3 +152,13 @@
; (#match? @_func "(^|\\.)writeFSharp(Bin)?$") ; (#match? @_func "(^|\\.)writeFSharp(Bin)?$")
; (#set! injection.language "f-sharp") ; (#set! injection.language "f-sharp")
; (#set! injection.combined)) ; (#set! injection.combined))
((apply_expression
function: (apply_expression function: (_) @_func
argument: (string_expression (string_fragment) @injection.filename))
argument: (indented_string_expression (string_fragment) @injection.content))
(#match? @_func "(^|\\.)write(Text|Script(Bin)?)$")
(#set! injection.combined))
((indented_string_expression (string_fragment) @injection.shebang @injection.content)
(#set! injection.combined))

@ -1,141 +1,26 @@
; Function calls (keyword) @keyword
(operator) @operator
(call_expression (int_literal) @constant.numeric.integer
function: (identifier) @function) (float_literal) @constant.numeric.float
(rune_literal) @constant.character
(call_expression (bool_literal) @constant.builtin.boolean
function: (selector_expression (nil) @constant.builtin
field: (field_identifier) @function))
; ; Function definitions
(function_declaration
name: (identifier) @function)
(proc_group
(identifier) @function)
; ; Identifiers
(type_identifier) @type
(field_identifier) @variable.other.member
(identifier) @variable
(const_declaration
(identifier) @constant)
(const_declaration_with_type
(identifier) @constant)
"any" @type
(directive_identifier) @constant
; ; Operators
[
"?"
"-"
"-="
":="
"!"
"!="
"*"
"*"
"*="
"/"
"/="
"&"
"&&"
"&="
"%"
"%="
"^"
"+"
"+="
"<-"
"<"
"<<"
"<<="
"<="
"="
"=="
">"
">="
">>"
">>="
"|"
"|="
"||"
"~"
".."
"..<"
"..="
"::"
] @operator
; ; Keywords
[
; "asm"
"auto_cast"
; "bit_set"
"cast"
; "context"
; "or_else"
; "or_return"
"in"
; "not_in"
"distinct"
"foreign"
"transmute"
; "typeid"
"break"
"case"
"continue"
"defer"
"else"
"using"
"when"
"where"
"fallthrough"
"for"
"proc"
"if"
"import"
"map"
"package"
"return"
"struct"
"union"
"enum"
"switch"
"dynamic"
] @keyword
; ; Literals
[ (type_identifier) @type
(interpreted_string_literal) (package_identifier) @namespace
(raw_string_literal) (label_identifier) @label
(rune_literal)
] @string
(interpreted_string_literal) @string
(raw_string_literal) @string
(escape_sequence) @constant.character.escape (escape_sequence) @constant.character.escape
(int_literal) @constant.numeric.integer (comment) @comment
(float_literal) @constant.numeric.float (const_identifier) @constant
(imaginary_literal) @constant.numeric
[
(true)
(false)
] @constant.builtin.boolean
[ (compiler_directive) @keyword.directive
(nil) (calling_convention) @string.special.symbol
(undefined)
] @constant.builtin
(comment) @comment.line (identifier) @variable
(pragma_identifier) @keyword.directive

@ -0,0 +1,16 @@
[
(foreign_block)
(block)
(compound_literal)
(proc_call)
(assignment_statement)
(const_declaration)
(var_declaration)
(switch_statement)
] @indent
[
")"
"}"
] @outdent

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

@ -25,11 +25,11 @@
arguments: (arguments (raw_string_literal) @injection.content) arguments: (arguments (raw_string_literal) @injection.content)
(#set! injection.language "regex")) (#set! injection.language "regex"))
; Highlight SQL in `sqlx::query!()` ; Highlight SQL in `sqlx::query!()`, `sqlx::query_scalar!()`, and `sqlx::query_scalar_unchecked!()`
(macro_invocation (macro_invocation
macro: (scoped_identifier macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx") path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query (#eq? @_query "query")) name: (identifier) @_query (#match? @_query "^query(_scalar|_scalar_unchecked)?$"))
(token_tree (token_tree
; Only the first argument is SQL ; Only the first argument is SQL
. .
@ -37,38 +37,11 @@
) )
(#set! injection.language "sql")) (#set! injection.language "sql"))
; Highlight SQL in `sqlx::query_as!()` ; Highlight SQL in `sqlx::query_as!()` and `sqlx::query_as_unchecked!()`
(macro_invocation (macro_invocation
macro: (scoped_identifier macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx") path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query_as (#eq? @_query_as "query_as")) name: (identifier) @_query_as (#match? @_query_as "^query_as(_unchecked)?$"))
(token_tree
; Only the second argument is SQL
.
; Allow anything as the first argument in case the user has lower case type
; names for some reason
(_)
[(string_literal) (raw_string_literal)] @injection.content
)
(#set! injection.language "sql"))
; Highlight SQL in `sqlx::query_unchecked!()`
(macro_invocation
macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query_as (#eq? @_query_as "query_unchecked"))
(token_tree
; Only the first argument is SQL
.
[(string_literal) (raw_string_literal)] @injection.content
)
(#set! injection.language "sql"))
; Highlight SQL in `sqlx::query_as_unchecked!()`
(macro_invocation
macro: (scoped_identifier
path: (identifier) @_sqlx (#eq? @_sqlx "sqlx")
name: (identifier) @_query_as (#eq? @_query_as "query_as_unchecked"))
(token_tree (token_tree
; Only the second argument is SQL ; Only the second argument is SQL
. .

@ -0,0 +1,29 @@
(template_body) @local.scope
(lambda_expression) @local.scope
(function_declaration
name: (identifier) @local.definition) @local.scope
(function_definition
name: (identifier) @local.definition)
(parameter
name: (identifier) @local.definition)
(binding
name: (identifier) @local.definition)
(val_definition
pattern: (identifier) @local.definition)
(var_definition
pattern: (identifier) @local.definition)
(val_declaration
name: (identifier) @local.definition)
(var_declaration
name: (identifier) @local.definition)
(identifier) @local.reference

@ -5,7 +5,6 @@
(ambient_declaration "global" @namespace) (ambient_declaration "global" @namespace)
; Variables ; Variables
(required_parameter (identifier) @variable.parameter) (required_parameter (identifier) @variable.parameter)
@ -22,8 +21,6 @@
(conditional_type ["?" ":"] @operator) (conditional_type ["?" ":"] @operator)
; Keywords ; Keywords
[ [
@ -50,16 +47,22 @@
"readonly" "readonly"
] @keyword.storage.modifier ] @keyword.storage.modifier
; inherits: ecma
; Types ; Types
(type_identifier) @type (type_identifier) @type
(predefined_type) @type.builtin (predefined_type) @type.builtin
(type_arguments (type_arguments
"<" @punctuation.bracket [
">" @punctuation.bracket) "<"
">"
] @punctuation.bracket)
(type_parameters
[
"<"
">"
] @punctuation.bracket)
((identifier) @type ((identifier) @type
(#match? @type "^[A-Z]")) (#match? @type "^[A-Z]"))
@ -75,3 +78,5 @@
(template_type (template_type
"${" @punctuation.special "${" @punctuation.special
"}" @punctuation.special) @embedded "}" @punctuation.special) @embedded
; inherits: ecma

@ -0,0 +1,138 @@
"attribute" = { fg = "blue", modifiers = ["italic"] }
"ui.virtual.wrap"="softwrap"
"keyword" = "keyword"
"keyword.control.conditional" = { fg = "conditional", modifiers = ["italic"] }
"keyword.directive" = "magenta" # -- preprocessor comments (#if in C)
"namespace" = { fg = "namespace", modifiers = ["italic"] }
"punctuation" = "gray06"
"punctuation.delimiter" = "gray06"
"operator" = "operator"
"special" = "yellow"
"variable" = {fg="fg"}
"variable.builtin" = "bright_blue"
"variable.parameter" = {fg="white", modifiers=["italic"]}
"variable.other.member" = "white"
"type" = "bright_blue"
"type.builtin" = "magenta"
"type.enum.variant" = "magenta"
"constructor" = "yellow"
"function" = {fg="function", modifiers=["italic"]}
"function.macro" = "bright_cyan"
"function.builtin" = "support_function"
"tag" = "tag"
"comment" = { fg = "comment", modifiers = ["italic"] }
"string" = "string"
"string.regexp" = "green"
"string.special" = "yellow"
"constant" = "constant"
"constant.builtin" = "yellow"
"constant.numeric" = "numeric"
"constant.character.escape" = "cyan"
# used for lifetimes
"label" = "yellow"
"markup.heading.marker" = { fg = "gray06" }
"markup.heading" = { fg = "bright_blue", modifiers = ["bold"] }
"markup.list" = "gray06"
"markup.bold" = { modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.link.url" = { fg = "green", modifiers = ["underlined"] }
"markup.link.text" = { fg = "blue", modifiers = ["italic"] }
"markup.raw" = "yellow"
"diff.plus" = "bright_green"
"diff.minus" = "red"
"diff.delta" = "bright_blue"
"ui.background" = { bg = "bg" }
"ui.background.separator" = { fg = "fg" }
"ui.linenr" = { fg = "gray04" }
"ui.linenr.selected" = { fg = "fg" }
"ui.statusline" = { fg = "status_line_fg", bg = "gray01" }
"ui.statusline.inactive" = { fg = "fg", bg = "gray01", modifiers = ["dim"] }
"ui.statusline.normal" = { fg = "bg", bg = "cyan", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "bg", bg = "blue", modifiers = ["bold"] }
"ui.statusline.select" = { fg = "bg", bg = "magenta", modifiers = ["bold"] }
"ui.popup" = { bg = "gray01" }
"ui.window" = { fg = "gray02" }
"ui.help" = { bg = "gray01", fg = "white" }
"ui.text" = { fg = "fg" }
"ui.text.focus" = { fg = "fg" }
"ui.virtual" = { fg = "gray02" }
"ui.virtual.ruler" = {bg="gray02"}
"ui.virtual.indent-guide" = { fg = "gray02" }
"ui.virtual.inlay-hint" = { fg = "gray03" }
"ui.selection" = { bg = "gray03" }
"ui.selection.primary" = { bg = "gray03" }
"ui.cursor" = {fg="bg", bg = "cursor" }
"ui.cursor.match" = { fg = "yellow", modifiers = ["bold", "underlined"] }
"ui.cursorline.primary" = { bg = "gray01" }
"ui.highlight" = { bg = "gray02" }
"ui.menu" = { fg = "white", bg = "gray01" }
"ui.menu.selected" = { fg = "bright_white", bg = "gray03" }
"ui.menu.scroll" = { fg = "gray04", bg = "gray01" }
diagnostic = { modifiers = ["underlined"] }
warning = "yellow"
error = "error"
info = "bright_blue"
hint = "bright_cyan"
[palette]
error="#fca5a5"
bg = "#0F1014"
fg = "#c9c7cd"
green = "#90b99f"
bright_green = "#9dc6ac"
yellow = "#e5c890"
blue = "#aca1cf"
bright_blue = "#b9aeda"
magenta = "#e29eca"
cyan = "#ea83a5"
bright_cyan = "#f591b2"
white = "#c1c0d4"
bright_white = "#cac9dd"
gray01 = "#1b1b1d"
gray02 = "#2a2a2d"
gray03 = "#3e3e43"
gray04 = "#57575f"
gray06 = "#9998a8"
gray07 = "#c1c0d4"
comment="#808080"
red="#e78284"
function="#e5c890"
support_function="#9898a6"
constant="#8eb6f5"
string="#9898a6"
tag="#9898a6"
keyword="#8eb6f5"
namespace= "#c58fff"
numeric= "#e9c46a"
status_line_fg = "#e5c890"
operator="#8eb6f5"
softwrap="#808080"
conditional="#a8a29e"
cursor="#e5c890"

@ -51,8 +51,8 @@
"variable" = { fg = "foreground" } "variable" = { fg = "foreground" }
"variable.builtin" = { fg = "purple", modifiers = ["italic"] } "variable.builtin" = { fg = "purple", modifiers = ["italic"] }
"variable.parameter" = { fg = "orange", modifiers = ["italic"] } "variable.parameter" = { fg = "orange", modifiers = ["italic"] }
"variable.other" = { fg = "cyan" } "variable.other" = { fg = "foreground" }
"variable.other.member" = { fg = "purple" } "variable.other.member" = { fg = "foreground" }
"diff.plus" = { fg = "green" } "diff.plus" = { fg = "green" }
@ -73,10 +73,11 @@
"ui.highlight.frameline" = { fg = "background", bg = "red" } "ui.highlight.frameline" = { fg = "background", bg = "red" }
"ui.linenr" = { fg = "comment" } "ui.linenr" = { fg = "comment" }
"ui.linenr.selected" = { fg = "foreground" } "ui.linenr.selected" = { fg = "foreground" }
"ui.menu" = { fg = "foreground", bg = "black" } "ui.menu" = { fg = "foreground", bg = "current_line" }
"ui.menu.selected" = { fg = "cyan", bg = "black" } "ui.menu.selected" = { fg = "current_line", bg = "purple", modifiers = ["dim"] }
"ui.menu.scroll" = { fg = "foreground", bg = "current_line" }
"ui.popup" = { fg = "foreground", bg = "black" } "ui.popup" = { fg = "foreground", bg = "black" }
"ui.selection.primary" = { bg = "selection_primary" } "ui.selection.primary" = { bg = "current_line" }
"ui.selection" = { bg = "selection" } "ui.selection" = { bg = "selection" }
"ui.statusline" = { fg = "foreground", bg = "darker" } "ui.statusline" = { fg = "foreground", bg = "darker" }
"ui.statusline.inactive" = { fg = "comment", bg = "darker" } "ui.statusline.inactive" = { fg = "comment", bg = "darker" }
@ -86,8 +87,8 @@
"ui.text" = { fg = "foreground" } "ui.text" = { fg = "foreground" }
"ui.text.focus" = { fg = "cyan" } "ui.text.focus" = { fg = "cyan" }
"ui.window" = { fg = "foreground" } "ui.window" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "subtle" } "ui.virtual.whitespace" = { fg = "current_line" }
"ui.virtual.wrap" = { fg = "subtle" } "ui.virtual.wrap" = { fg = "current_line" }
"ui.virtual.ruler" = { bg = "black" } "ui.virtual.ruler" = { bg = "black" }
"ui.virtual.inlay-hint" = { fg = "cyan" } "ui.virtual.inlay-hint" = { fg = "cyan" }
"ui.virtual.inlay-hint.parameter" = { fg = "cyan", modifiers = ["italic", "dim"] } "ui.virtual.inlay-hint.parameter" = { fg = "cyan", modifiers = ["italic", "dim"] }
@ -121,13 +122,12 @@ darker = "#222430"
black = "#191A21" black = "#191A21"
grey = "#666771" grey = "#666771"
comment = "#6272A4" comment = "#6272A4"
selection_primary = "#44475a" current_line = "#44475a"
selection = "#363848" selection = "#363848"
subtle = "#424450"
red = "#ff5555" red = "#ff5555"
orange = "#ffb86c" orange = "#ffb86c"
yellow = "#f1fa8c" yellow = "#f1fa8c"
green = "#50fa7b" green = "#50fa7b"
purple = "#BD93F9" purple = "#BD93F9"
cyan = "#8be9fd" cyan = "#8be9fd"
pink = "#ff79c6" pink = "#ff79c6"

@ -48,8 +48,7 @@
"ui.text.focus" = { fg = "ferra_coral" } "ui.text.focus" = { fg = "ferra_coral" }
"ui.menu" = { fg = "ferra_blush", bg = "ferra_ash" } "ui.menu" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.menu.selected" = { fg = "ferra_coral", bg = "ferra_ash" } "ui.menu.selected" = { fg = "ferra_coral", bg = "ferra_ash" }
"ui.selection" = { bg = "ferra_umber", fg = "ferra_night" } "ui.selection" = { bg = "ferra_umber" }
"ui.selection.primary" = { bg = "ferra_night", fg = "ferra_umber" }
"ui.virtual" = { fg = "ferra_bark" } "ui.virtual" = { fg = "ferra_bark" }
"ui.virtual.whitespace" = { fg = "ferra_bark" } "ui.virtual.whitespace" = { fg = "ferra_bark" }
"ui.virtual.ruler" = { bg = "ferra_ash" } "ui.virtual.ruler" = { bg = "ferra_ash" }

@ -19,8 +19,8 @@
"ui.cursor.primary" = { bg = "fg1", fg = "bg1" } # The primary cursor when there are multiple (shift-c). "ui.cursor.primary" = { bg = "fg1", fg = "bg1" } # The primary cursor when there are multiple (shift-c).
"ui.cursor.match" = { fg = "yellow", modifiers = ["bold"] } # The matching parentheses of that under the cursor. "ui.cursor.match" = { fg = "yellow", modifiers = ["bold"] } # The matching parentheses of that under the cursor.
"ui.selection" = { bg = "bg3" } # All currently selected text. "ui.selection" = { bg = "bg4" } # All currently selected text.
"ui.selection.primary" = { bg = "bg4" } # The primary selection when there are multiple. "ui.selection.primary" = { bg = "sel1" } # The primary selection when there are multiple.
"ui.cursorline.primary" = { bg = "bg3" } # The line of the primary cursor (if cursorline is enabled) "ui.cursorline.primary" = { bg = "bg3" } # The line of the primary cursor (if cursorline is enabled)
# "ui.cursorline.secondary" = { } # The lines of any other cursors (if cursorline is enabled) # "ui.cursorline.secondary" = { } # The lines of any other cursors (if cursorline is enabled)
# "ui.cursorcolumn.primary" = { } # The column of the primary cursor (if cursorcolumn is enabled) # "ui.cursorcolumn.primary" = { } # The column of the primary cursor (if cursorcolumn is enabled)
@ -41,6 +41,10 @@
"ui.statusline.insert" = { bg = "green", fg = "bg0", modifiers = ["bold"] } # Statusline mode during insert mode (only if editor.color-modes is enabled) "ui.statusline.insert" = { bg = "green", fg = "bg0", modifiers = ["bold"] } # Statusline mode during insert mode (only if editor.color-modes is enabled)
"ui.statusline.select" = { bg = "magenta", fg = "bg0", modifiers = ["bold"] } # Statusline mode during select mode (only if editor.color-modes is enabled) "ui.statusline.select" = { bg = "magenta", fg = "bg0", modifiers = ["bold"] } # Statusline mode during select mode (only if editor.color-modes is enabled)
"ui.bufferline" = { fg = "fg3", bg = "bg2", underline = { style = "line" } }
"ui.bufferline.active" = { fg = "fg2", bg = "bg4" }
"ui.bufferline.background" = { bg = "bg0" }
"ui.help" = { bg = "sel0", fg = "fg1" } # Description box for commands. "ui.help" = { bg = "sel0", fg = "fg1" } # Description box for commands.
"ui.menu" = { bg = "sel0", fg = "fg1" } # Code and command completion menus. "ui.menu" = { bg = "sel0", fg = "fg1" } # Code and command completion menus.

@ -5,6 +5,7 @@
## GENERAL ============================== ## GENERAL ==============================
'property' = { fg = "red" } # Regex group names.
"warning" = { fg ="yellow", modifiers = ["bold"] } # Editor warnings. "warning" = { fg ="yellow", modifiers = ["bold"] } # Editor warnings.
"error" = { bg = "mid-green", fg = "red", modifiers = ["bold"] } # Editor errors, like mis-typing a command. "error" = { bg = "mid-green", fg = "red", modifiers = ["bold"] } # Editor errors, like mis-typing a command.
"info" = { fg = "mid-blue", bg = "mid-green" } # Code diagnostic info in gutter (LSP). "info" = { fg = "mid-blue", bg = "mid-green" } # Code diagnostic info in gutter (LSP).
@ -29,11 +30,11 @@
'ui.text.focus' = { fg = "white", bg = "mid-green", modifiers = ["bold"] } # Selection highlight in buffer-picker or file-picker. 'ui.text.focus' = { fg = "white", bg = "mid-green", modifiers = ["bold"] } # Selection highlight in buffer-picker or file-picker.
'ui.text.info' = { } # Info popup contents (space mode menu). 'ui.text.info' = { } # Info popup contents (space mode menu).
'ui.cursor' = { fg = "light-blue", modifiers = ["reversed"] } # Fallback cursor colour, non-primary cursors when there are multiple (shift-c). 'ui.cursor' = { fg = "dark-green", bg = "white" } # Fallback cursor colour, non-primary cursors when there are multiple (shift-c).
'ui.cursor.primary' = { fg = "light-blue", modifiers = ["reversed"] } # The primary cursor when there are multiple (shift-c). 'ui.cursor.primary' = { fg = "dark-green", bg = "light-blue" } # The primary cursor when there are multiple (shift-c).
'ui.cursor.insert' = { fg = "light-blue" } # The cursor in insert mode (i). 'ui.cursor.insert' = { fg = "dark-green", bg = "light-blue" } # The cursor in insert mode (i).
'ui.cursor.select' = { fg = "light-blue" } # The cursor in select mode (v). 'ui.cursor.select' = { fg = "dark-green", bg = "light-blue" } # The cursor in select mode (v).
'ui.cursor.match' = { fg = "red", modifiers = ["bold", "reversed"] } # The matching parentheses of that under the cursor. 'ui.cursor.match' = { fg = "dark-green", bg = "red", modifiers = ["bold"] } # The matching parentheses of that under the cursor.
'ui.selection' = { bg = "autocomp-green" } # All currently selected text. 'ui.selection' = { bg = "autocomp-green" } # All currently selected text.
'ui.selection.primary' = { bg = "autocomp-green" } # The primary selection when there are multiple. 'ui.selection.primary' = { bg = "autocomp-green" } # The primary selection when there are multiple.
@ -44,8 +45,8 @@
'ui.virtual' = { fg = "mid-green" } # Namespace for additions to the editing area. 'ui.virtual' = { fg = "mid-green" } # Namespace for additions to the editing area.
'ui.virtual.ruler' = { bg = "mid-green" } # Vertical rulers (colored columns in editing area). 'ui.virtual.ruler' = { bg = "mid-green" } # Vertical rulers (colored columns in editing area).
'ui.virtual.whitespace' = { fg = "gray"} # Whitespace markers in editing area. 'ui.virtual.whitespace' = { fg = "light-gray"} # Whitespace markers in editing area.
'ui.virtual.indent-guide' = { fg = "gray" } # Indentation guides. 'ui.virtual.indent-guide' = { fg = "light-gray" } # Indentation guides.
'ui.statusline' = { fg = "light-green", bg = "autocomp-green"} # Status line. 'ui.statusline' = { fg = "light-green", bg = "autocomp-green"} # Status line.
'ui.statusline.inactive' = { fg = "white", bg = "mid-green"} # Status line in unfocused windows. 'ui.statusline.inactive' = { fg = "white", bg = "mid-green"} # Status line in unfocused windows.
@ -197,6 +198,7 @@ purple = "#918cff"
white = "#b1cace" white = "#b1cace"
orange = "#ffa864" orange = "#ffa864"
gray = "#5b858b" # mainly for comments/background text gray = "#5b858b" # mainly for comments/background text
light-gray = "#354e51" # used when whitespace rendering is enabled and for indent-guides
red = "#e34e1b" red = "#e34e1b"
dark-blue = "#19a2b7" dark-blue = "#19a2b7"

@ -14,6 +14,7 @@
"variable" = "#715ab1" "variable" = "#715ab1"
"variable.builtin" = "#715ab1" "variable.builtin" = "#715ab1"
"variable.parameter" = "#7590db" "variable.parameter" = "#7590db"
"variable.other.member" = "#002db3"
"type" = "#6c3163" "type" = "#6c3163"
"type.builtin" = "#6c3163" "type.builtin" = "#6c3163"
"constructor" = { fg = "#4e3163", modifiers = ["bold"] } "constructor" = { fg = "#4e3163", modifiers = ["bold"] }

@ -23,7 +23,7 @@
================================================================= =================================================================
= INTRODUCTION = = INTRODUCTION =
================================================================= =================================================================
Welcome to the Helix editor! Helix is different from editors Welcome to the Helix editor! Helix is different from editors
you might be used to in that it is modal, meaning that it has you might be used to in that it is modal, meaning that it has
different modes for editing text. The primary modes you will different modes for editing text. The primary modes you will
@ -941,7 +941,7 @@ lines.
--> A horse is a horse, of course, of course, --> A horse is a horse, of course, of course,
--> And no one can talk to a horse of course. --> And no one can talk to a horse of course.
Note: * is like a shorthand for "/ y as all it really does is Note: * is like a shorthand for "/y as all it really does is
copy the selection into the / register. copy the selection into the / register.
================================================================= =================================================================
@ -1146,13 +1146,13 @@ To uncomment the line, press Ctrl-c again.
= 11.2 COMMENTING MULTIPLE LINES = = 11.2 COMMENTING MULTIPLE LINES =
================================================================= =================================================================
Using the selections and multi-cursor functionality, you can Using the selections and multi-cursor functionality, you can
comment multiple lines as long as it is under the selection or comment multiple lines as long as it is under the selection or
cursors. cursors.
1. Move your cursor to the line marked with '-->' below. 1. Move your cursor to the line marked with '-->' below.
2. Now try to select or add more cursors the other lines marked 2. Now try to select or add more cursors the other lines marked
with '-->'. with '-->'.
3. Comment those lines. 3. Comment those lines.
--> How many are you going to comment? --> How many are you going to comment?
@ -1170,7 +1170,7 @@ multiple cursors, they won't be uncommented but commented again.
* Use Ctrl-c to comment a line under your cursor. Press Ctrl-c * Use Ctrl-c to comment a line under your cursor. Press Ctrl-c
again to uncomment. again to uncomment.
* To comment multiple lines, use the selections * To comment multiple lines, use the selections
and multi-cursors before typing Ctrl-c. and multi-cursors before typing Ctrl-c.
* Commented lines cannot be uncommented but commented again. * Commented lines cannot be uncommented but commented again.

@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
); );
} }
row.push( row.push(
lc.language_server lc.language_servers
.as_ref() .iter()
.map(|s| s.command.clone()) .filter_map(|ls| config.language_server.get(&ls.name))
.map(|c| md_mono(&c)) .map(|s| md_mono(&s.command.clone()))
.unwrap_or_default(), .collect::<Vec<_>>()
.join(", "),
); );
md.push_str(&md_table_row(&row)); md.push_str(&md_table_row(&row));

Loading…
Cancel
Save