diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 7d2f734aa..7adb53269 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v3 - name: Install nix - uses: cachix/install-nix-action@v20 + uses: cachix/install-nix-action@v22 - name: Authenticate with Cachix uses: cachix/cachix-action@v12 diff --git a/CHANGELOG.md b/CHANGELOG.md index 01184571e..9f7509b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors. @@ -1479,7 +1596,7 @@ to distinguish it in bug reports.. - The `runtime/` directory is now properly detected on binary releases and on cargo run. `~/.config/helix/runtime` can also be used. -- Registers can now be selected via " (for example `"ay`) +- Registers can now be selected via " (for example, `"ay`) - Support for Nix files was added - Movement is now fully tested and matches Kakoune implementation - A per-file LSP symbol picker was added to space+s diff --git a/Cargo.lock b/Cargo.lock index 9ebe89ecd..418926622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,8 +34,8 @@ dependencies = [ "abi_stable_shared", "as_derive_utils", "core_extensions", - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.63", + "quote 1.0.29", "rustc_version", "syn 1.0.109", "typed-arena", @@ -50,6 +50,15 @@ dependencies = [ "core_extensions", ] +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -73,7 +82,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "getrandom", "once_cell", "version_check", @@ -90,13 +99,25 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -108,9 +129,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arc-swap" @@ -131,8 +152,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff3c96645900a44cf11941c111bd08a6573b0e2f9f69bc9264b179d8fae753c4" dependencies = [ "core_extensions", - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.63", + "quote 1.0.29", "syn 1.0.109", ] @@ -153,11 +174,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" -version = "0.13.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "beef" @@ -182,9 +218,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bitmaps" @@ -197,9 +233,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" dependencies = [ "memchr", "once_cell", @@ -218,9 +254,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytecount" @@ -252,12 +288,6 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -270,20 +300,20 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "encoding_rs", "memchr", ] [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time 0.1.45", "wasm-bindgen", @@ -313,7 +343,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d" dependencies = [ - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -388,7 +418,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -397,7 +427,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -411,7 +441,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -421,19 +451,19 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", @@ -445,17 +475,17 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -493,88 +523,23 @@ dependencies = [ [[package]] name = "crossterm_winapi" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2 1.0.60", - "quote 1.0.28", - "scratch", - "syn 2.0.18", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", -] - [[package]] name = "dashmap" version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "num_cpus", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - -[[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]] name = "dlopen" version = "0.1.8" @@ -616,7 +581,7 @@ version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -628,6 +593,12 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -636,7 +607,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -661,12 +632,13 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51822eedc6129d8c4d96cec86d56b785e983f943c9ce9fb892e0c2a99a7f47a0" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "home", + "windows-sys", ] [[package]] @@ -678,6 +650,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fern" version = "0.6.2" @@ -693,17 +671,17 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall 0.2.16", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -717,9 +695,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -778,9 +756,9 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] @@ -833,32 +811,39 @@ dependencies = [ [[package]] name = "generational-arena" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "gix" -version = "0.44.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2353761ba46eabc95759eb1deed72c99cb31ad8930bc5d811c06e3f52b0feb" +checksum = "10f5281c55e0a7415877d91a15fae4a10ec7444615d64d78e48c07f20bcfcd9b" dependencies = [ "gix-actor", "gix-attributes", + "gix-commitgraph", "gix-config", "gix-credentials", "gix-date", @@ -873,6 +858,7 @@ dependencies = [ "gix-index", "gix-lock", "gix-mailmap", + "gix-negotiate", "gix-object", "gix-odb", "gix-pack", @@ -883,6 +869,7 @@ dependencies = [ "gix-revision", "gix-sec", "gix-tempfile", + "gix-trace", "gix-traverse", "gix-url", "gix-utils", @@ -898,9 +885,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.20.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848efa0f1210cea8638f95691c82a46f98a74b9e3524f01d4955ebc25a8f84f3" +checksum = "b70d0d809ee387113df810ab4ebe585a076e35ae6ed59b5b280072146955a3ff" dependencies = [ "bstr", "btoi", @@ -912,9 +899,9 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "371c78ac6b4ef130abedc0f09c8f4b43d846df62d2d1571ca4e8cc5479886760" +checksum = "e3772b0129dcd1fc73e985bbd08a1482d082097d2915cb1ee31ce8092b8e4434" dependencies = [ "bstr", "gix-glob", @@ -929,36 +916,50 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a95f4942360766c3880bdb2b4b57f1ef73b190fc424755e7fdf480430af618" +checksum = "311e2fa997be6560c564b070c5da2d56d038b645a94e1e5796d5d85a350da33c" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d39583cab06464b8bf73b3f1707458270f0e7383cb24c3c9c1a16e6f792978" +checksum = "39db5ed0fc0a2e9b1b8265993f7efdbc30379dec268f3b91b7af0c2de4672fdd" dependencies = [ "thiserror", ] [[package]] name = "gix-command" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c6f75c1e0f924de39e750880a6e21307194bb1ab773efe3c7d2d787277f8ab" +checksum = "bb49ab557a37b0abb2415bca2b10e541277dff0565deb5bd5e99fd95f93f51eb" dependencies = [ "bstr", ] +[[package]] +name = "gix-commitgraph" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed42baa50075d41c1a0931074ce1a97c5797c7c6fe7591d9f1f2dcd448532c26" +dependencies = [ + "bstr", + "gix-chunk", + "gix-features", + "gix-hash", + "memmap2 0.7.1", + "thiserror", +] + [[package]] name = "gix-config" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e8188bb673aeef4bb21dc8650084668e83ed944c1c6fcf22050b5e4de0ebdd" +checksum = "33b32541232a2c626849df7843e05b50cb43ac38a4f675abbe2f661874fc1e9d" dependencies = [ "bstr", "gix-config-value", @@ -978,11 +979,11 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a77b6c3e51bd6d8974ab80c7e7943b3f12abb8fa809834002db9742da6b4ac4" +checksum = "83960be5e99266bcf55dae5a24731bbd39f643bfb68f27e939d6b06836b5b87d" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.3", "bstr", "gix-path", "libc", @@ -991,9 +992,9 @@ dependencies = [ [[package]] name = "gix-credentials" -version = "0.13.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4896885f74b84a7bdcd0a2e32d9cb0a5082b34c8489c8fe1bfa94f155206b4f1" +checksum = "75a75565e0e6e7f80cfa4eb1b05cc448c6846ddd48dcf413a28875fbc11ee9af" dependencies = [ "bstr", "gix-command", @@ -1007,21 +1008,21 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99056f37270715f5c7584fd8b46899a2296af9cae92463bf58b8bd1f5a78e553" +checksum = "0213f923d63c2c7d10799c1977f42df38ec586ebbf1d14fd00dfa363ac994c2b" dependencies = [ "bstr", "itoa", "thiserror", - "time 0.3.20", + "time 0.3.22", ] [[package]] name = "gix-diff" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644a0f2768bc42d7a69289ada80c9e15c589caefc6a315d2307202df83ed1186" +checksum = "5049dd5a60d5608912da0ab184f35064901f192f4adf737716789715faffa080" dependencies = [ "gix-hash", "gix-object", @@ -1031,9 +1032,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.17.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0305d45385faeac734f1bda1fa7bad55b7d51416a26f6fb53d17a78186da0bd9" +checksum = "c14865cb9c6eb817d6a8d53595f1051239d2d31feae7a5e5b2f00910c94a8eb4" dependencies = [ "bstr", "dunce", @@ -1046,13 +1047,14 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf69b0f5c701cc3ae22d3204b671907668f6437ca88862d355eaf9bc47a4f897" +checksum = "06142d8cff5d17509399b04052b64d2f9b3a311d5cff0b1a32b220f62cd0d595" dependencies = [ "crc32fast", "flate2", "gix-hash", + "gix-trace", "libc", "once_cell", "prodash", @@ -1063,20 +1065,20 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b37a1832f691fdc09910bd267f9a2e413737c1f9ec68c6e31f9e802616278a9" +checksum = "bb15956bc0256594c62a2399fcf6958a02a11724217eddfdc2b49b21b6292496" dependencies = [ "gix-features", ] [[package]] name = "gix-glob" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035fd81df824cb4d987835120b6259d2bd39fbaf1e888cab9426dc687170191f" +checksum = "c18bdff83143d61e7d60da6183b87542a870d026b2a2d0b30170b8e9c0cd321a" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.3", "bstr", "gix-features", "gix-path", @@ -1084,9 +1086,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078eec3ac2808cc03f0bddd2704cb661da5c5dc33b41a9d7947b141d499c7c42" +checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272" dependencies = [ "hex", "thiserror", @@ -1094,20 +1096,20 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afebb85691c6a085b114e01a27f4a61364519298c5826cb87a45c304802299bc" +checksum = "9e133bc56d938eaec1c675af7c681a51de9662b0ada779f45607b967a10da77a" dependencies = [ "gix-hash", - "hashbrown 0.13.2", + "hashbrown 0.14.0", "parking_lot", ] [[package]] name = "gix-ignore" -version = "0.1.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d7fe0858fb52a7573e279201e09df990874e21d2ef3df4ac85653fb88442" +checksum = "ca801f2d0535210f77b33e2c067d565aedecacc82f1b3dbce26da1388ebc4634" dependencies = [ "bstr", "gix-glob", @@ -1117,11 +1119,11 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa282756760f79c401d4f4f42588fbb4aa27bbb4b0830f3b4d3480c21a4ac5a7" +checksum = "2ef2fa392d351e62ac3a6309146f61880abfbe0c07474e075d3b2ac78a6834a5" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.3", "bstr", "btoi", "filetime", @@ -1132,42 +1134,60 @@ dependencies = [ "gix-object", "gix-traverse", "itoa", - "memmap2", + "memmap2 0.5.10", "smallvec", "thiserror", ] [[package]] name = "gix-lock" -version = "5.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b80172055c5d8017a48ddac5cc7a95421c00211047db0165c97853c4f05194" +checksum = "714bcb13627995ac33716e9c5e4d25612b19947845395f64d2a9cbe6007728e4" dependencies = [ - "fastrand", "gix-tempfile", + "gix-utils", "thiserror", ] [[package]] name = "gix-mailmap" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8856cec3bdc3610c06970d28b6cb20a0c6621621cf9a8ec48cbd23f2630f362" +checksum = "d0bef8d360a6a9fc5a6d872471588d8ca7db77b940e48ff20c3b4706ad5f481d" dependencies = [ "bstr", "gix-actor", + "gix-date", + "thiserror", +] + +[[package]] +name = "gix-negotiate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b626aafb9f4088058f1baa5d2029b2191820c84f6c81e43535ba70bfdc7b7d56" +dependencies = [ + "bitflags 2.3.3", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", + "smallvec", "thiserror", ] [[package]] name = "gix-object" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9bb30ce0818d37096daa29efe361a4bc6dd0b51a5726598898be7e9a40a01e1" +checksum = "255e477ae4cc8d10778238f011e6125b01cc0e7067dc8df87acd67a428a81f20" dependencies = [ "bstr", "btoi", "gix-actor", + "gix-date", "gix-features", "gix-hash", "gix-validate", @@ -1180,11 +1200,12 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd87fd2a4884899954daa06371ecd55b40e2c4b708e94fe70d869864d1cd552" +checksum = "6b73469f145d1e6afbcfd0ab6499a366fbbcb958c2999d41d283d6c7b94024b9" dependencies = [ "arc-swap", + "gix-date", "gix-features", "gix-hash", "gix-object", @@ -1198,9 +1219,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.34.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9914b411b8068322b877af7774fd0f283b25b141969cef2536ed09a2cf9fac1" +checksum = "a1f3bcd1aaa72aea7163b147d2bde2480a01eadefc774a479d38f29920f7f1c8" dependencies = [ "clru", "gix-chunk", @@ -1212,7 +1233,7 @@ dependencies = [ "gix-path", "gix-tempfile", "gix-traverse", - "memmap2", + "memmap2 0.5.10", "parking_lot", "smallvec", "thiserror", @@ -1220,11 +1241,12 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6581146846102b54702f1cadb98f79f00b996bc8470edc24645f460060d276" +checksum = "dfca182d2575ded2ed38280f1ebf75cd5d3790b77e0872de07854cf085821fbe" dependencies = [ "bstr", + "gix-trace", "home", "once_cell", "thiserror", @@ -1232,9 +1254,9 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c5086dbabb66cb29d1dec4636cc0357e76fc95da682c149ec96dd97222697f" +checksum = "8dfd363fd89a40c1e7bff9c9c1b136cd2002480f724b0c627c1bc771cd5480ec" dependencies = [ "gix-command", "gix-config-value", @@ -1245,9 +1267,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a282f5a8d9ee0b09ec47390ac727350c48f2f5c76d803cd8da6b3e7ad56e0bcb" +checksum = "3874de636c2526de26a3405b8024b23ef1a327bebf4845d770d00d48700b6a40" dependencies = [ "bstr", "btoi", @@ -1256,11 +1278,12 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf64922331b0abd855e75ba3148b072ce2b99e31cd9d1998b87b341e9dbb67e" +checksum = "9b6c74873a9d8ff5d1310f2325f09164c15a91402ab5cde4d479ae12ff55ed69" dependencies = [ "gix-actor", + "gix-date", "gix-features", "gix-fs", "gix-hash", @@ -1269,16 +1292,16 @@ dependencies = [ "gix-path", "gix-tempfile", "gix-validate", - "memmap2", + "memmap2 0.5.10", "nom", "thiserror", ] [[package]] name = "gix-refspec" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f520fd43ef706cafe14f4d5a196303c173da1b8cea92ab30fef7d38e866f6015" +checksum = "ca1bc6c40bad62570683d642fcb04e977433ac8f76b674860ef7b1483c1f8990" dependencies = [ "bstr", "gix-hash", @@ -1290,25 +1313,41 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810f35e9afeccca999d5d348b239f9c162353127d2e13ff3240e31b919e35476" +checksum = "f3751d6643d731fc5829d2f43ca049f4333c968f30908220ba0783c9dfe5010c" dependencies = [ "bstr", "gix-date", "gix-hash", "gix-hashtable", "gix-object", + "gix-revwalk", + "thiserror", +] + +[[package]] +name = "gix-revwalk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144995229c6e5788b1c7386f8a3f7146ace3745c9a6b56cef9123a7d83b110c5" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", "thiserror", ] [[package]] name = "gix-sec" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59c51b67330c78abc069a3aec920dcb301b858739ca8414ce74c8df2d33734e" +checksum = "ede298863db2a0574a14070991710551e76d1f47c9783b62d4fcbca17f56371c" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.3", "gix-path", "libc", "windows", @@ -1316,10 +1355,11 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "5.0.2" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ceb30a610e3f5f2d5f9a5114689fde507ba9417705a8cf3429604275b2153c" +checksum = "4fac8310c17406ea619af72f42ee46dac795110f68f41b4f4fa231b69889c6a2" dependencies = [ + "gix-fs", "libc", "once_cell", "parking_lot", @@ -1328,23 +1368,33 @@ dependencies = [ "tempfile", ] +[[package]] +name = "gix-trace" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103eac621617be3ebe0605c9065ca51a223279a23218aaf67d10daa6e452f663" + [[package]] name = "gix-traverse" -version = "0.25.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5be1e807f288c33bb005075111886cceb43ed8a167b3182a0f62c186e2a0dd1" +checksum = "c3f6bba1686bfbc7e0e93d4932bc6e14d479c9c9524f7c8d65b25d2a9446a99e" dependencies = [ + "gix-commitgraph", + "gix-date", "gix-hash", "gix-hashtable", "gix-object", + "gix-revwalk", + "smallvec", "thiserror", ] [[package]] name = "gix-url" -version = "0.17.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7e76c8259755bc0ef8f6be85943475a3f1ee26ae82bcc621eb0e704be63bd9" +checksum = "beaede6dbc83f408b19adfd95bb52f1dbf01fb8862c3faf6c6243e2e67fcdfa1" dependencies = [ "bstr", "gix-features", @@ -1356,18 +1406,18 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10b69beac219acb8df673187a1f07dde2d74092f974fb3f9eb385aeb667c909" +checksum = "7058c94f4164fcf5b8457d35f6d8f6e1007f9f7f938c9c7684a7e01d23c6ddde" dependencies = [ - "fastrand", + "fastrand 2.0.0", ] [[package]] name = "gix-validate" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd629d3680773e1785e585d76fd4295b740b559cad9141517300d99a0c8c049" +checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13" dependencies = [ "bstr", "thiserror", @@ -1375,9 +1425,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.16.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4753efd398078a1d049a7ab581730491cb1bfc750e179a362be5bd35042f7b53" +checksum = "4ee22549d6723189366235e1c6959ccdac73b58197cdbb437684eaa2169edcb9" dependencies = [ "bstr", "filetime", @@ -1443,7 +1493,7 @@ dependencies = [ "encoding_rs_io", "grep-matcher", "log", - "memmap2", + "memmap2 0.5.10", ] [[package]] @@ -1467,11 +1517,12 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash 0.8.3", + "allocator-api2", ] [[package]] @@ -1480,12 +1531,12 @@ version = "0.6.0" dependencies = [ "ahash 0.8.3", "arc-swap", - "bitflags 2.2.1", + "bitflags 2.3.3", "chrono", "dunce", "encoding_rs", "etcetera", - "hashbrown 0.13.2", + "hashbrown 0.14.0", "helix-loader", "imara-diff", "indoc", @@ -1538,6 +1589,7 @@ dependencies = [ "threadpool", "toml", "tree-sitter", + "which", ] [[package]] @@ -1611,7 +1663,7 @@ dependencies = [ name = "helix-tui" version = "0.6.0" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.3", "cassowary", "crossterm 0.26.1", "helix-core", @@ -1645,7 +1697,7 @@ version = "0.6.0" dependencies = [ "anyhow", "arc-swap", - "bitflags 2.2.1", + "bitflags 2.3.3", "chardetng", "clipboard-win", "crossterm 0.26.1", @@ -1680,15 +1732,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.1" @@ -1703,18 +1746,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "winapi", + "windows-sys", ] [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1726,19 +1769,18 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1801,6 +1843,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "indoc" version = "2.0.1" @@ -1813,7 +1865,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1828,13 +1880,13 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1854,9 +1906,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1889,9 +1941,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -1899,7 +1951,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] @@ -1909,30 +1961,21 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" dependencies = [ - "cfg-if 1.0.0", - "windows-sys 0.48.0", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", + "cfg-if", + "windows-sys", ] [[package]] name = "linux-raw-sys" -version = "0.3.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1940,12 +1983,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "logos" @@ -1964,8 +2004,8 @@ checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" dependencies = [ "beef", "fnv", - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.63", + "quote 1.0.29", "regex-syntax 0.6.29", "syn 1.0.109", ] @@ -1998,11 +2038,20 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -2024,23 +2073,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2132,11 +2181,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.1", "libc", ] @@ -2149,11 +2198,20 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parking_lot" @@ -2167,15 +2225,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets", ] [[package]] @@ -2186,15 +2244,15 @@ checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" @@ -2231,24 +2289,24 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "23.1.2" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9516b775656bc3e8985e19cd4b8c0c0de045095074e453d2c0a513b5f978392d" +checksum = "3236ce1618b6da4c7b618e0143c4d5b5dc190f75f81c49f248221382f7e9e9ae" [[package]] name = "pulldown-cmark" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" dependencies = [ "bitflags 1.3.2", "memchr", @@ -2270,7 +2328,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d47bcfc3e13850589cf9338a02b6dfb5aebb3748a0f93a392e8df91d6193b6b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "smallvec", ] @@ -2285,11 +2343,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ - "proc-macro2 1.0.60", + "proc-macro2 1.0.63", ] [[package]] @@ -2349,26 +2407,15 @@ dependencies = [ "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]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick 1.0.1", + "aho-corasick 1.0.2", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -2385,9 +2432,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "repr_offset" @@ -2423,6 +2470,12 @@ dependencies = [ "str_indices", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2434,28 +2487,38 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.15" +version = "0.37.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2479,12 +2542,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - [[package]] name = "sct" version = "0.7.0" @@ -2503,29 +2560,29 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -2538,16 +2595,16 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -2718,9 +2775,9 @@ dependencies = [ name = "steel-derive" version = "0.2.0" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] @@ -2772,33 +2829,34 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.63", + "quote 1.0.29", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.18" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.63", + "quote 1.0.29", "unicode-ident", ] [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ - "cfg-if 1.0.0", - "fastrand", + "autocfg", + "cfg-if", + "fastrand 1.9.0", "redox_syscall 0.3.5", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2826,11 +2884,11 @@ dependencies = [ [[package]] name = "termini" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c0f7ecb9c2a380d2686a747e4fc574043712326e8d39fbd220ab3bd29768a12" +checksum = "2ad441d87dd98bc5eeb31cf2fb7e4839968763006b478efb38668a3bf9da0d59" dependencies = [ - "dirs-next", + "home", ] [[package]] @@ -2859,9 +2917,9 @@ version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] @@ -2870,7 +2928,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -2896,9 +2954,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "libc", @@ -2910,15 +2968,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -2940,11 +2998,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -2954,25 +3013,25 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 2.0.18", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", ] [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -2981,9 +3040,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" dependencies = [ "serde", "serde_spanned", @@ -2993,20 +3052,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -3079,9 +3138,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-linebreak" @@ -3128,27 +3187,27 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ "base64", "flate2", "log", "once_cell", "rustls", + "rustls-webpki", "serde", "serde_json", "url", - "webpki", "webpki-roots", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -3186,57 +3245,57 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 1.0.109", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.28", + "quote 1.0.29", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2 1.0.60", - "quote 1.0.28", - "syn 1.0.109", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 2.0.23", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "weak-table" @@ -3246,31 +3305,21 @@ checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "webpki", + "rustls-webpki", ] [[package]] @@ -3321,16 +3370,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets", ] [[package]] @@ -3339,117 +3379,60 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -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", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.0" @@ -3458,9 +3441,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] diff --git a/VERSION b/VERSION index 35371314c..527d78c51 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -23.03 \ No newline at end of file +23.05 \ No newline at end of file diff --git a/book/book.toml b/book/book.toml index 9835145ce..f68dece81 100644 --- a/book/book.toml +++ b/book/book.toml @@ -3,10 +3,10 @@ authors = ["Blaž Hrastnik"] language = "en" multilingual = false src = "src" -edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit" [output.html] cname = "docs.helix-editor.com" default-theme = "colibri" preferred-dark-theme = "colibri" git-repository-url = "https://github.com/helix-editor/helix" +edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}" diff --git a/book/src/configuration.md b/book/src/configuration.md index 253a07269..1b94ae856 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -52,6 +52,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `auto-format` | Enable automatic formatting on save | `true` | | `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` | +| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` | | `auto-info` | Whether to display info boxes | `true` | @@ -62,6 +63,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | +| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | ### `[editor.statusline]` Section @@ -117,6 +119,7 @@ The following statusline elements can be configured: | `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) | | `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) | | `version-control` | The current branch name or detached commit hash of the opened workspace | +| `register` | The current selected register | ### `[editor.lsp]` Section @@ -131,8 +134,8 @@ The following statusline elements can be configured: | `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. -[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. - Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances, please report any bugs you see so we can fix them! + +[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them! ### `[editor.cursor-shape]` Section diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index b08bf1558..c3eb0f96a 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -7,10 +7,11 @@ | beancount | ✓ | | | | | bibtex | ✓ | | | `texlab` | | bicep | ✓ | | | `bicep-langserver` | +| blueprint | ✓ | | | `blueprint-compiler` | | c | ✓ | ✓ | ✓ | `clangd` | | c-sharp | ✓ | ✓ | | `OmniSharp` | -| cabal | ✓ | | | | -| cairo | ✓ | | | | +| cabal | | | | | +| cairo | ✓ | ✓ | ✓ | `cairo-language-server` | | capnp | ✓ | | ✓ | | | clojure | ✓ | | | `clojure-lsp` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` | @@ -18,7 +19,7 @@ | common-lisp | ✓ | | | `cl-lsp` | | cpon | ✓ | | ✓ | | | cpp | ✓ | ✓ | ✓ | `clangd` | -| crystal | ✓ | ✓ | | | +| crystal | ✓ | ✓ | | `crystalline` | | css | ✓ | | | `vscode-css-language-server` | | cue | ✓ | | | `cuelsp` | | d | ✓ | ✓ | ✓ | `serve-d` | @@ -40,6 +41,7 @@ | erlang | ✓ | ✓ | | `erlang_ls` | | esdl | ✓ | | | | | fish | ✓ | ✓ | ✓ | | +| forth | ✓ | | | `forth-lsp` | | fortran | ✓ | | ✓ | `fortls` | | gdscript | ✓ | ✓ | ✓ | | | git-attributes | ✓ | | | | @@ -86,7 +88,7 @@ | markdoc | ✓ | | | `markdoc-ls` | | markdown | ✓ | | | `marksman` | | markdown.inline | ✓ | | | | -| matlab | ✓ | | | | +| matlab | ✓ | ✓ | ✓ | | | mermaid | ✓ | | | | | meson | ✓ | | ✓ | | | mint | | | | `mint` | @@ -141,6 +143,7 @@ | svelte | ✓ | | | `svelteserver` | | sway | ✓ | ✓ | ✓ | `forc` | | swift | ✓ | | | `sourcekit-lsp` | +| t32 | ✓ | | | | | tablegen | ✓ | ✓ | ✓ | | | task | ✓ | | | | | tfvars | ✓ | | ✓ | `terraform-ls` | @@ -156,9 +159,10 @@ | verilog | ✓ | ✓ | | `svlangserver` | | vhdl | ✓ | | | `vhdl_ls` | | vhs | ✓ | | | | -| vue | ✓ | | | `vls` | +| vue | ✓ | | | `vue-language-server` | | wast | ✓ | | | | | wat | ✓ | | | | +| webc | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | | wit | ✓ | | ✓ | | | xit | ✓ | | | | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index ae28a9ba0..a3949960f 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -12,7 +12,9 @@ | `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. | | `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. | | `: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. | | `: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.) | @@ -29,6 +31,7 @@ | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). | | `:theme` | Change the editor theme (show current theme if no name specified). | +| `:yank-join` | Yank joined selections. A separator can be provided as first argument. Default value is newline. | | `:clipboard-yank` | Yank main selection into system clipboard. | | `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | @@ -44,12 +47,12 @@ | `:show-directory`, `:pwd` | Show the current working directory. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:character-info`, `:char` | Get info about the character under the primary cursor. | -| `:reload` | Discard changes and reload from the source file. | -| `:reload-all` | Discard changes and reload all documents from the source files. | +| `:reload`, `:rl` | Discard changes and reload from the source file. | +| `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. | | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | -| `:lsp-restart` | Restarts the Language Server that is in use by the current doc | -| `:lsp-stop` | Stops 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 servers that are used by the current doc | | `: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-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index b92af4028..93ec013f5 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -9,6 +9,7 @@ below. necessary configuration for the new language. For more information on language configuration, refer to the [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 configuration, run the command `cargo xtask docgen` to update the [Language Support](../lang-support.md) documentation. diff --git a/book/src/install.md b/book/src/install.md index 169e6e0b6..4c3ccab3b 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -8,6 +8,7 @@ - [Fedora/RHEL](#fedorarhel) - [Arch Linux community](#arch-linux-community) - [NixOS](#nixos) + - [Flatpak](#flatpak) - [AppImage](#appimage) - [macOS](#macos) - [Homebrew Core](#homebrew-core) @@ -18,6 +19,9 @@ - [MSYS2](#msys2) - [Building from source](#building-from-source) - [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) - [Configure the desktop shortcut](#configure-the-desktop-shortcut) @@ -78,7 +82,10 @@ in the AUR, which builds the master branch. ### 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 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 @@ -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 `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 Install Helix using the Linux [AppImage](https://appimage.org/) format. @@ -143,9 +159,13 @@ pacman -S mingw-w64-ucrt-x86_64-helix Requirements: +Clone the Helix GitHub repository into a directory of your choice. The +examples in this documentation assume installation into either `~/src/` on +Linux and macOS, or `%userprofile%\src\` on Windows. + - The [Rust toolchain](https://www.rust-lang.org/tools/install) - The [Git version control system](https://git-scm.com/) -- A c++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang +- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly: @@ -155,19 +175,19 @@ RUSTFLAGS="-C target-feature=-crt-static" 1. Clone the repository: -```sh -git clone https://github.com/helix-editor/helix -cd helix -``` + ```sh + git clone https://github.com/helix-editor/helix + cd helix + ``` 2. Compile from source: -```sh -cargo install --path helix-term --locked -``` + ```sh + cargo install --path helix-term --locked + ``` -This command will create the `hx` executable and construct the tree-sitter -grammars in the local `runtime` folder. + This command will create the `hx` executable and construct the tree-sitter + grammars in the local `runtime` folder. > 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch > grammars with `hx --grammar fetch` and compile them with @@ -179,18 +199,22 @@ grammars in the local `runtime` folder. #### Linux and macOS -Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent: +The **runtime** directory is one below the Helix source, so either set a +`HELIX_RUNTIME` environment variable to point to that directory and add it to +your `~/.bashrc` or equivalent: ```sh -HELIX_RUNTIME=/home/user-name/src/helix/runtime +HELIX_RUNTIME=~/src/helix/runtime ``` -Or, create a symlink in `~/.config/helix` that links to the source code directory: +Or, create a symbolic link: ```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 Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for diff --git a/book/src/keymap.md b/book/src/keymap.md index 173728f27..153f3b648 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -15,7 +15,7 @@ - [Popup](#popup) - [Unimpaired](#unimpaired) - [Insert mode](#insert-mode) -- [Select / extend mode](#select-extend-mode) +- [Select / extend mode](#select--extend-mode) - [Picker](#picker) - [Prompt](#prompt) @@ -25,6 +25,8 @@ ## Normal mode +Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`. + ### Movement > NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line. @@ -32,8 +34,8 @@ | Key | Description | Command | | ----- | ----------- | ------- | | `h`, `Left` | Move left | `move_char_left` | -| `j`, `Down` | Move down | `move_line_down` | -| `k`, `Up` | Move up | `move_line_up` | +| `j`, `Down` | Move down | `move_visual_line_down` | +| `k`, `Up` | Move up | `move_visual_line_up` | | `l`, `Right` | Move right | `move_char_right` | | `w` | Move next word start | `move_next_word_start` | | `b` | Move previous word start | `move_prev_word_start` | @@ -111,7 +113,8 @@ | `s` | Select all regex matches inside selections | `select_regex` | | `S` | Split selection into sub selections on regex matches | `split_selection` | | `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` | | `_` | Trim whitespace from the selection | `trim_selections` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | @@ -218,6 +221,8 @@ Jumps to various locations. | `n` | Go to next buffer | `goto_next_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` | | `.` | 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 @@ -334,6 +339,8 @@ These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim ## Insert mode +Accessed by typing `i` in [normal mode](#normal-mode). + Insert mode bindings are minimal by default. Helix is designed to be a modal editor, and this is reflected in the user experience and internal mechanics. Changes to the text are only saved for undos when @@ -387,9 +394,11 @@ end = "no_op" ## Select / extend mode +Accessed by typing `v` in [normal mode](#normal-mode). + Select mode echoes Normal mode, but changes any movements to extend selections rather than replace them. Goto motions are also changed to -extend, so that `vgl` for example extends the selection to the end of +extend, so that `vgl`, for example, extends the selection to the end of the line. Search is also affected. By default, `n` and `N` will remove the current diff --git a/book/src/languages.md b/book/src/languages.md index fe4db1413..5e56a332f 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -7,21 +7,24 @@ in `languages.toml` files. There are three possible locations for a `languages.toml` file: -1. In the Helix source code, this lives in the +1. In the Helix source code, which lives in the [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml). It provides the default configurations for languages and language servers. 2. In your [configuration directory](./configuration.md). This overrides values - from the built-in language configuration. For example to disable + from the built-in language configuration. For example, to disable auto-LSP-formatting in Rust: -```toml -# in /helix/languages.toml + ```toml + # in /helix/languages.toml -[[language]] -name = "rust" -auto-format = false -``` + [language-server.mylang-lsp] + command = "mylang-lsp" + + [[language]] + name = "rust" + auto-format = false + ``` 3. In a `.helix` folder in your project. Language configuration may also be overridden local to a project by creating a `languages.toml` file in a @@ -41,8 +44,8 @@ injection-regex = "mylang" file-types = ["mylang", "myl"] comment-token = "#" indent = { tab-width = 2, unit = " " } -language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } } formatter = { command = "mylang-formatter" , args = ["--stdin"] } +language-servers = [ "mylang-lsp" ] ``` These configuration keys are available: @@ -50,6 +53,7 @@ These configuration keys are available: | Key | Description | | ---- | ----------- | | `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.` or `text.` 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. | | `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`) | | `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) | -| `language-server` | The Language Server to run. See the Language Server configuration section below. | -| `config` | Language Server configuration | +| `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) | | `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 | | `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 system, so this rule would match against `.git\config` files on Windows. -### Language Server configuration +## Language Server configuration -The `language-server` field takes the following keys: +Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml` + +For example: + +```toml +[language-server.mylang-lsp] +command = "mylang-lsp" +args = ["--stdio"] +config = { provideFormatter = true } +environment = { "ENV1" = "value1", "ENV2" = "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 } ] } +``` -| Key | Description | -| --- | ----------- | -| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` | -| `args` | A list of arguments to pass to the language server binary | -| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | -| `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` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | +These are the available options for a language server. -The top-level `config` field is used to configure the LSP initialization options. 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-16.md#document-formatting-request--leftwards_arrow_with_hook). -For example with typescript: +| 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: ```toml -[[language]] -name = "typescript" -auto-format = true +[language-server.typescript-language-server] # 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 } } ``` +### 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. + +As an 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 The source for a language's tree-sitter grammar is specified in a `[[grammar]]` diff --git a/book/src/themes.md b/book/src/themes.md index 56d0372ca..41a3fe101 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -296,7 +296,7 @@ These scopes are used for theming the editor interface: | `ui.window` | Borderlines separating splits | | `ui.help` | Description box for commands | | `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.info` | The key: command text in `ui.popup.info` boxes | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | diff --git a/book/src/usage.md b/book/src/usage.md index 81cf83725..3c48e3065 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -96,13 +96,13 @@ function or block of code. | `(`, `[`, `'`, etc. | Specified surround pairs | | `m` | The closest surround pair | | `f` | Function | -| `c` | Class | +| `t` | Type (or Class) | | `a` | Argument/parameter | -| `o` | Comment | -| `t` | Test | +| `c` | Comment | +| `T` | Test | | `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 some grammars][lang-support] currently have the query file implemented. Contributions are welcome! @@ -112,7 +112,7 @@ Contributions are welcome! Navigating between functions, classes, parameters, and other elements is possible using tree-sitter and textobject queries. For 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] diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml index b99738a18..f1b310db4 100644 --- a/contrib/Helix.appdata.xml +++ b/contrib/Helix.appdata.xml @@ -36,6 +36,9 @@ + + https://github.com/helix-editor/helix/releases/tag/23.05 + https://helix-editor.com/news/release-23-03-highlights/ diff --git a/docs/releases.md b/docs/releases.md index 6e7c37c6e..842886753 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,7 +1,7 @@ ## Checklist Helix releases are versioned in the Calendar Versioning scheme: -`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions +`YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions we'll use `` as a placeholder for the tag being published. * Merge the changelog PR @@ -30,7 +30,7 @@ we'll use `` as a placeholder for the tag being published. The changelog is currently created manually by reading through commits in the log since the last release. GitHub's compare view is a nice way to approach -this. For example when creating the 22.07 release notes, this compare link +this. For example, when creating the 22.07 release notes, this compare link may be used ``` diff --git a/docs/vision.md b/docs/vision.md index d49dd64d7..d9f1a8ac1 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -20,5 +20,5 @@ Vision statements are all well and good, but are also vague and subjective. Her * **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support. * **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.) * **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things. -* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. Right now we're thinking Wasm-based plugins. +* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. * **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can. diff --git a/flake.lock b/flake.lock index d33c404ef..8046f3590 100644 --- a/flake.lock +++ b/flake.lock @@ -3,15 +3,16 @@ "crane": { "flake": false, "locked": { - "lastModified": 1670900067, - "narHash": "sha256-VXVa+KBfukhmWizaiGiHRVX/fuk66P8dgSFfkVN4/MY=", + "lastModified": 1681175776, + "narHash": "sha256-7SsUy9114fryHAZ8p1L6G6YSu7jjz55FddEwa2U8XZc=", "owner": "ipetkov", "repo": "crane", - "rev": "59b31b41a589c0a65e4a1f86b0e5eac68081468b", + "rev": "445a3d222947632b5593112bb817850e8a9cf737", "type": "github" }, "original": { "owner": "ipetkov", + "ref": "v0.12.1", "repo": "crane", "type": "github" } @@ -62,11 +63,11 @@ ] }, "locked": { - "lastModified": 1680258209, - "narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=", + "lastModified": 1683212002, + "narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=", "owner": "nix-community", "repo": "dream2nix", - "rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35", + "rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096", "type": "github" }, "original": { @@ -94,11 +95,11 @@ ] }, "locked": { - "lastModified": 1680172861, - "narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=", + "lastModified": 1680698112, + "narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=", "owner": "davhau", "repo": "drv-parts", - "rev": "ced8a52f62b0a94244713df2225c05c85b416110", + "rev": "e8c2ec1157dc1edb002989669a0dbd935f430201", "type": "github" }, "original": { @@ -124,12 +125,15 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", "type": "github" }, "original": { @@ -141,11 +145,11 @@ "mk-naked-shell": { "flake": false, "locked": { - "lastModified": 1676572903, - "narHash": "sha256-oQoDHHUTxNVSURfkFcYLuAK+btjs30T4rbEUtCUyKy8=", + "lastModified": 1681286841, + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", "owner": "yusdacra", "repo": "mk-naked-shell", - "rev": "aeca9f8aa592f5e8f71f407d081cb26fd30c5a57", + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", "type": "github" }, "original": { @@ -167,11 +171,11 @@ ] }, "locked": { - "lastModified": 1680329418, - "narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=", + "lastModified": 1683699050, + "narHash": "sha256-UWKQpzVcSshB+sU2O8CCHjOSTQrNS7Kk9V3+UeBsJpg=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540", + "rev": "ed27173cd1b223f598343ea3c15aacb1d140feac", "type": "github" }, "original": { @@ -182,11 +186,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1680213900, - "narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", + "lastModified": 1683408522, + "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e3652e0735fbec227f342712f180f4f21f0594f2", + "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7", "type": "github" }, "original": { @@ -199,11 +203,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1678375444, - "narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=", + "lastModified": 1682879489, + "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e", + "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", "type": "github" }, "original": { @@ -237,11 +241,11 @@ ] }, "locked": { - "lastModified": 1679737941, - "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", + "lastModified": 1683560683, + "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", + "rev": "006c75898cf814ef9497252b022e91c946ba8e17", "type": "github" }, "original": { @@ -255,11 +259,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1679737941, - "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", + "lastModified": 1683560683, + "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", + "rev": "006c75898cf814ef9497252b022e91c946ba8e17", "type": "github" }, "original": { @@ -284,11 +288,11 @@ ] }, "locked": { - "lastModified": 1680315536, - "narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=", + "lastModified": 1683771545, + "narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "5c8c151bdd639074a0051325c16df1a64ee23497", + "rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed", "type": "github" }, "original": { @@ -296,6 +300,21 @@ "repo": "rust-overlay", "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", diff --git a/flake.nix b/flake.nix index 6dcaf6cc4..81f0a05c9 100644 --- a/flake.nix +++ b/flake.nix @@ -64,7 +64,7 @@ }; in inp.parts.lib.mkFlake {inputs = inp;} { - imports = [inp.nci.flakeModule]; + imports = [inp.nci.flakeModule inp.parts.flakeModules.easyOverlay]; systems = [ "x86_64-linux" "x86_64-darwin" @@ -146,6 +146,10 @@ packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {}; packages.default = config.packages.helix; + overlayAttrs = { + inherit (config.packages) helix; + }; + devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: { nativeBuildInputs = (old.nativeBuildInputs or []) diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ce82ace21..25b1bbd4f 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -26,12 +26,12 @@ unicode-general-category = "0.6" # slab = "0.4.2" slotmap = "1.0" tree-sitter = "0.20" -once_cell = "1.17" +once_cell = "1.18" arc-swap = "1" regex = "1" -bitflags = "2.2" +bitflags = "2.3" ahash = "0.8.3" -hashbrown = { version = "0.13.2", features = ["raw"] } +hashbrown = { version = "0.14.0", features = ["raw"] } dunce = "1.0" log = "0.4" @@ -45,7 +45,7 @@ encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } -etcetera = "0.7" +etcetera = "0.8" textwrap = "0.16.0" steel-core = { path = "../../../steel/crates/steel-core", version = "0.2.0", features = ["modules", "anyhow", "blocking_requests"] } diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 58ddb0383..0b75d2a58 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -43,6 +43,7 @@ pub struct Diagnostic { pub message: String, pub severity: Option, pub code: Option, + pub language_server_id: usize, pub tags: Vec, pub source: Option, pub data: Option, diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 2948181d0..caf3f3bec 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -20,6 +20,10 @@ pub enum IndentStyle { Spaces(u8), } +// 16 spaces +const INDENTS: &str = " "; +const MAX_INDENT: u8 = 16; + impl IndentStyle { /// Creates an `IndentStyle` from an indentation string. /// @@ -28,10 +32,10 @@ impl IndentStyle { #[inline] pub fn from_str(indent: &str) -> Self { // XXX: do we care about validating the input more than this? Probably not...? - debug_assert!(!indent.is_empty() && indent.len() <= 8); + debug_assert!(!indent.is_empty() && indent.len() <= MAX_INDENT as usize); if indent.starts_with(' ') { - IndentStyle::Spaces(indent.len() as u8) + IndentStyle::Spaces(indent.len().clamp(1, MAX_INDENT as usize) as u8) } else { IndentStyle::Tabs } @@ -41,20 +45,13 @@ impl IndentStyle { pub fn as_str(&self) -> &'static str { match *self { IndentStyle::Tabs => "\t", - IndentStyle::Spaces(1) => " ", - IndentStyle::Spaces(2) => " ", - IndentStyle::Spaces(3) => " ", - IndentStyle::Spaces(4) => " ", - IndentStyle::Spaces(5) => " ", - IndentStyle::Spaces(6) => " ", - IndentStyle::Spaces(7) => " ", - IndentStyle::Spaces(8) => " ", - - // Unsupported indentation style. This should never happen, - // but just in case fall back to two spaces. IndentStyle::Spaces(n) => { - debug_assert!(n > 0 && n <= 8); // Always triggers. `debug_panic!()` wanted. - " " + // Unsupported indentation style. This should never happen, + debug_assert!(n > 0 && n <= MAX_INDENT); + + // Either way, clamp to the nearest supported value + let closest_n = n.clamp(1, MAX_INDENT) as usize; + &INDENTS[0..closest_n] } } } @@ -76,9 +73,9 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option { // Build a histogram of the indentation *increases* between // subsequent lines, ignoring lines that are all whitespace. // - // Index 0 is for tabs, the rest are 1-8 spaces. - let histogram: [usize; 9] = { - let mut histogram = [0; 9]; + // Index 0 is for tabs, the rest are 1-MAX_INDENT spaces. + let histogram: [usize; MAX_INDENT as usize + 1] = { + let mut histogram = [0; MAX_INDENT as usize + 1]; let mut prev_line_is_tabs = false; let mut prev_line_leading_count = 0usize; @@ -137,7 +134,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option { histogram[0] += 1; } else { let amount = leading_count - prev_line_leading_count; - if amount <= 8 { + if amount <= MAX_INDENT as usize { histogram[amount] += 1; } } @@ -1195,4 +1192,20 @@ mod test { 3 ); } + + #[test] + fn test_large_indent_level() { + let tab_width = 16; + let indent_width = 16; + let line = Rope::from(" fn new"); // 16 spaces + assert_eq!( + indent_level_for_line(line.slice(..), tab_width, indent_width), + 1 + ); + let line = Rope::from(" fn new"); // 32 spaces + assert_eq!( + indent_level_for_line(line.slice(..), tab_width, indent_width), + 2 + ); + } } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index b5c2065b6..8c0321f7a 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -68,5 +68,5 @@ pub use syntax::Syntax; pub use diagnostic::Diagnostic; -pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING}; -pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction}; +pub use line_ending::{LineEnding, NATIVE_LINE_ENDING}; +pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction}; diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 953d567d5..36c02a941 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -1,9 +1,9 @@ use crate::{Rope, RopeSlice}; #[cfg(target_os = "windows")] -pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf; +pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::Crlf; #[cfg(not(target_os = "windows"))] -pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF; +pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF; /// Represents one of the valid Unicode line endings. #[derive(PartialEq, Eq, Copy, Clone, Debug)] diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 0189deddb..7fda6d7e5 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -1,7 +1,15 @@ +use std::iter; + +use ropey::RopeSlice; use tree_sitter::Node; -use crate::{Rope, Syntax}; +use crate::movement::Direction::{self, Backward, Forward}; +use crate::Syntax; + +const MAX_PLAINTEXT_SCAN: usize = 10000; +const MATCH_LIMIT: usize = 16; +// Limit matching pairs to only ( ) { } [ ] < > ' ' " " const PAIRS: &[(char, char)] = &[ ('(', ')'), ('{', '}'), @@ -11,17 +19,17 @@ const PAIRS: &[(char, char)] = &[ ('\"', '\"'), ]; -// limit matching pairs to only ( ) { } [ ] < > ' ' " " - -// Returns the position of the matching bracket under cursor. -// -// If the cursor is one the opening bracket, the position of -// 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. +/// 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 +/// 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(syntax: &Syntax, doc: &Rope, pos: usize) -> Option { +pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option { if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { return None; } @@ -39,46 +47,155 @@ pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option< // // If no surrounding scope is found, the function returns `None`. #[must_use] -pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option { +pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option { find_pair(syntax, doc, pos, true) } -fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option { +fn find_pair( + syntax: &Syntax, + doc: RopeSlice, + pos_: usize, + traverse_parents: bool, +) -> Option { let tree = syntax.tree(); - let pos = doc.char_to_byte(pos); + let pos = doc.char_to_byte(pos_); - let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; + let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?; loop { - let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; - let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); + if node.is_named() { + let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; + let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); + + if is_valid_pair(doc, start_char, end_char) { + if end_byte == pos { + return Some(start_char); + } - if is_valid_pair(doc, start_char, end_char) { - if end_byte == pos { - return Some(start_char); + // We return the end char if the cursor is either on the start char + // or at some arbitrary position between start and end char. + if traverse_parents || start_byte == pos { + return Some(end_char); + } } - // We return the end char if the cursor is either on the start char - // or at some arbitrary position between start and end char. - return Some(end_char); } + // this node itselt wasn't a pair but maybe its siblings are - if traverse_parents { - node = node.parent()?; - } else { - return None; + // check if we are *on* the pair (special cased so we don't look + // at the current node twice and to jump to the start on that case) + if let Some(open) = as_close_pair(doc, &node) { + if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) { + return Some(pair_start); + } } + + if !traverse_parents { + // check if we are *on* the opening pair (special cased here as + // an opptimization since we only care about bracket on the cursor + // here) + if let Some(close) = as_open_pair(doc, &node) { + if let Some(pair_end) = find_pair_end(doc, node.next_sibling(), close, Forward) { + return Some(pair_end); + } + } + if node.is_named() { + break; + } + } + + for close in + iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT) + { + let Some(open) = as_close_pair(doc, &close) else { continue; }; + if find_pair_end(doc, Some(node), open, Backward).is_some() { + return doc.try_byte_to_char(close.start_byte()).ok(); + } + } + let Some(parent) = node.parent() else { break; }; + node = parent; + } + let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; + if node.child_count() != 0 { + return None; } + let node_start = doc.byte_to_char(node.start_byte()); + find_matching_bracket_plaintext(doc.byte_slice(node.byte_range()), pos_ - node_start) + .map(|pos| pos + node_start) +} + +/// 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_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option { + // 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 { PAIRS.iter().any(|(l, r)| *l == c || *r == c) } -fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool { +fn is_forward_bracket(c: char) -> bool { + PAIRS.iter().any(|(l, _)| *l == c) +} + +fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) } -fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> { +fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> { let len = doc.len_bytes(); let start_byte = node.start_byte(); @@ -90,3 +207,85 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> { Some((start_byte, end_byte)) } + +/// Tests if this node is a pair close char and returns the expected open char +fn as_close_pair(doc: RopeSlice, node: &Node) -> Option { + let close = as_char(doc, node)?.1; + PAIRS + .iter() + .find_map(|&(open, close_)| (close_ == close).then_some(open)) +} + +/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char +/// +/// # Returns +/// +/// The position of the found node or `None` otherwise +fn find_pair_end( + doc: RopeSlice, + node: Option, + end_char: char, + direction: Direction, +) -> Option { + let advance = match direction { + Forward => Node::next_sibling, + Backward => Node::prev_sibling, + }; + iter::successors(node, advance) + .take(MATCH_LIMIT) + .find_map(|node| { + let (pos, c) = as_char(doc, &node)?; + (end_char == c).then_some(pos) + }) +} + +/// Tests if this node is a pair close char and returns the expected open char +fn as_open_pair(doc: RopeSlice, node: &Node) -> Option { + let open = as_char(doc, node)?.1; + PAIRS + .iter() + .find_map(|&(open_, close)| (open_ == open).then_some(close)) +} + +/// If node is a single char return it (and its char position) +fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> { + // TODO: multi char/non ASCII pairs + if node.byte_range().len() != 1 { + return None; + } + let pos = doc.try_byte_to_char(node.start_byte()).ok()?; + Some((pos, doc.char(pos))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_matching_bracket_current_line_plaintext() { + let assert = |input: &str, pos, expected| { + let input = RopeSlice::from(input); + let actual = find_matching_bracket_plaintext(input, pos); + assert_eq!(expected, actual.unwrap()); + + let actual = find_matching_bracket_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); + } +} diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index b44d149fb..2b29f36de 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -1,4 +1,4 @@ -use std::iter; +use std::{cmp::Reverse, iter}; use ropey::iter::Chars; use tree_sitter::{Node, QueryCursor}; @@ -177,6 +177,10 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran word_move(slice, range, count, WordMotionTarget::PrevWordStart) } +pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevWordEnd) +} + pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { word_move(slice, range, count, WordMotionTarget::NextLongWordStart) } @@ -189,8 +193,8 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) - word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) } -pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevWordEnd) +pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd) } fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { @@ -199,6 +203,7 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevWordEnd + | WordMotionTarget::PrevLongWordEnd ); // Special-case early-out. @@ -377,6 +382,7 @@ pub enum WordMotionTarget { NextLongWordStart, NextLongWordEnd, PrevLongWordStart, + PrevLongWordEnd, } pub trait CharHelpers { @@ -393,6 +399,7 @@ impl CharHelpers for Chars<'_> { WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevWordEnd + | WordMotionTarget::PrevLongWordEnd ); // Reverse the iterator if needed for the motion direction. @@ -479,7 +486,7 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo is_word_boundary(prev_ch, next_ch) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) } - WordMotionTarget::NextLongWordStart => { + WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => { is_long_word_boundary(prev_ch, next_ch) && (char_is_line_ending(next_ch) || !next_ch.is_whitespace()) } @@ -520,10 +527,10 @@ pub fn goto_treesitter_object( let node = match dir { Direction::Forward => nodes .filter(|n| n.start_byte() > byte_pos) - .min_by_key(|n| n.start_byte())?, + .min_by_key(|n| (n.start_byte(), Reverse(n.end_byte())))?, Direction::Backward => nodes .filter(|n| n.end_byte() < byte_pos) - .max_by_key(|n| n.end_byte())?, + .max_by_key(|n| (n.end_byte(), Reverse(n.start_byte())))?, }; let len = slice.len_bytes(); @@ -1445,6 +1452,100 @@ mod test { } } + #[test] + fn test_behaviour_when_moving_to_end_of_prev_long_words() { + let tests = [ + ( + "Basic backward motion from the middle of a word", + vec![(1, Range::new(3, 3), Range::new(4, 0))], + ), + ("Starting from after boundary retreats the anchor", + vec![(1, Range::new(0, 9), Range::new(8, 0))], + ), + ( + "Jump to end of a word succeeded by whitespace", + vec![(1, Range::new(10, 10), Range::new(10, 4))], + ), + ( + " Jump to start of line from end of word preceded by whitespace", + vec![(1, Range::new(3, 4), Range::new(4, 0))], + ), + ("Previous anchor is irrelevant for backward motions", + vec![(1, Range::new(12, 5), Range::new(6, 0))]), + ( + " Starting from whitespace moves to first space in sequence", + vec![(1, Range::new(0, 4), Range::new(4, 0))], + ), + ("Identifiers_with_underscores are considered a single word", + vec![(1, Range::new(0, 20), Range::new(20, 0))]), + ( + "Jumping\n \nback through a newline selects whitespace", + vec![(1, Range::new(0, 13), Range::new(12, 8))], + ), + ( + "Jumping to start of word from the end selects the word", + vec![(1, Range::new(6, 7), Range::new(7, 0))], + ), + ( + "alphanumeric.!,and.?=punctuation are treated exactly the same", + vec![(1, Range::new(29, 30), Range::new(30, 0))], + ), + ( + "... ... punctuation and spaces behave as expected", + vec![ + (1, Range::new(0, 10), Range::new(9, 3)), + (1, Range::new(10, 6), Range::new(7, 3)), + ], + ), + (".._.._ punctuation is joined by underscores into a single block", + vec![(1, Range::new(0, 6), Range::new(6, 0))]), + ( + "Newlines\n\nare bridged seamlessly.", + vec![(1, Range::new(0, 10), Range::new(8, 0))], + ), + ( + "Jumping \n\n\n\n\nback from within a newline group selects previous block", + vec![(1, Range::new(0, 13), Range::new(11, 7))], + ), + ( + "Failed motions do not modify the range", + vec![(0, Range::new(3, 0), Range::new(3, 0))], + ), + ( + "Multiple motions at once resolve correctly", + vec![(3, Range::new(19, 19), Range::new(8, 0))], + ), + ( + "Excessive motions are performed partially", + vec![(999, Range::new(40, 40), Range::new(9, 0))], + ), + ( + "", // Edge case of moving backwards in empty string + vec![(1, Range::new(0, 0), Range::new(0, 0))], + ), + ( + "\n\n\n\n\n", // Edge case of moving backwards in all newlines + vec![(1, Range::new(5, 5), Range::new(0, 0))], + ), + (" \n \nJumping back through alternated space blocks and newlines selects the space blocks", + vec![ + (1, Range::new(0, 8), Range::new(7, 4)), + (1, Range::new(7, 4), Range::new(3, 0)), + ]), + ("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation", + vec![ + (1, Range::new(0, 8), Range::new(7, 0)), + ]), + ]; + + for (sample, scenario) in tests { + for (count, begin, expected_end) in scenario.into_iter() { + let range = move_prev_long_word_end(Rope::from(sample).slice(..), begin, count); + assert_eq!(range, expected_end, "Case failed: [{}]", sample); + } + } + } + #[test] fn test_behaviour_when_moving_to_prev_paragraph_single() { let tests = [ diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 259b131a4..9104c2099 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -161,34 +161,35 @@ impl Range { self.from() <= pos && pos < self.to() } - /// Map a range through a set of changes. Returns a new range representing the same position - /// after the changes are applied. - pub fn map(self, changes: &ChangeSet) -> Self { + /// Map a range through a set of changes. Returns a new range representing + /// the same position after the changes are applied. Note that this + /// function runs in O(N) (N is number of changes) and can therefore + /// cause performance problems if run for a large number of ranges as the + /// complexity is then O(MN) (for multicuror M=N usually). Instead use + /// [Selection::map] or [ChangeSet::update_positions] instead + pub fn map(mut self, changes: &ChangeSet) -> Self { use std::cmp::Ordering; - let (anchor, head) = match self.anchor.cmp(&self.head) { - Ordering::Equal => ( - changes.map_pos(self.anchor, Assoc::After), - changes.map_pos(self.head, Assoc::After), - ), - Ordering::Less => ( - changes.map_pos(self.anchor, Assoc::After), - changes.map_pos(self.head, Assoc::Before), - ), - Ordering::Greater => ( - changes.map_pos(self.anchor, Assoc::Before), - changes.map_pos(self.head, Assoc::After), - ), - }; - - // We want to return a new `Range` with `horiz == None` every time, - // even if the anchor and head haven't changed, because we don't - // know if the *visual* position hasn't changed due to - // character-width or grapheme changes earlier in the text. - Self { - anchor, - head, - old_visual_position: None, + if changes.is_empty() { + return self; } + + let positions_to_map = match self.anchor.cmp(&self.head) { + Ordering::Equal => [ + (&mut self.anchor, Assoc::After), + (&mut self.head, Assoc::After), + ], + Ordering::Less => [ + (&mut self.anchor, Assoc::After), + (&mut self.head, Assoc::Before), + ], + Ordering::Greater => [ + (&mut self.head, Assoc::After), + (&mut self.anchor, Assoc::Before), + ], + }; + changes.update_positions(positions_to_map.into_iter()); + self.old_visual_position = None; + self } /// Extend the range to cover at least `from` `to`. @@ -451,17 +452,36 @@ impl Selection { /// Map selections over a set of changes. Useful for adjusting the selection position after /// applying changes to a document. pub fn map(self, changes: &ChangeSet) -> Self { + self.map_no_normalize(changes).normalize() + } + + /// Map selections over a set of changes. Useful for adjusting the selection position after + /// applying changes to a document. Doesn't normalize the selection + pub fn map_no_normalize(mut self, changes: &ChangeSet) -> Self { if changes.is_empty() { return self; } - Self::new( - self.ranges - .into_iter() - .map(|range| range.map(changes)) - .collect(), - self.primary_index, - ) + let positions_to_map = self.ranges.iter_mut().flat_map(|range| { + use std::cmp::Ordering; + range.old_visual_position = None; + match range.anchor.cmp(&range.head) { + Ordering::Equal => [ + (&mut range.anchor, Assoc::After), + (&mut range.head, Assoc::After), + ], + Ordering::Less => [ + (&mut range.anchor, Assoc::After), + (&mut range.head, Assoc::Before), + ], + Ordering::Greater => [ + (&mut range.head, Assoc::After), + (&mut range.anchor, Assoc::Before), + ], + } + }); + changes.update_positions(positions_to_map); + self } pub fn ranges(&self) -> &[Range] { @@ -497,6 +517,9 @@ impl Selection { /// Normalizes a `Selection`. fn normalize(mut self) -> Self { + if self.len() < 2 { + return self; + } let mut primary = self.ranges[self.primary_index]; self.ranges.sort_unstable_by_key(Range::from); @@ -522,7 +545,14 @@ impl Selection { 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 { let mut primary = self.ranges[self.primary_index]; @@ -554,17 +584,12 @@ impl Selection { assert!(!ranges.is_empty()); debug_assert!(primary_index < ranges.len()); - let mut selection = Self { + let selection = Self { ranges, primary_index, }; - if selection.ranges.len() > 1 { - // TODO: only normalize if needed (any ranges out of order) - selection = selection.normalize(); - } - - selection + selection.normalize() } /// Takes a closure and maps each `Range` over the closure. diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index f430aee8a..b96cce5a0 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -397,15 +397,10 @@ mod test { let selections: SmallVec<[Range; 1]> = spec .match_indices('^') - .into_iter() .map(|(i, _)| Range::point(i)) .collect(); - let expectations: Vec = spec - .match_indices('_') - .into_iter() - .map(|(i, _)| i) - .collect(); + let expectations: Vec = spec.match_indices('_').map(|(i, _)| i).collect(); (rope, Selection::new(selections, 0), expectations) } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 17c419f43..2aba73880 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap}; use std::{ borrow::Cow, cell::RefCell, - collections::{HashMap, VecDeque}, - fmt, + collections::{HashMap, HashSet, VecDeque}, + fmt::{self, Display}, hash::{Hash, Hasher}, mem::{replace, transmute}, path::{Path, PathBuf}, @@ -26,7 +26,7 @@ use std::{ }; 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}; @@ -48,6 +48,21 @@ where .transpose() } +fn deserialize_tab_width<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + usize::deserialize(deserializer).and_then(|n| { + if n > 0 && n <= 16 { + Ok(n) + } else { + Err(serde::de::Error::custom( + "tab width must be a value from 1 to 16 inclusive", + )) + } + }) +} + pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, @@ -60,8 +75,11 @@ fn default_timeout() -> u64 { } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct Configuration { pub language: Vec, + #[serde(default)] + pub language_server: HashMap, } impl Default for Configuration { @@ -75,7 +93,10 @@ impl Default for Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[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, // csharp, rust, typescriptreact, for the language-server pub scope: String, // source.rust pub file_types: Vec, // filename extension or ends_with? #[serde(default)] @@ -85,9 +106,6 @@ pub struct LanguageConfiguration { pub text_width: Option, pub soft_wrap: Option, - #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] - pub config: Option, - #[serde(default)] pub auto_format: bool, @@ -107,8 +125,13 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 - #[serde(skip_serializing_if = "Option::is_none")] - pub language_server: Option, + #[serde( + default, + skip_serializing_if = "Vec::is_empty", + serialize_with = "serialize_lang_features", + deserialize_with = "deserialize_lang_features" + )] + pub language_servers: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option, @@ -187,9 +210,12 @@ impl<'de> Deserialize<'de> for FileType { M: serde::de::MapAccess<'de>, { match map.next_entry::()? { - Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix( - suffix.replace('/', &std::path::MAIN_SEPARATOR.to_string()), - )), + Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({ + // 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!( "unknown key in `file-types` list: {}", key @@ -205,6 +231,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, + #[serde(default, skip_serializing_if = "HashSet::is_empty")] + except_features: HashSet, + name: String, + }, + Simple(String), +} + +#[derive(Debug, Default)] +pub struct LanguageServerFeatures { + pub name: String, + pub only: HashSet, + pub excluded: HashSet, +} + +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, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw: Vec = 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( + map: &Vec, + serializer: S, +) -> Result +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)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -214,9 +367,10 @@ pub struct LanguageServerConfiguration { pub args: Vec, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub environment: HashMap, + #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] + pub config: Option, #[serde(default = "default_timeout")] pub timeout: u64, - pub language_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -285,6 +439,7 @@ pub struct DebuggerQuirks { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IndentationConfiguration { + #[serde(deserialize_with = "deserialize_tab_width")] pub tab_width: usize, pub unit: String, } @@ -602,6 +757,8 @@ pub struct Loader { language_config_ids_by_suffix: HashMap, language_config_ids_by_shebang: HashMap, + language_server_configs: HashMap, + scopes: ArcSwap>, } @@ -609,6 +766,7 @@ impl Loader { pub fn new(config: Configuration) -> Self { let mut loader = Self { language_configs: Vec::new(), + language_server_configs: config.language_server, language_config_ids_by_extension: HashMap::new(), language_config_ids_by_suffix: HashMap::new(), language_config_ids_by_shebang: HashMap::new(), @@ -733,6 +891,10 @@ impl Loader { self.language_configs.iter() } + pub fn language_server_configs(&self) -> &HashMap { + &self.language_server_configs + } + pub fn set_scopes(&self, scopes: Vec) { self.scopes.store(Arc::new(scopes)); @@ -776,7 +938,11 @@ fn byte_range_to_str(range: std::ops::Range, source: RopeSlice) -> Cow, loader: Arc) -> Self { + pub fn new( + source: &Rope, + config: Arc, + loader: Arc, + ) -> Option { let root_layer = LanguageLayer { tree: None, config, @@ -801,11 +967,13 @@ impl Syntax { loader, }; - syntax - .update(source, source, &ChangeSet::new(source)) - .unwrap(); + let res = syntax.update(source, source, &ChangeSet::new(source)); - syntax + if res.is_err() { + log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}"); + return None; + } + Some(syntax) } pub fn update( @@ -933,6 +1101,7 @@ impl Syntax { PARSER.with(|ts_parser| { 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); // TODO: might need to set cursor range cursor.set_byte_range(0..usize::MAX); @@ -1244,7 +1413,7 @@ impl LanguageLayer { &mut |byte, _| { if byte <= source.len_bytes() { let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); - chunk[byte - start_byte..].as_bytes() + &chunk.as_bytes()[byte - start_byte..] } else { // out of range &[] @@ -2371,7 +2540,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 query = Query::new(language, query_str).unwrap(); @@ -2379,7 +2551,7 @@ mod test { let mut cursor = QueryCursor::new(); 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 mut test = |capture, range| { @@ -2430,7 +2602,10 @@ mod test { .map(String::from) .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 config = HighlightConfiguration::new( @@ -2450,7 +2625,7 @@ mod test { 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 root = tree.root_node(); assert_eq!(root.kind(), "source_file"); @@ -2533,11 +2708,14 @@ mod test { ) { 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 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() diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index d8e581aae..9d2a3e5c4 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -1,10 +1,11 @@ use smallvec::SmallVec; use crate::{Range, Rope, Selection, Tendril}; -use std::borrow::Cow; +use std::{borrow::Cow, iter::once}; /// (from, to, replacement) pub type Change = (usize, usize, Option); +pub type Deletion = (usize, usize); // TODO: pub(crate) #[derive(Debug, Clone, PartialEq, Eq)] @@ -325,20 +326,72 @@ impl ChangeSet { self.changes.is_empty() || self.changes == [Operation::Retain(self.len)] } - /// Map a position through the changes. + /// Map a (mostly) *sorted* list of positions through the changes. /// - /// `assoc` indicates which size to associate the position with. `Before` will keep the - /// position close to the character before, and will place it before insertions over that - /// range, or at that point. `After` will move it forward, placing it at the end of such - /// insertions. - pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize { + /// This is equivalent to updating each position with `map_pos`: + /// + /// ``` no-compile + /// for (pos, assoc) in positions { + /// *pos = changes.map_pos(*pos, assoc); + /// } + /// ``` + /// However this function is significantly faster for sorted lists running + /// in `O(N+M)` instead of `O(NM)`. This function also handles unsorted/ + /// partially sorted lists. However, in that case worst case complexity is + /// again `O(MN)`. For lists that are often/mostly sorted (like the end of diagnostic ranges) + /// performance is usally close to `O(N + M)` + pub fn update_positions<'a>(&self, positions: impl Iterator) { use Operation::*; + + let mut positions = positions.peekable(); + let mut old_pos = 0; let mut new_pos = 0; + let mut iter = self.changes.iter().enumerate().peekable(); + + 'outer: loop { + macro_rules! map { + ($map: expr, $i: expr) => { + loop { + let Some((pos, assoc)) = positions.peek_mut() else { return; }; + if **pos < old_pos { + // Positions are not sorted, revert to the last Operation that + // contains this position and continue iterating from there. + // We can unwrap here since `pos` can not be negative + // (unsigned integer) and iterating backwards to the start + // should always move us back to the start + for (i, change) in self.changes[..$i].iter().enumerate().rev() { + match change { + Retain(i) => { + old_pos -= i; + new_pos -= i; + } + Delete(i) => { + old_pos -= i; + } + Insert(ins) => { + new_pos -= ins.chars().count(); + } + } + if old_pos <= **pos { + iter = self.changes[i..].iter().enumerate().peekable(); + } + } + debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); + continue 'outer; + } + let Some(new_pos) = $map(**pos, *assoc) else { break; }; + **pos = new_pos; + positions.next(); + } + }; + } - let mut iter = self.changes.iter().peekable(); + let Some((i, change)) = iter.next() else { + map!(|pos, _| (old_pos == pos).then_some(new_pos), self.changes.len()); + break; + }; - while let Some(change) = iter.next() { let len = match change { Delete(i) | Retain(i) => *i, Insert(_) => 0, @@ -347,46 +400,51 @@ impl ChangeSet { match change { Retain(_) => { - if old_end > pos { - return new_pos + (pos - old_pos); - } + map!( + |pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)), + i + ); new_pos += len; } Delete(_) => { // in range - if old_end > pos { - return new_pos; - } + map!(|pos, _| (old_end > pos).then_some(new_pos), i); } Insert(s) => { let ins = s.chars().count(); // a subsequent delete means a replace, consume it - if let Some(Delete(len)) = iter.peek() { + if let Some((_, Delete(len))) = iter.peek() { iter.next(); old_end = old_pos + len; // in range of replaced text - if old_end > pos { - // at point or tracking before - if pos == old_pos || assoc == Assoc::Before { - return new_pos; - } else { - // place to end of insert - return new_pos + ins; - } - } + map!( + |pos, assoc| (old_end > pos).then(|| { + // at point or tracking before + if pos == old_pos || assoc == Assoc::Before { + new_pos + } else { + // place to end of insert + new_pos + ins + } + }), + i + ); } else { // at insert point - if old_pos == pos { - // return position before inserted text - if assoc == Assoc::Before { - return new_pos; - } else { - // after text - return new_pos + ins; - } - } + map!( + |pos, assoc| (old_pos == pos).then(|| { + // return position before inserted text + if assoc == Assoc::Before { + new_pos + } else { + // after text + new_pos + ins + } + }), + i + ); } new_pos += ins; @@ -394,14 +452,20 @@ impl ChangeSet { } old_pos = old_end; } + let out_of_bounds: Vec<_> = positions.collect(); - if pos > old_pos { - panic!( - "Position {} is out of range for changeset len {}!", - pos, old_pos - ) - } - new_pos + panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",) + } + + /// Map a position through the changes. + /// + /// `assoc` indicates which side to associate the position with. `Before` will keep the + /// position close to the character before, and will place it before insertions over that + /// range, or at that point. `After` will move it forward, placing it at the end of such + /// insertions. + pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize { + self.update_positions(once((&mut pos, assoc))); + pos } pub fn changes_iter(&self) -> ChangeIterator { @@ -534,6 +598,46 @@ impl Transaction { Self::from(changeset) } + /// Generate a transaction from a set of potentially overlapping deletions + /// by merging overlapping deletions together. + pub fn delete(doc: &Rope, deletions: I) -> Self + where + I: Iterator, + { + 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. pub fn change_by_selection(doc: &Rope, selection: &Selection, f: F) -> Self where @@ -580,6 +684,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(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. pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self { Self::change_by_selection(doc, selection, |range| { @@ -752,6 +866,20 @@ mod test { }; assert_eq!(cs.map_pos(2, Assoc::Before), 2); assert_eq!(cs.map_pos(2, Assoc::After), 2); + // unsorted selection + let cs = ChangeSet { + changes: vec![ + Insert("ab".into()), + Delete(2), + Insert("cd".into()), + Delete(2), + ], + len: 4, + len_after: 4, + }; + let mut positions = [4, 2]; + cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After))); + assert_eq!(positions, [4, 2]); } #[test] diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index f558f86f3..409706bb9 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -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 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 text = doc.slice(..); diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index ff8ffb1c8..3e7fc2e78 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -17,17 +17,18 @@ path = "src/main.rs" anyhow = "1" serde = { version = "1.0", features = ["derive"] } toml = "0.7" -etcetera = "0.7" +etcetera = "0.8" tree-sitter = "0.20" -once_cell = "1.17" +once_cell = "1.18" log = "0.4" +which = "4.4" # TODO: these two should be on !wasm32 only # cloning/compiling tree-sitter grammars cc = { version = "1" } threadpool = { version = "1.0" } -tempfile = "3.5.0" +tempfile = "3.6.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libloading = "0.8" diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 34ce9d6dc..d2ba7e55f 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -85,7 +85,16 @@ pub fn get_language(name: &str) -> Result { Ok(language) } +fn ensure_git_is_available() -> Result<()> { + match which::which("git") { + Ok(_cmd) => Ok(()), + Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")), + } +} + pub fn fetch_grammars() -> Result<()> { + ensure_git_is_available()?; + // We do not need to fetch local grammars. let mut grammars = get_grammar_configs()?; grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. })); @@ -145,6 +154,8 @@ pub fn fetch_grammars() -> Result<()> { } pub fn build_grammars(target: Option) -> Result<()> { + ensure_git_is_available()?; + let grammars = get_grammar_configs()?; println!("Building {} grammars", grammars.len()); let results = run_parallel(grammars, move |grammar| { diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 0dae0a925..b4c8271c8 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -217,6 +217,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)] mod merge_toml_tests { use std::str; @@ -289,21 +307,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) -} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index f85265152..5143236de 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -24,7 +24,7 @@ lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } serde_json = "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-stream = "0.1.12" +tokio = { version = "1.28", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio-stream = "0.1.14" which = "4.4" parking_lot = "0.12.1" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 89b714e21..a3711317a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -4,7 +4,7 @@ use crate::{ 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 lsp::{ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, @@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { #[derive(Debug)] pub struct Client { id: usize, + name: String, _process: Child, server_tx: UnboundedSender, request_counter: AtomicU64, @@ -166,8 +167,7 @@ impl Client { tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); } - #[allow(clippy::type_complexity)] - #[allow(clippy::too_many_arguments)] + #[allow(clippy::type_complexity, clippy::too_many_arguments)] pub fn start( cmd: &str, args: &[String], @@ -176,6 +176,7 @@ impl Client { root_markers: &[String], manual_roots: &[PathBuf], id: usize, + name: String, req_timeout: u64, doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { @@ -200,7 +201,7 @@ impl Client { let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); 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 = path::get_normalized_path(&workspace); let root = find_lsp_workspace( @@ -225,6 +226,7 @@ impl Client { let client = Self { id, + name, _process: process, server_tx, request_counter: AtomicU64::new(0), @@ -240,6 +242,10 @@ impl Client { Ok((client, server_rx, initialize_notify)) } + pub fn name(&self) -> &str { + &self.name + } + pub fn id(&self) -> usize { self.id } @@ -270,6 +276,87 @@ impl Client { .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 { self.capabilities() .position_encoding @@ -645,7 +732,11 @@ impl Client { // Calculation is therefore a bunch trickier. 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 { mut line, mut character, @@ -662,7 +753,11 @@ impl Client { line += 1; character = 0; } 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 } @@ -683,7 +778,7 @@ impl Client { } Delete(_) => { 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 changes.push(lsp::TextDocumentContentChangeEvent { @@ -700,7 +795,8 @@ impl Client { // a subsequent delete means a replace, consume it let end = if let Some(Delete(len)) = iter.peek() { 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(); @@ -1286,21 +1382,13 @@ impl Client { Some(self.call::(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( &self, text_document: lsp::TextDocumentIdentifier, position: lsp::Position, new_name: String, ) -> Option>> { - if !self.supports_rename() { + if !self.supports_feature(LanguageServerFeature::RenameSymbol) { return None; } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 31ee1d75c..277a4c28b 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -12,24 +12,21 @@ pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; use helix_core::{ path, - syntax::{LanguageConfiguration, LanguageServerConfiguration}, + syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures}, }; use tokio::sync::mpsc::UnboundedReceiver; use std::{ - collections::{hash_map::Entry, HashMap}, + collections::HashMap, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, + sync::Arc, }; use thiserror::Error; use tokio_stream::wrappers::UnboundedReceiverStream; pub type Result = core::result::Result; -type LanguageId = String; +pub type LanguageServerName = String; #[derive(Error, Debug)] pub enum Error { @@ -49,7 +46,7 @@ pub enum Error { Other(#[from] anyhow::Error), } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum OffsetEncoding { /// UTF-8 code units aka bytes Utf8, @@ -380,7 +377,7 @@ pub mod util { .expect("transaction must be valid for primary selection"); let removed_text = text.slice(removed_start..removed_end); - let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping( + let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping( doc, selection, |range| { @@ -423,6 +420,11 @@ pub mod util { return transaction; } + // Don't normalize to avoid merging/reording selections which would + // break the association between tabstops and selections. Most ranges + // will be replaced by tabstops anyways and the final selection will be + // normalized anyways + selection = selection.map_no_normalize(changes); let mut mapped_selection = SmallVec::with_capacity(selection.len()); let mut mapped_primary_idx = 0; let primary_range = selection.primary(); @@ -431,7 +433,6 @@ pub mod util { mapped_primary_idx = mapped_selection.len() } - let range = range.map(changes); let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty()); let Some(tabstops) = tabstops else{ // no tabstop normal mapping @@ -624,23 +625,18 @@ impl Notification { #[derive(Debug)] pub struct Registry { - inner: HashMap)>>, - - counter: AtomicUsize, + inner: HashMap>>, + syn_loader: Arc, + counter: usize, pub incoming: SelectAll>, } -impl Default for Registry { - fn default() -> Self { - Self::new() - } -} - impl Registry { - pub fn new() -> Self { + pub fn new(syn_loader: Arc) -> Self { Self { inner: HashMap::new(), - counter: AtomicUsize::new(0), + syn_loader, + counter: 0, incoming: SelectAll::new(), } } @@ -649,65 +645,92 @@ impl Registry { self.inner .values() .flatten() - .find(|(client_id, _)| client_id == &id) - .map(|(_, client)| client.as_ref()) + .find(|client| client.id() == id) + .map(|client| &**client) } pub fn remove_by_id(&mut self, id: usize) { - self.inner.retain(|_, clients| { - clients.retain(|&(client_id, _)| client_id != id); - !clients.is_empty() - }) + self.inner.retain(|_, language_servers| { + language_servers.retain(|ls| id != ls.id()); + !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> { + 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( &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result>> { - let config = match &language_config.language_server { - Some(config) => config, - None => return Ok(None), - }; - - let scope = language_config.scope.clone(); - - match self.inner.entry(scope) { - Entry::Vacant(_) => Ok(None), - Entry::Occupied(mut entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - - let NewClientResult(client, incoming) = start_client( - id, - language_config, - config, - doc_path, - root_dirs, - enable_snippets, - )?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - - let old_clients = entry.insert(vec![(id, client.clone())]); - - for (_, old_client) in old_clients { - tokio::spawn(async move { - let _ = old_client.force_shutdown().await; - }); + ) -> Result>> { + language_config + .language_servers + .iter() + .filter_map(|LanguageServerFeatures { name, .. }| { + if self.inner.contains_key(name) { + let client = match self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) { + Ok(client) => client, + error => return Some(error), + }; + let old_clients = self + .inner + .insert(name.clone(), vec![client.clone()]) + .unwrap(); + + for old_client in old_clients { + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } + + Some(Ok(client)) + } else { + None } - - Ok(Some(client)) - } - } + }) + .collect() } - pub fn stop(&mut self, language_config: &LanguageConfiguration) { - let scope = language_config.scope.clone(); - - if let Some(clients) = self.inner.remove(&scope) { - for (_, client) in clients { + pub fn stop(&mut self, name: &str) { + if let Some(clients) = self.inner.remove(name) { + for client in clients { tokio::spawn(async move { let _ = client.force_shutdown().await; }); @@ -721,37 +744,34 @@ impl Registry { doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result>> { - let config = match &language_config.language_server { - Some(config) => config, - None => return Ok(None), - }; - - let clients = self.inner.entry(language_config.scope.clone()).or_default(); - // check if we already have a client for this documents root that we can reuse - 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(Some(client.1.clone())); - } - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - - let NewClientResult(client, incoming) = start_client( - id, - language_config, - config, - doc_path, - root_dirs, - enable_snippets, - )?; - clients.push((id, client.clone())); - self.incoming.push(UnboundedReceiverStream::new(incoming)); - Ok(Some(client)) + ) -> Result>> { + language_config + .language_servers + .iter() + .map(|LanguageServerFeatures { name, .. }| { + if let Some(clients) = self.inner.get(name) { + if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { + client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) + }) { + return Ok((name.to_owned(), client.clone())); + } + } + let client = self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + )?; + let clients = self.inner.entry(name.clone()).or_default(); + clients.push(client.clone()); + Ok((name.clone(), client)) + }) + .collect() } pub fn iter_clients(&self) -> impl Iterator> { - self.inner.values().flatten().map(|(_, client)| client) + self.inner.values().flatten() } } @@ -833,26 +853,28 @@ impl LspProgressMap { } } -struct NewClientResult(Arc, UnboundedReceiver<(usize, Call)>); +struct NewClient(Arc, UnboundedReceiver<(usize, Call)>); /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// it is only called when it makes sense. fn start_client( id: usize, + name: String, config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, -) -> Result { +) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, &ls_config.args, - config.config.clone(), + ls_config.config.clone(), ls_config.environment.clone(), &config.roots, config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), id, + name, ls_config.timeout, doc_path, )?; @@ -886,7 +908,7 @@ fn start_client( initialize_notify.notify_one(); }); - Ok(NewClientResult(client, incoming)) + Ok(NewClient(client, incoming)) } /// Find an LSP workspace of a file using the following mechanism: diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 3e3e06eec..9fdd30aa0 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -38,6 +38,7 @@ enum ServerMessage { #[derive(Debug)] pub struct Transport { id: usize, + name: String, pending_requests: Mutex>>>, } @@ -47,6 +48,7 @@ impl Transport { server_stdin: BufWriter, server_stderr: BufReader, id: usize, + name: String, ) -> ( UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedSender, @@ -58,6 +60,7 @@ impl Transport { let transport = Self { id, + name, pending_requests: Mutex::new(HashMap::default()), }; @@ -83,6 +86,7 @@ impl Transport { async fn recv_server_message( reader: &mut (impl AsyncBufRead + Unpin + Send), buffer: &mut String, + language_server_name: &str, ) -> Result { let mut content_length = None; loop { @@ -124,7 +128,7 @@ impl Transport { reader.read_exact(&mut content).await?; 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) let output: serde_json::Result = serde_json::from_str(msg); @@ -135,12 +139,13 @@ impl Transport { async fn recv_server_error( err: &mut (impl AsyncBufRead + Unpin + Send), buffer: &mut String, + language_server_name: &str, ) -> Result<()> { buffer.truncate(0); if err.read_line(buffer).await? == 0 { return Err(Error::StreamClosed); }; - error!("err <- {:?}", buffer); + error!("{language_server_name} err <- {buffer:?}"); Ok(()) } @@ -162,15 +167,17 @@ impl Transport { Payload::Notification(value) => serde_json::to_string(&value)?, 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( &self, server_stdin: &mut BufWriter, request: String, + language_server_name: &str, ) -> Result<()> { - info!("-> {}", request); + info!("{language_server_name} -> {request}"); // send the headers server_stdin @@ -189,9 +196,13 @@ impl Transport { &self, client_tx: &UnboundedSender<(usize, jsonrpc::Call)>, msg: ServerMessage, + language_server_name: &str, ) -> Result<()> { 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) => { client_tx .send((self.id, call)) @@ -202,14 +213,18 @@ impl Transport { 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 { jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => { - info!("<- {}", result); + info!("{language_server_name} <- {}", result); (id, Ok(result)) } jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => { - error!("<- {}", error); + error!("{language_server_name} <- {error}"); (id, Err(error.into())) } }; @@ -240,12 +255,17 @@ impl Transport { ) { let mut recv_buffer = String::new(); 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) => { - match transport.process_server_message(&client_tx, msg).await { + match transport + .process_server_message(&client_tx, msg, &transport.name) + .await + { Ok(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{} err: <- {err:?}", transport.name); break; } }; @@ -270,7 +290,7 @@ impl Transport { params: jsonrpc::Params::None, })); match transport - .process_server_message(&client_tx, notification) + .process_server_message(&client_tx, notification, &transport.name) .await { Ok(_) => {} @@ -281,20 +301,22 @@ impl Transport { break; } Err(err) => { - error!("err: <- {:?}", err); + error!("{} err: <- {err:?}", transport.name); break; } } } } - async fn err(_transport: Arc, mut server_stderr: BufReader) { + async fn err(transport: Arc, mut server_stderr: BufReader) { let mut recv_buffer = String::new(); 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(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{} err: <- {err:?}", transport.name); break; } } @@ -331,6 +353,11 @@ impl Transport { } } + fn is_shutdown(payload: &Payload) -> bool { + use lsp_types::request::{Request, Shutdown}; + matches!(payload, Payload::Request { value: jsonrpc::MethodCall { method, .. }, .. } if method == Shutdown::METHOD) + } + // TODO: events that use capabilities need to do the right thing loop { @@ -348,10 +375,11 @@ impl Transport { method: lsp_types::notification::Initialized::METHOD.to_string(), 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(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{language_server_name} err: <- {err:?}"); } } @@ -361,14 +389,17 @@ impl Transport { match transport.send_payload_to_server(&mut server_stdin, msg).await { Ok(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{language_server_name} err: <- {err:?}"); } } } } msg = client_rx.recv() => { if let Some(msg) = msg { - if is_pending && !is_initialize(&msg) { + if is_pending && is_shutdown(&msg) { + log::info!("Language server not initialized, shutting down"); + break; + } else if is_pending && !is_initialize(&msg) { // ignore notifications if let Payload::Notification(_) = msg { continue; @@ -380,7 +411,7 @@ impl Transport { match transport.send_payload_to_server(&mut server_stdin, msg).await { Ok(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{} err: <- {err:?}", transport.name); } } } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 7155f49eb..c08d6a867 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -31,7 +31,7 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" } helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" -once_cell = "1.17" +once_cell = "1.18" which = "4.4" @@ -73,7 +73,7 @@ dlopen_derive = "0.1.4" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } -libc = "0.2.142" +libc = "0.2.147" [build-dependencies] helix-loader = { version = "0.6", path = "../helix-loader" } @@ -81,4 +81,4 @@ helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] smallvec = "1.10" indoc = "2.0.1" -tempfile = "3.4.0" +tempfile = "3.6.0" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a92406067..5d2307bb3 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -30,6 +30,7 @@ use crate::{ use log::{debug, error, warn}; use std::{ + collections::btree_map::Entry, io::{stdin, stdout}, path::Path, sync::Arc, @@ -230,8 +231,14 @@ impl Application { #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] - let signals = Signals::new([signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1]) - .context("build signal handler")?; + let signals = Signals::new([ + signal::SIGTSTP, + signal::SIGCONT, + signal::SIGUSR1, + signal::SIGTERM, + signal::SIGINT, + ]) + .context("build signal handler")?; let mut app = Self { compositor, @@ -330,7 +337,9 @@ impl Application { biased; Some(signal) = self.signals.next() => { - self.handle_signals(signal).await; + if !self.handle_signals(signal).await { + return false; + }; } Some(event) = input_stream.next() => { self.handle_terminal_events(event).await; @@ -459,10 +468,12 @@ impl Application { #[cfg(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))] - pub async fn handle_signals(&mut self, signal: i32) { + pub async fn handle_signals(&mut self, signal: i32) -> bool { match signal { signal::SIGTSTP => { self.restore_term().unwrap(); @@ -516,8 +527,14 @@ impl Application { self.refresh_config(); self.render().await; } + signal::SIGTERM | signal::SIGINT => { + self.restore_term().unwrap(); + return false; + } _ => unreachable!(), } + + true } pub async fn handle_idle_timeout(&mut self) { @@ -582,7 +599,7 @@ impl Application { let doc = doc_mut!(self.editor, &doc_save_event.doc_id); let id = doc.id(); doc.detect_language(loader); - let _ = self.editor.refresh_language_server(id); + self.editor.refresh_language_servers(id); } // TODO: fix being overwritten by lsp @@ -680,6 +697,18 @@ impl Application { ) { 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 { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { let notification = match Notification::parse(&method, params) { @@ -695,14 +724,7 @@ impl Application { match notification { Notification::Initialized => { - let 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; - } - }; + let language_server = language_server!(); // Trigger a workspace/didChangeConfiguration notification after initialization. // This might not be required by the spec but Neovim does this as well, so it's @@ -711,9 +733,10 @@ impl Application { tokio::spawn(language_server.did_change_configuration(config.clone())); } - let docs = self.editor.documents().filter(|doc| { - doc.language_server().map(|server| server.id()) == Some(server_id) - }); + let docs = self + .editor + .documents() + .filter(|doc| doc.supports_language_server(server_id)); // trigger textDocument/didOpen for docs that are already open for doc in docs { @@ -733,7 +756,7 @@ impl Application { )); } } - Notification::PublishDiagnostics(mut params) => { + Notification::PublishDiagnostics(params) => { let path = match params.uri.to_file_path() { Ok(path) => path, Err(_) => { @@ -741,6 +764,12 @@ impl Application { return; } }; + let language_server = language_server!(); + if !language_server.is_initialized() { + log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name()); + return; + } + let offset_encoding = language_server.offset_encoding(); let doc = self.editor.document_by_path_mut(&path).filter(|doc| { if let Some(version) = params.version { if version != doc.version() { @@ -763,18 +792,11 @@ impl Application { use helix_core::diagnostic::{Diagnostic, Range, Severity::*}; 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 let start = if let Some(start) = lsp_pos_to_pos( text, diagnostic.range.start, - language_server.offset_encoding(), + offset_encoding, ) { start } else { @@ -782,11 +804,9 @@ impl Application { return None; }; - let end = if let Some(end) = lsp_pos_to_pos( - text, - diagnostic.range.end, - language_server.offset_encoding(), - ) { + let end = if let Some(end) = + lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding) + { end } else { log::warn!("lsp position out of bounds - {:?}", diagnostic); @@ -825,14 +845,19 @@ impl Application { None => None, }; - let tags = if let Some(ref tags) = diagnostic.tags { - let new_tags = tags.iter().filter_map(|tag| { - match *tag { - lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), - lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), - _ => None - } - }).collect(); + let tags = if let Some(tags) = &diagnostic.tags { + let new_tags = tags + .iter() + .filter_map(|tag| match *tag { + lsp::DiagnosticTag::DEPRECATED => { + Some(DiagnosticTag::Deprecated) + } + lsp::DiagnosticTag::UNNECESSARY => { + Some(DiagnosticTag::Unnecessary) + } + _ => None, + }) + .collect(); new_tags } else { @@ -848,25 +873,40 @@ impl Application { tags, source: diagnostic.source.clone(), data: diagnostic.data.clone(), + language_server_id: server_id, }) }) .collect(); - doc.set_diagnostics(diagnostics); + doc.replace_diagnostics(diagnostics, server_id); } - // Sort diagnostics first by severity and then by line numbers. - // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order - params + let mut diagnostics = params .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 // 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. - self.editor - .diagnostics - .insert(params.uri, params.diagnostics); + match self.editor.diagnostics.entry(params.uri) { + Entry::Occupied(o) => { + 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) => { log::warn!("unhandled window/showMessage: {:?}", params); @@ -963,24 +1003,18 @@ impl Application { Notification::Exit => { self.editor.set_status("Language server exited"); - // Clear any diagnostics for documents with this server open. - let urls: Vec<_> = self - .editor - .documents_mut() - .filter_map(|doc| { - if doc.language_server().map(|server| server.id()) - == Some(server_id) - { - doc.set_diagnostics(Vec::new()); - doc.url() - } else { - None - } - }) - .collect(); + // LSPs may produce diagnostics for files that haven't been opened in helix, + // we need to clear those and remove the entries from the list if this leads to + // an empty diagnostic list for said files + for diags in self.editor.diagnostics.values_mut() { + diags.retain(|(_, lsp_id)| *lsp_id != server_id); + } - for url in urls { - self.editor.diagnostics.remove(&url); + self.editor.diagnostics.retain(|_, diags| !diags.is_empty()); + + // 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. @@ -1031,47 +1065,48 @@ impl Application { Ok(serde_json::Value::Null) } Ok(MethodCall::ApplyWorkspaceEdit(params)) => { - let res = apply_workspace_edit( - &mut self.editor, - helix_lsp::OffsetEncoding::Utf8, - ¶ms.edit, - ); - - Ok(json!(lsp::ApplyWorkspaceEditResponse { - applied: res.is_ok(), - failure_reason: res.as_ref().err().map(|err| err.kind.to_string()), - failed_change: res - .as_ref() - .err() - .map(|err| err.failed_change_idx as u32), - })) + let language_server = language_server!(); + if language_server.is_initialized() { + let offset_encoding = language_server.offset_encoding(); + let res = apply_workspace_edit( + &mut self.editor, + offset_encoding, + ¶ms.edit, + ); + + Ok(json!(lsp::ApplyWorkspaceEditResponse { + applied: res.is_ok(), + failure_reason: res.as_ref().err().map(|err| err.kind.to_string()), + failed_change: res + .as_ref() + .err() + .map(|err| err.failed_change_idx as u32), + })) + } else { + Err(helix_lsp::jsonrpc::Error { + code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest, + message: "Server must be initialized to request workspace edits" + .to_string(), + data: None, + }) + } } Ok(MethodCall::WorkspaceFolders) => { - let language_server = - self.editor.language_servers.get_by_id(server_id).unwrap(); - - Ok(json!(&*language_server.workspace_folders().await)) + Ok(json!(&*language_server!().workspace_folders().await)) } Ok(MethodCall::WorkspaceConfiguration(params)) => { + let language_server = language_server!(); let result: Vec<_> = params .items .iter() .map(|item| { - let mut config = match &item.scope_uri { - 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()?, - }; + let mut config = language_server.config()?; if let Some(section) = item.section.as_ref() { - for part in section.split('.') { - config = config.get(part)?; + // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server') + if !section.is_empty() { + for part in section.split('.') { + config = config.get(part)?; + } } } Some(config) @@ -1092,15 +1127,7 @@ impl Application { } }; - let 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; - } - }; - - tokio::spawn(language_server.reply(id, reply)); + tokio::spawn(language_server!().reply(id, reply)); } Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 214a73821..6e40a191f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -28,17 +28,18 @@ use helix_core::{ regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, selection, shellwords, surround, + syntax::LanguageServerFeature, text_annotations::TextAnnotations, textobject, tree_sitter::Node, unicode::width::UnicodeWidthChar, - visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, - Selection, SmallVec, Tendril, Transaction, + visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, + RopeSlice, Selection, SmallVec, Tendril, Transaction, }; use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, - editor::{Action, Motion}, + editor::{Action, CompleteAction, Motion}, info::Info, input::KeyEvent, keyboard::KeyCode, @@ -60,13 +61,13 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker, Popup, Prompt, PromptEvent, }, }; 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::HashSet, num::NonZeroUsize}; @@ -104,6 +105,13 @@ impl<'a> Context<'a> { })); } + /// Call `replace_or_push` on the Compositor + pub fn replace_or_push_layer(&mut self, id: &'static str, component: T) { + self.callback = Some(Box::new(move |compositor: &mut Compositor, _| { + compositor.replace_or_push(id, component); + })); + } + #[inline] pub fn on_next_key( &mut self, @@ -298,6 +306,7 @@ impl MappableCommand { move_next_long_word_start, "Move to start of next long word", move_prev_long_word_start, "Move to start of previous long word", move_next_long_word_end, "Move to end of next long word", + move_prev_long_word_end, "Move to end of previous long word", extend_next_word_start, "Extend to start of next word", extend_prev_word_start, "Extend to start of previous word", extend_next_word_end, "Extend to end of next word", @@ -305,6 +314,7 @@ impl MappableCommand { extend_next_long_word_start, "Extend to start of next long word", extend_prev_long_word_start, "Extend to start of previous long word", extend_next_long_word_end, "Extend to end of next long word", + extend_prev_long_word_end, "Extend to end of prev long word", find_till_char, "Move till next occurrence of char", find_next_char, "Move to next occurrence of char", extend_till_char, "Extend till next occurrence of char", @@ -326,6 +336,7 @@ impl MappableCommand { select_regex, "Select all regex matches inside selections", split_selection, "Split selections on regex matches", split_selection_on_newline, "Split selection on newlines", + merge_selections, "Merge selections", merge_consecutive_selections, "Merge consecutive selections", search, "Search for regex pattern", rsearch, "Reverse search for regex pattern", @@ -424,6 +435,7 @@ impl MappableCommand { later, "Move forward in history", commit_undo_checkpoint, "Commit changes to new checkpoint", yank, "Yank selection", + yank_joined, "Join and yank selections", yank_joined_to_clipboard, "Join and yank selections to clipboard", yank_main_selection_to_clipboard, "Yank main selection to clipboard", yank_joined_to_primary_clipboard, "Join and yank selections to primary clipboard", @@ -454,6 +466,7 @@ impl MappableCommand { rotate_selections_backward, "Rotate selections backward", rotate_selection_contents_forward, "Rotate selection contents forward", rotate_selection_contents_backward, "Rotate selections contents backward", + reverse_selection_contents, "Reverse selections contents", expand_selection, "Expand selection to parent syntax node", shrink_selection, "Shrink selection to previously expanded syntax node", select_next_sibling, "Select next sibling in syntax tree", @@ -866,54 +879,50 @@ fn extend_to_line_start(cx: &mut Context) { } fn kill_to_line_start(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let line = range.cursor_line(text); - let first_char = text.line_to_char(line); - let anchor = range.cursor(text); - let head = if anchor == first_char && line != 0 { - // select until previous line - line_end_char_index(&text, line - 1) - } else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) { - if first_char + pos < anchor { - // select until first non-blank in line if cursor is after it - first_char + pos + delete_by_selection_insert_mode( + cx, + move |text, range| { + let line = range.cursor_line(text); + let first_char = text.line_to_char(line); + let anchor = range.cursor(text); + let head = if anchor == first_char && line != 0 { + // select until previous line + line_end_char_index(&text, line - 1) + } else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) { + if first_char + pos < anchor { + // select until first non-blank in line if cursor is after it + first_char + pos + } else { + // select until start of line + first_char + } } else { // select until start of line first_char - } - } else { - // select until start of line - first_char - }; - Range::new(head, anchor) - }); - delete_selection_insert_mode(doc, view, &selection); - - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + }; + (head, anchor) + }, + Direction::Backward, + ); } fn kill_to_line_end(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let line = range.cursor_line(text); - let line_end_pos = line_end_char_index(&text, line); - 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); + delete_by_selection_insert_mode( + cx, + |text, range| { + let line = range.cursor_line(text); + let line_end_pos = line_end_char_index(&text, line); + let pos = range.cursor(text); - 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) { @@ -1140,6 +1149,10 @@ fn move_prev_long_word_start(cx: &mut Context) { move_word_impl(cx, movement::move_prev_long_word_start) } +fn move_prev_long_word_end(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_long_word_end) +} + fn move_next_long_word_end(cx: &mut Context) { move_word_impl(cx, movement::move_next_long_word_end) } @@ -1297,6 +1310,10 @@ fn extend_prev_long_word_start(cx: &mut Context) { extend_word_impl(cx, movement::move_prev_long_word_start) } +fn extend_prev_long_word_end(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_long_word_end) +} + fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } @@ -1339,7 +1356,7 @@ where find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); 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); }))); }) } @@ -1811,6 +1828,12 @@ fn split_selection_on_newline(cx: &mut Context) { 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) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id).clone().merge_consecutive_ranges(); @@ -2211,7 +2234,7 @@ fn global_search(cx: &mut Context) { return; } - let picker = FilePicker::new( + let picker = Picker::new( all_matches, current_path, move |cx, FileResult { path, line_num }, action| { @@ -2239,11 +2262,9 @@ fn global_search(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); align_view(doc, view, Align::Center); - }, - |_editor, FileResult { path, line_num }| { + }).with_preview(|_editor, FileResult { path, line_num }| { Some((path.clone().into(), Some((*line_num, *line_num)))) - }, - ); + }); compositor.push(Box::new(overlaid(picker))); }, )); @@ -2386,9 +2407,8 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) { }; // then delete - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - (range.from(), range.to(), None) - }); + let transaction = + Transaction::delete_by_selection(doc.text(), selection, |range| (range.from(), range.to())); doc.apply(&transaction, view.id); match op { @@ -2403,11 +2423,49 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) { } #[inline] -fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: &Selection) { - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - (range.from(), range.to(), None) - }); +fn delete_by_selection_insert_mode( + cx: &mut Context, + 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); + lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); } fn delete_selection(cx: &mut Context) { @@ -2597,22 +2655,18 @@ fn buffer_picker(cx: &mut Context) { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = FilePicker::new( - items, - (), - |cx, meta, action| { - cx.editor.switch(meta.id, action); - }, - |editor, meta| { - let doc = &editor.documents.get(&meta.id)?; - let &view_id = doc.selections().keys().next()?; - let line = doc - .selection(view_id) - .primary() - .cursor_line(doc.text().slice(..)); - Some((meta.id.into(), Some((line, line)))) - }, - ); + let picker = Picker::new(items, (), |cx, meta, action| { + cx.editor.switch(meta.id, action); + }) + .with_preview(|editor, meta| { + let doc = &editor.documents.get(&meta.id)?; + let &view_id = doc.selections().keys().next()?; + let line = doc + .selection(view_id) + .primary() + .cursor_line(doc.text().slice(..)); + Some((meta.id.into(), Some((line, line)))) + }); cx.push_layer(Box::new(overlaid(picker))); } @@ -2678,7 +2732,7 @@ fn jumplist_picker(cx: &mut Context) { } }; - let picker = FilePicker::new( + let picker = Picker::new( cx.editor .tree .views() @@ -2696,12 +2750,12 @@ fn jumplist_picker(cx: &mut Context) { doc.set_selection(view.id, meta.selection.clone()); view.ensure_cursor_in_view_center(doc, config.scrolloff); }, - |editor, meta| { - let doc = &editor.documents.get(&meta.id)?; - let line = meta.selection.primary().cursor_line(doc.text().slice(..)); - Some((meta.path.clone()?.into(), Some((line, line)))) - }, - ); + ) + .with_preview(|editor, meta| { + let doc = &editor.documents.get(&meta.id)?; + let line = meta.selection.primary().cursor_line(doc.text().slice(..)); + Some((meta.id.into(), Some((line, line)))) + }); cx.push_layer(Box::new(overlaid(picker))); } @@ -2735,6 +2789,9 @@ impl ui::menu::Item for MappableCommand { } pub fn command_palette(cx: &mut Context) { + let register = cx.register; + let count = cx.count; + cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { let keymap = compositor.find::().unwrap().keymaps.map() @@ -2752,8 +2809,8 @@ pub fn command_palette(cx: &mut Context) { let picker = Picker::new(commands, keymap, move |cx, command, _action| { let mut ctx = Context { - register: None, - count: std::num::NonZeroUsize::new(1), + register, + count, editor: cx.editor, callback: None, on_next_key_callback: None, @@ -2792,24 +2849,87 @@ fn last_picker(cx: &mut Context) { })); } -// I inserts at the first nonwhitespace character of each line with a selection +/// Fallback position to use for [`insert_with_indent`]. +enum IndentFallbackPos { + LineStart, + LineEnd, +} + +// `I` inserts at the first nonwhitespace character of each line with a selection. +// If the line is empty, automatically indent. fn insert_at_line_start(cx: &mut Context) { - goto_first_nonwhitespace(cx); - enter_insert_mode(cx); + insert_with_indent(cx, IndentFallbackPos::LineStart); } -// A inserts at the end of each line with a selection +// `A` inserts at the end of each line with a selection. +// If the line is empty, automatically indent. fn insert_at_line_end(cx: &mut Context) { + insert_with_indent(cx, IndentFallbackPos::LineEnd); +} + +// Enter insert mode and auto-indent the current line if it is empty. +// If the line is not empty, move the cursor to the specified fallback position. +fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) { enter_insert_mode(cx); + let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id).clone().transform(|range| { - let text = doc.text().slice(..); - let line = range.cursor_line(text); - let pos = line_end_char_index(&text, line); - Range::new(pos, pos) + let text = doc.text().slice(..); + let contents = doc.text(); + let selection = doc.selection(view.id); + + let language_config = doc.language_config(); + let syntax = doc.syntax(); + let tab_width = doc.tab_width(); + + let mut ranges = SmallVec::with_capacity(selection.len()); + let mut offs = 0; + + let mut transaction = Transaction::change_by_selection(contents, selection, |range| { + let cursor_line = range.cursor_line(text); + let cursor_line_start = text.line_to_char(cursor_line); + + if line_end_char_index(&text, cursor_line) == cursor_line_start { + // line is empty => auto indent + let line_end_index = cursor_line_start; + + let indent = indent::indent_for_newline( + language_config, + syntax, + &doc.indent_style, + tab_width, + text, + cursor_line, + line_end_index, + cursor_line, + ); + + // calculate new selection ranges + let pos = offs + cursor_line_start; + let indent_width = indent.chars().count(); + ranges.push(Range::point(pos + indent_width)); + offs += indent_width; + + (line_end_index, line_end_index, Some(indent.into())) + } else { + // move cursor to the fallback position + let pos = match cursor_fallback { + IndentFallbackPos::LineStart => { + find_first_non_whitespace_char(text.line(cursor_line)) + .map(|ws_offset| ws_offset + cursor_line_start) + .unwrap_or(cursor_line_start) + } + IndentFallbackPos::LineEnd => line_end_char_index(&text, cursor_line), + }; + + ranges.push(range.put_cursor(text, pos + offs, cx.editor.mode == Mode::Select)); + + (cursor_line_start, cursor_line_start, None) + } }); - doc.set_selection(view.id, selection); + + transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); + doc.apply(&transaction, view.id); } // Creates an LspCallback that waits for formatting changes to be computed. When they're done, @@ -3067,7 +3187,7 @@ fn exit_select_mode(cx: &mut Context) { fn goto_first_diag(cx: &mut Context) { 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), None => return, }; @@ -3076,7 +3196,7 @@ fn goto_first_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) { 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), None => return, }; @@ -3092,10 +3212,9 @@ fn goto_next_diag(cx: &mut Context) { .cursor(doc.text().slice(..)); let diag = doc - .diagnostics() - .iter() + .shown_diagnostics() .find(|diag| diag.range.start > cursor_pos) - .or_else(|| doc.diagnostics().first()); + .or_else(|| doc.shown_diagnostics().next()); let selection = match diag { Some(diag) => Selection::single(diag.range.start, diag.range.end), @@ -3113,11 +3232,10 @@ fn goto_prev_diag(cx: &mut Context) { .cursor(doc.text().slice(..)); let diag = doc - .diagnostics() - .iter() + .shown_diagnostics() .rev() .find(|diag| diag.range.start < cursor_pos) - .or_else(|| doc.diagnostics().last()); + .or_else(|| doc.shown_diagnostics().last()); let selection = match diag { // NOTE: the selection is reversed because we're jumping to the @@ -3272,23 +3390,19 @@ pub mod insert { use helix_lsp::lsp; // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let capabilities = language_server.capabilities(); + let trigger_completion = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .any(|ls| { + // TODO: what if trigger is multiple chars long + matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), + .. + }) if triggers.iter().any(|trigger| trigger.contains(ch))) + }); - if let Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }) = &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); - } + if trigger_completion { + cx.editor.clear_idle_timer(); + super::completion(cx); } } @@ -3296,12 +3410,12 @@ pub mod insert { use helix_lsp::lsp; // if ch matches signature_help char, trigger let doc = doc_mut!(cx.editor); - // The language_server!() macro is not used here since it will - // print an "LSP not active for current buffer" message on - // every keypress. - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, + // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow + let Some(language_server) = doc + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .next() + else { + return; }; let capabilities = language_server.capabilities(); @@ -3505,10 +3619,10 @@ pub mod insert { let auto_pairs = doc.auto_pairs(cx.editor); 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); if pos == 0 { - return (pos, pos, None); + return (pos, pos); } 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. @@ -3516,11 +3630,7 @@ pub mod insert { if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') { if text.get_char(pos.saturating_sub(1)) == Some('\t') { // fast path, delete one char - ( - graphemes::nth_prev_grapheme_boundary(text, pos, 1), - pos, - None, - ) + (graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos) } else { let width: usize = fragment .chars() @@ -3547,7 +3657,7 @@ pub mod insert { _ => break, } } - (start, pos, None) // delete! + (start, pos) // delete! } } else { match ( @@ -3565,17 +3675,12 @@ pub mod insert { ( graphemes::nth_prev_grapheme_boundary(text, pos, count), graphemes::nth_next_grapheme_boundary(text, pos, count), - None, ) } _ => // delete 1 char { - ( - graphemes::nth_prev_grapheme_boundary(text, pos, count), - pos, - None, - ) + (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos) } } } @@ -3588,50 +3693,40 @@ pub mod insert { pub fn delete_char_forward(cx: &mut Context) { let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { + delete_by_selection_insert_mode( + cx, + |text, range| { let pos = range.cursor(text); - ( - pos, - graphemes::nth_next_grapheme_boundary(text, pos, count), - None, - ) - }); - doc.apply(&transaction, view.id); - - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + (pos, graphemes::nth_next_grapheme_boundary(text, pos, count)) + }, + Direction::Forward, + ) } pub fn delete_word_backward(cx: &mut Context) { let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let anchor = movement::move_prev_word_start(text, range, count).from(); - let next = Range::new(anchor, range.cursor(text)); - exclude_cursor(text, next, range) - }); - delete_selection_insert_mode(doc, view, &selection); - - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + delete_by_selection_insert_mode( + cx, + |text, range| { + let anchor = movement::move_prev_word_start(text, *range, count).from(); + let next = Range::new(anchor, range.cursor(text)); + let range = exclude_cursor(text, next, *range); + (range.from(), range.to()) + }, + Direction::Backward, + ); } pub fn delete_word_forward(cx: &mut Context) { let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let head = movement::move_next_word_end(text, range, count).to(); - Range::new(range.cursor(text), head) - }); - - delete_selection_insert_mode(doc, view, &selection); - - lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + delete_by_selection_insert_mode( + cx, + |text, range| { + let head = movement::move_next_word_end(text, *range, count).to(); + (range.cursor(text), head) + }, + Direction::Forward, + ); } } @@ -3714,6 +3809,38 @@ fn yank(cx: &mut Context) { exit_select_mode(cx); } +fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id); + let joined = selection + .fragments(text) + .fold(String::new(), |mut acc, fragment| { + if !acc.is_empty() { + acc.push_str(separator); + } + acc.push_str(&fragment); + acc + }); + + let msg = format!( + "joined and yanked {} selection(s) to register {}", + selection.len(), + register, + ); + + editor.registers.write(register, vec![joined]); + editor.set_status(msg); +} + +fn yank_joined(cx: &mut Context) { + let line_ending = doc!(cx.editor).line_ending; + let register = cx.register.unwrap_or('"'); + yank_joined_impl(cx.editor, line_ending.as_str(), register); + exit_select_mode(cx); +} + fn yank_joined_to_clipboard_impl( editor: &mut Editor, separator: &str, @@ -4115,55 +4242,60 @@ fn format_selections(cx: &mut Context) { use helix_lsp::{lsp, util::range_to_lsp_range}; let (view, doc) = current!(cx.editor); + let view_id = view.id; // via lsp if available // TODO: else via tree-sitter indentation calculations - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, + if doc.selection(view_id).len() != 1 { + cx.editor + .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 = doc - .selection(view.id) + .selection(view_id) .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(); - if ranges.len() != 1 { - cx.editor - .set_error("format_selections only supports a single selection for now"); - return; - } - // TODO: handle fails // TODO: concurrent map over all ranges let range = ranges[0]; - let request = match language_server.text_document_range_formatting( - doc.identifier(), - range, - lsp::FormattingOptions::default(), - None, - ) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support range formatting"); - return; - } - }; + let future = language_server + .text_document_range_formatting( + doc.identifier(), + range, + lsp::FormattingOptions::default(), + None, + ) + .unwrap(); - 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( - doc.text(), - edits, - language_server.offset_encoding(), - ); + let transaction = + helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding); - doc.apply(&transaction, view.id); + doc.apply(&transaction, view_id); } fn join_selections_impl(cx: &mut Context, select_space: bool) { @@ -4293,21 +4425,53 @@ pub fn completion(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, + let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion + { + savepoint.clone() + } else { + doc.savepoint(view) }; - let offset_encoding = language_server.offset_encoding(); - let text = doc.text().slice(..); - let cursor = doc.selection(view.id).primary().cursor(text); - - let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); + let text = savepoint.text.clone(); + let cursor = savepoint.cursor(); + + let mut seen_language_servers = HashSet::new(); + + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .filter(|ls| seen_language_servers.insert(ls.id())) + .map(|language_server| { + let language_server_id = language_server.id(); + let offset_encoding = language_server.offset_encoding(); + let pos = pos_to_lsp_pos(&text, cursor, offset_encoding); + let doc_id = doc.identifier(); + let completion_request = language_server.completion(doc_id, pos, None).unwrap(); + + async move { + let json = completion_request.await?; + let response: Option = 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) { - Some(future) => future, - None => return, - }; + anyhow::Ok(items) + } + }) + .collect(); // setup a channel that allows the request to be canceled let (tx, rx) = oneshot::channel(); @@ -4316,12 +4480,20 @@ pub fn completion(cx: &mut Context) { // and the associated request is automatically dropped cx.editor.completion_request_handle = Some(tx); let future = async move { + let items_future = async move { + let mut items = Vec::new(); + // TODO if one completion request errors, all other completion requests are discarded (even if they're valid) + while let Some(mut lsp_items) = futures.try_next().await? { + items.append(&mut lsp_items); + } + anyhow::Ok(items) + }; tokio::select! { biased; _ = rx => { - Ok(serde_json::Value::Null) + Ok(Vec::new()) } - res = future => { + res = items_future => { res } } @@ -4337,7 +4509,6 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); - let savepoint = doc.savepoint(view); let trigger_doc = doc.id(); let trigger_view = view.id; @@ -4356,9 +4527,9 @@ pub fn completion(cx: &mut Context) { }, )); - cx.callback( - future, - move |editor, compositor, response: Option| { + cx.jobs.callback(async move { + let items = future.await?; + let call = move |editor: &mut Editor, compositor: &mut Compositor| { let (view, doc) = current_ref!(editor); // check if the completion request is stale. // @@ -4369,16 +4540,6 @@ pub fn completion(cx: &mut Context) { 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() { // editor.set_error("No completion available"); return; @@ -4389,7 +4550,6 @@ pub fn completion(cx: &mut Context) { editor, savepoint, items, - offset_encoding, start_offset, trigger_offset, size, @@ -4403,8 +4563,9 @@ pub fn completion(cx: &mut Context) { { compositor.remove(SignatureHelp::ID); } - }, - ); + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }); } // comments @@ -4439,7 +4600,13 @@ fn rotate_selections_backward(cx: &mut Context) { rotate_selections(cx, Direction::Backward) } -fn rotate_selection_contents(cx: &mut Context, direction: Direction) { +enum ReorderStrategy { + RotateForward, + RotateBackward, + Reverse, +} + +fn reorder_selection_contents(cx: &mut Context, strategy: ReorderStrategy) { let count = cx.count; let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -4457,9 +4624,10 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { for chunk in fragments.chunks_mut(group) { // TODO: also modify main index - match direction { - Direction::Forward => chunk.rotate_right(1), - Direction::Backward => chunk.rotate_left(1), + match strategy { + ReorderStrategy::RotateForward => chunk.rotate_right(1), + ReorderStrategy::RotateBackward => chunk.rotate_left(1), + ReorderStrategy::Reverse => chunk.reverse(), }; } @@ -4476,10 +4644,13 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { } fn rotate_selection_contents_forward(cx: &mut Context) { - rotate_selection_contents(cx, Direction::Forward) + reorder_selection_contents(cx, ReorderStrategy::RotateForward) } fn rotate_selection_contents_backward(cx: &mut Context) { - rotate_selection_contents(cx, Direction::Backward) + reorder_selection_contents(cx, ReorderStrategy::RotateBackward) +} +fn reverse_selection_contents(cx: &mut Context) { + reorder_selection_contents(cx, ReorderStrategy::Reverse) } // tree sitter node selection @@ -4561,20 +4732,23 @@ fn select_prev_sibling(cx: &mut Context) { fn match_brackets(cx: &mut Context) { 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 text = doc.text().slice(..); - let selection = doc.selection(view.id).clone().transform(|range| { - if let Some(pos) = - match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.cursor(text)) - { - range.put_cursor(text, pos, cx.editor.mode == Mode::Select) - } else { - range - } - }); - doc.set_selection(view.id, selection); - } + let selection = doc.selection(view.id).clone().transform(|range| { + let pos = range.cursor(text_slice); + if let Some(matched_pos) = doc.syntax().map_or_else( + || match_brackets::find_matching_bracket_plaintext(text.slice(..), pos), + |syntax| match_brackets::find_matching_bracket_fuzzy(syntax, text.slice(..), pos), + ) { + range.put_cursor(text_slice, matched_pos, is_select) + } else { + range + } + }); + + doc.set_selection(view.id, selection); } // @@ -5201,9 +5375,10 @@ async fn shell_impl_async( let output = if let Some(mut stdin) = process.stdin.take() { let input_task = tokio::spawn(async move { 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! { process.wait_with_output(), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 8efdc9cfa..70a5ec212 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -2,7 +2,7 @@ use super::{Context, Editor}; use crate::{ compositor::{self, Compositor}, job::{Callback, Jobs}, - ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, + ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text}, }; use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; @@ -73,21 +73,19 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let picker = FilePicker::new( - threads, - thread_states, - move |cx, thread, _action| callback_fn(cx.editor, thread), - move |editor, thread| { - let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; - let frame = frames.get(0)?; - let path = frame.source.as_ref()?.path.clone()?; - let pos = Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), - )); - Some((path.into(), pos)) - }, - ); + let picker = Picker::new(threads, thread_states, move |cx, thread, _action| { + callback_fn(cx.editor, thread) + }) + .with_preview(move |editor, thread| { + let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; + let frame = frames.get(0)?; + let path = frame.source.as_ref()?.path.clone()?; + let pos = Some(( + frame.line.saturating_sub(1), + frame.end_line.unwrap_or(frame.line).saturating_sub(1), + )); + Some((path.into(), pos)) + }); compositor.push(Box::new(picker)); }, ); @@ -580,7 +578,7 @@ pub fn dap_variables(cx: &mut Context) { let contents = Text::from(tui::text::Text::from(variables)); 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) { @@ -728,39 +726,35 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let picker = FilePicker::new( - frames, - (), - move |cx, frame, _action| { - let debugger = debugger!(cx.editor); - // TODO: this should be simpler to find - let pos = debugger.stack_frames[&thread_id] - .iter() - .position(|f| f.id == frame.id); - debugger.active_frame = pos; - - let frame = debugger.stack_frames[&thread_id] - .get(pos.unwrap_or(0)) - .cloned(); - if let Some(frame) = &frame { - jump_to_stack_frame(cx.editor, frame); - } - }, - move |_editor, frame| { - frame - .source - .as_ref() - .and_then(|source| source.path.clone()) - .map(|path| { - ( - path.into(), - Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), - )), - ) - }) - }, - ); + let picker = Picker::new(frames, (), move |cx, frame, _action| { + let debugger = debugger!(cx.editor); + // TODO: this should be simpler to find + let pos = debugger.stack_frames[&thread_id] + .iter() + .position(|f| f.id == frame.id); + debugger.active_frame = pos; + + let frame = debugger.stack_frames[&thread_id] + .get(pos.unwrap_or(0)) + .cloned(); + if let Some(frame) = &frame { + jump_to_stack_frame(cx.editor, frame); + } + }) + .with_preview(move |_editor, frame| { + frame + .source + .as_ref() + .and_then(|source| source.path.clone()) + .map(|path| { + ( + path.into(), + Some(( + frame.line.saturating_sub(1), + frame.end_line.unwrap_or(frame.line).saturating_sub(1), + )), + ) + }) + }); cx.push_layer(Box::new(picker)) } diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index 07c68570f..a91e34d8d 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -26,7 +26,7 @@ use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule}; use crate::{ compositor::{self, Component, Compositor}, job::{self, Callback}, - keymap::{merge_keys, Keymap}, + keymap::{merge_keys, KeyTrie}, ui::{self, menu::Item, overlay::overlaid, Popup, Prompt, PromptEvent}, }; @@ -256,7 +256,7 @@ impl SharedKeyBindingsEventQueue { .push_back(other_as_json); } - pub fn get() -> Option> { + pub fn get() -> Option> { let mut guard = KEYBINDING_QUEUE.raw_bindings.lock().unwrap(); if let Some(initial) = guard.pop_front() { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0ad6fb7eb..55153648a 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,4 +1,4 @@ -use futures_util::FutureExt; +use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt}; use helix_lsp::{ block_on, lsp::{ @@ -6,8 +6,10 @@ use helix_lsp::{ NumberOrString, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, - OffsetEncoding, + Client, OffsetEncoding, }; +use serde_json::Value; +use tokio_stream::StreamExt; use tui::{ text::{Span, Spans}, widgets::Row, @@ -15,7 +17,9 @@ use tui::{ use super::{align_view, push_jump, Align, Context, Editor, Open}; -use helix_core::{path, text_annotations::InlineAnnotation, Selection}; +use helix_core::{ + path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, +}; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, editor::Action, @@ -25,32 +29,42 @@ use helix_view::{ use crate::{ compositor::{self, Compositor}, + job::Callback, ui::{ - self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, - Popup, PromptEvent, + self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, + PromptEvent, }, }; use std::{ - cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc, + cmp::Ordering, + collections::{BTreeMap, HashSet}, + fmt::Write, + future::Future, + path::PathBuf, + sync::Arc, }; -/// Gets the language server that is attached to a document, and -/// if it's not active displays a status message. Using this macro -/// in a context where the editor automatically queries the LSP -/// (instead of when the user explicitly does so via a keybind like -/// `gd`) will spam the "LSP inactive" status message confusingly. +/// Gets the first language server that is attached to a document which supports a specific feature. +/// If there is no configured language server that supports the feature, this displays a status message. +/// Using this macro in a context where the editor automatically queries the LSP +/// (instead of when the user explicitly does so via a keybind like `gd`) +/// will spam the "No configured language server supports " status message confusingly. #[macro_export] -macro_rules! language_server { - ($editor:expr, $doc:expr) => { - match $doc.language_server() { +macro_rules! language_server_with_feature { + ($editor:expr, $doc:expr, $feature:expr) => {{ + let language_server = $doc.language_servers_with_feature($feature).next(); + match language_server { Some(language_server) => language_server, None => { - $editor.set_status("Language server not active for current buffer"); + $editor.set_status(format!( + "No configured language server supports {}", + $feature + )); return; } } - }; + }}; } impl ui::menu::Item for lsp::Location { @@ -87,20 +101,30 @@ impl ui::menu::Item for lsp::Location { } } -impl ui::menu::Item for lsp::SymbolInformation { +struct SymbolInformationItem { + symbol: lsp::SymbolInformation, + offset_encoding: OffsetEncoding, +} + +impl ui::menu::Item for SymbolInformationItem { /// Path to currently focussed document type Data = Option; fn format(&self, current_doc_path: &Self::Data) -> Row { - if current_doc_path.as_ref() == Some(&self.location.uri) { - self.name.as_str().into() + if current_doc_path.as_ref() == Some(&self.symbol.location.uri) { + self.symbol.name.as_str().into() } else { - match self.location.uri.to_file_path() { + match self.symbol.location.uri.to_file_path() { Ok(path) => { let get_relative_path = path::get_relative_path(path.as_path()); - format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into() + format!( + "{} ({})", + &self.symbol.name, + get_relative_path.to_string_lossy() + ) + .into() } - Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(), + Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(), } } } @@ -116,6 +140,7 @@ struct DiagnosticStyles { struct PickerDiagnostic { url: lsp::Url, diag: lsp::Diagnostic, + offset_encoding: OffsetEncoding, } impl ui::menu::Item for PickerDiagnostic { @@ -211,50 +236,44 @@ fn jump_to_location( align_view(doc, view, Align::Center); } -fn sym_picker( - symbols: Vec, - current_path: Option, - offset_encoding: OffsetEncoding, -) -> FilePicker { +type SymbolPicker = Picker; + +fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - FilePicker::new( - symbols, - current_path.clone(), - move |cx, symbol, action| { - let (view, doc) = current!(cx.editor); - push_jump(view, doc); - - if current_path.as_ref() != Some(&symbol.location.uri) { - let uri = &symbol.location.uri; - let path = match uri.to_file_path() { - Ok(path) => path, - Err(_) => { - let err = format!("unable to convert URI to filepath: {}", uri); - cx.editor.set_error(err); - return; - } - }; - if let Err(err) = cx.editor.open(&path, action) { - let err = format!("failed to open document: {}: {}", uri, err); - log::error!("{}", err); + Picker::new(symbols, current_path.clone(), move |cx, item, action| { + let (view, doc) = current!(cx.editor); + push_jump(view, doc); + + if current_path.as_ref() != Some(&item.symbol.location.uri) { + let uri = &item.symbol.location.uri; + let path = match uri.to_file_path() { + Ok(path) => path, + Err(_) => { + let err = format!("unable to convert URI to filepath: {}", uri); cx.editor.set_error(err); return; } + }; + if let Err(err) = cx.editor.open(&path, action) { + let err = format!("failed to open document: {}: {}", uri, err); + log::error!("{}", err); + cx.editor.set_error(err); + return; } + } - let (view, doc) = current!(cx.editor); + let (view, doc) = current!(cx.editor); - if let Some(range) = - lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) - { - // we flip the range so that the cursor sits on the start of the symbol - // (for example start of the function). - doc.set_selection(view.id, Selection::single(range.head, range.anchor)); - align_view(doc, view, Align::Center); - } - }, - move |_editor, symbol| Some(location_to_file_location(&symbol.location)), - ) + if let Some(range) = + lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding) + { + // we flip the range so that the cursor sits on the start of the symbol + // (for example start of the function). + doc.set_selection(view.id, Selection::single(range.head, range.anchor)); + align_view(doc, view, Align::Center); + } + }) + .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location))) .truncate_start(false) } @@ -266,22 +285,25 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap>, + diagnostics: BTreeMap>, current_path: Option, format: DiagnosticsFormat, - offset_encoding: OffsetEncoding, -) -> FilePicker { +) -> Picker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs let mut flat_diag = Vec::new(); for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); - for diag in diags { - flat_diag.push(PickerDiagnostic { - url: url.clone(), - diag, - }); + + for (diag, ls) in diags { + if let Some(ls) = cx.editor.language_server_by_id(ls) { + flat_diag.push(PickerDiagnostic { + url: url.clone(), + diag, + offset_encoding: ls.offset_encoding(), + }); + } } } @@ -292,10 +314,16 @@ fn diag_picker( error: cx.editor.theme.get("error"), }; - FilePicker::new( + Picker::new( flat_diag, (styles, format), - move |cx, PickerDiagnostic { url, diag }, action| { + move |cx, + PickerDiagnostic { + url, + diag, + offset_encoding, + }, + action| { if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -306,143 +334,180 @@ fn diag_picker( let (view, doc) = current!(cx.editor); - if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) { + if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) { // we flip the range so that the cursor sits on the start of the symbol // (for example start of the function). doc.set_selection(view.id, Selection::single(range.head, range.anchor)); align_view(doc, view, Align::Center); } }, - move |_editor, PickerDiagnostic { url, diag }| { - let location = lsp::Location::new(url.clone(), diag.range); - Some(location_to_file_location(&location)) - }, ) + .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| { + let location = lsp::Location::new(url.clone(), diag.range); + Some(location_to_file_location(&location)) + }) .truncate_start(false) } pub fn symbol_picker(cx: &mut Context) { fn nested_to_flat( - list: &mut Vec, + list: &mut Vec, file: &lsp::TextDocumentIdentifier, symbol: lsp::DocumentSymbol, + offset_encoding: OffsetEncoding, ) { #[allow(deprecated)] - list.push(lsp::SymbolInformation { - name: symbol.name, - kind: symbol.kind, - tags: symbol.tags, - deprecated: symbol.deprecated, - location: lsp::Location::new(file.uri.clone(), symbol.selection_range), - container_name: None, + list.push(SymbolInformationItem { + symbol: lsp::SymbolInformation { + name: symbol.name, + kind: symbol.kind, + tags: symbol.tags, + deprecated: symbol.deprecated, + location: lsp::Location::new(file.uri.clone(), symbol.selection_range), + container_name: None, + }, + offset_encoding, }); for child in symbol.children.into_iter().flatten() { - nested_to_flat(list, file, child); + nested_to_flat(list, file, child, offset_encoding); } } let doc = doc!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let current_url = doc.url(); - let offset_encoding = language_server.offset_encoding(); + let mut seen_language_servers = HashSet::new(); - let future = match language_server.document_symbols(doc.identifier()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support document symbols"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option| { - if let Some(symbols) = response { + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) + .filter(|ls| seen_language_servers.insert(ls.id())) + .map(|language_server| { + let request = language_server.document_symbols(doc.identifier()).unwrap(); + let offset_encoding = language_server.offset_encoding(); + let doc_id = doc.identifier(); + + async move { + let json = request.await?; + let response: Option = serde_json::from_value(json)?; + let symbols = match response { + Some(symbols) => symbols, + None => return anyhow::Ok(vec![]), + }; // lsp has two ways to represent symbols (flat/nested) // convert the nested variant to flat, so that we have a homogeneous list let symbols = match symbols { - lsp::DocumentSymbolResponse::Flat(symbols) => symbols, + lsp::DocumentSymbolResponse::Flat(symbols) => symbols + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(), lsp::DocumentSymbolResponse::Nested(symbols) => { - let doc = doc!(editor); let mut flat_symbols = Vec::new(); for symbol in symbols { - nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol) + nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) } flat_symbols } }; - - let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlaid(picker))) + Ok(symbols) } - }, - ) + }) + .collect(); + let current_url = doc.url(); + + if futures.is_empty() { + cx.editor + .set_error("No configured language server supports document symbols"); + return; + } + + cx.jobs.callback(async move { + let mut symbols = Vec::new(); + // TODO if one symbol request errors, all other requests are discarded (even if they're valid) + while let Some(mut lsp_items) = futures.try_next().await? { + symbols.append(&mut lsp_items); + } + let call = move |_editor: &mut Editor, compositor: &mut Compositor| { + let picker = sym_picker(symbols, current_url); + compositor.push(Box::new(overlaid(picker))) + }; + + Ok(Callback::EditorCompositor(Box::new(call))) + }); } pub fn workspace_symbol_picker(cx: &mut Context) { let doc = doc!(cx.editor); - let current_url = doc.url(); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - let future = match language_server.workspace_symbols("".to_string()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support workspace symbols"); - return; + if doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) + .count() + == 0 + { + cx.editor + .set_error("No configured language server supports workspace symbols"); + return; + } + + let get_symbols = move |pattern: String, editor: &mut Editor| { + let doc = doc!(editor); + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) + .filter(|ls| seen_language_servers.insert(ls.id())) + .map(|language_server| { + let request = language_server.workspace_symbols(pattern.clone()).unwrap(); + let offset_encoding = language_server.offset_encoding(); + async move { + let json = request.await?; + + let response = + serde_json::from_value::>>(json)? + .unwrap_or_default() + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(); + + anyhow::Ok(response) + } + }) + .collect(); + + if futures.is_empty() { + editor.set_error("No configured language server supports workspace symbols"); } - }; - cx.callback( - future, - move |_editor, compositor, response: Option>| { - let symbols = response.unwrap_or_default(); - let picker = sym_picker(symbols, current_url, offset_encoding); - let get_symbols = |query: String, editor: &mut Editor| { - let doc = doc!(editor); - let language_server = match doc.language_server() { - Some(s) => s, - None => { - // This should not generally happen since the picker will not - // even open in the first place if there is no server. - return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed(); - } - }; - let symbol_request = match language_server.workspace_symbols(query) { - Some(future) => future, - None => { - // This should also not happen since the language server must have - // supported workspace symbols before to reach this block. - return async move { - Err(anyhow::anyhow!( - "Language server does not support workspace symbols" - )) - } - .boxed(); - } - }; + async move { + let mut symbols = Vec::new(); + // TODO if one symbol request errors, all other requests are discarded (even if they're valid) + while let Some(mut lsp_items) = futures.try_next().await? { + symbols.append(&mut lsp_items); + } + anyhow::Ok(symbols) + } + .boxed() + }; - let future = async move { - let json = symbol_request.await?; - let response: Option> = - serde_json::from_value(json)?; + let current_url = doc.url(); + let initial_symbols = get_symbols("".to_owned(), cx.editor); - Ok(response.unwrap_or_default()) - }; - future.boxed() - }; + cx.jobs.callback(async move { + let symbols = initial_symbols.await?; + let call = move |_editor: &mut Editor, compositor: &mut Compositor| { + let picker = sym_picker(symbols, current_url); let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); compositor.push(Box::new(overlaid(dyn_picker))) - }, - ) + }; + + Ok(Callback::EditorCompositor(Box::new(call))) + }); } pub fn diagnostics_picker(cx: &mut Context) { let doc = doc!(cx.editor); - let language_server = language_server!(cx.editor, doc); if let Some(current_url) = doc.url() { - let offset_encoding = language_server.offset_encoding(); let diagnostics = cx .editor .diagnostics @@ -454,7 +519,6 @@ pub fn diagnostics_picker(cx: &mut Context) { [(current_url.clone(), diagnostics)].into(), Some(current_url), DiagnosticsFormat::HideSourcePath, - offset_encoding, ); cx.push_layer(Box::new(overlaid(picker))); } @@ -462,24 +526,27 @@ pub fn diagnostics_picker(cx: &mut Context) { pub fn workspace_diagnostics_picker(cx: &mut Context) { let doc = doc!(cx.editor); - let language_server = language_server!(cx.editor, doc); let current_url = doc.url(); - let offset_encoding = language_server.offset_encoding(); + // TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents let diagnostics = cx.editor.diagnostics.clone(); let picker = diag_picker( cx, diagnostics, current_url, DiagnosticsFormat::ShowSourcePath, - offset_encoding, ); cx.push_layer(Box::new(overlaid(picker))); } -impl ui::menu::Item for lsp::CodeActionOrCommand { +struct CodeActionOrCommandItem { + lsp_item: lsp::CodeActionOrCommand, + language_server_id: usize, +} + +impl ui::menu::Item for CodeActionOrCommandItem { type Data = (); fn format(&self, _data: &Self::Data) -> Row { - match self { + match &self.lsp_item { lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), } @@ -546,45 +613,42 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool { pub fn code_action(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let selection_range = doc.selection(view.id).primary(); - let offset_encoding = language_server.offset_encoding(); - let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding); - - let future = match language_server.code_actions( - doc.identifier(), - range, - // Filter and convert overlapping diagnostics - lsp::CodeActionContext { - diagnostics: doc - .diagnostics() - .iter() - .filter(|&diag| { - selection_range - .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end)) - }) - .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding)) - .collect(), - only: None, - trigger_kind: Some(CodeActionTriggerKind::INVOKED), - }, - ) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support code actions"); - return; - } - }; + let mut seen_language_servers = HashSet::new(); - cx.callback( - future, - move |editor, compositor, response: Option| { + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::CodeAction) + .filter(|ls| seen_language_servers.insert(ls.id())) + // TODO this should probably already been filtered in something like "language_servers_with_feature" + .filter_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let language_server_id = language_server.id(); + let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding); + // Filter and convert overlapping diagnostics + let code_action_context = lsp::CodeActionContext { + diagnostics: doc + .diagnostics() + .iter() + .filter(|&diag| { + selection_range + .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end)) + }) + .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding)) + .collect(), + only: None, + trigger_kind: Some(CodeActionTriggerKind::INVOKED), + }; + let code_action_request = + language_server.code_actions(doc.identifier(), range, code_action_context)?; + Some((code_action_request, language_server_id)) + }) + .map(|(request, ls_id)| async move { + let json = request.await?; + let response: Option = serde_json::from_value(json)?; let mut actions = match response { Some(a) => a, - None => return, + None => return anyhow::Ok(Vec::new()), }; // remove disabled code actions @@ -596,11 +660,6 @@ pub fn code_action(cx: &mut Context) { ) }); - if actions.is_empty() { - editor.set_status("No code actions available"); - return; - } - // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. // Many details are modeled after vscode because language servers are usually tested against it. // VScode sorts the codeaction two times: @@ -636,18 +695,51 @@ pub fn code_action(cx: &mut Context) { .reverse() }); - let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { + Ok(actions + .into_iter() + .map(|lsp_item| CodeActionOrCommandItem { + lsp_item, + language_server_id: ls_id, + }) + .collect()) + }) + .collect(); + + if futures.is_empty() { + cx.editor + .set_error("No configured language server supports code actions"); + return; + } + + cx.jobs.callback(async move { + let mut actions = Vec::new(); + // TODO if one code action request errors, all other requests are ignored (even if they're valid) + while let Some(mut lsp_items) = futures.try_next().await? { + actions.append(&mut lsp_items); + } + + let call = move |editor: &mut Editor, compositor: &mut Compositor| { + if actions.is_empty() { + editor.set_error("No code actions available"); + return; + } + let mut picker = ui::Menu::new(actions, (), move |editor, action, event| { if event != PromptEvent::Validate { return; } // always present here - let code_action = code_action.unwrap(); + let action = action.unwrap(); + let Some(language_server) = editor.language_server_by_id(action.language_server_id) else { + editor.set_error("Language Server disappeared"); + return; + }; + let offset_encoding = language_server.offset_encoding(); - match code_action { + match &action.lsp_item { lsp::CodeActionOrCommand::Command(command) => { log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); + execute_lsp_command(editor, action.language_server_id, command.clone()); } lsp::CodeActionOrCommand::CodeAction(code_action) => { log::debug!("code action: {:?}", code_action); @@ -659,7 +751,7 @@ pub fn code_action(cx: &mut Context) { // if code action provides both edit and command first the edit // should be applied and then the command if let Some(command) = &code_action.command { - execute_lsp_command(editor, command.clone()); + execute_lsp_command(editor, action.language_server_id, command.clone()); } } } @@ -668,8 +760,10 @@ pub fn code_action(cx: &mut Context) { let popup = Popup::new("code-action", picker).with_scrollbar(false); compositor.replace_or_push("code-action", popup); - }, - ) + }; + + Ok(Callback::EditorCompositor(Box::new(call))) + }); } impl ui::menu::Item for lsp::Command { @@ -679,13 +773,13 @@ impl ui::menu::Item for lsp::Command { } } -pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { - let doc = doc!(editor); - let language_server = language_server!(editor, doc); - +pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) { // the command is executed on the server and communicated back // to the client asynchronously using workspace edits - let future = match language_server.command(cmd) { + let future = match editor + .language_server_by_id(language_server_id) + .and_then(|language_server| language_server.command(cmd)) + { Some(future) => future, None => { editor.set_error("Language server does not support executing commands"); @@ -949,14 +1043,10 @@ fn goto_impl( editor.set_error("No definition found."); } _locations => { - let picker = FilePicker::new( - locations, - cwdir, - move |cx, location, action| { - jump_to_location(cx.editor, location, offset_encoding, action) - }, - move |_editor, location| Some(location_to_file_location(location)), - ); + let picker = Picker::new(locations, cwdir, move |cx, location, action| { + jump_to_location(cx.editor, location, offset_encoding, action) + }) + .with_preview(move |_editor, location| Some(location_to_file_location(location))); compositor.push(Box::new(overlaid(picker))); } } @@ -977,21 +1067,17 @@ fn to_locations(definitions: Option) -> Vec(cx: &mut Context, feature: LanguageServerFeature, request_provider: P) +where + P: Fn(&Client, lsp::Position, lsp::TextDocumentIdentifier) -> Option, + F: Future> + 'static + Send, +{ let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); + let language_server = language_server_with_feature!(cx.editor, doc, feature); + let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.goto_declaration(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-declaration"); - return; - } - }; + let future = request_provider(language_server, pos, doc.identifier()).unwrap(); cx.callback( future, @@ -1002,102 +1088,56 @@ pub fn goto_declaration(cx: &mut Context) { ); } -pub fn goto_definition(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.goto_definition(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-definition"); - return; - } - }; +pub fn goto_declaration(cx: &mut Context) { + goto_single_impl( + cx, + LanguageServerFeature::GotoDeclaration, + |ls, pos, doc_id| ls.goto_declaration(doc_id, pos, None), + ); +} - cx.callback( - future, - move |editor, compositor, response: Option| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, +pub fn goto_definition(cx: &mut Context) { + goto_single_impl( + cx, + LanguageServerFeature::GotoDefinition, + |ls, pos, doc_id| ls.goto_definition(doc_id, pos, None), ); } pub fn goto_type_definition(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.goto_type_definition(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-type-definition"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, + goto_single_impl( + cx, + LanguageServerFeature::GotoTypeDefinition, + |ls, pos, doc_id| ls.goto_type_definition(doc_id, pos, None), ); } pub fn goto_implementation(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.goto_implementation(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-implementation"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, + goto_single_impl( + cx, + LanguageServerFeature::GotoImplementation, + |ls, pos, doc_id| ls.goto_implementation(doc_id, pos, None), ); } pub fn goto_reference(cx: &mut Context) { let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); + // TODO could probably support multiple language servers, + // not sure if there's a real practical use case for this though + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference); + let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.goto_reference( - doc.identifier(), - pos, - config.lsp.goto_reference_include_declaration, - None, - ) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-reference"); - return; - } - }; + let future = language_server + .goto_reference( + doc.identifier(), + pos, + config.lsp.goto_reference_include_declaration, + None, + ) + .unwrap(); cx.callback( future, @@ -1108,7 +1148,7 @@ pub fn goto_reference(cx: &mut Context) { ); } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum SignatureHelpInvoked { Manual, Automatic, @@ -1120,35 +1160,31 @@ pub fn signature_help(cx: &mut Context) { pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { let (view, doc) = current!(cx.editor); - let was_manually_invoked = invoked == SignatureHelpInvoked::Manual; - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => { - // Do not show the message if signature help was invoked - // automatically on backspace, trigger characters, etc. - if was_manually_invoked { - cx.editor - .set_status("Language server not active for current buffer"); - } - return; - } - }; - let offset_encoding = language_server.offset_encoding(); - - let pos = doc.position(view.id, offset_encoding); + // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it + let future = doc + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .find_map(|language_server| { + let pos = doc.position(view.id, language_server.offset_encoding()); + language_server.text_document_signature_help(doc.identifier(), pos, None) + }); - let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) { - Some(f) => f, - None => { - if was_manually_invoked { - cx.editor - .set_error("Language server does not support signature-help"); - } - return; + let Some(future) = future else { + // Do not show the message if signature help was invoked + // automatically on backspace, trigger characters, etc. + if invoked == SignatureHelpInvoked::Manual { + cx.editor.set_error("No configured language server supports signature-help"); } + return; }; + signature_help_impl_with_future(cx, future.boxed(), invoked); +} +pub fn signature_help_impl_with_future( + cx: &mut Context, + future: BoxFuture<'static, helix_lsp::Result>, + invoked: SignatureHelpInvoked, +) { cx.callback( future, move |editor, compositor, response: Option| { @@ -1156,7 +1192,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { if !(config.lsp.auto_signature_help || SignatureHelp::visible_popup(compositor).is_some() - || was_manually_invoked) + || invoked == SignatureHelpInvoked::Manual) { return; } @@ -1165,7 +1201,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { // it very probably means the server was a little slow to respond and the user has // already moved on to something else, making a signature help popup will just be an // annoyance, see https://github.com/helix-editor/helix/issues/3112 - if !was_manually_invoked && editor.mode != Mode::Insert { + if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert { return; } @@ -1255,21 +1291,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); + // TODO support multiple language servers (merge UI somehow) + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover); // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier - - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.text_document_hover(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support hover"); - return; - } - }; + let pos = doc.position(view.id, language_server.offset_encoding()); + let future = language_server + .text_document_hover(doc.identifier(), pos, None) + .unwrap(); cx.callback( future, @@ -1349,7 +1379,11 @@ pub fn rename_symbol(cx: &mut Context) { } } - fn create_rename_prompt(editor: &Editor, prefill: String) -> Box { + fn create_rename_prompt( + editor: &Editor, + prefill: String, + language_server_id: Option, + ) -> Box { let prompt = ui::Prompt::new( "rename-to:".into(), None, @@ -1358,22 +1392,22 @@ pub fn rename_symbol(cx: &mut Context) { if event != PromptEvent::Validate { return; } - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); + let Some(language_server) = doc + .language_servers_with_feature(LanguageServerFeature::RenameSymbol) + .find(|ls| language_server_id.map_or(true, |id| id == ls.id())) + else { + cx.editor.set_error("No configured language server supports symbol renaming"); + return; + }; + + let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); + let future = language_server + .rename_symbol(doc.identifier(), pos, input.to_string()) + .unwrap(); - let future = - match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support symbol renaming"); - return; - } - }; match block_on(future) { Ok(edits) => { let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); @@ -1387,21 +1421,28 @@ pub fn rename_symbol(cx: &mut Context) { Box::new(prompt) } - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - - if !language_server.supports_rename() { - cx.editor - .set_error("Language server does not support symbol renaming"); - return; - } - - let pos = doc.position(view.id, offset_encoding); + let (view, doc) = current_ref!(cx.editor); + + let language_server_with_prepare_rename_support = doc + .language_servers_with_feature(LanguageServerFeature::RenameSymbol) + .find(|ls| { + matches!( + ls.capabilities().rename_provider, + Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + .. + })) + ) + }); - match language_server.prepare_rename(doc.identifier(), pos) { - // Language server supports textDocument/prepareRename, use it. - Some(future) => cx.callback( + if let Some(language_server) = language_server_with_prepare_rename_support { + let ls_id = language_server.id(); + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server + .prepare_rename(doc.identifier(), pos) + .unwrap(); + cx.callback( future, move |editor, compositor, response: Option| { let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response) @@ -1413,39 +1454,27 @@ pub fn rename_symbol(cx: &mut Context) { } }; - let prompt = create_rename_prompt(editor, prefill); + let prompt = create_rename_prompt(editor, prefill, Some(ls_id)); compositor.push(prompt); }, - ), - // Language server does not support textDocument/prepareRename, fall back - // to word boundary selection. - None => { - let prefill = get_prefill_from_word_boundary(cx.editor); - - let prompt = create_rename_prompt(cx.editor, prefill); - - cx.push_layer(prompt); - } - }; + ); + } else { + let prefill = get_prefill_from_word_boundary(cx.editor); + let prompt = create_rename_prompt(cx.editor, prefill, None); + cx.push_layer(prompt); + } } pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight); let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None) - { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support document highlight"); - return; - } - }; + let future = language_server + .text_document_document_highlight(doc.identifier(), pos, None) + .unwrap(); cx.callback( future, @@ -1455,10 +1484,8 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { _ => return, }; let (view, doc) = current!(editor); - let language_server = language_server!(editor, doc); - let offset_encoding = language_server.offset_encoding(); let text = doc.text(); - let pos = doc.selection(view.id).primary().head; + let pos = doc.selection(view.id).primary().cursor(text.slice(..)); // We must find the range that contains our primary cursor to prevent our primary cursor to move let mut primary_index = 0; @@ -1502,63 +1529,51 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let language_server = doc.language_server()?; - - let capabilities = language_server.capabilities(); - - let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider { - Some( - lsp::OneOf::Left(true) - | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)), - ) => { - let doc_text = doc.text(); - let len_lines = doc_text.len_lines(); - - // Compute ~3 times the current view height of inlay hints, that way some scrolling - // will not show half the view with hints and half without while still being faster - // than computing all the hints for the full file (which could be dozens of time - // longer than the view is). - let view_height = view.inner_height(); - let first_visible_line = - doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars())); - let first_line = first_visible_line.saturating_sub(view_height); - let last_line = first_visible_line - .saturating_add(view_height.saturating_mul(2)) - .min(len_lines); - - let new_doc_inlay_hint_id = DocumentInlayHintsId { - first_line, - last_line, - }; - // Don't recompute the annotations in case nothing has changed about the view - if !doc.inlay_hints_oudated - && doc - .inlay_hints(view_id) - .map_or(false, |dih| dih.id == new_doc_inlay_hint_id) - { - return None; - } + let language_server = doc + .language_servers_with_feature(LanguageServerFeature::InlayHints) + .next()?; + + let doc_text = doc.text(); + let len_lines = doc_text.len_lines(); + + // Compute ~3 times the current view height of inlay hints, that way some scrolling + // will not show half the view with hints and half without while still being faster + // than computing all the hints for the full file (which could be dozens of time + // longer than the view is). + let view_height = view.inner_height(); + let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars())); + let first_line = first_visible_line.saturating_sub(view_height); + let last_line = first_visible_line + .saturating_add(view_height.saturating_mul(2)) + .min(len_lines); + + let new_doc_inlay_hints_id = DocumentInlayHintsId { + first_line, + last_line, + }; + // Don't recompute the annotations in case nothing has changed about the view + if !doc.inlay_hints_oudated + && doc + .inlay_hints(view_id) + .map_or(false, |dih| dih.id == new_doc_inlay_hints_id) + { + return None; + } - let doc_slice = doc_text.slice(..); - let first_char_in_range = doc_slice.line_to_char(first_line); - let last_char_in_range = doc_slice.line_to_char(last_line); + let doc_slice = doc_text.slice(..); + let first_char_in_range = doc_slice.line_to_char(first_line); + let last_char_in_range = doc_slice.line_to_char(last_line); - let range = helix_lsp::util::range_to_lsp_range( - doc_text, - helix_core::Range::new(first_char_in_range, last_char_in_range), - language_server.offset_encoding(), - ); + let range = helix_lsp::util::range_to_lsp_range( + doc_text, + helix_core::Range::new(first_char_in_range, last_char_in_range), + language_server.offset_encoding(), + ); - ( - language_server.text_document_range_inlay_hints(doc.identifier(), range, None), - new_doc_inlay_hint_id, - ) - } - _ => return None, - }; + let offset_encoding = language_server.offset_encoding(); let callback = super::make_job_callback( - future?, + language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?, move |editor, _compositor, response: Option>| { // The config was modified or the window was closed while the request was in flight if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() { @@ -1572,8 +1587,8 @@ fn compute_inlay_hints_for_view( }; // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated - let (mut hints, offset_encoding) = match (response, doc.language_server()) { - (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()), + let mut hints = match response { + Some(hints) if !hints.is_empty() => hints, _ => { doc.set_inlay_hints( view_id, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d336f953e..d179da477 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -385,6 +385,36 @@ fn force_write( write_impl(cx, args.first(), true) } +fn write_buffer_close( + cx: &mut compositor::Context, + args: &[Cow], + 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], + 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( cx: &mut compositor::Context, _args: &[Cow], @@ -868,6 +898,25 @@ fn yank_main_selection_to_clipboard( yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) } +fn yank_joined( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + ensure!(args.len() <= 1, ":yank-join takes at most 1 argument"); + + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); + let register = cx.editor.selected_register.unwrap_or('"'); + yank_joined_impl(cx.editor, separator, register); + Ok(()) +} + fn yank_joined_to_clipboard( cx: &mut compositor::Context, args: &[Cow], @@ -1302,26 +1351,22 @@ fn lsp_workspace_command( if event != PromptEvent::Validate { return Ok(()); } - - let (_, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => { - cx.editor - .set_status("Language server not active for current buffer"); - return Ok(()); - } + let doc = doc!(cx.editor); + let Some((language_server_id, options)) = doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) + .find_map(|ls| { + ls.capabilities() + .execute_command_provider + .as_ref() + .map(|options| (ls.id(), options)) + }) + 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() { let commands = options .commands @@ -1335,8 +1380,8 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::Picker::new(commands, (), |cx, command, _action| { - execute_lsp_command(cx.editor, command.clone()); + let picker = ui::Picker::new(commands, (), move |cx, command, _action| { + execute_lsp_command(cx.editor, language_server_id, command.clone()); }); compositor.push(Box::new(overlaid(picker))) }, @@ -1349,6 +1394,7 @@ fn lsp_workspace_command( if options.commands.iter().any(|c| c == &command) { execute_lsp_command( cx.editor, + language_server_id, helix_lsp::lsp::Command { title: command.clone(), arguments: None, @@ -1380,7 +1426,6 @@ fn lsp_restart( .language_config() .context("LSP not defined for the current document")?; - let scope = config.scope.clone(); cx.editor.language_servers.restart( config, doc.path(), @@ -1393,13 +1438,22 @@ fn lsp_restart( .editor .documents() .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, }) .collect(); for document_id in document_ids_to_refresh { - cx.editor.refresh_language_server(document_id); + cx.editor.refresh_language_servers(document_id); } Ok(()) @@ -1414,22 +1468,18 @@ fn lsp_stop( return Ok(()); } - let doc = doc!(cx.editor); + let ls_shutdown_names = doc!(cx.editor) + .language_servers() + .map(|ls| ls.name().to_string()) + .collect::>(); - let ls_id = doc - .language_server() - .map(|ls| ls.id()) - .context("LSP not running for the current document")?; + for ls_name in &ls_shutdown_names { + cx.editor.language_servers.stop(ls_name); - let config = doc - .language_config() - .context("LSP not defined for the current document")?; - cx.editor.language_servers.stop(config); - - for doc in cx.editor.documents_mut() { - if doc.language_server().map_or(false, |ls| ls.id() == ls_id) { - doc.set_language_server(None); - doc.set_diagnostics(Default::default()); + for doc in cx.editor.documents_mut() { + if let Some(client) = doc.remove_language_server_by_name(ls_name) { + doc.clear_diagnostics(client.id()); + } } } @@ -1737,7 +1787,7 @@ fn set_option( *value = if value.is_string() { // JSON strings require quotes, so we can't .parse() directly - serde_json::Value::String(arg.to_string()) + Value::String(arg.to_string()) } else { arg.parse().map_err(field_error)? }; @@ -1762,8 +1812,8 @@ fn toggle_option( return Ok(()); } - if args.len() != 1 { - anyhow::bail!("Bad arguments. Usage: `:toggle key`"); + if args.is_empty() { + anyhow::bail!("Bad arguments. Usage: `:toggle key [values]?`"); } let key = &args[0].to_lowercase(); @@ -1773,22 +1823,43 @@ fn toggle_option( let pointer = format!("/{}", key.replace('.', "/")); let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; - let Value::Bool(old_value) = *value else { - anyhow::bail!("Key `{}` is not toggle-able", key) + *value = match value { + Value::Bool(ref value) => { + ensure!( + args.len() == 1, + "Bad arguments. For boolean configurations use: `:toggle key`" + ); + Value::Bool(!value) + } + Value::String(ref value) => { + ensure!( + args.len() > 2, + "Bad arguments. For string configurations use: `:toggle key val1 val2 ...`", + ); + + Value::String( + args[1..] + .iter() + .skip_while(|e| *e != value) + .nth(1) + .unwrap_or_else(|| &args[1]) + .to_string(), + ) + } + Value::Null | Value::Object(_) | Value::Array(_) | Value::Number(_) => { + anyhow::bail!("Configuration {key} does not support toggle yet") + } }; - let new_value = !old_value; - *value = Value::Bool(new_value); - // This unwrap should never fail because we only replace one boolean value - // with another, maintaining a valid json config - let config = serde_json::from_value(config).unwrap(); + let status = format!("'{key}' is now set to {value}"); + let config = serde_json::from_value(config) + .map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?; cx.editor .config_events .0 .send(ConfigEvent::Update(config))?; - cx.editor - .set_status(format!("Option `{}` is now set to `{}`", key, new_value)); + cx.editor.set_status(status); Ok(()) } @@ -1823,7 +1894,7 @@ fn language( doc.detect_indent_and_line_ending(); let id = doc.id(); - cx.editor.refresh_language_server(id); + cx.editor.refresh_language_servers(id); Ok(()) } @@ -2327,10 +2398,24 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "write!", 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, 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 { name: "new", aliases: &["n"], @@ -2448,6 +2533,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: theme, signature: CommandSignature::positional(&[completers::theme]), }, + TypableCommand { + name: "yank-join", + aliases: &[], + doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.", + fun: yank_joined, + signature: CommandSignature::none(), + }, TypableCommand { name: "clipboard-yank", aliases: &[], @@ -2555,14 +2647,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ }, TypableCommand { name: "reload", - aliases: &[], + aliases: &["rl"], doc: "Discard changes and reload from the source file.", fun: reload, signature: CommandSignature::none(), }, TypableCommand { name: "reload-all", - aliases: &[], + aliases: &["rla"], doc: "Discard changes and reload all documents from the source files.", fun: reload_all, signature: CommandSignature::none(), @@ -2584,14 +2676,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "lsp-restart", 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, signature: CommandSignature::none(), }, TypableCommand { name: "lsp-stop", 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, signature: CommandSignature::none(), }, @@ -2859,13 +2951,10 @@ pub(super) fn command_mode(cx: &mut Context) { } else { // Otherwise, use the command's completer and the last shellword // 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) } else { - ( - words.last().unwrap(), - shellwords.parts().last().unwrap().len(), - ) + (words.last().unwrap(), words.last().unwrap().len()) }; let argument_number = argument_number_of(&shellwords); @@ -2874,13 +2963,13 @@ pub(super) fn command_mode(cx: &mut Context) { .get(&words[0] as &str) .map(|tc| tc.completer_for_argument_number(argument_number)) { - completer(editor, part) + completer(editor, word) .into_iter() .map(|(range, file)| { let file = shellwords::escape(file); // offset ranges to input - let offset = input.len() - part_len; + let offset = input.len() - word_len; let range = (range.start + offset)..; (range, file) }) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index ee15b1c77..75f857139 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,5 +1,5 @@ use crate::keymap; -use crate::keymap::{merge_keys, Keymap}; +use crate::keymap::{merge_keys, KeyTrie, Keymaps}; use helix_loader::merge_toml_values; use helix_view::document::Mode; use serde::Deserialize; @@ -12,7 +12,7 @@ use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq)] pub struct Config { pub theme: Option, - pub keys: HashMap, + pub keys: HashMap, pub editor: helix_view::editor::Config, } @@ -20,7 +20,7 @@ pub struct Config { #[serde(deny_unknown_fields)] pub struct ConfigRaw { pub theme: Option, - pub keys: Option>, + pub keys: Option>, pub editor: Option, } @@ -59,7 +59,7 @@ impl Config { pub fn load( global: Result, local: Result, - engine_overlay: Option>, + engine_overlay: Option>, ) -> Result { let global_config: Result = global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); @@ -152,7 +152,6 @@ mod tests { #[test] fn parsing_keymaps_config_file() { use crate::keymap; - use crate::keymap::Keymap; use helix_core::hashmap; use helix_view::document::Mode; @@ -169,13 +168,13 @@ mod tests { merge_keys( &mut keys, hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" + Mode::Insert => keymap!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" + }), + Mode::Normal => keymap!({ "Normal mode" "A-F12" => move_next_word_end, - })), + }), }, ); diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 480c2c675..8f9218777 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> { for lang in &syn_loader_conf.language { column(&lang.language_id, Color::Reset); - let lsp = lang - .language_server - .as_ref() - .map(|lsp| lsp.command.to_string()); + // TODO multiple language servers (check binary for each supported language server, not just the first) + + let lsp = lang.language_servers.first().and_then(|ls| { + syn_loader_conf + .language_server + .get(&ls.name) + .map(|config| config.command.clone()) + }); check_binary(lsp); 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( "language server", - lang.language_server - .as_ref() - .map(|lsp| lsp.command.to_string()), + lang.language_servers.first().and_then(|ls| { + syn_loader_conf + .language_server + .get(&ls.name) + .map(|config| config.command.clone()) + }), )?; probe_protocol( diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 3033c6a48..5a72a35a5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -18,7 +18,7 @@ use std::{ pub use default::default; use macros::key; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" name: String, @@ -52,10 +52,6 @@ impl KeyTrieNode { } } - pub fn name(&self) -> &str { - &self.name - } - /// Merge another Node in. Leaves and subnodes from the other node replace /// corresponding keyevent in self, except when both other and self have /// subnodes for same key. In that case the merge is recursive. @@ -77,49 +73,40 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); + let mut body: Vec<(BTreeSet, &str)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { - KeyTrie::Leaf(cmd) => { + KeyTrie::MappableCommand(cmd) => { if cmd.name() == "no_op" { continue; } cmd.doc() } - KeyTrie::Node(n) => n.name(), + KeyTrie::Node(n) => &n.name, KeyTrie::Sequence(_) => "[Multiple commands]", }; - match body.iter().position(|(d, _)| d == &desc) { + match body.iter().position(|(_, d)| d == &desc) { Some(pos) => { - body[pos].1.insert(key); + body[pos].0.insert(key); } - None => body.push((desc, BTreeSet::from([key]))), + None => body.push((BTreeSet::from([key]), desc)), } } - body.sort_unstable_by_key(|(_, keys)| { + body.sort_unstable_by_key(|(keys, _)| { self.order .iter() .position(|&k| k == *keys.iter().next().unwrap()) .unwrap() }); - let prefix = format!("{} ", self.name()); - if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { - body = body - .into_iter() - .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) - .collect(); - } - Info::from_keymap(self.name(), body) - } - /// Get a reference to the key trie node's order. - pub fn order(&self) -> &[KeyEvent] { - self.order.as_slice() - } -} -impl Default for KeyTrieNode { - fn default() -> Self { - Self::new("", HashMap::new(), Vec::new()) + let body: Vec<_> = body + .into_iter() + .map(|(events, desc)| { + let events = events.iter().map(ToString::to_string).collect::>(); + (events.join(", "), desc) + }) + .collect(); + Info::new(&self.name, &body) } } @@ -145,7 +132,7 @@ impl DerefMut for KeyTrieNode { #[derive(Debug, Clone, PartialEq)] pub enum KeyTrie { - Leaf(MappableCommand), + MappableCommand(MappableCommand), Sequence(Vec), Node(KeyTrieNode), } @@ -174,7 +161,7 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { { command .parse::() - .map(KeyTrie::Leaf) + .map(KeyTrie::MappableCommand) .map_err(E::custom) } @@ -208,17 +195,43 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { } impl KeyTrie { + pub fn reverse_map(&self) -> ReverseKeymap { + // recursively visit all nodes in keymap + fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec) { + match node { + KeyTrie::MappableCommand(cmd) => { + let name = cmd.name(); + if name != "no_op" { + cmd_map.entry(name.into()).or_default().push(keys.clone()) + } + } + KeyTrie::Node(next) => { + for (key, trie) in &next.map { + keys.push(*key); + map_node(cmd_map, trie, keys); + keys.pop(); + } + } + KeyTrie::Sequence(_) => {} + }; + } + + let mut res = HashMap::new(); + map_node(&mut res, self, &mut Vec::new()); + res + } + pub fn node(&self) -> Option<&KeyTrieNode> { match *self { KeyTrie::Node(ref node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, } } pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { match *self { KeyTrie::Node(ref mut node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, } } @@ -235,7 +248,7 @@ impl KeyTrie { trie = match trie { KeyTrie::Node(map) => map.get(key), // leaf encountered while keys left to process - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, + KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None, }? } Some(trie) @@ -256,75 +269,11 @@ pub enum KeymapResult { Cancelled(Vec), } -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(transparent)] -pub struct Keymap { - /// Always a Node - root: KeyTrie, -} - /// A map of command names to keybinds that will execute the command. pub type ReverseKeymap = HashMap>>; -impl Keymap { - pub fn new(root: KeyTrie) -> Self { - Keymap { root } - } - - pub fn reverse_map(&self) -> ReverseKeymap { - // recursively visit all nodes in keymap - fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec) { - match node { - KeyTrie::Leaf(cmd) => match cmd { - MappableCommand::Typable { name, .. } => { - cmd_map.entry(name.into()).or_default().push(keys.clone()) - } - MappableCommand::Static { name, .. } => cmd_map - .entry(name.to_string()) - .or_default() - .push(keys.clone()), - }, - KeyTrie::Node(next) => { - for (key, trie) in &next.map { - keys.push(*key); - map_node(cmd_map, trie, keys); - keys.pop(); - } - } - KeyTrie::Sequence(_) => {} - }; - } - - let mut res = HashMap::new(); - map_node(&mut res, &self.root, &mut Vec::new()); - res - } - - pub fn root(&self) -> &KeyTrie { - &self.root - } - - pub fn merge(&mut self, other: Self) { - self.root.merge_nodes(other.root); - } -} - -impl Deref for Keymap { - type Target = KeyTrieNode; - - fn deref(&self) -> &Self::Target { - self.root.node().unwrap() - } -} - -impl Default for Keymap { - fn default() -> Self { - Self::new(KeyTrie::Node(KeyTrieNode::default())) - } -} - pub struct Keymaps { - pub map: Box>>, + pub map: Box>>, /// Stores pending keys waiting for the next key. This is relative to a /// sticky node if one is in use. state: Vec, @@ -333,7 +282,7 @@ pub struct Keymaps { } impl Keymaps { - pub fn new(map: Box>>) -> Self { + pub fn new(map: Box>>) -> Self { Self { map, state: Vec::new(), @@ -341,7 +290,7 @@ impl Keymaps { } } - pub fn map(&self) -> DynGuard> { + pub fn map(&self) -> DynGuard> { self.map.load() } @@ -373,11 +322,11 @@ impl Keymaps { let first = self.state.get(0).unwrap_or(&key); let trie_node = match self.sticky { Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), - None => Cow::Borrowed(&keymap.root), + None => Cow::Borrowed(keymap), }; let trie = match trie_node.search(&[*first]) { - Some(KeyTrie::Leaf(ref cmd)) => { + Some(KeyTrie::MappableCommand(ref cmd)) => { return KeymapResult::Matched(cmd.clone()); } Some(KeyTrie::Sequence(ref cmds)) => { @@ -396,7 +345,7 @@ impl Keymaps { } KeymapResult::Pending(map.clone()) } - Some(KeyTrie::Leaf(cmd)) => { + Some(KeyTrie::MappableCommand(cmd)) => { self.state.clear(); KeymapResult::Matched(cmd.clone()) } @@ -416,9 +365,13 @@ impl Default for Keymaps { } /// Merge default config keys with user overwritten keys for custom user config. -pub fn merge_keys(dst: &mut HashMap, mut delta: HashMap) { +pub fn merge_keys(dst: &mut HashMap, mut delta: HashMap) { for (mode, keys) in dst { - keys.merge(delta.remove(mode).unwrap_or_default()) + keys.merge_nodes( + delta + .remove(mode) + .unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())), + ) } } @@ -447,8 +400,7 @@ mod tests { #[test] fn merge_partial_keys() { let keymap = hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" + Mode::Normal => keymap!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, @@ -457,7 +409,6 @@ mod tests { "g" => delete_char_forward, }, }) - ) }; let mut merged_keyamp = default(); merge_keys(&mut merged_keyamp, keymap.clone()); @@ -484,32 +435,45 @@ mod tests { let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Assumes that `g` is a node in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_line_end), + keymap.search(&[key!('g'), key!('$')]).unwrap(), + &KeyTrie::MappableCommand(MappableCommand::goto_line_end), "Leaf should be present in merged subnode" ); // Assumes that `gg` is in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::delete_char_forward), + keymap.search(&[key!('g'), key!('g')]).unwrap(), + &KeyTrie::MappableCommand(MappableCommand::delete_char_forward), "Leaf should replace old leaf in merged subnode" ); // Assumes that `ge` is in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_last_line), + keymap.search(&[key!('g'), key!('e')]).unwrap(), + &KeyTrie::MappableCommand(MappableCommand::goto_last_line), "Old leaves in subnode should be present in merged node" ); - assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0); + assert!( + merged_keyamp + .get(&Mode::Normal) + .and_then(|key_trie| key_trie.node()) + .unwrap() + .len() + > 1 + ); + assert!( + merged_keyamp + .get(&Mode::Insert) + .and_then(|key_trie| key_trie.node()) + .unwrap() + .len() + > 0 + ); } #[test] fn order_should_be_set() { let keymap = hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" + Mode::Normal => keymap!({ "Normal mode" "space" => { "" "s" => { "" "v" => vsplit, @@ -517,7 +481,6 @@ mod tests { }, }, }) - ) }; let mut merged_keyamp = default(); merge_keys(&mut merged_keyamp, keymap.clone()); @@ -525,22 +488,19 @@ mod tests { let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( - keymap - .root() - .search(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - &KeyTrie::Leaf(MappableCommand::vsplit), + keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(), + &KeyTrie::MappableCommand(MappableCommand::vsplit), "Leaf should be present in merged subnode" ); // Make sure an order was set during merge - let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order().is_empty()) + let node = keymap.search(&[crate::key!(' ')]).unwrap(); + assert!(!node.node().unwrap().order.as_slice().is_empty()) } #[test] fn aliased_modes_are_same_in_default_keymap() { let keymaps = Keymaps::default().map(); - let root = keymaps.get(&Mode::Normal).unwrap().root(); + let root = keymaps.get(&Mode::Normal).unwrap(); assert_eq!( root.search(&[key!(' '), key!('w')]).unwrap(), root.search(&["C-w".parse::().unwrap()]).unwrap(), @@ -563,7 +523,7 @@ mod tests { }, "j" | "k" => move_line_down, }); - let keymap = Keymap::new(normal_mode); + let keymap = normal_mode; let mut reverse_map = keymap.reverse_map(); // sort keybindings in order to have consistent tests @@ -611,7 +571,7 @@ mod tests { modifiers: KeyModifiers::NONE, }; - let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new( + let expectation = KeyTrie::Node(KeyTrieNode::new( "", hashmap! { key => KeyTrie::Sequence(vec!{ @@ -628,7 +588,7 @@ mod tests { }) }, vec![key], - ))); + )); assert_eq!(toml::from_str(keys), Ok(expectation)); } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 9bd002809..c84c616c6 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; use super::macros::keymap; -use super::{Keymap, Mode}; +use super::{KeyTrie, Mode}; use helix_core::hashmap; -pub fn default() -> HashMap { +pub fn default() -> HashMap { let normal = keymap!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_visual_line_down, @@ -79,6 +79,7 @@ pub fn default() -> HashMap { "s" => select_regex, "A-s" => split_selection_on_newline, + "A-minus" => merge_selections, "A-_" => merge_consecutive_selections, "S" => split_selection, ";" => collapse_selection, @@ -379,8 +380,8 @@ pub fn default() -> HashMap { "end" => goto_line_end_newline, }); hashmap!( - Mode::Normal => Keymap::new(normal), - Mode::Select => Keymap::new(select), - Mode::Insert => Keymap::new(insert), + Mode::Normal => normal, + Mode::Select => select, + Mode::Insert => insert, ) } diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index c4a1bfbb3..15d2aa53b 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -62,12 +62,11 @@ macro_rules! alt { }; } -/// Macro for defining the root of a `Keymap` object. Example: +/// Macro for defining a `KeyTrie`. Example: /// /// ``` /// # use helix_core::hashmap; /// # use helix_term::keymap; -/// # use helix_term::keymap::Keymap; /// let normal_mode = keymap!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" @@ -76,12 +75,12 @@ macro_rules! alt { /// }, /// "j" | "down" => move_line_down, /// }); -/// let keymap = Keymap::new(normal_mode); +/// let keymap = normal_mode; /// ``` #[macro_export] macro_rules! keymap { (@trie $cmd:ident) => { - $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) + $crate::keymap::KeyTrie::MappableCommand($crate::commands::MappableCommand::$cmd) }; (@trie diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 9d06e283d..b2d9f7bef 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -68,7 +68,7 @@ FLAGS: -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml -c, --config Specifies a file to use for configuration -v Increases logging verbosity each use for up to 3 times - --log Specifies a file to use for logging + --log Specifies a file to use for logging (default file: {}) -V, --version Prints version information --vsplit Splits all given files vertically into different windows diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index bc216509f..1ebcd192c 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,8 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor}; use crate::commands; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; -use helix_lsp::{lsp, util}; -use lsp::CompletionItem; +use helix_lsp::{lsp, util, OffsetEncoding}; impl menu::Item for CompletionItem { type Data = (); @@ -26,28 +25,30 @@ impl menu::Item for CompletionItem { #[inline] fn filter_text(&self, _data: &Self::Data) -> Cow { - self.filter_text + self.item + .filter_text .as_ref() - .unwrap_or(&self.label) + .unwrap_or(&self.item.label) .as_str() .into() } fn format(&self, _data: &Self::Data) -> menu::Row { - let deprecated = self.deprecated.unwrap_or_default() - || self.tags.as_ref().map_or(false, |tags| { + let deprecated = self.item.deprecated.unwrap_or_default() + || self.item.tags.as_ref().map_or(false, |tags| { tags.contains(&lsp::CompletionItemTag::DEPRECATED) }); + menu::Row::new(vec![ menu::Cell::from(Span::styled( - self.label.as_str(), + self.item.label.as_str(), if deprecated { Style::default().add_modifier(Modifier::CROSSED_OUT) } else { Style::default() }, )), - menu::Cell::from(match self.kind { + menu::Cell::from(match self.item.kind { Some(lsp::CompletionItemKind::TEXT) => "text", Some(lsp::CompletionItemKind::METHOD) => "method", Some(lsp::CompletionItemKind::FUNCTION) => "function", @@ -79,15 +80,17 @@ impl menu::Item for CompletionItem { } 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. pub struct Completion { popup: Popup>, @@ -104,21 +107,21 @@ impl Completion { editor: &Editor, savepoint: Arc, mut items: Vec, - offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, ) -> Self { + let preview_completion_insert = editor.config().preview_completion_insert; let replace_mode = editor.config().completion_replace; // 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 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, view_id: ViewId, - item: &CompletionItem, - offset_encoding: helix_lsp::OffsetEncoding, + item: &lsp::CompletionItem, + offset_encoding: OffsetEncoding, trigger_offset: usize, include_placeholder: bool, replace_mode: bool, @@ -209,77 +212,108 @@ impl Completion { let (view, doc) = current!(editor); - // if more text was entered, remove it - doc.restore(view, &savepoint); + macro_rules! language_server { + ($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 { - PromptEvent::Abort => { - editor.last_completion = None; - } - PromptEvent::Update => { + PromptEvent::Abort => {} + PromptEvent::Update if preview_completion_insert => { + // 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 let item = item.unwrap(); let transaction = item_to_transaction( doc, view.id, - item, - offset_encoding, + &item.item, + language_server!(item).offset_encoding(), trigger_offset, true, replace_mode, ); - - // initialize a savepoint - doc.apply(&transaction, view.id); - - editor.last_completion = Some(CompleteAction { - trigger_offset, - changes: completion_changes(&transaction, trigger_offset), - }); + doc.apply_temporary(&transaction, view.id); } + PromptEvent::Update => {} PromptEvent::Validate => { + if let Some(CompleteAction::Selected { savepoint }) = + editor.last_completion.take() + { + doc.restore(view, &savepoint, false); + } // 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( doc, view.id, - item, + &item.item, offset_encoding, trigger_offset, false, replace_mode, ); - doc.apply(&transaction, view.id); - editor.last_completion = Some(CompleteAction { + editor.last_completion = Some(CompleteAction::Applied { trigger_offset, changes: completion_changes(&transaction, trigger_offset), }); - // apply additional edits, mostly used to auto import unqualified types - let resolved_item = if item - .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()) - { + // TODO: add additional _edits to completion_changes? + if let Some(additional_edits) = item.item.additional_text_edits { if !additional_edits.is_empty() { let transaction = util::generate_transaction_from_edits( doc.text(), - additional_edits.clone(), + additional_edits, offset_encoding, // TODO: should probably transcode in Client ); doc.apply(&transaction, view.id); @@ -304,11 +338,9 @@ impl Completion { } fn resolve_completion_item( - doc: &Document, + language_server: &helix_lsp::Client, completion_item: lsp::CompletionItem, - ) -> Option { - let language_server = doc.language_server()?; - + ) -> Option { let future = language_server.resolve_completion_item(completion_item)?; let response = helix_lsp::block_on(future); match response { @@ -359,7 +391,7 @@ impl Completion { 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); } @@ -375,20 +407,14 @@ impl Completion { // > 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 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, }; - let language_server = match doc!(cx.editor).language_server() { - Some(language_server) => language_server, - None => return false, - }; + let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; }; // This method should not block the compositor so we handle the response asynchronously. - let future = match language_server.resolve_completion_item(current_item.clone()) { - Some(future) => future, - None => return false, - }; + let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; }; cx.callback( future, @@ -403,6 +429,12 @@ impl Completion { .unwrap() .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); } }, @@ -457,25 +489,25 @@ impl Component for Completion { 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::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::PlainText, value: contents, })) => { // 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 { kind: lsp::MarkupKind::Markdown, value: contents, })) => { // 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 - markdowned(language, option.detail.as_deref(), None) + markdowned(language, option.item.detail.as_deref(), None) } None => return, }; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4093827af..afbe46c55 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -19,7 +19,7 @@ use helix_core::{ syntax::{self, HighlightEvent}, text_annotations::TextAnnotations, unicode::width::UnicodeWidthStr, - visual_offset_from_block, Position, Range, Selection, Transaction, + visual_offset_from_block, Change, Position, Range, Selection, Transaction, }; use helix_view::{ 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 super::statusline; +use super::{completion::CompletionItem, statusline}; use super::{document::LineDecoration, lsp::SignatureHelp}; pub struct EditorView { @@ -48,7 +48,10 @@ pub struct EditorView { #[derive(Debug, Clone)] pub enum InsertEvent { Key(KeyEvent), - CompletionApply(CompleteAction), + CompletionApply { + trigger_offset: usize, + changes: Vec, + }, TriggerCompletion, RequestCompletion, } @@ -103,7 +106,7 @@ impl EditorView { // Set DAP highlights, if needed. 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 line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { if pos.doc_line != dap_line { @@ -498,7 +501,9 @@ impl EditorView { use helix_core::match_brackets; let pos = doc.selection(view.id).primary().cursor(text); - if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) { + if let Some(pos) = + match_brackets::find_matching_bracket(syntax, doc.text().slice(..), pos) + { // ensure col is on screen if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") { return vec![(highlight, pos..pos + 1)]; @@ -647,7 +652,7 @@ impl EditorView { .primary() .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 }); @@ -825,7 +830,7 @@ impl EditorView { } (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion - self.completion = None; + self.clear_completion(cxt.editor); cxt.editor.completion_request_handle = None; // TODO: Use an on_mode_change hook to remove signature help @@ -917,22 +922,26 @@ impl EditorView { for key in self.last_insert.1.clone() { match key { InsertEvent::Key(key) => self.insert_mode(cxt, key), - InsertEvent::CompletionApply(compl) => { + InsertEvent::CompletionApply { + trigger_offset, + changes, + } => { let (view, doc) = current!(cxt.editor); 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 cursor = doc.selection(view.id).primary().cursor(text); - let shift_position = - |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; + let shift_position = |pos: usize| -> usize { + (pos + cursor).saturating_sub(trigger_offset) + }; let tx = Transaction::change( doc.text(), - compl.changes.iter().cloned().map(|(start, end, t)| { + changes.iter().cloned().map(|(start, end, t)| { (shift_position(start), shift_position(end), t) }), ); @@ -963,6 +972,8 @@ impl EditorView { self.handle_keymap_event(mode, cxt, event); if self.keymaps.pending().is_empty() { cxt.editor.count = None + } else { + cxt.editor.selected_register = cxt.register.take(); } } } @@ -973,20 +984,13 @@ impl EditorView { &mut self, editor: &mut Editor, savepoint: Arc, - items: Vec, - offset_encoding: helix_lsp::OffsetEncoding, + items: Vec, start_offset: usize, trigger_offset: usize, size: Rect, ) -> Option { - let mut completion = Completion::new( - editor, - savepoint, - items, - offset_encoding, - start_offset, - trigger_offset, - ); + let mut completion = + Completion::new(editor, savepoint, items, start_offset, trigger_offset); if completion.is_empty() { // skip if we got no completion results @@ -1005,6 +1009,21 @@ impl EditorView { pub fn clear_completion(&mut self, editor: &mut Editor) { 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 editor.clear_idle_timer(); // don't retrigger @@ -1291,12 +1310,22 @@ impl Component for EditorView { jobs: cx.jobs, scroll: None, }; - completion.handle_event(event, &mut cx) - }; - if let EventResult::Consumed(callback) = res { - consumed = true; + if let EventResult::Consumed(callback) = + 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() { // assume close_fn self.clear_completion(cx.editor); @@ -1312,10 +1341,6 @@ impl Component for EditorView { // if completion didn't take the event, we pass it onto commands 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); // record last_insert key diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index fea3de78f..def64434a 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -51,7 +51,7 @@ pub fn highlighted_code_block<'a>( language.into(), )) .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 { Some(s) => s, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b06..155f24356 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -17,11 +17,11 @@ mod text; use crate::compositor::{Component, Compositor}; use crate::filter_picker_entry; use crate::job::{self, Callback}; -pub use completion::Completion; +pub use completion::{Completion, CompletionItem}; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; +pub use picker::{DynamicPicker, FileLocation, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -158,7 +158,7 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -217,27 +217,24 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - FilePicker::new( - files, - root, - move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }, - |_editor, path| Some((path.clone().into(), None)), - ) + Picker::new(files, root, move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }) + .with_preview(|_editor, path| Some((path.clone().into(), None))) } pub mod completers { use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; + use helix_core::syntax::LanguageServerFeature; use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::theme; use helix_view::{editor::Config, Editor}; @@ -393,20 +390,11 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { let matcher = Matcher::default(); - let (_, doc) = current_ref!(editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => { - return vec![]; - } - }; - - let options = match &language_server.capabilities().execute_command_provider { - Some(options) => options, - None => { - return vec![]; - } + let Some(options) = doc!(editor) + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) + .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) + else { + return vec![]; }; let mut matches: Vec<_> = options diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e7a7de909..13746cfc8 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,7 +1,9 @@ use crate::{ alt, - compositor::{Component, Compositor, Context, Event, EventResult}, - ctrl, key, shift, + compositor::{self, Component, Compositor, Context, Event, EventResult}, + ctrl, + job::Callback, + key, shift, ui::{ self, document::{render_document, LineDecoration, LinePos, TextRenderer}, @@ -9,7 +11,7 @@ use crate::{ EditorView, }, }; -use futures_util::future::BoxFuture; +use futures_util::{future::BoxFuture, FutureExt}; use tui::{ buffer::Buffer as Surface, layout::Constraint, @@ -26,7 +28,7 @@ use std::{collections::HashMap, io::Read, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; use helix_core::{ movement::Direction, text_annotations::TextAnnotations, - unicode::segmentation::UnicodeSegmentation, Position, + unicode::segmentation::UnicodeSegmentation, Position, Syntax, }; use helix_view::{ editor::Action, @@ -75,16 +77,6 @@ type FileCallback = Box Option>; /// File path and range of lines (used to align and highlight lines) pub type FileLocation = (PathOrId, Option<(usize, usize)>); -pub struct FilePicker { - picker: Picker, - pub truncate_start: bool, - /// Caches paths to documents - preview_cache: HashMap, - read_buffer: Vec, - /// Given an item in the picker, return the file path and line number to display. - file_fn: FileCallback, -} - pub enum CachedPreview { Document(Box), Binary, @@ -122,286 +114,6 @@ impl Preview<'_, '_> { } } -impl FilePicker { - pub fn new( - options: Vec, - editor_data: T::Data, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, - preview_fn: impl Fn(&Editor, &T) -> Option + 'static, - ) -> Self { - let truncate_start = true; - let mut picker = Picker::new(options, editor_data, callback_fn); - picker.truncate_start = truncate_start; - - Self { - picker, - truncate_start, - preview_cache: HashMap::new(), - read_buffer: Vec::with_capacity(1024), - file_fn: Box::new(preview_fn), - } - } - - pub fn truncate_start(mut self, truncate_start: bool) -> Self { - self.truncate_start = truncate_start; - self.picker.truncate_start = truncate_start; - self - } - - fn current_file(&self, editor: &Editor) -> Option { - self.picker - .selection() - .and_then(|current| (self.file_fn)(editor, current)) - .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line))) - } - - /// Get (cached) preview for a given path. If a document corresponding - /// to the path is already open in the editor, it is used instead. - fn get_preview<'picker, 'editor>( - &'picker mut self, - path_or_id: PathOrId, - editor: &'editor Editor, - ) -> Preview<'picker, 'editor> { - match path_or_id { - PathOrId::Path(path) => { - let path = &path; - if let Some(doc) = editor.document_by_path(path) { - return Preview::EditorDocument(doc); - } - - if self.preview_cache.contains_key(path) { - return Preview::Cached(&self.preview_cache[path]); - } - - let data = std::fs::File::open(path).and_then(|file| { - let metadata = file.metadata()?; - // Read up to 1kb to detect the content type - let n = file.take(1024).read_to_end(&mut self.read_buffer)?; - let content_type = content_inspector::inspect(&self.read_buffer[..n]); - self.read_buffer.clear(); - Ok((metadata, content_type)) - }); - let preview = data - .map( - |(metadata, content_type)| match (metadata.len(), content_type) { - (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, - (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => { - CachedPreview::LargeFile - } - _ => { - // TODO: enable syntax highlighting; blocked by async rendering - Document::open(path, None, None, editor.config.clone()) - .map(|doc| CachedPreview::Document(Box::new(doc))) - .unwrap_or(CachedPreview::NotFound) - } - }, - ) - .unwrap_or(CachedPreview::NotFound); - self.preview_cache.insert(path.to_owned(), preview); - Preview::Cached(&self.preview_cache[path]) - } - PathOrId::Id(id) => { - let doc = editor.documents.get(&id).unwrap(); - Preview::EditorDocument(doc) - } - } - } - - fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult { - // Try to find a document in the cache - let doc = self - .current_file(cx.editor) - .and_then(|(path, _range)| match path { - PathOrId::Id(doc_id) => Some(doc_mut!(cx.editor, &doc_id)), - PathOrId::Path(path) => match self.preview_cache.get_mut(&path) { - Some(CachedPreview::Document(doc)) => Some(doc), - _ => None, - }, - }); - - // Then attempt to highlight it if it has no language set - if let Some(doc) = doc { - if doc.language_config().is_none() { - let loader = cx.editor.syn_loader.clone(); - doc.detect_language(loader); - } - - // 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) - } -} - -impl Component for FilePicker { - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - // +---------+ +---------+ - // |prompt | |preview | - // +---------+ | | - // |picker | | | - // | | | | - // +---------+ +---------+ - - let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; - // -- Render the frame: - // clear area - let background = cx.editor.theme.get("ui.background"); - let text = cx.editor.theme.get("ui.text"); - surface.clear_with(area, background); - - let picker_width = if render_preview { - area.width / 2 - } else { - area.width - }; - - let picker_area = area.with_width(picker_width); - self.picker.render(picker_area, surface, cx); - - if !render_preview { - return; - } - - let preview_area = area.clip_left(picker_width); - - // don't like this but the lifetime sucks - let block = Block::default().borders(Borders::ALL); - - // calculate the inner area inside the box - let inner = block.inner(preview_area); - // 1 column gap on either side - let margin = Margin::horizontal(1); - let inner = inner.inner(&margin); - block.render(preview_area, surface); - - if let Some((path, range)) = self.current_file(cx.editor) { - let preview = self.get_preview(path, cx.editor); - let doc = match preview.document() { - Some(doc) => doc, - None => { - let alt_text = preview.placeholder(); - let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; - let y = inner.y + inner.height / 2; - surface.set_stringn(x, y, alt_text, inner.width as usize, text); - return; - } - }; - - // align to middle - let first_line = range - .map(|(start, end)| { - let height = end.saturating_sub(start) + 1; - let middle = start + (height.saturating_sub(1) / 2); - middle.saturating_sub(inner.height as usize / 2).min(start) - }) - .unwrap_or(0); - - let offset = ViewPosition { - anchor: doc.text().line_to_char(first_line), - horizontal_offset: 0, - vertical_offset: 0, - }; - - let mut highlights = EditorView::doc_syntax_highlights( - doc, - offset.anchor, - area.height, - &cx.editor.theme, - ); - for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) { - if spans.is_empty() { - continue; - } - highlights = Box::new(helix_core::syntax::merge(highlights, spans)); - } - let mut decorations: Vec> = Vec::new(); - - if let Some((start, end)) = range { - let style = cx - .editor - .theme - .try_get("ui.highlight") - .unwrap_or_else(|| cx.editor.theme.get("ui.selection")); - let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| { - if (start..=end).contains(&pos.doc_line) { - let area = Rect::new( - renderer.viewport.x, - renderer.viewport.y + pos.visual_line, - renderer.viewport.width, - 1, - ); - renderer.surface.set_style(area, style) - } - }; - decorations.push(Box::new(draw_highlight)) - } - - render_document( - surface, - inner, - doc, - offset, - // TODO: compute text annotations asynchronously here (like inlay hints) - &TextAnnotations::default(), - highlights, - &cx.editor.theme, - &mut decorations, - &mut [], - ); - } - } - - fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { - if let Event::IdleTimeout = event { - return self.handle_idle_timeout(ctx); - } - // TODO: keybinds for scrolling preview - self.picker.handle_event(event, ctx) - } - - fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { - self.picker.cursor(area, ctx) - } - - fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { - let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW { - width / 2 - } else { - width - }; - self.picker.required_size((picker_width, height))?; - Some((width, height)) - } -} - -#[derive(PartialEq, Eq, Debug)] -struct PickerMatch { - score: i64, - index: usize, - len: usize, -} - -impl PickerMatch { - fn key(&self) -> impl Ord { - (cmp::Reverse(self.score), self.len, self.index) - } -} - -impl PartialOrd for PickerMatch { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PickerMatch { - fn cmp(&self, other: &Self) -> Ordering { - self.key().cmp(&other.key()) - } -} - -type PickerCallback = Box; - pub struct Picker { options: Vec, editor_data: T::Data, @@ -416,17 +128,22 @@ pub struct Picker { // pattern: String, prompt: Prompt, previous_pattern: (String, FuzzyQuery), - /// Whether to truncate the start (default true) - pub truncate_start: bool, /// Whether to show the preview panel (default true) show_preview: bool, /// Constraints for tabular formatting widths: Vec, callback_fn: PickerCallback, + + pub truncate_start: bool, + /// Caches paths to documents + preview_cache: HashMap, + read_buffer: Vec, + /// Given an item in the picker, return the file path and line number to display. + file_fn: Option>, } -impl Picker { +impl Picker { pub fn new( options: Vec, editor_data: T::Data, @@ -452,6 +169,9 @@ impl Picker { callback_fn: Box::new(callback_fn), completion_height: 0, widths: Vec::new(), + preview_cache: HashMap::new(), + read_buffer: Vec::with_capacity(1024), + file_fn: None, }; picker.calculate_column_widths(); @@ -472,6 +192,19 @@ impl Picker { picker } + pub fn truncate_start(mut self, truncate_start: bool) -> Self { + self.truncate_start = truncate_start; + self + } + + pub fn with_preview( + mut self, + preview_fn: impl Fn(&Editor, &T) -> Option + 'static, + ) -> Self { + self.file_fn = Some(Box::new(preview_fn)); + self + } + pub fn set_options(&mut self, new_options: Vec) { self.options = new_options; self.cursor = 0; @@ -638,92 +371,127 @@ impl Picker { } EventResult::Consumed(None) } -} - -// process: -// - read all the files into a list, maxed out at a large value -// - on input change: -// - score all the names in relation to input -impl Component for Picker { - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - self.completion_height = viewport.1.saturating_sub(4); - Some(viewport) + fn current_file(&self, editor: &Editor) -> Option { + self.selection() + .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) + .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line))) } - fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let key_event = match event { - Event::Key(event) => *event, - Event::Paste(..) => return self.prompt_handle_event(event, cx), - Event::Resize(..) => return EventResult::Consumed(None), - _ => return EventResult::Ignored(None), - }; - - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| { - // remove the layer - compositor.last_picker = compositor.pop(); - }))); - - // So that idle timeout retriggers - cx.editor.reset_idle_timer(); - - match key_event { - shift!(Tab) | key!(Up) | ctrl!('p') => { - self.move_by(1, Direction::Backward); - } - key!(Tab) | key!(Down) | ctrl!('n') => { - self.move_by(1, Direction::Forward); - } - key!(PageDown) | ctrl!('d') => { - self.page_down(); - } - key!(PageUp) | ctrl!('u') => { - self.page_up(); - } - key!(Home) => { - self.to_start(); - } - key!(End) => { - self.to_end(); - } - key!(Esc) | ctrl!('c') => { - return close_fn; - } - alt!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Load); - } - } - key!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Replace); - } - return close_fn; - } - ctrl!('s') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::HorizontalSplit); + /// Get (cached) preview for a given path. If a document corresponding + /// to the path is already open in the editor, it is used instead. + fn get_preview<'picker, 'editor>( + &'picker mut self, + path_or_id: PathOrId, + editor: &'editor Editor, + ) -> Preview<'picker, 'editor> { + match path_or_id { + PathOrId::Path(path) => { + let path = &path; + if let Some(doc) = editor.document_by_path(path) { + return Preview::EditorDocument(doc); } - return close_fn; - } - ctrl!('v') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::VerticalSplit); + + if self.preview_cache.contains_key(path) { + return Preview::Cached(&self.preview_cache[path]); } - return close_fn; + + let data = std::fs::File::open(path).and_then(|file| { + let metadata = file.metadata()?; + // Read up to 1kb to detect the content type + let n = file.take(1024).read_to_end(&mut self.read_buffer)?; + let content_type = content_inspector::inspect(&self.read_buffer[..n]); + self.read_buffer.clear(); + Ok((metadata, content_type)) + }); + let preview = data + .map( + |(metadata, content_type)| match (metadata.len(), content_type) { + (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, + (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => { + CachedPreview::LargeFile + } + _ => { + // TODO: enable syntax highlighting; blocked by async rendering + Document::open(path, None, None, editor.config.clone()) + .map(|doc| CachedPreview::Document(Box::new(doc))) + .unwrap_or(CachedPreview::NotFound) + } + }, + ) + .unwrap_or(CachedPreview::NotFound); + self.preview_cache.insert(path.to_owned(), preview); + Preview::Cached(&self.preview_cache[path]) } - ctrl!('t') => { - self.toggle_preview(); + PathOrId::Id(id) => { + let doc = editor.documents.get(&id).unwrap(); + Preview::EditorDocument(doc) } - _ => { - self.prompt_handle_event(event, cx); + } + } + + 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 + let doc = match ¤t_file { + PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id), + PathOrId::Path(path) => match self.preview_cache.get_mut(path) { + Some(CachedPreview::Document(ref mut doc)) => doc, + _ => return EventResult::Consumed(None), + }, + }; + + let mut callback: Option = None; + + // Then attempt to highlight it if it has no language set + 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 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::>() 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)) } } - 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) } - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let text_style = cx.editor.theme.get("ui.text"); let selected = cx.editor.theme.get("ui.text.focus"); let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); @@ -889,6 +657,205 @@ impl Component for Picker { ); } + fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // -- Render the frame: + // clear area + let background = cx.editor.theme.get("ui.background"); + let text = cx.editor.theme.get("ui.text"); + surface.clear_with(area, background); + + // don't like this but the lifetime sucks + let block = Block::default().borders(Borders::ALL); + + // calculate the inner area inside the box + let inner = block.inner(area); + // 1 column gap on either side + let margin = Margin::horizontal(1); + let inner = inner.inner(&margin); + block.render(area, surface); + + if let Some((path, range)) = self.current_file(cx.editor) { + let preview = self.get_preview(path, cx.editor); + let doc = match preview.document() { + Some(doc) => doc, + None => { + let alt_text = preview.placeholder(); + let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; + let y = inner.y + inner.height / 2; + surface.set_stringn(x, y, alt_text, inner.width as usize, text); + return; + } + }; + + // align to middle + let first_line = range + .map(|(start, end)| { + let height = end.saturating_sub(start) + 1; + let middle = start + (height.saturating_sub(1) / 2); + middle.saturating_sub(inner.height as usize / 2).min(start) + }) + .unwrap_or(0); + + let offset = ViewPosition { + anchor: doc.text().line_to_char(first_line), + horizontal_offset: 0, + vertical_offset: 0, + }; + + let mut highlights = EditorView::doc_syntax_highlights( + doc, + offset.anchor, + area.height, + &cx.editor.theme, + ); + for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) { + if spans.is_empty() { + continue; + } + highlights = Box::new(helix_core::syntax::merge(highlights, spans)); + } + let mut decorations: Vec> = Vec::new(); + + if let Some((start, end)) = range { + let style = cx + .editor + .theme + .try_get("ui.highlight") + .unwrap_or_else(|| cx.editor.theme.get("ui.selection")); + let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| { + if (start..=end).contains(&pos.doc_line) { + let area = Rect::new( + renderer.viewport.x, + renderer.viewport.y + pos.visual_line, + renderer.viewport.width, + 1, + ); + renderer.surface.set_style(area, style) + } + }; + decorations.push(Box::new(draw_highlight)) + } + + render_document( + surface, + inner, + doc, + offset, + // TODO: compute text annotations asynchronously here (like inlay hints) + &TextAnnotations::default(), + highlights, + &cx.editor.theme, + &mut decorations, + &mut [], + ); + } + } +} + +impl Component for Picker { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // +---------+ +---------+ + // |prompt | |preview | + // +---------+ | | + // |picker | | | + // | | | | + // +---------+ +---------+ + + let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; + + let picker_width = if render_preview { + area.width / 2 + } else { + area.width + }; + + let picker_area = area.with_width(picker_width); + self.render_picker(picker_area, surface, cx); + + if render_preview { + let preview_area = area.clip_left(picker_width); + self.render_preview(preview_area, surface, cx); + } + } + + fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { + if let Event::IdleTimeout = event { + return self.handle_idle_timeout(ctx); + } + // TODO: keybinds for scrolling preview + + let key_event = match event { + Event::Key(event) => *event, + Event::Paste(..) => return self.prompt_handle_event(event, ctx), + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored(None), + }; + + let close_fn = + EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| { + // remove the layer + compositor.last_picker = compositor.pop(); + }))); + + // So that idle timeout retriggers + ctx.editor.reset_idle_timer(); + + match key_event { + shift!(Tab) | key!(Up) | ctrl!('p') => { + self.move_by(1, Direction::Backward); + } + key!(Tab) | key!(Down) | ctrl!('n') => { + self.move_by(1, Direction::Forward); + } + key!(PageDown) | ctrl!('d') => { + self.page_down(); + } + key!(PageUp) | ctrl!('u') => { + self.page_up(); + } + key!(Home) => { + self.to_start(); + } + key!(End) => { + self.to_end(); + } + key!(Esc) | ctrl!('c') => { + return close_fn; + } + alt!(Enter) => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::Load); + } + } + key!(Enter) => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::Replace); + } + return close_fn; + } + ctrl!('s') => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::HorizontalSplit); + } + return close_fn; + } + ctrl!('v') => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::VerticalSplit); + } + return close_fn; + } + ctrl!('t') => { + self.toggle_preview(); + } + _ => { + self.prompt_handle_event(event, ctx); + } + } + + EventResult::Consumed(None) + } + fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { let block = Block::default().borders(Borders::ALL); // calculate the inner area inside the box @@ -899,8 +866,40 @@ impl Component for Picker { self.prompt.cursor(area, editor) } + + fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { + self.completion_height = height.saturating_sub(4); + Some((width, height)) + } +} + +#[derive(PartialEq, Eq, Debug)] +struct PickerMatch { + score: i64, + index: usize, + len: usize, +} + +impl PickerMatch { + fn key(&self) -> impl Ord { + (cmp::Reverse(self.score), self.len, self.index) + } +} + +impl PartialOrd for PickerMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PickerMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.key().cmp(&other.key()) + } } +type PickerCallback = Box; + /// Returns a new list of options to replace the contents of the picker /// when called with the current picker query, pub type DynQueryCallback = @@ -909,7 +908,7 @@ pub type DynQueryCallback = /// A picker that updates its contents via a callback whenever the /// query string changes. Useful for live grep, workspace symbols, etc. pub struct DynamicPicker { - file_picker: FilePicker, + file_picker: Picker, query_callback: DynQueryCallback, query: String, } @@ -917,7 +916,7 @@ pub struct DynamicPicker { impl DynamicPicker { pub const ID: &'static str = "dynamic-picker"; - pub fn new(file_picker: FilePicker, query_callback: DynQueryCallback) -> Self { + pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { Self { file_picker, query_callback, @@ -933,7 +932,7 @@ impl Component for DynamicPicker { fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let event_result = self.file_picker.handle_event(event, cx); - let current_query = self.file_picker.picker.prompt.line(); + let current_query = self.file_picker.prompt.line(); if !matches!(event, Event::IdleTimeout) || self.query == *current_query { return event_result; @@ -945,17 +944,16 @@ impl Component for DynamicPicker { cx.jobs.callback(async move { let new_options = new_options.await?; - let callback = - crate::job::Callback::EditorCompositor(Box::new(move |editor, compositor| { - // Wrapping of pickers in overlay is done outside the picker code, - // so this is fragile and will break if wrapped in some other widget. - let picker = match compositor.find_id::>>(Self::ID) { - Some(overlay) => &mut overlay.content.file_picker.picker, - None => return, - }; - picker.set_options(new_options); - editor.reset_idle_timer(); - })); + let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { + // Wrapping of pickers in overlay is done outside the picker code, + // so this is fragile and will break if wrapped in some other widget. + let picker = match compositor.find_id::>>(Self::ID) { + Some(overlay) => &mut overlay.content.file_picker, + None => return, + }; + picker.set_options(new_options); + editor.reset_idle_timer(); + })); anyhow::Ok(callback) }); EventResult::Consumed(None) diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 784f746c3..29625f36c 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -164,6 +164,7 @@ where helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::Custom => render_custom_text, + helix_view::editor::StatusLineElement::Register => render_register, } } @@ -201,15 +202,15 @@ where ); } +// TODO think about handling multiple language servers fn render_lsp_spinner(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option