mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
338 Commits
string-lsp
...
inlay-hint
Author | SHA1 | Date | |
---|---|---|---|
|
cbea11947d | ||
|
a2c580c4ae | ||
|
cf7eb5517f | ||
|
388a3b78e3 | ||
|
d43de14807 | ||
|
04d1180a0c | ||
|
5adb4b7413 | ||
|
13b2dc31f5 | ||
|
0ee5850016 | ||
|
8ff544757f | ||
|
f07c1c1b29 | ||
|
8ad6e53b1f | ||
|
6bedca8064 | ||
|
7e7a98560e | ||
|
6da1a79d80 | ||
|
2cc33b5c47 | ||
|
683fac65e7 | ||
|
2d4c2a170c | ||
|
14cab4ba62 | ||
|
3a63e85b6a | ||
|
f6cb90593d | ||
|
1c9a5bd366 | ||
|
1dee64f7ec | ||
|
b7d735ffe6 | ||
|
7ca916ab73 | ||
|
8e65077065 | ||
|
d6cacb2731 | ||
|
ccf9564123 | ||
|
e7c82a34a5 | ||
|
33c17d48ff | ||
|
6f463dbeb3 | ||
|
70a60efcbe | ||
|
9d31e4df11 | ||
|
27ca9d2c33 | ||
|
e56d3abb0a | ||
|
9574e551cf | ||
|
44bddf51b7 | ||
|
b47c9da3a1 | ||
|
fdaf12a35d | ||
|
0d84bd563c | ||
|
1bd7a3901c | ||
|
694b61514f | ||
|
7f416704b1 | ||
|
430ce9c46b | ||
|
d1e0891260 | ||
|
e74956fa4d | ||
|
8d590e8aee | ||
|
63ed85bc62 | ||
|
9bd3cecd49 | ||
|
8df58b2e17 | ||
|
f9360fb27e | ||
|
88a254d8bf | ||
|
c5c9e65cc4 | ||
|
9db6c534a3 | ||
|
ff558f9105 | ||
|
b38eae1f98 | ||
|
67879a1e5b | ||
|
aa20eb8e7f | ||
|
ee9db440ce | ||
|
296eb9be83 | ||
|
dc4761ad3a | ||
|
2d3b75a8c5 | ||
|
8da226f0b4 | ||
|
fab08c0981 | ||
|
b6e58c0fa4 | ||
|
19558839b7 | ||
|
c4d314d7ba | ||
|
b423ed42f1 | ||
|
a3fa65880e | ||
|
b1ee4ab5c6 | ||
|
fbc0f956b3 | ||
|
486f4297b7 | ||
|
28e69f09fc | ||
|
ab56f9e26b | ||
|
1d453785e5 | ||
|
671a6036b3 | ||
|
82f8ac208f | ||
|
9440feae7c | ||
|
1a28999002 | ||
|
0efa8207d8 | ||
|
e1c7a1ed77 | ||
|
7bebe0a70e | ||
|
682967d328 | ||
|
1e8774a030 | ||
|
c36408457a | ||
|
1dd8a19ad6 | ||
|
43eab10a4c | ||
|
83d4ca41cc | ||
|
534d0907d3 | ||
|
bb3af143f1 | ||
|
26cb3c20e7 | ||
|
69f25a85da | ||
|
8cb0d869e6 | ||
|
c98302a543 | ||
|
0ba2e05a6f | ||
|
e1060a2785 | ||
|
fcddd50325 | ||
|
35575b0b0f | ||
|
7e87a36e93 | ||
|
6182bdc860 | ||
|
e1c26ebfc7 | ||
|
3683cd9ea3 | ||
|
0deb8bbce6 | ||
|
3d7e2730e7 | ||
|
6304e7b2a7 | ||
|
d031260180 | ||
|
e0da129727 | ||
|
b8912adbbf | ||
|
1c0b36b1b4 | ||
|
e35d420199 | ||
|
48194825b9 | ||
|
82f07fe6d1 | ||
|
1c47aec30c | ||
|
ef375d690e | ||
|
0445062d27 | ||
|
46728046fd | ||
|
7275b7f850 | ||
|
ed3bc2b294 | ||
|
3ccf8d58de | ||
|
efb44e0b22 | ||
|
144a4f402f | ||
|
df752bbd45 | ||
|
d8c4c7c26f | ||
|
c3e9a0d607 | ||
|
258e3e1136 | ||
|
5a66270c00 | ||
|
6aa82bb3f8 | ||
|
518d054fcb | ||
|
35faa73be1 | ||
|
7a3470c48d | ||
|
199dc05a04 | ||
|
5e2501da30 | ||
|
a03becf021 | ||
|
a6f94e04e6 | ||
|
2197b3cfa0 | ||
|
a19c95a0a7 | ||
|
ff012e844f | ||
|
fcfa70e66c | ||
|
1b89f998e8 | ||
|
c36ed6ad92 | ||
|
18b9eb9e06 | ||
|
b0e1eaf50d | ||
|
a36730cb21 | ||
|
75abc23428 | ||
|
313a6479b1 | ||
|
62625eda46 | ||
|
16ff06370f | ||
|
ee33a84489 | ||
|
1258111394 | ||
|
26db54155e | ||
|
d456377821 | ||
|
d0d16931e3 | ||
|
8995ccaae2 | ||
|
066e938ba0 | ||
|
e882a750ea | ||
|
ebdab86ce6 | ||
|
ab6a92ed49 | ||
|
e22bbf5489 | ||
|
0ab403d428 | ||
|
b8bfc44e42 | ||
|
5952d564d1 | ||
|
382401020c | ||
|
93fa990e56 | ||
|
70d452db3e | ||
|
369f2bb93d | ||
|
0f594c35f2 | ||
|
de11273857 | ||
|
5c1f3f814f | ||
|
4e0fc0efc6 | ||
|
1ab35ade2d | ||
|
018081a5b1 | ||
|
f0fa905622 | ||
|
5532ef35d9 | ||
|
0ea401d2d7 | ||
|
e70f8833e2 | ||
|
30616344d7 | ||
|
17ffa38a5a | ||
|
c3620b7116 | ||
|
e9c16b7fc5 | ||
|
8439ce5683 | ||
|
6edff24c81 | ||
|
47f84d04ea | ||
|
2367b20318 | ||
|
28047fed7f | ||
|
025719c1d8 | ||
|
80dbe030a1 | ||
|
6906164177 | ||
|
d285a8a9e5 | ||
|
1a821ac726 | ||
|
f7394d53fd | ||
|
ba116b47a0 | ||
|
c9dc940428 | ||
|
8328c422b7 | ||
|
6049f2035b | ||
|
8d6efaf350 | ||
|
98ddbf0086 | ||
|
0c8f0c0334 | ||
|
fec5101a41 | ||
|
9bc63c1c59 | ||
|
20151a5594 | ||
|
51832b02c9 | ||
|
39b72329b4 | ||
|
0364521dca | ||
|
f5f9f499cf | ||
|
b00b475dfe | ||
|
9829ac0c02 | ||
|
7c907e66f4 | ||
|
259be07f05 | ||
|
360c6bb061 | ||
|
4919058e90 | ||
|
23b424a46d | ||
|
aac0ce5fd1 | ||
|
899afad4a6 | ||
|
3fdd98979c | ||
|
de738bac6a | ||
|
81708b70e6 | ||
|
8bf9adf7b6 | ||
|
9088f8a599 | ||
|
a63a2ad281 | ||
|
4ded712dbd | ||
|
151caeacc6 | ||
|
d4ade40983 | ||
|
0b9701e899 | ||
|
9d6ea773e9 | ||
|
6b044aeb29 | ||
|
8af33108f6 | ||
|
1afa63d457 | ||
|
5f62c5c24c | ||
|
fa27ae16a7 | ||
|
8986f8b953 | ||
|
650af50c13 | ||
|
c1d382a532 | ||
|
168b11e091 | ||
|
f70923c448 | ||
|
8f1585a097 | ||
|
0d5f6f04c9 | ||
|
122bbea7cf | ||
|
088ba58af5 | ||
|
ce348d84f6 | ||
|
def6139abd | ||
|
cf7b36f0bf | ||
|
c81e0136c5 | ||
|
dca235c5c8 | ||
|
fcf981bbd7 | ||
|
d123193902 | ||
|
430414979d | ||
|
6593969f8d | ||
|
cb0f201d0e | ||
|
032dadaf37 | ||
|
7dea2b0ddd | ||
|
76a8682c4d | ||
|
060255344c | ||
|
09b2f6ab5f | ||
|
64aca8b350 | ||
|
ccdb710431 | ||
|
ba4793fca0 | ||
|
e7ac2fcdec | ||
|
7cc93eb1c5 | ||
|
2c09a35ccf | ||
|
954c97f2b5 | ||
|
076d8bd173 | ||
|
343397391f | ||
|
69068770c8 | ||
|
4c41c5250c | ||
|
ffdfb59033 | ||
|
d4ee22b483 | ||
|
29dda1403f | ||
|
19f7bc9ecb | ||
|
3318953bf6 | ||
|
4bd17e542e | ||
|
99d33c741a | ||
|
ca19496eed | ||
|
f69659c5be | ||
|
27bb2447db | ||
|
3d772afc8b | ||
|
60bff8feee | ||
|
134aebf8cc | ||
|
367ccc1c64 | ||
|
e01775a667 | ||
|
0f2ce303c5 | ||
|
b05971f178 | ||
|
a539199666 | ||
|
e440e54e79 | ||
|
8f5f818c88 | ||
|
b26903cd13 | ||
|
9721144e03 | ||
|
a83c23bb03 | ||
|
931dd9c1dc | ||
|
917174e546 | ||
|
a0bd39d40e | ||
|
e698b20245 | ||
|
5616f1d66d | ||
|
217818681e | ||
|
03f35af9c1 | ||
|
6c9619d094 | ||
|
e856dde738 | ||
|
f80ae997f2 | ||
|
64b38d1a28 | ||
|
377e36908a | ||
|
fa4aa0fb42 | ||
|
2b8f8df1af | ||
|
eed052e86b | ||
|
0654a1f058 | ||
|
353176082e | ||
|
b47b946c47 | ||
|
827deba74c | ||
|
dabfb6ceea | ||
|
9dcc1f06b0 | ||
|
eaa7c8e9f6 | ||
|
4817bfa003 | ||
|
e0bccd2c58 | ||
|
38e8382b01 | ||
|
c9cc14728f | ||
|
b1759f998d | ||
|
4a59f68a0d | ||
|
2b4a77b9bf | ||
|
41763b4851 | ||
|
bbeb99dc40 | ||
|
9cc056e755 | ||
|
6d07ae4f07 | ||
|
073efe48f9 | ||
|
127567df8e | ||
|
4b288e4de7 | ||
|
19c91dfb80 | ||
|
a5a7cff311 | ||
|
7b9b9329b9 | ||
|
c262fe41ab | ||
|
a074129f9c | ||
|
eda96cc285 | ||
|
c63616b421 | ||
|
faf0eee3c9 | ||
|
a2b241eb08 | ||
|
0fd4a4aae2 | ||
|
9e77c44b38 | ||
|
ea17b9edb7 | ||
|
4f63a46e14 | ||
|
ac4c017165 | ||
|
b946b21b01 |
3
.envrc
3
.envrc
@@ -1,7 +1,8 @@
|
||||
watch_file shell.nix
|
||||
watch_file default.nix
|
||||
watch_file flake.lock
|
||||
watch_file rust-toolchain.toml
|
||||
|
||||
# try to use flakes, if it fails use normal nix (ie. shell.nix)
|
||||
use flake || use nix
|
||||
eval "$shellHook"
|
||||
eval "$shellHook"
|
||||
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -8,9 +8,6 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
tree-sitter:
|
||||
patterns:
|
||||
- "tree-sitter*"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
|
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -10,6 +10,8 @@ on:
|
||||
|
||||
env:
|
||||
MSRV: "1.76"
|
||||
# This key can be changed to bust the cache of tree-sitter grammars.
|
||||
GRAMMAR_CACHE_VERSION: ""
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -29,6 +31,13 @@ jobs:
|
||||
with:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache tree-sitter grammars
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
|
||||
|
||||
- name: Run cargo check
|
||||
run: cargo check
|
||||
|
||||
@@ -52,12 +61,12 @@ jobs:
|
||||
with:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache test tree-sitter grammar
|
||||
- name: Cache tree-sitter grammars
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
restore-keys: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-
|
||||
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
|
||||
|
||||
- name: Run cargo test
|
||||
run: cargo test --workspace
|
||||
@@ -87,6 +96,13 @@ jobs:
|
||||
with:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache tree-sitter grammars
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
|
||||
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --all --check
|
||||
|
||||
@@ -115,6 +131,13 @@ jobs:
|
||||
with:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache tree-sitter grammars
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
|
||||
|
||||
- name: Validate queries
|
||||
run: cargo xtask query-check
|
||||
|
||||
|
4
.github/workflows/cachix.yml
vendored
4
.github/workflows/cachix.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v30
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Authenticate with Cachix
|
||||
uses: cachix/cachix-action@v15
|
||||
uses: cachix/cachix-action@v16
|
||||
with:
|
||||
name: helix
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Github Pages
|
||||
name: GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
|
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
@@ -61,17 +61,17 @@ jobs:
|
||||
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
|
||||
include:
|
||||
- build: x86_64-linux
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
- build: aarch64-linux
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: aarch64-unknown-linux-gnu
|
||||
cross: true
|
||||
# - build: riscv64-linux
|
||||
# os: ubuntu-latest
|
||||
# os: ubuntu-22.04
|
||||
# rust: stable
|
||||
# target: riscv64gc-unknown-linux-gnu
|
||||
# cross: true
|
||||
@@ -147,16 +147,8 @@ jobs:
|
||||
if: "!matrix.skip_tests"
|
||||
run: ${{ env.CARGO }} test --release --locked --target ${{ matrix.target }} --workspace
|
||||
|
||||
- name: Set profile.release.strip = true
|
||||
shell: bash
|
||||
run: |
|
||||
cat >> .cargo/config.toml <<EOF
|
||||
[profile.release]
|
||||
strip = true
|
||||
EOF
|
||||
|
||||
- name: Build release binary
|
||||
run: ${{ env.CARGO }} build --release --locked --target ${{ matrix.target }}
|
||||
run: ${{ env.CARGO }} build --profile opt --locked --target ${{ matrix.target }}
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
@@ -183,7 +175,7 @@ jobs:
|
||||
|
||||
mkdir -p "$APP.AppDir"/usr/{bin,lib/helix}
|
||||
|
||||
cp "target/${{ matrix.target }}/release/hx" "$APP.AppDir/usr/bin/hx"
|
||||
cp "target/${{ matrix.target }}/opt/hx" "$APP.AppDir/usr/bin/hx"
|
||||
rm -rf runtime/grammars/sources
|
||||
cp -r runtime "$APP.AppDir/usr/lib/helix/runtime"
|
||||
|
||||
@@ -206,14 +198,25 @@ jobs:
|
||||
mv "$APP-$VERSION-$ARCH.AppImage" \
|
||||
"$APP-$VERSION-$ARCH.AppImage.zsync" dist
|
||||
|
||||
- name: Build Deb
|
||||
shell: bash
|
||||
if: matrix.build == 'x86_64-linux'
|
||||
run: |
|
||||
cargo install cargo-deb
|
||||
mkdir -p target/release
|
||||
cp target/${{ matrix.target }}/opt/hx target/release/
|
||||
cargo deb --no-build
|
||||
mkdir -p dist
|
||||
mv target/debian/*.deb dist/
|
||||
|
||||
- name: Build archive
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
if [ "${{ matrix.os }}" = "windows-2019" ]; then
|
||||
cp "target/${{ matrix.target }}/release/hx.exe" "dist/"
|
||||
cp "target/${{ matrix.target }}/opt/hx.exe" "dist/"
|
||||
else
|
||||
cp "target/${{ matrix.target }}/release/hx" "dist/"
|
||||
cp "target/${{ matrix.target }}/opt/hx" "dist/"
|
||||
fi
|
||||
if [ -d runtime/grammars/sources ]; then
|
||||
rm -rf runtime/grammars/sources
|
||||
@@ -241,6 +244,7 @@ jobs:
|
||||
set -ex
|
||||
|
||||
source="$(pwd)"
|
||||
tag=${GITHUB_REF_NAME//\//}
|
||||
mkdir -p runtime/grammars/sources
|
||||
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
|
||||
rm -rf grammars
|
||||
@@ -254,7 +258,7 @@ jobs:
|
||||
if [[ $platform =~ "windows" ]]; then
|
||||
exe=".exe"
|
||||
fi
|
||||
pkgname=helix-$GITHUB_REF_NAME-$platform
|
||||
pkgname=helix-$tag-$platform
|
||||
mkdir -p $pkgname
|
||||
cp $source/LICENSE $source/README.md $pkgname
|
||||
mkdir $pkgname/contrib
|
||||
@@ -265,6 +269,7 @@ jobs:
|
||||
|
||||
if [[ "$platform" = "x86_64-linux" ]]; then
|
||||
mv bins-$platform/helix-*.AppImage* dist/
|
||||
mv bins-$platform/*.deb dist/
|
||||
fi
|
||||
|
||||
if [ "$exe" = "" ]; then
|
||||
@@ -274,7 +279,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
tar cJf dist/helix-$GITHUB_REF_NAME-source.tar.xz -C $source .
|
||||
tar cJf dist/helix-$tag-source.tar.xz -C $source .
|
||||
mv dist $source/
|
||||
|
||||
- name: Upload binaries to release
|
||||
|
309
CHANGELOG.md
309
CHANGELOG.md
@@ -1,3 +1,302 @@
|
||||
<!--
|
||||
# YY.0M (YYYY-0M-0D)
|
||||
|
||||
Breaking changes:
|
||||
|
||||
Features:
|
||||
|
||||
Commands:
|
||||
|
||||
Usability improvements:
|
||||
|
||||
Fixes:
|
||||
|
||||
Themes:
|
||||
|
||||
New languages:
|
||||
|
||||
Updated languages and queries:
|
||||
|
||||
Packaging:
|
||||
-->
|
||||
|
||||
# 25.01.1 (2025-01-19)
|
||||
|
||||
25.01.1 is a patch release focusing on fixing bugs and panics from changes in 25.01.
|
||||
|
||||
Usability improvements:
|
||||
|
||||
* Run external formatters from the document's directory ([#12315](https://github.com/helix-editor/helix/pull/12315))
|
||||
|
||||
Fixes:
|
||||
|
||||
* Fix blank buffer picker preview on doc with no views ([917174e](https://github.com/helix-editor/helix/commit/917174e))
|
||||
* Fix `join_selections` behavior on tabs ([#12452](https://github.com/helix-editor/helix/pull/12452))
|
||||
* Fix recognition for color LSP completion hex codes for some language servers ([#12501](https://github.com/helix-editor/helix/pull/12501))
|
||||
* Fix offsets to selections updated by `open_below`/`open_above` (`o`/`O`) in multi-cursor scenarios ([#12465](https://github.com/helix-editor/helix/pull/12465))
|
||||
* Fix offsets to selections updated by `insert_newline` when trimming whitespace in multi-cursor scenarios ([4bd17e5](https://github.com/helix-editor/helix/commit/4bd17e5))
|
||||
* Fix panic in path completion from resolving variables like `${HOME:-$HOME}` ([#12556](https://github.com/helix-editor/helix/pull/12556))
|
||||
* Prevent line comment continuation when using `change_selection` (`c`) on a line above a comment ([#12575](https://github.com/helix-editor/helix/pull/12575))
|
||||
|
||||
Themes:
|
||||
|
||||
* Update `onelight` ([#12399](https://github.com/helix-editor/helix/pull/12399))
|
||||
* Add cursorline color to iceberg themes ([#12404](https://github.com/helix-editor/helix/pull/12404))
|
||||
* Update `special`, `ui.text.directory` and `ui.virtual.wrap` in `dark_plus` ([#12530](https://github.com/helix-editor/helix/pull/12530))
|
||||
|
||||
New languages:
|
||||
|
||||
* CodeQL ([#12470](https://github.com/helix-editor/helix/pull/12470))
|
||||
* Gren ([#12525](https://github.com/helix-editor/helix/pull/12525))
|
||||
|
||||
Updated languages and queries:
|
||||
|
||||
* Fix Teal LSP name ([#12395](https://github.com/helix-editor/helix/pull/12395))
|
||||
* Highlight `:` in Rust as a delimiter ([#12408](https://github.com/helix-editor/helix/pull/12408))
|
||||
* Update Swift highlights ([#12409](https://github.com/helix-editor/helix/pull/12409))
|
||||
* Highlight JSX attributes as `@attribute` ([#12416](https://github.com/helix-editor/helix/pull/12416))
|
||||
* Improve markdown heading highlights ([#12417](https://github.com/helix-editor/helix/pull/12417))
|
||||
* Add comment tokens configuration for JSONC ([b26903c](https://github.com/helix-editor/helix/commit/b26903c))
|
||||
* Highlight the never type `!` as a type in Rust ([#12485](https://github.com/helix-editor/helix/pull/12485))
|
||||
* Expand builtin function highlights for ECMA languages, Rust and Haskell ([#12488](https://github.com/helix-editor/helix/pull/12488))
|
||||
* Recognize `.clang-tidy` as YAML ([#12498](https://github.com/helix-editor/helix/pull/12498))
|
||||
* Update MATLAB grammar and indent queries ([#12518](https://github.com/helix-editor/helix/pull/12518))
|
||||
* Recognize `rockspec` as Lua ([#12516](https://github.com/helix-editor/helix/pull/12516))
|
||||
* Add `///` to Dart comment tokens configuration ([99d33c7](https://github.com/helix-editor/helix/commit/99d33c7))
|
||||
* Update Solidity grammar and queries ([#12457](https://github.com/helix-editor/helix/pull/12457))
|
||||
* Update Spade grammar and queries ([#12583](https://github.com/helix-editor/helix/pull/12583))
|
||||
* Re-enable Hare fetching and building by default ([#11507](https://github.com/helix-editor/helix/pull/11507))
|
||||
|
||||
Packaging:
|
||||
|
||||
* `--version` now prints a leading zero for single-digit months, for example `25.01` (03f35af)
|
||||
* Pin the Ubuntu GitHub Actions runners used for releases to `ubuntu-22.04` ([#12464](https://github.com/helix-editor/helix/pull/12464))
|
||||
* Produce a Debian package (`.deb` file) in the release GitHub Actions workflow ([#12453](https://github.com/helix-editor/helix/pull/12453))
|
||||
|
||||
# 25.01 (2025-01-03)
|
||||
|
||||
As always, a big thank you to all of the contributors! This release saw changes from 171 contributors.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* The `editor.lsp.display-messages` key now controls messages sent with the LSP `window/showMessage` notification rather than progress messages. If you want to enable progress messages you should now enable the `editor.lsp.display-progress-messages` key instead. ([#5535](https://github.com/helix-editor/helix/pull/5535))
|
||||
|
||||
Features:
|
||||
|
||||
* Big refactor for `Picker`s ([#9647](https://github.com/helix-editor/helix/pull/9647), [#11209](https://github.com/helix-editor/helix/pull/11209), [#11216](https://github.com/helix-editor/helix/pull/11216), [#11211](https://github.com/helix-editor/helix/pull/11211), [#11343](https://github.com/helix-editor/helix/pull/11343), [#11406](https://github.com/helix-editor/helix/pull/11406))
|
||||
* Use a table layout and allow filtering by column
|
||||
* Reimplement `global_search` to allow changing the query dynamically
|
||||
* Add an alternative "inline" display for LSP diagnostics ([#6417](https://github.com/helix-editor/helix/pull/6417), [#11815](https://github.com/helix-editor/helix/pull/11815))
|
||||
* Support defining keybindings as macros ([#4709](https://github.com/helix-editor/helix/pull/4709))
|
||||
* Continue line comments in `o`/`O` and on `<ret>` in insert mode ([#10996](https://github.com/helix-editor/helix/pull/10996), [#12213](https://github.com/helix-editor/helix/pull/12213), [#12215](https://github.com/helix-editor/helix/pull/12215))
|
||||
* Allow configuring and switching clipboard providers at runtime ([#10839](https://github.com/helix-editor/helix/pull/10839), [b855cd0](https://github.com/helix-editor/helix/commit/b855cd0), [467fad5](https://github.com/helix-editor/helix/commit/467fad5), [191b0f0](https://github.com/helix-editor/helix/commit/191b0f0))
|
||||
* Add support for path completion ([#2608](https://github.com/helix-editor/helix/pull/2608))
|
||||
* Support bindings with the Super (Cmd/Win/Meta) modifier ([#6592](https://github.com/helix-editor/helix/pull/6592))
|
||||
* Support rendering and jumping between tabstops in snippet completions ([#9801](https://github.com/helix-editor/helix/pull/9801))
|
||||
* Allow theming directory completions ([#12205](https://github.com/helix-editor/helix/pull/12205), [#12295](https://github.com/helix-editor/helix/pull/12295))
|
||||
|
||||
Commands:
|
||||
|
||||
* Add commands to move within snake_case or camelCase words ([#8147](https://github.com/helix-editor/helix/pull/8147))
|
||||
* Add `search_selection_detect_word_boundaries` ([#12126](https://github.com/helix-editor/helix/pull/12126))
|
||||
* This command takes the `*` key in normal and select mode, replacing `search_selection` which was moved to `A-*`.
|
||||
|
||||
Usability improvements:
|
||||
|
||||
* Add `:edit` and `:e` aliases for `:open` ([#11186](https://github.com/helix-editor/helix/pull/11186), [#11196](https://github.com/helix-editor/helix/pull/11196))
|
||||
* Trim trailing newline from pipe command outputs when the input doesn't have a trailing newline ([#11183](https://github.com/helix-editor/helix/pull/11183), [4f63a46](https://github.com/helix-editor/helix/commit/4f63a46))
|
||||
* Add `:mv` alias for `:move` ([#11256](https://github.com/helix-editor/helix/pull/11256))
|
||||
* Return document display name instead of absolute path from the `%` special register ([#11275](https://github.com/helix-editor/helix/pull/11275))
|
||||
* Track view position on a per-view instead of per-document basis ([#10559](https://github.com/helix-editor/helix/pull/10559))
|
||||
* Improve scrolloff calculation to leave a gap in the middle ([#11323](https://github.com/helix-editor/helix/pull/11323))
|
||||
* Show a popup for stderr printed by failed `:sh` commands ([#11239](https://github.com/helix-editor/helix/pull/11239))
|
||||
* Add statusline errors when nothing is selected with `s`, `K`, `A-K` ([#11370](https://github.com/helix-editor/helix/pull/11370))
|
||||
* Add `.svn` as a workspace root marker ([#11429](https://github.com/helix-editor/helix/pull/11429))
|
||||
* Trim the end of `:sh` outputs ([#11161](https://github.com/helix-editor/helix/pull/11161))
|
||||
* Show LSP `window/showMessage` messages in the statusline ([#5535](https://github.com/helix-editor/helix/pull/5535))
|
||||
* Support finding workspace directories via `.jj` directories ([#11685](https://github.com/helix-editor/helix/pull/11685))
|
||||
* Join single-line comments with `join_selections` (`J`) ([#11742](https://github.com/helix-editor/helix/pull/11742))
|
||||
* Show anonymous syntax tree nodes in `:tree-sitter-subtree` ([#11663](https://github.com/helix-editor/helix/pull/11663), [38e8382](https://github.com/helix-editor/helix/commit/38e8382))
|
||||
* Save an undo checkpoint before paste in insert mode ([#8121](https://github.com/helix-editor/helix/pull/8121))
|
||||
* Only break on ASCII spaces in `:reflow` ([#12048](https://github.com/helix-editor/helix/pull/12048))
|
||||
* Add a `default-yank-register` config option ([#11430](https://github.com/helix-editor/helix/pull/11430))
|
||||
* Show a statusline error for `:format` when a formatter is not available ([#12183](https://github.com/helix-editor/helix/pull/12183))
|
||||
* Change to the home directory with `:cd` with no arguments ([#12042](https://github.com/helix-editor/helix/pull/12042))
|
||||
* Change default comment token to `#` for unrecognized files ([#12080](https://github.com/helix-editor/helix/pull/12080), [#12266](https://github.com/helix-editor/helix/pull/12266), [bae6a58](https://github.com/helix-editor/helix/commit/bae6a58))
|
||||
* Trim all trailing whitespace on `insert_newline` ([#12177](https://github.com/helix-editor/helix/pull/12177))
|
||||
* Change to the prior directory with `:cd -` ([#12194](https://github.com/helix-editor/helix/pull/12194))
|
||||
* Allow parsing `-` (with no modifiers) as a keybinding ([#12191](https://github.com/helix-editor/helix/pull/12191))
|
||||
* Improve opening statusline and error messages when opening duplicate files or directories ([#12199](https://github.com/helix-editor/helix/pull/12199))
|
||||
* Trim trailing colons in paths passed on the argv ([#9963](https://github.com/helix-editor/helix/pull/9963))
|
||||
* Show tree-sitter parser availability in `hx --health <lang>` ([#12228](https://github.com/helix-editor/helix/pull/12228))
|
||||
* Show a preview block for colors in the LSP completion menu ([#12299](https://github.com/helix-editor/helix/pull/12299))
|
||||
* Add infobox help for `surround_add`, `surround_replace` and `surround_delete` ([#12262](https://github.com/helix-editor/helix/pull/12262))
|
||||
|
||||
Fixes:
|
||||
|
||||
* Respect document indentation settings in `format_selections` (`=`) ([#11169](https://github.com/helix-editor/helix/pull/11169))
|
||||
* Avoid switching the current document to normal mode during an LSP `workspace/applyEdit` operation ([#11176](https://github.com/helix-editor/helix/pull/11176))
|
||||
* Fix off-by-one in LSP `find_completion_range` ([#11266](https://github.com/helix-editor/helix/pull/11266))
|
||||
* Prefer file-system mtime to local system time for detecting external modifications ([#11142](https://github.com/helix-editor/helix/pull/11142), [#11352](https://github.com/helix-editor/helix/pull/11352), [#11358](https://github.com/helix-editor/helix/pull/11358), [#11361](https://github.com/helix-editor/helix/pull/11361))
|
||||
* Fix writing of hardlinks ([#11340](https://github.com/helix-editor/helix/pull/11340))
|
||||
* Prevent language servers from being automatically restarted when stopped with `:lsp-stop` ([#11321](https://github.com/helix-editor/helix/pull/11321))
|
||||
* Stable-sort LSP text edits ([#11357](https://github.com/helix-editor/helix/pull/11357))
|
||||
* Fix determination of current language layer in documents with nested language injections ([#11365](https://github.com/helix-editor/helix/pull/11365))
|
||||
* Fix a panic from `:move`ing a file to a new extension which starts a language server ([#11387](https://github.com/helix-editor/helix/pull/11387))
|
||||
* Fix a panic from duplicating the diff gutter ([#11092](https://github.com/helix-editor/helix/pull/11092))
|
||||
* Keep cursor position when exactly replacing text ([#5930](https://github.com/helix-editor/helix/pull/5930))
|
||||
* Fix a panic from `jump_backward` on a newly opened split ([#11508](https://github.com/helix-editor/helix/pull/11508))
|
||||
* Fix a panic from language servers sending an unknown diagnostic severity ([#11569](https://github.com/helix-editor/helix/pull/11569))
|
||||
* Fix a panic when drawing at the edge of the screen ([#11737](https://github.com/helix-editor/helix/pull/11737))
|
||||
* Fix git repo detection on symlinks ([#11732](https://github.com/helix-editor/helix/pull/11732))
|
||||
* Fix a panic from a language server sending an out-of-range active signature index in `textDocument/signatureHelp` ([#11825](https://github.com/helix-editor/helix/pull/11825))
|
||||
* Fix a panic from using `C-k` in a prompt ending in a multi-byte character ([#12237](https://github.com/helix-editor/helix/pull/12237))
|
||||
* Expand tildes in paths passed to `:read` ([#12271](https://github.com/helix-editor/helix/pull/12271))
|
||||
* Respect per-language `workspace-lsp-roots` configuration when opening new documents ([#12223](https://github.com/helix-editor/helix/pull/12223))
|
||||
* Consistently replace line-endings in paste/replace commands ([c262fe4](https://github.com/helix-editor/helix/commit/c262fe4))
|
||||
* Fix formatting in error statusline messages when inspecting variables in DAP ([#12354](https://github.com/helix-editor/helix/pull/12354))
|
||||
* Fix invisible printing of headers in `--health` output on light terminals ([#12355](https://github.com/helix-editor/helix/pull/12355))
|
||||
* Accept integers serialized as floats in the JSONRPC `id` field ([#12376](https://github.com/helix-editor/helix/pull/12376))
|
||||
|
||||
Themes:
|
||||
|
||||
* Bring `kanagawa` colors better in line with neovim version ([#11187](https://github.com/helix-editor/helix/pull/11187), [#11270](https://github.com/helix-editor/helix/pull/11270))
|
||||
* Add `ao` ([#11063](https://github.com/helix-editor/helix/pull/11063))
|
||||
* Update `dark_plus` ([#11415](https://github.com/helix-editor/helix/pull/11415))
|
||||
* Add `iceberg-light` and `iceberg-dark` ([#10674](https://github.com/helix-editor/helix/pull/10674))
|
||||
* Update everforest themes ([#11459](https://github.com/helix-editor/helix/pull/11459))
|
||||
* Update gruvbox themes ([#11477](https://github.com/helix-editor/helix/pull/11477))
|
||||
* Change primary selection cursor color for `naysayer` ([#11617](https://github.com/helix-editor/helix/pull/11617))
|
||||
* Style picker column names in `horizon-dark` ([#11649](https://github.com/helix-editor/helix/pull/11649))
|
||||
* Style picker column names in Darcula themes ([#11649](https://github.com/helix-editor/helix/pull/11649))
|
||||
* Update diagnostics colors in `snazzy` ([#11731](https://github.com/helix-editor/helix/pull/11731))
|
||||
* Update bogster themes ([#11353](https://github.com/helix-editor/helix/pull/11353))
|
||||
* Highlight `keyword.storage` in `onedark` ([#11802](https://github.com/helix-editor/helix/pull/11802))
|
||||
* Add `ui.virtual.jump-label` to `serika-dark` ([#11911](https://github.com/helix-editor/helix/pull/11911))
|
||||
* Add `adwaita-light` ([#10869](https://github.com/helix-editor/helix/pull/10869))
|
||||
* Add seoul256 themes ([#11466](https://github.com/helix-editor/helix/pull/11466))
|
||||
* Add yo themes ([#11703](https://github.com/helix-editor/helix/pull/11703))
|
||||
* Add `eiffel` ([#11679](https://github.com/helix-editor/helix/pull/11679))
|
||||
* Add `carbonfox` ([#11558](https://github.com/helix-editor/helix/pull/11558))
|
||||
* Set tags color in monokai themes ([#11917](https://github.com/helix-editor/helix/pull/11917))
|
||||
* Improve readability of spacebones picker selection ([#12064](https://github.com/helix-editor/helix/pull/12064))
|
||||
* Update modus themes ([#11949](https://github.com/helix-editor/helix/pull/11949))
|
||||
* Use bold for statusline mode indicator in `onedarker` ([#11958](https://github.com/helix-editor/helix/pull/11958))
|
||||
* Update hex themes, add a new hex theme ([#10849](https://github.com/helix-editor/helix/pull/10849))
|
||||
* Add `sunset` ([#12093](https://github.com/helix-editor/helix/pull/12093))
|
||||
* Add bufferline highlighting for flexoki themes ([#12146](https://github.com/helix-editor/helix/pull/12146))
|
||||
* Add colors for (un)checked list items to catppuccin themes ([#12167](https://github.com/helix-editor/helix/pull/12167))
|
||||
* Update `voxed` ([#9328](https://github.com/helix-editor/helix/pull/9328))
|
||||
* Add `vintage` ([#9361](https://github.com/helix-editor/helix/pull/9361))
|
||||
* Add directory style to everforest themes ([#12287](https://github.com/helix-editor/helix/pull/12287))
|
||||
* Add inactive text and update jump label highlights in `dark_plus` ([#12289](https://github.com/helix-editor/helix/pull/12289))
|
||||
* Sync changes with catppuccin themes ([#12304](https://github.com/helix-editor/helix/pull/12304))
|
||||
* Add `ui.text.directory` to `nightfox` ([#12328](https://github.com/helix-editor/helix/pull/12328))
|
||||
* Add `ui.text.directory` to `sunset` ([#12328](https://github.com/helix-editor/helix/pull/12328))
|
||||
* Add `diagnostic.unnecessary` to Catppuccin themes ([#12391](https://github.com/helix-editor/helix/pull/12391))
|
||||
|
||||
New languages:
|
||||
|
||||
* `jjdescription` ([#11271](https://github.com/helix-editor/helix/pull/11271), [#11857](https://github.com/helix-editor/helix/pull/11857), [#12305](https://github.com/helix-editor/helix/pull/12305))
|
||||
* i3wm and Sway configs ([#11424](https://github.com/helix-editor/helix/pull/11424))
|
||||
* TypeSpec ([#11412](https://github.com/helix-editor/helix/pull/11412))
|
||||
* jq ([#11393](https://github.com/helix-editor/helix/pull/11393))
|
||||
* Thrift ([#11367](https://github.com/helix-editor/helix/pull/11367))
|
||||
* Gherkin ([#11083](https://github.com/helix-editor/helix/pull/11083))
|
||||
* Circom ([#11676](https://github.com/helix-editor/helix/pull/11676))
|
||||
* Dune ([#11829](https://github.com/helix-editor/helix/pull/11829))
|
||||
* Snakemake ([#11858](https://github.com/helix-editor/helix/pull/11858), [#11936](https://github.com/helix-editor/helix/pull/11936))
|
||||
* Cylc ([#11830](https://github.com/helix-editor/helix/pull/11830))
|
||||
* textproto ([#11874](https://github.com/helix-editor/helix/pull/11874))
|
||||
* Spade ([#11448](https://github.com/helix-editor/helix/pull/11448), [#12276](https://github.com/helix-editor/helix/pull/12276))
|
||||
* NestedText ([#11987](https://github.com/helix-editor/helix/pull/11987))
|
||||
* Quint ([#11898](https://github.com/helix-editor/helix/pull/11898))
|
||||
* Amber-lang ([#12021](https://github.com/helix-editor/helix/pull/12021))
|
||||
* Vento ([#12147](https://github.com/helix-editor/helix/pull/12147))
|
||||
* Teal ([#12081](https://github.com/helix-editor/helix/pull/12081))
|
||||
* Koto ([#12307](https://github.com/helix-editor/helix/pull/12307))
|
||||
* NGINX ([#12309](https://github.com/helix-editor/helix/pull/12309))
|
||||
|
||||
Updated languages and queries:
|
||||
|
||||
* Add comment injections for Hare ([#11173](https://github.com/helix-editor/helix/pull/11173))
|
||||
* Improve highlights for `blade.php` files ([#11138](https://github.com/helix-editor/helix/pull/11138))
|
||||
* Update tree-sitter-slint ([#11224](https://github.com/helix-editor/helix/pull/11224), [#11757](https://github.com/helix-editor/helix/pull/11757), [#12297](https://github.com/helix-editor/helix/pull/12297))
|
||||
* Recognize `just` files as Just ([#11286](https://github.com/helix-editor/helix/pull/11286))
|
||||
* Recognize `mdx` as Markdown ([#11122](https://github.com/helix-editor/helix/pull/11122))
|
||||
* Update Just grammar and queries ([#11306](https://github.com/helix-editor/helix/pull/11306))
|
||||
* Recognize `tclsh` as TCL ([#11236](https://github.com/helix-editor/helix/pull/11236))
|
||||
* Update Godot grammar and queries ([#11235](https://github.com/helix-editor/helix/pull/11235))
|
||||
* Update Gleam grammar and queries ([#11427](https://github.com/helix-editor/helix/pull/11427))
|
||||
* Add `mesonlsp` for Meson ([#11416](https://github.com/helix-editor/helix/pull/11416))
|
||||
* Update HTML highlights ([#11400](https://github.com/helix-editor/helix/pull/11400))
|
||||
* Add comment textobjects for Verilog ([#11388](https://github.com/helix-editor/helix/pull/11388))
|
||||
* Switch tree-sitter-just grammar ([#11380](https://github.com/helix-editor/helix/pull/11380), [#11606](https://github.com/helix-editor/helix/pull/11606), [#12141](https://github.com/helix-editor/helix/pull/12141))
|
||||
* Update tree-sitter-fsharp ([#11061](https://github.com/helix-editor/helix/pull/11061))
|
||||
* Add `nixd` for Nix ([#10767](https://github.com/helix-editor/helix/pull/10767))
|
||||
* Highlight types and enum members from the Rust prelude ([#8535](https://github.com/helix-editor/helix/pull/8535))
|
||||
* Improve textobjects for HCL, Nix ([#11513](https://github.com/helix-editor/helix/pull/11513))
|
||||
* Add textobjects queries for docker-compose, dockerfile, env, git-config, hcl, hocon, prisma, SQL and YAML ([#11513](https://github.com/helix-editor/helix/pull/11513))
|
||||
* Recognize cshtml files as HTML ([#11540](https://github.com/helix-editor/helix/pull/11540))
|
||||
* Set a memory limit for the Lean language server ([#11683](https://github.com/helix-editor/helix/pull/11683))
|
||||
* Add configurations for jedi and ruff language servers ([#11630](https://github.com/helix-editor/helix/pull/11630))
|
||||
* Update Vue highlights ([#11706](https://github.com/helix-editor/helix/pull/11706))
|
||||
* Switch tree-sitter-hcl grammar ([#11749](https://github.com/helix-editor/helix/pull/11749))
|
||||
* Fix `odinfmt` formatter configuration ([#11759](https://github.com/helix-editor/helix/pull/11759))
|
||||
* Recognize `rbs` files as Ruby ([#11786](https://github.com/helix-editor/helix/pull/11786))
|
||||
* Update tree-sitter-nickel ([#11771](https://github.com/helix-editor/helix/pull/11771))
|
||||
* Recognize `ldtk` and `ldtkl` files as JSON ([#11793](https://github.com/helix-editor/helix/pull/11793))
|
||||
* Fix highlights for builtin functions in Fish ([#11792](https://github.com/helix-editor/helix/pull/11792))
|
||||
* Add `superhtml` for HTML ([#11609](https://github.com/helix-editor/helix/pull/11609))
|
||||
* Add a configuration for the Vale language server ([#11636](https://github.com/helix-editor/helix/pull/11636))
|
||||
* Add Erlang Language Platform (`elp`) for Erlang ([#11499](https://github.com/helix-editor/helix/pull/11499))
|
||||
* Update Odin highlights ([#11804](https://github.com/helix-editor/helix/pull/11804))
|
||||
* Remove auto-pairs for single quotes in SML ([#11838](https://github.com/helix-editor/helix/pull/11838))
|
||||
* Add `glsl_analyzer` for GLSL ([#11891](https://github.com/helix-editor/helix/pull/11891))
|
||||
* Recognize `.prettierrc` as YAML ([#11997](https://github.com/helix-editor/helix/pull/11997))
|
||||
* Fix `swift-format` formatter configuration ([#12052](https://github.com/helix-editor/helix/pull/12052))
|
||||
* Add `package.json` and `tsconfig.json` as JS/TS workspace roots ([#10652](https://github.com/helix-editor/helix/pull/10652))
|
||||
* Add "INVARIANT" to comment error highlights ([#12094](https://github.com/helix-editor/helix/pull/12094))
|
||||
* Update Rescript grammar and queries ([#11165](https://github.com/helix-editor/helix/pull/11165))
|
||||
* Update tree-sitter-nasm ([#11795](https://github.com/helix-editor/helix/pull/11795))
|
||||
* Update LLVM grammars ([#11851](https://github.com/helix-editor/helix/pull/11851))
|
||||
* Update Perl and Pod grammars ([#11848](https://github.com/helix-editor/helix/pull/11848))
|
||||
* Add Nim injections in Nix ([#11837](https://github.com/helix-editor/helix/pull/11837))
|
||||
* Recognize `livemd` as Markdown ([#12034](https://github.com/helix-editor/helix/pull/12034))
|
||||
* Update Unison grammar and queries ([#12039](https://github.com/helix-editor/helix/pull/12039))
|
||||
* Turn off Swift auto-format by default ([#12071](https://github.com/helix-editor/helix/pull/12071))
|
||||
* Recognize `.swift-format` as JSON ([#12071](https://github.com/helix-editor/helix/pull/12071))
|
||||
* Recognize `.clangd` and `.clang-format` as YAML ([#12032](https://github.com/helix-editor/helix/pull/12032))
|
||||
* Recognize `ssh_config.d/*.conf` as sshclientconfig ([#11947](https://github.com/helix-editor/helix/pull/11947))
|
||||
* Update comment token configs for Zig ([#12049](https://github.com/helix-editor/helix/pull/12049))
|
||||
* Update tree-sitter-bicep ([#11525](https://github.com/helix-editor/helix/pull/11525))
|
||||
* Add `hyperls` for Hyperlang ([#11056](https://github.com/helix-editor/helix/pull/11056))
|
||||
* Add highlight queries for Solidity ([#12102](https://github.com/helix-editor/helix/pull/12102))
|
||||
* Recognize `WORKSPACE.bzlmod` as Starlark ([#12103](https://github.com/helix-editor/helix/pull/12103))
|
||||
* Update Ada grammar and queries ([#12131](https://github.com/helix-editor/helix/pull/12131))
|
||||
* Restrict Hocon file-types glob patterns ([#12156](https://github.com/helix-editor/helix/pull/12156))
|
||||
* Update Mojo language server to Magic ([#12195](https://github.com/helix-editor/helix/pull/12195))
|
||||
* Switch tree-sitter-v grammar ([#12236](https://github.com/helix-editor/helix/pull/12236))
|
||||
* Add "COMPLIANCE" to comment error highlights ([#12094](https://github.com/helix-editor/helix/pull/12094))
|
||||
* Add a language server configuration for `ltex-ls-plus` ([#12251](https://github.com/helix-editor/helix/pull/12251))
|
||||
* Update tree-sitter-dockerfile ([#12230](https://github.com/helix-editor/helix/pull/12230))
|
||||
* Add `]` to PHP outdents ([#12286](https://github.com/helix-editor/helix/pull/12286))
|
||||
* Add textobjects for Odin ([#12302](https://github.com/helix-editor/helix/pull/12302))
|
||||
* Update tree-sitter-heex and queries ([#12334](https://github.com/helix-editor/helix/pull/12334))
|
||||
* Update protobuf highlights ([#12339](https://github.com/helix-editor/helix/pull/12339))
|
||||
* Switch tree-sitter-query (TSQ) grammar ([#12148](https://github.com/helix-editor/helix/pull/12148), [e0bccd2](https://github.com/helix-editor/helix/commit/e0bccd2))
|
||||
* Add block comment configurations for jinja and nunjucks ([#12348](https://github.com/helix-editor/helix/pull/12348))
|
||||
* Add `uv` shebang for python ([#12360](https://github.com/helix-editor/helix/pull/12360))
|
||||
* Update tree-sitter-vento ([#12368](https://github.com/helix-editor/helix/pull/12368))
|
||||
* Switch Protobuf tree-sitter grammar ([#12225](https://github.com/helix-editor/helix/pull/12225))
|
||||
* Recognize `hypr/*.conf` as Hyprland ([#12384](https://github.com/helix-editor/helix/pull/12384))
|
||||
|
||||
Packaging:
|
||||
|
||||
* Add completions for Nushell ([#11262](https://github.com/helix-editor/helix/pull/11262), [#11346](https://github.com/helix-editor/helix/pull/11346))
|
||||
* Fix completion of flags in Bash completions ([#11246](https://github.com/helix-editor/helix/pull/11246))
|
||||
* Include shell completions in Nix outputs ([#11518](https://github.com/helix-editor/helix/pull/11518))
|
||||
|
||||
# 24.07 (2024-07-14)
|
||||
|
||||
Thanks to all of the contributors! This release has changes from 160 contributors.
|
||||
@@ -436,7 +735,7 @@ Updated languages and queries:
|
||||
- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772))
|
||||
- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839))
|
||||
- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806))
|
||||
- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
|
||||
- Fix "braket" typo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
|
||||
- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775))
|
||||
- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541))
|
||||
- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254))
|
||||
@@ -643,7 +942,7 @@ Updated languages and queries:
|
||||
- Add Fortran comment injections ([#7305](https://github.com/helix-editor/helix/pull/7305))
|
||||
- Switch Vue language server to `vue-language-server` ([#7312](https://github.com/helix-editor/helix/pull/7312))
|
||||
- Update tree-sitter-sql ([#7387](https://github.com/helix-editor/helix/pull/7387), [#8464](https://github.com/helix-editor/helix/pull/8464))
|
||||
- Replace the MATLAB tre-sitter grammar ([#7388](https://github.com/helix-editor/helix/pull/7388), [#7442](https://github.com/helix-editor/helix/pull/7442), [#7491](https://github.com/helix-editor/helix/pull/7491), [#7493](https://github.com/helix-editor/helix/pull/7493), [#7511](https://github.com/helix-editor/helix/pull/7511), [#7532](https://github.com/helix-editor/helix/pull/7532), [#8040](https://github.com/helix-editor/helix/pull/8040))
|
||||
- Replace the MATLAB tree-sitter grammar ([#7388](https://github.com/helix-editor/helix/pull/7388), [#7442](https://github.com/helix-editor/helix/pull/7442), [#7491](https://github.com/helix-editor/helix/pull/7491), [#7493](https://github.com/helix-editor/helix/pull/7493), [#7511](https://github.com/helix-editor/helix/pull/7511), [#7532](https://github.com/helix-editor/helix/pull/7532), [#8040](https://github.com/helix-editor/helix/pull/8040))
|
||||
- Highlight TOML table headers ([#7441](https://github.com/helix-editor/helix/pull/7441))
|
||||
- Recognize `cppm` file-type as C++ ([#7492](https://github.com/helix-editor/helix/pull/7492))
|
||||
- Refactor ecma language queries into private and public queries ([#7207](https://github.com/helix-editor/helix/pull/7207))
|
||||
@@ -1130,7 +1429,7 @@ Features:
|
||||
- Support underline styles and colors ([#4061](https://github.com/helix-editor/helix/pull/4061), [98c121c](https://github.com/helix-editor/helix/commit/98c121c))
|
||||
- Inheritance for themes ([#3067](https://github.com/helix-editor/helix/pull/3067), [#4096](https://github.com/helix-editor/helix/pull/4096))
|
||||
- Cursorcolumn ([#4084](https://github.com/helix-editor/helix/pull/4084))
|
||||
- Overhauled system for writing files and quiting ([#2267](https://github.com/helix-editor/helix/pull/2267), [#4397](https://github.com/helix-editor/helix/pull/4397))
|
||||
- Overhauled system for writing files and quitting ([#2267](https://github.com/helix-editor/helix/pull/2267), [#4397](https://github.com/helix-editor/helix/pull/4397))
|
||||
- Autosave when terminal loses focus ([#3178](https://github.com/helix-editor/helix/pull/3178))
|
||||
- Use OSC52 as a fallback for the system clipboard ([#3220](https://github.com/helix-editor/helix/pull/3220))
|
||||
- Show git diffs in the gutter ([#3890](https://github.com/helix-editor/helix/pull/3890), [#5012](https://github.com/helix-editor/helix/pull/5012), [#4995](https://github.com/helix-editor/helix/pull/4995))
|
||||
@@ -1285,7 +1584,7 @@ Themes:
|
||||
- Update `pop-dark` ([#4323](https://github.com/helix-editor/helix/pull/4323))
|
||||
- Update `rose_pine` ([#4221](https://github.com/helix-editor/helix/pull/4221))
|
||||
- Add `kanagawa` ([#4300](https://github.com/helix-editor/helix/pull/4300))
|
||||
- Add `hex_steel`, `hex_toxic` and `hex_lavendar` ([#4367](https://github.com/helix-editor/helix/pull/4367), [#4990](https://github.com/helix-editor/helix/pull/4990))
|
||||
- Add `hex_steel`, `hex_toxic` and `hex_lavender` ([#4367](https://github.com/helix-editor/helix/pull/4367), [#4990](https://github.com/helix-editor/helix/pull/4990))
|
||||
- Update `tokyonight` and `tokyonight_storm` ([#4415](https://github.com/helix-editor/helix/pull/4415))
|
||||
- Update `gruvbox` ([#4626](https://github.com/helix-editor/helix/pull/4626))
|
||||
- Update `dark_plus` ([#4661](https://github.com/helix-editor/helix/pull/4661), [#4678](https://github.com/helix-editor/helix/pull/4678))
|
||||
@@ -1452,7 +1751,7 @@ Usability improvements and fixes:
|
||||
- Introduce `keyword.storage` highlight scope ([#2731](https://github.com/helix-editor/helix/pull/2731))
|
||||
- Handle symlinks more consistently ([#2718](https://github.com/helix-editor/helix/pull/2718))
|
||||
- Improve markdown list rendering ([#2687](https://github.com/helix-editor/helix/pull/2687))
|
||||
- Update auto-pairs and idle-timout settings when the config is reloaded ([#2736](https://github.com/helix-editor/helix/pull/2736))
|
||||
- Update auto-pairs and idle-timeout settings when the config is reloaded ([#2736](https://github.com/helix-editor/helix/pull/2736))
|
||||
- Fix panic on closing last buffer ([#2658](https://github.com/helix-editor/helix/pull/2658))
|
||||
- Prevent modifying jumplist until jumping to a reference ([#2670](https://github.com/helix-editor/helix/pull/2670))
|
||||
- Ensure `:quit` and `:quit!` take no arguments ([#2654](https://github.com/helix-editor/helix/pull/2654))
|
||||
|
730
Cargo.lock
generated
730
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -22,13 +22,12 @@ default-members = [
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
# debug = true
|
||||
|
||||
[profile.opt]
|
||||
inherits = "release"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
# strip = "debuginfo" # TODO: or strip = true
|
||||
strip = true
|
||||
opt-level = 3
|
||||
|
||||
[profile.integration]
|
||||
@@ -42,9 +41,15 @@ tree-sitter = { version = "0.22" }
|
||||
nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "2.0"
|
||||
tempfile = "3.19.1"
|
||||
bitflags = "2.9"
|
||||
unicode-segmentation = "1.2"
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
foldhash = "0.1"
|
||||
parking_lot = "0.12"
|
||||
|
||||
[workspace.package]
|
||||
version = "24.7.0"
|
||||
version = "25.1.1"
|
||||
edition = "2021"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
categories = ["editor"]
|
||||
|
@@ -12,9 +12,12 @@
|
||||
- [Syntax aware motions](./syntax-aware-motions.md)
|
||||
- [Pickers](./pickers.md)
|
||||
- [Keymap](./keymap.md)
|
||||
- [Command line](./command-line.md)
|
||||
- [Commands](./commands.md)
|
||||
- [Language support](./lang-support.md)
|
||||
- [Migrating from Vim](./from-vim.md)
|
||||
- [Ecosystem](./ecosystem.md)
|
||||
- [Migrating from Vim](./from-vim.md)
|
||||
- [Helix mode in other software](./other-software.md)
|
||||
- [Configuration](./configuration.md)
|
||||
- [Editor](./editor.md)
|
||||
- [Themes](./themes.md)
|
||||
|
@@ -7,6 +7,7 @@
|
||||
- [Note to packagers](#note-to-packagers)
|
||||
- [Validating the installation](#validating-the-installation)
|
||||
- [Configure the desktop shortcut](#configure-the-desktop-shortcut)
|
||||
- [Building the Debian package](#building-the-debian-package)
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -63,11 +64,9 @@ export HELIX_RUNTIME=~/src/helix/runtime
|
||||
Or, create a symbolic link:
|
||||
|
||||
```sh
|
||||
ln -Ts $PWD/runtime ~/.config/helix/runtime
|
||||
ln -Tsf $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
|
||||
@@ -75,7 +74,7 @@ Either set the `HELIX_RUNTIME` environment variable to point to the runtime file
|
||||
Cmd:
|
||||
|
||||
```sh
|
||||
setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
|
||||
setx HELIX_RUNTIME "%userprofile%\src\helix\runtime"
|
||||
```
|
||||
|
||||
> 💡 `%userprofile%` resolves to your user directory like
|
||||
@@ -162,3 +161,39 @@ file. For example, to use `kitty`:
|
||||
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
|
||||
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
|
||||
```
|
||||
|
||||
### Building the Debian package
|
||||
|
||||
If the `.deb` file provided on the release page uses a `libc` version higher
|
||||
than that used by your Debian, Ubuntu, or Mint system, you can build the package
|
||||
from source to match your system's dependencies.
|
||||
|
||||
Install `cargo-deb`, the tool used for building the `.deb` file:
|
||||
|
||||
```sh
|
||||
cargo install cargo-deb
|
||||
```
|
||||
|
||||
After cloning and entering the Helix repository as previously described,
|
||||
use the following command to build the release binary and package it into a `.deb` file in a single step.
|
||||
|
||||
```sh
|
||||
cargo deb -- --locked
|
||||
```
|
||||
|
||||
> 💡 This locks you into the `--release` profile. But you can also build helix in any way you like.
|
||||
> As long as you leave a `target/release/hx` file, it will get packaged with `cargo deb --no-build`
|
||||
|
||||
> 💡 Don't worry about the repeated
|
||||
> ```
|
||||
> warning: Failed to find dependency specification
|
||||
> ```
|
||||
> warnings. Cargo deb just reports which packaged files it didn't derive dependencies for. But
|
||||
> so far the dependency deriving seams very good, even if some of the grammar files are skipped.
|
||||
|
||||
You can find the resulted `.deb` in `target/debian/`. It should contain everything it needs, including the
|
||||
|
||||
- completions for bash, fish, zsh
|
||||
- .desktop file
|
||||
- icon (though desktop environments might use their own since the name of the package is correctly `helix`)
|
||||
- launcher to the binary with the runtime
|
||||
|
82
book/src/command-line.md
Normal file
82
book/src/command-line.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Command line
|
||||
|
||||
- [Quoting](#quoting)
|
||||
- [Flags](#flags)
|
||||
- [Expansions](#expansions)
|
||||
- [Exceptions](#exceptions)
|
||||
|
||||
The command line is used for executing [typable commands](./commands.md#typable-commands) like `:write` or `:quit`. Press `:` to activate the command line.
|
||||
|
||||
Typable commands optionally accept arguments. `:write` for example accepts an optional path to write the file contents. The command line also supports a quoting syntax for arguments, flags to modify command behaviors, and _expansions_ - a way to insert values from the editor. Most commands support these features but some have custom parsing rules (see the [exceptions](#exceptions) below).
|
||||
|
||||
## Quoting
|
||||
|
||||
By default, command arguments are split on tabs and space characters. `:open README.md CHANGELOG.md` for example should open two files, `README.md` and `CHANGELOG.md`. Arguments that contain spaces can be surrounded in single quotes (`'`) or backticks (`` ` ``) to prevent the space from separating the argument, like `:open 'a b.txt'`.
|
||||
|
||||
Double quotes may be used the same way, but double quotes _expand_ their inner content. `:echo "%{cursor_line}"` for example may print `1` because of the expansion for the `cursor_line` variable. `:echo '%{cursor_line}'` though prints `%{cursor_line}` literally: content within single quotes or backticks is interpreted as-is.
|
||||
|
||||
On Unix systems the backslash character may be used to escape certain characters depending on where it is used. Within an argument which isn't surround in quotes, the backslash can be used to escape the space or tab characters: `:open a\ b.txt` is equivalent to `:open 'a b.txt'`. The backslash may also be used to escape quote characters (`'`, `` ` ``, `"`) or the percent token (`%`) when used at the beginning of an argument. `:echo \%%sh{foo}` for example prints `%sh{foo}` instead of invoking a `foo` shell command and `:echo \"quote` prints `"quote`. The backslash character is treated literally in any other situation on Unix systems and always on Windows: `:echo \n` always prints `\n`.
|
||||
|
||||
## Flags
|
||||
|
||||
Command flags are optional switches that can be used to alter the behavior of a command. For example the `:sort` command accepts an optional `--reverse` (or `-r` for short) flag which causes the sort command to reverse the sorting direction. Typing the `-` character shows completions for the current command's flags, if any.
|
||||
|
||||
The `--` flag specifies the end of flags. All arguments after `--` are treated as positional arguments: `:open -- -a.txt` opens a file called `-a.txt`.
|
||||
|
||||
## Expansions
|
||||
|
||||
Expansions are patterns that Helix recognizes and replaces within the command line. Helix recognizes anything starting with a percent token (`%`) as an expansion, for example `%sh{echo hi!}`. Expansions are particularly useful when used in commands like `:echo` or `:noop` for executing simple scripts. For example:
|
||||
|
||||
```toml
|
||||
[keys.normal]
|
||||
# Print the current line's git blame information to the statusline.
|
||||
space.B = ":echo %sh{git blame -L %{cursor_line},+1 %{buffer_name}}"
|
||||
```
|
||||
|
||||
Expansions take the form `%[<kind>]<open><contents><close>`. In `%sh{echo hi!}`, for example, the kind is `sh` - the shell expansion - and the contents are "echo hi!", with `{` and `}` acting as opening and closing delimiters. The following open/close characters are recognized as expansion delimiter pairs: `(`/`)`, `[`/`]`, `{`/`}` and `<`/`>`. Plus the single characters `'`, `"` or `|` may be used instead: `%{cursor_line}` is equivalent to `%<cursor_line>`, `%[cursor_line]` or `%|cursor_line|`.
|
||||
|
||||
To escape a percent character instead of treating it as an expansion, use two percent characters consecutively. To execute a shell command like `date -u +'%Y-%m-%d'`, double the percent characters: `:echo %sh{date -u +'%%Y-%%m-%%d'}`.
|
||||
|
||||
When no `<kind>` is provided, Helix will expand a **variable**. For example `%{cursor_line}` can be used as in argument to insert the line number. `:echo %{cursor_line}` for instance may print `1` to the statusline.
|
||||
|
||||
The following variables are supported:
|
||||
|
||||
| Name | Description |
|
||||
|--- |--- |
|
||||
| `cursor_line` | The line number of the primary cursor in the currently focused document, starting at 1. |
|
||||
| `cursor_column` | The column number of the primary cursor in the currently focused document, starting at 1. This is counted as the number of grapheme clusters from the start of the line rather than bytes or codepoints. |
|
||||
| `buffer_name` | The relative path of the currently focused document. `[scratch]` is expanded instead for scratch buffers. |
|
||||
| `line_ending` | A string containing the line ending of the currently focused document. For example on Unix systems this is usually a line-feed character (`\n`) but on Windows systems this may be a carriage-return plus a line-feed (`\r\n`). The line ending kind of the currently focused document can be inspected with the `:line-ending` command. |
|
||||
|
||||
Aside from editor variables, the following expansions may be used:
|
||||
|
||||
* Unicode `%u{..}`. The contents may contain up to six hexadecimal numbers corresponding to a Unicode codepoint value. For example `:echo %u{25CF}` prints `●` to the statusline.
|
||||
* Shell `%sh{..}`. The contents are passed to the configured shell command. For example `:echo %sh{echo "20 * 5" | bc}` may print `100` on the statusline on when using a shell with `echo` and the `bc` calculator installed. Shell expansions are evaluated recursively. `%sh{echo '%{buffer_name}:%{cursor_line}'}` for example executes a command like `echo 'README.md:1'`: the variables within the `%sh{..}` expansion are evaluated before executing the shell command.
|
||||
|
||||
As mentioned above, double quotes can be used to surround arguments containing spaces but also support expansions within the quoted content unlike singe quotes or backticks. For example `:echo "circle: %u{25CF}"` prints `circle: ●` to the statusline while `:echo 'circle: %u{25CF}'` prints `circle: %u{25CF}`.
|
||||
|
||||
Note that expansions are only evaluated once the Enter key is pressed in command mode.
|
||||
|
||||
## Exceptions
|
||||
|
||||
The following commands support expansions but otherwise pass the given argument directly to the shell program without interpreting quotes:
|
||||
|
||||
* `:insert-output`
|
||||
* `:append-output`
|
||||
* `:pipe`
|
||||
* `:pipe-to`
|
||||
* `:run-shell-command`
|
||||
|
||||
For example executing `:sh echo "%{buffer_name}:%{cursor_column}"` would pass text like `echo "README.md:1"` as an argument to the shell program: the expansions are evaluated but not the quotes. As mentioned above, percent characters can be used in shell commands by doubling the percent character. To insert the output of a command like `date -u +'%Y-%m-%d'` use `:insert-output date -u +'%%Y-%%m-%%d'`.
|
||||
|
||||
The `:set-option` and `:toggle-option` commands use regular parsing for the first argument - the config option name - and parse the rest depending on the config option's type. `:set-option` interprets the second argument as a string for string config options and parses everything else as JSON.
|
||||
|
||||
`:toggle-option`'s behavior depends on the JSON type of the config option supplied as the first argument:
|
||||
|
||||
* Booleans: only the config option name should be provided. For example `:toggle-option auto-format` will flip the `auto-format` option.
|
||||
* Strings: the rest of the command line is parsed with regular quoting rules. For example `:toggle-option indent-heuristic hybrid tree-sitter simple` cycles through "hybrid", "tree-sitter" and "simple" values on each invocation of the command.
|
||||
* Numbers, arrays and objects: the rest of the command line is parsed as a stream of JSON values. For example `:toggle-option rulers [81] [51, 73]` cycles through `[81]` and `[51, 73]`.
|
||||
|
||||
When providing multiple values to `:toggle-option` there should be no duplicates. `:toggle-option indent-heuristic hybrid simple tree-sitter simple` for example would only toggle between "hybrid" and "tree-sitter" values.
|
||||
|
||||
`:lsp-workspace-command` works similarly to `:toggle-option`. The first argument (if present) is parsed according to normal rules. The rest of the line is parsed as JSON values. Unlike `:toggle-option`, string arguments for a command must be quoted. For example `:lsp-workspace-command lsp.Command "foo" "bar"`.
|
3
book/src/ecosystem.md
Normal file
3
book/src/ecosystem.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ecosystem
|
||||
|
||||
This section has information related to the wider Helix ecosystem.
|
@@ -1,11 +1,13 @@
|
||||
## Editor
|
||||
|
||||
- [`[editor]` Section](#editor-section)
|
||||
- [`[editor.clipboard-provider]` Section](#editorclipboard-provider-section)
|
||||
- [`[editor.statusline]` Section](#editorstatusline-section)
|
||||
- [`[editor.lsp]` Section](#editorlsp-section)
|
||||
- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
|
||||
- [`[editor.file-picker]` Section](#editorfile-picker-section)
|
||||
- [`[editor.auto-pairs]` Section](#editorauto-pairs-section)
|
||||
- [`[editor.auto-save]` Section](#editorauto-save-section)
|
||||
- [`[editor.search]` Section](#editorsearch-section)
|
||||
- [`[editor.whitespace]` Section](#editorwhitespace-section)
|
||||
- [`[editor.indent-guides]` Section](#editorindent-guides-section)
|
||||
@@ -51,11 +53,14 @@
|
||||
| `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` |
|
||||
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
|
||||
| `trim-final-newlines` | Whether to automatically remove line-endings after the final one on write | `false` |
|
||||
| `trim-trailing-whitespace` | Whether to automatically remove whitespace preceding line endings on write | `false` |
|
||||
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
|
||||
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
|
||||
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
|
||||
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
|
||||
| `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. |
|
||||
| `editor-config` | Whether to read settings from [EditorConfig](https://editorconfig.org) files | `true` |
|
||||
|
||||
### `[editor.clipboard-provider]` Section
|
||||
|
||||
@@ -68,7 +73,7 @@ For instance, setting it to use OSC 52 termcodes, the configuration would be:
|
||||
clipboard-provider = "termcode"
|
||||
```
|
||||
|
||||
Alternatively, Helix can be configured to use arbitary commands for clipboard integration:
|
||||
Alternatively, Helix can be configured to use arbitrary commands for clipboard integration:
|
||||
|
||||
```toml
|
||||
[editor.clipboard-provider.custom]
|
||||
@@ -143,9 +148,11 @@ The following statusline elements can be configured:
|
||||
| Key | Description | Default |
|
||||
| --- | ----------- | ------- |
|
||||
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
|
||||
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
|
||||
| `display-messages` | Display LSP `window/showMessage` messages below statusline[^1] | `true` |
|
||||
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
|
||||
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
|
||||
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
|
||||
| `display-color-swatches` | Show color swatches next to colors | `true` |
|
||||
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
|
||||
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
|
||||
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
|
||||
|
@@ -4,11 +4,11 @@
|
||||
| adl | ✓ | ✓ | ✓ | |
|
||||
| agda | ✓ | | | |
|
||||
| amber | ✓ | | | |
|
||||
| astro | ✓ | | | |
|
||||
| astro | ✓ | | | `astro-ls` |
|
||||
| awk | ✓ | ✓ | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
| bass | ✓ | | | `bass` |
|
||||
| beancount | ✓ | | | |
|
||||
| beancount | ✓ | | | `beancount-language-server` |
|
||||
| bibtex | ✓ | | | `texlab` |
|
||||
| bicep | ✓ | | | `bicep-langserver` |
|
||||
| bitbake | ✓ | | | `bitbake-language-server` |
|
||||
@@ -23,12 +23,14 @@
|
||||
| circom | ✓ | | | `circom-lsp` |
|
||||
| clojure | ✓ | | | `clojure-lsp` |
|
||||
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
|
||||
| codeql | ✓ | ✓ | | `codeql` |
|
||||
| comment | ✓ | | | |
|
||||
| common-lisp | ✓ | | ✓ | `cl-lsp` |
|
||||
| cpon | ✓ | | ✓ | |
|
||||
| cpp | ✓ | ✓ | ✓ | `clangd` |
|
||||
| crystal | ✓ | ✓ | | `crystalline` |
|
||||
| css | ✓ | | ✓ | `vscode-css-language-server` |
|
||||
| csv | ✓ | | | |
|
||||
| cue | ✓ | | | `cuelsp` |
|
||||
| cylc | ✓ | ✓ | ✓ | |
|
||||
| d | ✓ | ✓ | ✓ | `serve-d` |
|
||||
@@ -37,6 +39,7 @@
|
||||
| devicetree | ✓ | | | |
|
||||
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
|
||||
| diff | ✓ | | | |
|
||||
| djot | ✓ | | | |
|
||||
| docker-compose | ✓ | ✓ | ✓ | `docker-compose-langserver`, `yaml-language-server` |
|
||||
| dockerfile | ✓ | ✓ | | `docker-langserver` |
|
||||
| dot | ✓ | | | `dot-language-server` |
|
||||
@@ -54,15 +57,17 @@
|
||||
| erb | ✓ | | | |
|
||||
| erlang | ✓ | ✓ | | `erlang_ls`, `elp` |
|
||||
| esdl | ✓ | | | |
|
||||
| fga | ✓ | ✓ | ✓ | |
|
||||
| fidl | ✓ | | | |
|
||||
| fish | ✓ | ✓ | ✓ | |
|
||||
| fish | ✓ | ✓ | ✓ | `fish-lsp` |
|
||||
| forth | ✓ | | | `forth-lsp` |
|
||||
| fortran | ✓ | | ✓ | `fortls` |
|
||||
| fsharp | ✓ | | | `fsautocomplete` |
|
||||
| gas | ✓ | ✓ | | |
|
||||
| gas | ✓ | ✓ | | `asm-lsp` |
|
||||
| gdscript | ✓ | ✓ | ✓ | |
|
||||
| gemini | ✓ | | | |
|
||||
| gherkin | ✓ | | | |
|
||||
| ghostty | ✓ | | | |
|
||||
| git-attributes | ✓ | | | |
|
||||
| git-commit | ✓ | ✓ | | |
|
||||
| git-config | ✓ | ✓ | | |
|
||||
@@ -80,6 +85,7 @@
|
||||
| gowork | ✓ | | | `gopls` |
|
||||
| gpr | ✓ | | | `ada_language_server` |
|
||||
| graphql | ✓ | ✓ | | `graphql-lsp` |
|
||||
| gren | ✓ | ✓ | | |
|
||||
| groovy | ✓ | | | |
|
||||
| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
|
||||
| hare | ✓ | | | |
|
||||
@@ -97,6 +103,7 @@
|
||||
| idris | | | | `idris2-lsp` |
|
||||
| iex | ✓ | | | |
|
||||
| ini | ✓ | | | |
|
||||
| ink | ✓ | | | |
|
||||
| inko | ✓ | ✓ | ✓ | |
|
||||
| janet | ✓ | | | |
|
||||
| java | ✓ | ✓ | ✓ | `jdtls` |
|
||||
@@ -114,7 +121,7 @@
|
||||
| just | ✓ | ✓ | ✓ | |
|
||||
| kdl | ✓ | ✓ | ✓ | |
|
||||
| koka | ✓ | | ✓ | `koka` |
|
||||
| kotlin | ✓ | | | `kotlin-language-server` |
|
||||
| kotlin | ✓ | ✓ | ✓ | `kotlin-language-server` |
|
||||
| koto | ✓ | ✓ | ✓ | `koto-ls` |
|
||||
| latex | ✓ | ✓ | | `texlab` |
|
||||
| ld | ✓ | | ✓ | |
|
||||
@@ -127,6 +134,7 @@
|
||||
| log | ✓ | | | |
|
||||
| lpf | ✓ | | | |
|
||||
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
|
||||
| mail | ✓ | ✓ | | |
|
||||
| make | ✓ | | ✓ | |
|
||||
| markdoc | ✓ | | | `markdoc-ls` |
|
||||
| markdown | ✓ | | | `marksman`, `markdown-oxide` |
|
||||
@@ -138,11 +146,12 @@
|
||||
| mojo | ✓ | ✓ | ✓ | `magic` |
|
||||
| move | ✓ | | | |
|
||||
| msbuild | ✓ | | ✓ | |
|
||||
| nasm | ✓ | ✓ | | |
|
||||
| nasm | ✓ | ✓ | | `asm-lsp` |
|
||||
| nestedtext | ✓ | ✓ | ✓ | |
|
||||
| nginx | ✓ | | | |
|
||||
| nickel | ✓ | | ✓ | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
|
||||
| nix | ✓ | ✓ | | `nil`, `nixd` |
|
||||
| nix | ✓ | ✓ | ✓ | `nil`, `nixd` |
|
||||
| nu | ✓ | | | `nu` |
|
||||
| nunjucks | ✓ | | | |
|
||||
| ocaml | ✓ | | ✓ | `ocamllsp` |
|
||||
@@ -159,15 +168,15 @@
|
||||
| pest | ✓ | ✓ | ✓ | `pest-language-server` |
|
||||
| php | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| php-only | ✓ | | | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | `termux-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | `pkl-lsp` |
|
||||
| po | ✓ | ✓ | | |
|
||||
| pod | ✓ | | | |
|
||||
| ponylang | ✓ | ✓ | ✓ | |
|
||||
| powershell | ✓ | | | |
|
||||
| prisma | ✓ | ✓ | | `prisma-language-server` |
|
||||
| prolog | | | | `swipl` |
|
||||
| protobuf | ✓ | ✓ | ✓ | `bufls`, `pb` |
|
||||
| protobuf | ✓ | ✓ | ✓ | `buf`, `pb`, `protols` |
|
||||
| prql | ✓ | | | |
|
||||
| purescript | ✓ | ✓ | | `purescript-language-server` |
|
||||
| python | ✓ | ✓ | ✓ | `ruff`, `jedi-language-server`, `pylsp` |
|
||||
@@ -182,7 +191,7 @@
|
||||
| robot | ✓ | | | `robotframework_ls` |
|
||||
| ron | ✓ | | ✓ | |
|
||||
| rst | ✓ | | | |
|
||||
| ruby | ✓ | ✓ | ✓ | `solargraph` |
|
||||
| ruby | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
| sage | ✓ | ✓ | | |
|
||||
| scala | ✓ | ✓ | ✓ | `metals` |
|
||||
@@ -194,11 +203,12 @@
|
||||
| sml | ✓ | | | |
|
||||
| snakemake | ✓ | | ✓ | `pylsp` |
|
||||
| solidity | ✓ | ✓ | | `solc` |
|
||||
| sourcepawn | ✓ | ✓ | | `sourcepawn-studio` |
|
||||
| spade | ✓ | | ✓ | `spade-language-server` |
|
||||
| spicedb | ✓ | | | |
|
||||
| sql | ✓ | ✓ | | |
|
||||
| sshclientconfig | ✓ | | | |
|
||||
| starlark | ✓ | ✓ | | |
|
||||
| starlark | ✓ | ✓ | ✓ | `starpls` |
|
||||
| strace | ✓ | | | |
|
||||
| supercollider | ✓ | | | |
|
||||
| svelte | ✓ | | ✓ | `svelteserver` |
|
||||
@@ -209,19 +219,21 @@
|
||||
| tact | ✓ | ✓ | ✓ | |
|
||||
| task | ✓ | | | |
|
||||
| tcl | ✓ | | ✓ | |
|
||||
| teal | ✓ | | | |
|
||||
| teal | ✓ | | | `teal-language-server` |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| tera | ✓ | | | |
|
||||
| textproto | ✓ | ✓ | ✓ | |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
| thrift | ✓ | | | |
|
||||
| tlaplus | ✓ | | | |
|
||||
| todotxt | ✓ | | | |
|
||||
| toml | ✓ | ✓ | | `taplo` |
|
||||
| tsq | ✓ | | | |
|
||||
| tsq | ✓ | | | `ts_query_ls` |
|
||||
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| twig | ✓ | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typespec | ✓ | ✓ | ✓ | `tsp-server` |
|
||||
| typst | ✓ | | | `tinymist`, `typst-lsp` |
|
||||
| typst | ✓ | | | `tinymist` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | ✓ | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
@@ -233,14 +245,16 @@
|
||||
| vhs | ✓ | | | |
|
||||
| vue | ✓ | | | `vue-language-server` |
|
||||
| wast | ✓ | | | |
|
||||
| wat | ✓ | | | |
|
||||
| wat | ✓ | | | `wat_server` |
|
||||
| webc | ✓ | | | |
|
||||
| wgsl | ✓ | | | `wgsl_analyzer` |
|
||||
| werk | ✓ | | | |
|
||||
| wgsl | ✓ | | | `wgsl-analyzer` |
|
||||
| wit | ✓ | | ✓ | |
|
||||
| wren | ✓ | ✓ | ✓ | |
|
||||
| xit | ✓ | | | |
|
||||
| xml | ✓ | | ✓ | |
|
||||
| xtc | ✓ | | | |
|
||||
| yaml | ✓ | ✓ | ✓ | `yaml-language-server`, `ansible-language-server` |
|
||||
| yara | ✓ | | | `yls` |
|
||||
| yuck | ✓ | | | |
|
||||
| zig | ✓ | ✓ | ✓ | `zls` |
|
||||
|
@@ -100,6 +100,9 @@
|
||||
| `file_picker` | Open file picker | normal: `` <space>f ``, select: `` <space>f `` |
|
||||
| `file_picker_in_current_buffer_directory` | Open file picker at current buffer's directory | |
|
||||
| `file_picker_in_current_directory` | Open file picker at current working directory | normal: `` <space>F ``, select: `` <space>F `` |
|
||||
| `file_explorer` | Open file explorer in workspace root | normal: `` <space>e ``, select: `` <space>e `` |
|
||||
| `file_explorer_in_current_buffer_directory` | Open file explorer at current buffer's directory | normal: `` <space>E ``, select: `` <space>E `` |
|
||||
| `file_explorer_in_current_directory` | Open file explorer at current working directory | |
|
||||
| `code_action` | Perform code action | normal: `` <space>a ``, select: `` <space>a `` |
|
||||
| `buffer_picker` | Open buffer picker | normal: `` <space>b ``, select: `` <space>b `` |
|
||||
| `jumplist_picker` | Open jumplist picker | normal: `` <space>j ``, select: `` <space>j `` |
|
||||
@@ -236,6 +239,7 @@
|
||||
| `wonly` | Close windows except current | normal: `` <C-w>o ``, `` <space>wo ``, `` <C-w><C-o> ``, `` <space>w<C-o> ``, select: `` <C-w>o ``, `` <space>wo ``, `` <C-w><C-o> ``, `` <space>w<C-o> `` |
|
||||
| `select_register` | Select register | normal: `` " ``, select: `` " `` |
|
||||
| `insert_register` | Insert register | insert: `` <C-r> `` |
|
||||
| `copy_between_registers` | Copy between two registers | |
|
||||
| `align_view_middle` | Align view middle | normal: `` Zm ``, `` zm ``, select: `` Zm ``, `` zm `` |
|
||||
| `align_view_top` | Align view top | normal: `` Zt ``, `` zt ``, select: `` Zt ``, `` zt `` |
|
||||
| `align_view_center` | Align view center | normal: `` Zc ``, `` Zz ``, `` zc ``, `` zz ``, select: `` Zc ``, `` Zz ``, `` zc ``, `` zz `` |
|
||||
|
@@ -52,8 +52,8 @@
|
||||
| `: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 servers used by the current doc |
|
||||
| `:lsp-stop` | Stops the language servers that are used by the current doc |
|
||||
| `:lsp-restart` | Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied |
|
||||
| `:lsp-stop` | Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied |
|
||||
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
|
||||
| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. |
|
||||
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
|
||||
@@ -67,10 +67,9 @@
|
||||
| `:goto`, `:g` | Goto line number. |
|
||||
| `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). |
|
||||
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
|
||||
| `:toggle-option`, `:toggle` | Toggle a boolean config option at runtime.<br>For example to toggle smart case search, use `:toggle search.smart-case`. |
|
||||
| `:toggle-option`, `:toggle` | Toggle a config option at runtime.<br>For example to toggle smart case search, use `:toggle search.smart-case`. |
|
||||
| `:get-option`, `:get` | Get the current value of a config option. |
|
||||
| `:sort` | Sort ranges in selection. |
|
||||
| `:rsort` | Sort ranges in selection in reverse order. |
|
||||
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
|
||||
| `:tree-sitter-subtree`, `:ts-subtree` | Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries. |
|
||||
| `:config-reload` | Refresh user config. |
|
||||
@@ -88,3 +87,5 @@
|
||||
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
|
||||
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
|
||||
| `:read`, `:r` | Load a file into buffer |
|
||||
| `:echo` | Prints the given arguments to the statusline. |
|
||||
| `:noop` | Does nothing. |
|
||||
|
@@ -34,16 +34,10 @@ below.
|
||||
2. Create a new directory for the language with the path
|
||||
`runtime/queries/<name>/`.
|
||||
3. Refer to the
|
||||
[tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
|
||||
[tree-sitter website](https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting.html#highlights)
|
||||
for more information on writing queries.
|
||||
4. A list of highlight captures can be found [on the themes page](https://docs.helix-editor.com/themes.html#scopes).
|
||||
|
||||
> 💡 In Helix, the first matching query takes precedence when evaluating
|
||||
> queries, which is different from other editors such as Neovim where the last
|
||||
> matching query supersedes the ones before it. See
|
||||
> [this issue](https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090)
|
||||
> for an example.
|
||||
|
||||
## Common issues
|
||||
|
||||
- If you encounter errors when running Helix after switching branches, you may
|
||||
|
@@ -57,4 +57,4 @@ second argument (a string).
|
||||
- `#any-of?` (standard):
|
||||
The first argument (a capture) must be one of the other arguments (strings).
|
||||
|
||||
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
|
||||
[upstream-docs]: https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting.html#language-injection
|
||||
|
@@ -44,6 +44,6 @@ in its `textobjects.scm` file, function navigation should also work automaticall
|
||||
`function.movement` should be defined only if the node captured by `function.around`
|
||||
doesn't make sense in a navigation context.
|
||||
|
||||
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
|
||||
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
|
||||
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html
|
||||
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers/queries/2-operators.html#capturing-nodes
|
||||
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/textobjects.scm&type=Code&ref=advsearch&l=&l=
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# Installing Helix
|
||||
|
||||
To install Helix, follow the instructions specific to your operating system.
|
||||
The typical way to install Helix is via [your operating system's package manager](./package-managers.md).
|
||||
|
||||
Note that:
|
||||
|
||||
- To get the latest nightly version of Helix, you need to
|
||||
@@ -8,7 +9,7 @@ Note that:
|
||||
|
||||
- To take full advantage of Helix, install the language servers for your
|
||||
preferred programming languages. See the
|
||||
[wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
|
||||
[wiki](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations)
|
||||
for instructions.
|
||||
|
||||
## Pre-built binaries
|
||||
|
@@ -67,8 +67,9 @@ These configuration keys are available:
|
||||
| `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 |
|
||||
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
|
||||
| `soft-wrap` | [editor.softwrap](./editor.md#editorsoft-wrap-section)
|
||||
| `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` |
|
||||
| `rulers` | Overrides the `editor.rulers` config key for the language. |
|
||||
| `path-completion` | Overrides the `editor.path-completion` config key for the language. |
|
||||
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
|
||||
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
|
||||
@@ -241,4 +242,4 @@ use-grammars = { except = [ "yaml", "json" ] }
|
||||
|
||||
When omitted, all grammars are fetched and built.
|
||||
|
||||
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
|
||||
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting.html#language-injection
|
||||
|
32
book/src/other-software.md
Normal file
32
book/src/other-software.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Helix mode in other software
|
||||
|
||||
Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adopt if it can be used consistently in many editing contexts. Yet, certain use cases cannot easily be addressed directly in Helix. Similar to vim, this leads to the creation of "Helix mode" in various other software products, allowing Helix-style editing for a greater variety of use cases.
|
||||
|
||||
"Helix mode" is frequently still in early stages or missing entirely. For such cases, we also link to relevant bugs or discussions.
|
||||
|
||||
## Other editors
|
||||
|
||||
| Editor | Plugin or feature providing Helix editing | Comments
|
||||
| --- | --- | --- |
|
||||
| [Vim](https://www.vim.org/) | [helix.vim](https://github.com/chtenb/helix.vim) config |
|
||||
| [IntelliJ IDEA](https://www.jetbrains.com/idea/) / [Android Studio](https://developer.android.com/studio)| [IdeaVim](https://plugins.jetbrains.com/plugin/164-ideavim) plugin + [helix.idea.vim](https://github.com/chtenb/helix.vim) config | Minimum recommended version is IdeaVim 2.19.0.
|
||||
| [Visual Studio Code](https://code.visualstudio.com/) | [Dance](https://marketplace.visualstudio.com/items?itemName=gregoire.dance) extension, or its [Helix fork](https://marketplace.visualstudio.com/items?itemName=kend.dancehelixkey) | The Helix fork has diverged. You can also use the original Dance and tweak its keybindings directly (try [this config](https://github.com/71/dance/issues/299#issuecomment-1655509531)).
|
||||
| [Visual Studio Code](https://code.visualstudio.com/) | [Helix for VS Code](https://marketplace.visualstudio.com/items?itemName=jasew.vscode-helix-emulation) extension|
|
||||
| [Zed](https://zed.dev/) | native via keybindings ([Bug](https://github.com/zed-industries/zed/issues/4642)) |
|
||||
| [CodeMirror](https://codemirror.net/) | [codemirror-helix](https://gitlab.com/_rvidal/codemirror-helix) |
|
||||
|
||||
|
||||
## Shells
|
||||
|
||||
| Shell | Plugin or feature providing Helix editing
|
||||
| --- | ---
|
||||
| Fish | [Feature Request](https://github.com/fish-shell/fish-shell/issues/7748)
|
||||
| Fish | [fish-helix](https://github.com/sshilovsky/fish-helix/tree/main)
|
||||
| Zsh | [helix-zsh](https://github.com/john-h-k/helix-zsh)
|
||||
| Nushell | [Feature Request](https://github.com/nushell/reedline/issues/639)
|
||||
|
||||
## Other software
|
||||
|
||||
| Software | Plugin or feature providing Helix editing. | Comments
|
||||
| --- | --- | --- |
|
||||
| [Obsidian](https://obsidian.md/) | [Obsidian-Helix](https://github.com/Sinono3/obsidian-helix) | Uses `codemirror-helix` listed above.
|
@@ -1,7 +1,8 @@
|
||||
## Package managers
|
||||
|
||||
- [Linux](#linux)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Ubuntu/Debian](#ubuntudebian)
|
||||
- [Ubuntu (PPA)](#ubuntu-ppa)
|
||||
- [Fedora/RHEL](#fedorarhel)
|
||||
- [Arch Linux extra](#arch-linux-extra)
|
||||
- [NixOS](#nixos)
|
||||
@@ -23,7 +24,14 @@
|
||||
|
||||
The following third party repositories are available:
|
||||
|
||||
### Ubuntu
|
||||
### Ubuntu/Debian
|
||||
|
||||
Install the Debian package from the release page.
|
||||
|
||||
If you are running a system older than Ubuntu 22.04, Mint 21, or Debian 12, you can build the `.deb` file locally
|
||||
[from source](./building-from-source.md#building-the-debian-package).
|
||||
|
||||
### Ubuntu (PPA)
|
||||
|
||||
Add the `PPA` for Helix:
|
||||
|
||||
@@ -121,7 +129,7 @@ brew install helix
|
||||
### MacPorts
|
||||
|
||||
```sh
|
||||
port install helix
|
||||
sudo port install helix
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
@@ -136,7 +136,7 @@ The following is a list of scopes available to use for styling:
|
||||
|
||||
#### Syntax highlighting
|
||||
|
||||
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme).
|
||||
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting.html#highlights).
|
||||
|
||||
When determining styling for a highlight, the longest matching theme key will be used. For example, if the highlight is `function.builtin.static`, the key `function.builtin` will be used instead of `function`.
|
||||
|
||||
@@ -336,5 +336,6 @@ These scopes are used for theming the editor interface:
|
||||
| `diagnostic.error` | Diagnostics error (editing area) |
|
||||
| `diagnostic.unnecessary` | Diagnostics with unnecessary tag (editing area) |
|
||||
| `diagnostic.deprecated` | Diagnostics with deprecated tag (editing area) |
|
||||
| `tabstop` | Snippet placeholder |
|
||||
|
||||
[editor-section]: ./configuration.md#editor-section
|
||||
|
@@ -25,7 +25,7 @@ Inspired by [Kakoune](http://kakoune.org/), Helix follows the `selection → act
|
||||
|
||||
## Multiple selections
|
||||
|
||||
Also inspired by Kakoune, multiple selections are a core mode of interaction in Helix. For example, the standard way of replacing multiple instance of a word is to first select all instances (so there is one selection per instance) and then use the change action (`c`) to edit them all at the same time.
|
||||
Also inspired by Kakoune, multiple selections are a core mode of interaction in Helix. For example, the standard way of replacing multiple instances of a word is to first select all instances (so there is one selection per instance) and then use the change action (`c`) to edit them all at the same time.
|
||||
|
||||
## Motions
|
||||
|
||||
|
@@ -47,6 +47,12 @@
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<releases>
|
||||
<release version="25.01.1" date="2025-01-19">
|
||||
<url>https://github.com/helix-editor/helix/releases/tag/25.01.1</url>
|
||||
</release>
|
||||
<release version="25.01" date="2025-01-03">
|
||||
<url>https://helix-editor.com/news/release-25-01-highlights/</url>
|
||||
</release>
|
||||
<release version="24.07" date="2024-07-14">
|
||||
<url>https://github.com/helix-editor/helix/releases/tag/24.07</url>
|
||||
</release>
|
||||
|
@@ -9,23 +9,23 @@ _hx() {
|
||||
|
||||
case "$prev" in
|
||||
-g | --grammar)
|
||||
COMPREPLY=($(compgen -W 'fetch build' -- "$cur"))
|
||||
mapfile -t COMPREPLY < <(compgen -W 'fetch build' -- "$cur")
|
||||
return 0
|
||||
;;
|
||||
--health)
|
||||
languages=$(hx --health | tail -n '+7' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g')
|
||||
COMPREPLY=($(compgen -W """$languages""" -- "$cur"))
|
||||
mapfile -t COMPREPLY < <(compgen -W """$languages""" -- "$cur")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$2" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2"""))
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2""")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=($(compgen -fd -- """$2"""))
|
||||
mapfile -t COMPREPLY < <(compgen -fd -- """$2""")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
@@ -20,7 +20,7 @@ var config = [ "--config" "-c" ]
|
||||
set edit:completion:arg-completer[hx] = {|@args|
|
||||
var n = (count $args)
|
||||
if (>= $n 3) {
|
||||
# Stop completions if passed arg will take presedence
|
||||
# Stop completions if passed arg will take precedence
|
||||
# and invalidate further input
|
||||
if (has-value $skips $args[-2]) {
|
||||
return
|
||||
|
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# NOTE: the `+N` syntax is not supported in Nushell (https://github.com/nushell/nushell/issues/13418)
|
||||
# so it has not been specified here and will not be proposed in the autocompletion of Nushell.
|
||||
# The help message won't be overriden though, so it will still be present here
|
||||
# The help message won't be overridden though, so it will still be present here
|
||||
|
||||
def health_categories [] {
|
||||
let languages = ^hx --health languages | detect columns | get Language | filter { $in != null }
|
||||
|
3
contrib/hx_launcher.sh
Executable file
3
contrib/hx_launcher.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
HELIX_RUNTIME=/usr/lib/helix/runtime exec /usr/lib/helix/hx "$@"
|
90
default.nix
90
default.nix
@@ -1,8 +1,84 @@
|
||||
# Flake's default package for non-flake-enabled nix instances
|
||||
let
|
||||
compat = builtins.fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz";
|
||||
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
|
||||
};
|
||||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
callPackage,
|
||||
runCommand,
|
||||
installShellFiles,
|
||||
git,
|
||||
gitRev ? null,
|
||||
...
|
||||
}: let
|
||||
fs = lib.fileset;
|
||||
|
||||
src = fs.difference (fs.gitTracked ./.) (fs.unions [
|
||||
./.envrc
|
||||
./rustfmt.toml
|
||||
./screenshot.png
|
||||
./book
|
||||
./docs
|
||||
./runtime
|
||||
./flake.lock
|
||||
(fs.fileFilter (file: lib.strings.hasInfix ".git" file.name) ./.)
|
||||
(fs.fileFilter (file: file.hasExt "svg") ./.)
|
||||
(fs.fileFilter (file: file.hasExt "md") ./.)
|
||||
(fs.fileFilter (file: file.hasExt "nix") ./.)
|
||||
]);
|
||||
|
||||
# Next we actually need to build the grammars and the runtime directory
|
||||
# that they reside in. It is built by calling the derivation in the
|
||||
# grammars.nix file, then taking the runtime directory in the git repo
|
||||
# and hooking symlinks up to it.
|
||||
grammars = callPackage ./grammars.nix {};
|
||||
runtimeDir = runCommand "helix-runtime" {} ''
|
||||
mkdir -p $out
|
||||
ln -s ${./runtime}/* $out
|
||||
rm -r $out/grammars
|
||||
ln -s ${grammars} $out/grammars
|
||||
'';
|
||||
in
|
||||
(import compat {src = ./.;}).defaultNix
|
||||
rustPlatform.buildRustPackage (self: {
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
# This is not allowed in nixpkgs but is very convenient here: it allows us to
|
||||
# avoid specifying `outputHashes` here for any git dependencies we might take
|
||||
# on temporarily.
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
git
|
||||
];
|
||||
|
||||
buildType = "release";
|
||||
|
||||
name = with builtins; (fromTOML (readFile ./helix-term/Cargo.toml)).package.name;
|
||||
src = fs.toSource {
|
||||
root = ./.;
|
||||
fileset = src;
|
||||
};
|
||||
|
||||
# Helix attempts to reach out to the network and get the grammars. Nix doesn't allow this.
|
||||
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
|
||||
|
||||
# So Helix knows what rev it is.
|
||||
HELIX_NIX_BUILD_REV = gitRev;
|
||||
|
||||
doCheck = false;
|
||||
strictDeps = true;
|
||||
|
||||
# Sets the Helix runtime dir to the grammars
|
||||
env.HELIX_DEFAULT_RUNTIME = "${runtimeDir}";
|
||||
|
||||
# Get all the application stuff in the output directory.
|
||||
postInstall = ''
|
||||
mkdir -p $out/lib
|
||||
installShellCompletion ${./contrib/completion}/hx.{bash,fish,zsh}
|
||||
mkdir -p $out/share/{applications,icons/hicolor/{256x256,scalable}/apps}
|
||||
cp ${./contrib/Helix.desktop} $out/share/applications
|
||||
cp ${./logo.svg} $out/share/icons/hicolor/scalable/apps/helix.svg
|
||||
cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps
|
||||
'';
|
||||
|
||||
meta.mainProgram = "hx";
|
||||
})
|
||||
|
@@ -15,6 +15,8 @@ Some suggestions to get started:
|
||||
- If your preferred language is missing, integrating a tree-sitter grammar for
|
||||
it and defining syntax highlight queries for it is straight forward and
|
||||
doesn't require much knowledge of the internals.
|
||||
- If you don't use the Nix development shell and are getting your rust-analyzer binary from rustup, you may need to run `rustup component add rust-analyzer`.
|
||||
This is because `rust-toolchain.toml` selects our MSRV for the development toolchain but doesn't download the matching rust-analyzer automatically.
|
||||
|
||||
We provide an [architecture.md][architecture.md] that should give you
|
||||
a good overview of the internals.
|
||||
|
@@ -16,7 +16,7 @@ being published.
|
||||
* Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html)
|
||||
* Tag and push
|
||||
* Switch to master and pull
|
||||
* `git tag -s -m "<tag>" -a <tag> && git push` (note the `-s` which signs the tag)
|
||||
* `git tag -s -m "<tag>" -a <tag> && git push origin <tag>` (note the `-s` which signs the tag)
|
||||
* Wait for the Release CI to finish
|
||||
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
|
||||
* Edit the new release
|
||||
|
34
flake.lock
generated
34
flake.lock
generated
@@ -1,30 +1,15 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1727974419,
|
||||
"narHash": "sha256-WD0//20h+2/yPGkO88d2nYbb23WMWYvnRyDQ9Dx4UHg=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "37e4f9f0976cb9281cd3f0c70081e5e0ecaee93f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -35,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1728018373,
|
||||
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
|
||||
"lastModified": 1740560979,
|
||||
"narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
|
||||
"rev": "5135c59491985879812717f4c9fea69604e7f26f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -51,7 +36,6 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
@@ -64,11 +48,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728268235,
|
||||
"narHash": "sha256-lJMFnMO4maJuNO6PQ5fZesrTmglze3UFTTBuKGwR1Nw=",
|
||||
"lastModified": 1740623427,
|
||||
"narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "25685cc2c7054efc31351c172ae77b21814f2d42",
|
||||
"rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
204
flake.nix
204
flake.nix
@@ -8,181 +8,79 @@
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane.url = "github:ipetkov/crane";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
crane,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
}: let
|
||||
gitRev = self.rev or self.dirtyRev or null;
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [(import rust-overlay)];
|
||||
};
|
||||
mkRootPath = rel:
|
||||
builtins.path {
|
||||
path = "${toString ./.}/${rel}";
|
||||
name = rel;
|
||||
};
|
||||
filteredSource = let
|
||||
pathsToIgnore = [
|
||||
".envrc"
|
||||
".ignore"
|
||||
".github"
|
||||
".gitignore"
|
||||
"logo_dark.svg"
|
||||
"logo_light.svg"
|
||||
"rust-toolchain.toml"
|
||||
"rustfmt.toml"
|
||||
"runtime"
|
||||
"screenshot.png"
|
||||
"book"
|
||||
"docs"
|
||||
"README.md"
|
||||
"CHANGELOG.md"
|
||||
"shell.nix"
|
||||
"default.nix"
|
||||
"grammars.nix"
|
||||
"flake.nix"
|
||||
"flake.lock"
|
||||
];
|
||||
ignorePaths = path: type: let
|
||||
inherit (nixpkgs) lib;
|
||||
# split the nix store path into its components
|
||||
components = lib.splitString "/" path;
|
||||
# drop off the `/nix/hash-source` section from the path
|
||||
relPathComponents = lib.drop 4 components;
|
||||
# reassemble the path components
|
||||
relPath = lib.concatStringsSep "/" relPathComponents;
|
||||
in
|
||||
lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
|
||||
in
|
||||
builtins.path {
|
||||
name = "helix-source";
|
||||
path = toString ./.;
|
||||
# filter out unnecessary paths
|
||||
filter = ignorePaths;
|
||||
};
|
||||
makeOverridableHelix = old: config: let
|
||||
grammars = pkgs.callPackage ./grammars.nix config;
|
||||
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
|
||||
mkdir -p $out
|
||||
ln -s ${mkRootPath "runtime"}/* $out
|
||||
rm -r $out/grammars
|
||||
ln -s ${grammars} $out/grammars
|
||||
'';
|
||||
helix-wrapped =
|
||||
pkgs.runCommand
|
||||
old.name
|
||||
{
|
||||
inherit (old) pname version;
|
||||
meta = old.meta or {};
|
||||
passthru =
|
||||
(old.passthru or {})
|
||||
// {
|
||||
unwrapped = old;
|
||||
};
|
||||
nativeBuildInputs = [pkgs.makeWrapper];
|
||||
makeWrapperArgs = config.makeWrapperArgs or [];
|
||||
}
|
||||
''
|
||||
cp -rs --no-preserve=mode,ownership ${old} $out
|
||||
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
|
||||
'';
|
||||
in
|
||||
helix-wrapped
|
||||
// {
|
||||
override = makeOverridableHelix old;
|
||||
passthru =
|
||||
helix-wrapped.passthru
|
||||
// {
|
||||
wrapper = old: makeOverridableHelix old config;
|
||||
};
|
||||
};
|
||||
stdenv =
|
||||
if pkgs.stdenv.isLinux
|
||||
then pkgs.stdenv
|
||||
else pkgs.clangStdenv;
|
||||
rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment --cfg tokio_unstable";
|
||||
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
|
||||
commonArgs = {
|
||||
inherit stdenv;
|
||||
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname;
|
||||
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version;
|
||||
src = filteredSource;
|
||||
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
|
||||
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
|
||||
buildInputs = [stdenv.cc.cc.lib];
|
||||
nativeBuildInputs = [pkgs.installShellFiles];
|
||||
# disable tests
|
||||
doCheck = false;
|
||||
meta.mainProgram = "hx";
|
||||
|
||||
# Get Helix's MSRV toolchain to build with by default.
|
||||
msrvToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
msrvPlatform = pkgs.makeRustPlatform {
|
||||
cargo = msrvToolchain;
|
||||
rustc = msrvToolchain;
|
||||
};
|
||||
cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs;
|
||||
in {
|
||||
packages = {
|
||||
helix-unwrapped = craneLibStable.buildPackage (commonArgs
|
||||
// {
|
||||
cargoArtifacts = craneLibStable.buildDepsOnly commonArgs;
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps
|
||||
cp contrib/Helix.desktop $out/share/applications
|
||||
cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
|
||||
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
|
||||
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
|
||||
'';
|
||||
});
|
||||
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
|
||||
default = self.packages.${system}.helix;
|
||||
packages = rec {
|
||||
helix = pkgs.callPackage ./default.nix {inherit gitRev;};
|
||||
|
||||
/**
|
||||
The default Helix build. Uses the latest stable Rust toolchain, and unstable
|
||||
nixpkgs.
|
||||
|
||||
The build inputs can be overriden with the following:
|
||||
|
||||
packages.${system}.default.override { rustPlatform = newPlatform; };
|
||||
|
||||
Overriding a derivation attribute can be done as well:
|
||||
|
||||
packages.${system}.default.overrideAttrs { buildType = "debug"; };
|
||||
*/
|
||||
default = helix;
|
||||
};
|
||||
|
||||
checks = {
|
||||
# Build the crate itself
|
||||
inherit (self.packages.${system}) helix;
|
||||
|
||||
clippy = craneLibMSRV.cargoClippy (commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||
});
|
||||
|
||||
fmt = craneLibMSRV.cargoFmt commonArgs;
|
||||
|
||||
doc = craneLibMSRV.cargoDoc (commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
|
||||
test = craneLibMSRV.cargoTest (commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
checks.helix = self.outputs.packages.${system}.helix.override {
|
||||
buildType = "debug";
|
||||
rustPlatform = msrvPlatform;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = builtins.attrValues self.checks.${system};
|
||||
nativeBuildInputs = with pkgs;
|
||||
[lld_13 cargo-flamegraph rust-analyzer]
|
||||
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
|
||||
++ (lib.optional stdenv.isLinux pkgs.lldb)
|
||||
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
|
||||
shellHook = ''
|
||||
export HELIX_RUNTIME="$PWD/runtime"
|
||||
export RUST_BACKTRACE="1"
|
||||
export RUSTFLAGS="''${RUSTFLAGS:-""} ${rustFlagsEnv}"
|
||||
'';
|
||||
};
|
||||
# Devshell behavior is preserved.
|
||||
devShells.default = let
|
||||
commonRustFlagsEnv = "-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable";
|
||||
platformRustFlagsEnv = pkgs.lib.optionalString pkgs.stdenv.isLinux "-Clink-arg=-Wl,--no-rosegment";
|
||||
in
|
||||
pkgs.mkShell
|
||||
{
|
||||
inputsFrom = [self.checks.${system}.helix];
|
||||
nativeBuildInputs = with pkgs;
|
||||
[
|
||||
lld
|
||||
cargo-flamegraph
|
||||
rust-bin.nightly.latest.rust-analyzer
|
||||
]
|
||||
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin)
|
||||
++ (lib.optional stdenv.isLinux lldb)
|
||||
++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation);
|
||||
shellHook = ''
|
||||
export RUST_BACKTRACE="1"
|
||||
export RUSTFLAGS="''${RUSTFLAGS:-""} ${commonRustFlagsEnv} ${platformRustFlagsEnv}"
|
||||
'';
|
||||
};
|
||||
})
|
||||
// {
|
||||
overlays.default = final: prev: {
|
||||
inherit (self.packages.${final.system}) helix;
|
||||
helix = final.callPackage ./default.nix {inherit gitRev;};
|
||||
};
|
||||
};
|
||||
|
||||
|
34
grammars.nix
34
grammars.nix
@@ -32,10 +32,10 @@
|
||||
# If `use-grammars.except` is set, use all other grammars.
|
||||
# Otherwise use all grammars.
|
||||
useGrammar = grammar:
|
||||
if languagesConfig?use-grammars.only then
|
||||
builtins.elem grammar.name languagesConfig.use-grammars.only
|
||||
else if languagesConfig?use-grammars.except then
|
||||
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
|
||||
if languagesConfig ? use-grammars.only
|
||||
then builtins.elem grammar.name languagesConfig.use-grammars.only
|
||||
else if languagesConfig ? use-grammars.except
|
||||
then !(builtins.elem grammar.name languagesConfig.use-grammars.except)
|
||||
else true;
|
||||
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
|
||||
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
|
||||
@@ -66,10 +66,10 @@
|
||||
version = grammar.source.rev;
|
||||
|
||||
src = source;
|
||||
sourceRoot = if builtins.hasAttr "subpath" grammar.source then
|
||||
"source/${grammar.source.subpath}"
|
||||
else
|
||||
"source";
|
||||
sourceRoot =
|
||||
if builtins.hasAttr "subpath" grammar.source
|
||||
then "source/${grammar.source.subpath}"
|
||||
else "source";
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
@@ -116,17 +116,21 @@
|
||||
'';
|
||||
};
|
||||
grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
|
||||
builtGrammars = builtins.map (grammar: {
|
||||
inherit (grammar) name;
|
||||
value = buildGrammar grammar;
|
||||
}) grammarsToBuild;
|
||||
builtGrammars =
|
||||
builtins.map (grammar: {
|
||||
inherit (grammar) name;
|
||||
value = buildGrammar grammar;
|
||||
})
|
||||
grammarsToBuild;
|
||||
extensibleGrammars =
|
||||
lib.makeExtensible (self: builtins.listToAttrs builtGrammars);
|
||||
overlayedGrammars = lib.pipe extensibleGrammars
|
||||
overlaidGrammars =
|
||||
lib.pipe extensibleGrammars
|
||||
(builtins.map (overlay: grammar: grammar.extend overlay) grammarOverlays);
|
||||
grammarLinks = lib.mapAttrsToList
|
||||
grammarLinks =
|
||||
lib.mapAttrsToList
|
||||
(name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
|
||||
(lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
|
||||
(lib.filterAttrs (n: v: lib.isDerivation v) overlaidGrammars);
|
||||
in
|
||||
runCommand "consolidated-helix-grammars" {} ''
|
||||
mkdir -p $out
|
||||
|
@@ -20,10 +20,10 @@ helix-stdx = { path = "../helix-stdx" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
smallvec = "1.13"
|
||||
ropey.workspace = true
|
||||
smallvec = "1.14"
|
||||
smartstring = "1.0.1"
|
||||
unicode-segmentation = "1.12"
|
||||
unicode-segmentation.workspace = true
|
||||
# unicode-width is changing width definitions
|
||||
# that both break our logic and disagree with common
|
||||
# width definitions in terminals, we need to replace it.
|
||||
@@ -33,13 +33,12 @@ unicode-width = "=0.1.12"
|
||||
unicode-general-category = "1.0"
|
||||
slotmap.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.20"
|
||||
once_cell = "1.21"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
bitflags = "2.6"
|
||||
bitflags.workspace = true
|
||||
ahash = "0.8.11"
|
||||
hashbrown = { version = "0.14.5", features = ["raw"] }
|
||||
dunce = "1.0"
|
||||
url = "2.5.4"
|
||||
|
||||
log = "0.4"
|
||||
@@ -48,20 +47,19 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
|
||||
imara-diff = "0.1.7"
|
||||
imara-diff = "0.1.8"
|
||||
|
||||
encoding_rs = "0.8"
|
||||
|
||||
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
|
||||
|
||||
etcetera = "0.8"
|
||||
textwrap = "0.16.1"
|
||||
textwrap = "0.16.2"
|
||||
|
||||
nucleo.workspace = true
|
||||
parking_lot = "0.12"
|
||||
globset = "0.4.15"
|
||||
regex-cursor = "0.1.4"
|
||||
parking_lot.workspace = true
|
||||
globset = "0.4.16"
|
||||
regex-cursor = "0.1.5"
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = { version = "1", default-features = false }
|
||||
indoc = "2.0.5"
|
||||
indoc = "2.0.6"
|
||||
|
1270
helix-core/src/command_line.rs
Normal file
1270
helix-core/src/command_line.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -204,13 +204,9 @@ pub fn find_block_comments(
|
||||
range: *range,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_margin: selection_slice
|
||||
.get_char(after_start)
|
||||
.map_or(false, |c| c == ' '),
|
||||
start_margin: selection_slice.get_char(after_start) == Some(' '),
|
||||
end_margin: after_start != before_end
|
||||
&& selection_slice
|
||||
.get_char(before_end)
|
||||
.map_or(false, |c| c == ' '),
|
||||
&& (selection_slice.get_char(before_end) == Some(' ')),
|
||||
start_token: start_token.to_string(),
|
||||
end_token: end_token.to_string(),
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Transaction;
|
||||
use crate::{diagnostic::LanguageServerId, Transaction};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct CompletionItem {
|
||||
@@ -8,5 +8,18 @@ pub struct CompletionItem {
|
||||
pub label: Cow<'static, str>,
|
||||
pub kind: Cow<'static, str>,
|
||||
/// Containing Markdown
|
||||
pub documentation: String,
|
||||
pub documentation: Option<String>,
|
||||
pub provider: CompletionProvider,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum CompletionProvider {
|
||||
Lsp(LanguageServerId),
|
||||
Path,
|
||||
}
|
||||
|
||||
impl From<LanguageServerId> for CompletionProvider {
|
||||
fn from(id: LanguageServerId) -> Self {
|
||||
CompletionProvider::Lsp(id)
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
//! LSP diagnostic utility types.
|
||||
use std::fmt;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
pub use helix_stdx::range::Range;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,8 +50,35 @@ pub struct Diagnostic {
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// TODO turn this into an enum + feature flag when lsp becomes optional
|
||||
pub type DiagnosticProvider = LanguageServerId;
|
||||
/// The source of a diagnostic.
|
||||
///
|
||||
/// This type is cheap to clone: all data is either `Copy` or wrapped in an `Arc`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum DiagnosticProvider {
|
||||
Lsp {
|
||||
/// The ID of the language server which sent the diagnostic.
|
||||
server_id: LanguageServerId,
|
||||
/// An optional identifier under which diagnostics are managed by the client.
|
||||
///
|
||||
/// `identifier` is a field from the LSP "Pull Diagnostics" feature meant to provide an
|
||||
/// optional "namespace" for diagnostics: a language server can respond to a diagnostics
|
||||
/// pull request with an identifier and these diagnostics should be treated as separate
|
||||
/// from push diagnostics. Rust-analyzer uses this feature for example to provide Cargo
|
||||
/// diagnostics with push and internal diagnostics with pull. The push diagnostics should
|
||||
/// not clear the pull diagnostics and vice-versa.
|
||||
identifier: Option<Arc<str>>,
|
||||
},
|
||||
// Future internal features can go here...
|
||||
}
|
||||
|
||||
impl DiagnosticProvider {
|
||||
pub fn language_server_id(&self) -> Option<LanguageServerId> {
|
||||
match self {
|
||||
Self::Lsp { server_id, .. } => Some(*server_id),
|
||||
// _ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// while I would prefer having this in helix-lsp that necessitates a bunch of
|
||||
// conversions I would rather not add. I think its fine since this just a very
|
||||
|
@@ -19,10 +19,12 @@ mod test;
|
||||
|
||||
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
|
||||
|
||||
use helix_stdx::rope::{RopeGraphemes, RopeSliceExt};
|
||||
|
||||
use crate::graphemes::{Grapheme, GraphemeStr};
|
||||
use crate::syntax::Highlight;
|
||||
use crate::text_annotations::TextAnnotations;
|
||||
use crate::{Position, RopeGraphemes, RopeSlice};
|
||||
use crate::{Position, RopeSlice};
|
||||
|
||||
/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -219,7 +221,7 @@ impl<'t> DocumentFormatter<'t> {
|
||||
text_fmt,
|
||||
annotations,
|
||||
visual_pos: Position { row: 0, col: 0 },
|
||||
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
|
||||
graphemes: text.slice(block_char_idx..).graphemes(),
|
||||
char_pos: block_char_idx,
|
||||
exhausted: false,
|
||||
indent_level: None,
|
||||
@@ -370,8 +372,8 @@ impl<'t> DocumentFormatter<'t> {
|
||||
match col.cmp(&(self.text_fmt.viewport_width as usize)) {
|
||||
// The EOF char and newline chars are always selectable in helix. That means
|
||||
// that wrapping happens "too-early" if a word fits a line perfectly. This
|
||||
// is intentional so that all selectable graphemes are always visisble (and
|
||||
// therefore the cursor never dissapears). However if the user manually set a
|
||||
// is intentional so that all selectable graphemes are always visible (and
|
||||
// therefore the cursor never disappears). However if the user manually set a
|
||||
// lower softwrap width then this is undesirable. Just increasing the viewport-
|
||||
// width by one doesn't work because if a line is wrapped multiple times then
|
||||
// some words may extend past the specified width.
|
||||
@@ -380,9 +382,10 @@ impl<'t> DocumentFormatter<'t> {
|
||||
// by a newline/eof character here.
|
||||
Ordering::Equal
|
||||
if self.text_fmt.soft_wrap_at_text_width
|
||||
&& self.peek_grapheme(col, char_pos).map_or(false, |grapheme| {
|
||||
grapheme.is_newline() || grapheme.is_eof()
|
||||
}) => {}
|
||||
&& self
|
||||
.peek_grapheme(col, char_pos)
|
||||
.is_some_and(|grapheme| grapheme.is_newline() || grapheme.is_eof()) => {
|
||||
}
|
||||
Ordering::Equal if word_width > self.text_fmt.max_wrap as usize => return,
|
||||
Ordering::Greater if word_width > self.text_fmt.max_wrap as usize => {
|
||||
self.peeked_grapheme = self.word_buf.pop();
|
||||
|
@@ -102,6 +102,14 @@ fn long_word_softwrap() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_multichar_grapheme() {
|
||||
assert_eq!(
|
||||
softwrap_text("xxxx xxxx xxx a\u{0301}bc\n"),
|
||||
"xxxx xxxx xxx \n.ábc \n "
|
||||
)
|
||||
}
|
||||
|
||||
fn softwrap_text_at_text_width(text: &str) -> String {
|
||||
let mut text_fmt = TextFormat::new_test(true);
|
||||
text_fmt.soft_wrap_at_text_width = true;
|
||||
|
333
helix-core/src/editor_config.rs
Normal file
333
helix-core/src/editor_config.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
//! Support for [EditorConfig](https://EditorConfig.org) configuration loading.
|
||||
//!
|
||||
//! EditorConfig is an editor-agnostic format for specifying configuration in an INI-like, human
|
||||
//! friendly syntax in `.editorconfig` files (which are intended to be checked into VCS). This
|
||||
//! module provides functions to search for all `.editorconfig` files that apply to a given path
|
||||
//! and returns an `EditorConfig` type containing any specified configuration options.
|
||||
//!
|
||||
//! At time of writing, this module follows the [spec](https://spec.editorconfig.org/) at
|
||||
//! version 0.17.2.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
num::{NonZeroU16, NonZeroU8},
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
use globset::{GlobBuilder, GlobMatcher};
|
||||
|
||||
use crate::{
|
||||
indent::{IndentStyle, MAX_INDENT},
|
||||
LineEnding,
|
||||
};
|
||||
|
||||
/// Configuration declared for a path in `.editorconfig` files.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct EditorConfig {
|
||||
pub indent_style: Option<IndentStyle>,
|
||||
pub tab_width: Option<NonZeroU8>,
|
||||
pub line_ending: Option<LineEnding>,
|
||||
pub encoding: Option<&'static Encoding>,
|
||||
// pub spelling_language: Option<SpellingLanguage>,
|
||||
pub trim_trailing_whitespace: Option<bool>,
|
||||
pub insert_final_newline: Option<bool>,
|
||||
pub max_line_length: Option<NonZeroU16>,
|
||||
}
|
||||
|
||||
impl EditorConfig {
|
||||
/// Finds any configuration in `.editorconfig` files which applies to the given path.
|
||||
///
|
||||
/// If no configuration applies then `EditorConfig::default()` is returned.
|
||||
pub fn find(path: &Path) -> Self {
|
||||
let mut configs = Vec::new();
|
||||
// <https://spec.editorconfig.org/#file-processing>
|
||||
for ancestor in path.ancestors() {
|
||||
let editor_config_file = ancestor.join(".editorconfig");
|
||||
let Ok(contents) = fs::read_to_string(&editor_config_file) else {
|
||||
continue;
|
||||
};
|
||||
let ini = match contents.parse::<Ini>() {
|
||||
Ok(ini) => ini,
|
||||
Err(err) => {
|
||||
log::warn!("Ignoring EditorConfig file at '{editor_config_file:?}' because a glob failed to compile: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let is_root = ini.pairs.get("root").map(AsRef::as_ref) == Some("true");
|
||||
configs.push((ini, ancestor));
|
||||
// > The search shall stop if an EditorConfig file is found with the `root` key set to
|
||||
// > `true` in the preamble or when reaching the root filesystem directory.
|
||||
if is_root {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut pairs = Pairs::new();
|
||||
// Reverse the configuration stack so that the `.editorconfig` files closest to `path`
|
||||
// are applied last and overwrite settings in files closer to the search ceiling.
|
||||
//
|
||||
// > If multiple EditorConfig files have matching sections, the pairs from the closer
|
||||
// > EditorConfig file are read last, so pairs in closer files take precedence.
|
||||
for (config, dir) in configs.into_iter().rev() {
|
||||
let relative_path = path.strip_prefix(dir).expect("dir is an ancestor of path");
|
||||
|
||||
for section in config.sections {
|
||||
if section.glob.is_match(relative_path) {
|
||||
log::info!(
|
||||
"applying EditorConfig from section '{}' in file {:?}",
|
||||
section.glob.glob(),
|
||||
dir.join(".editorconfig")
|
||||
);
|
||||
pairs.extend(section.pairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::from_pairs(pairs)
|
||||
}
|
||||
|
||||
fn from_pairs(pairs: Pairs) -> Self {
|
||||
enum IndentSize {
|
||||
Tab,
|
||||
Spaces(NonZeroU8),
|
||||
}
|
||||
|
||||
// <https://spec.editorconfig.org/#supported-pairs>
|
||||
let indent_size = pairs.get("indent_size").and_then(|value| {
|
||||
if value.as_ref() == "tab" {
|
||||
Some(IndentSize::Tab)
|
||||
} else if let Ok(spaces) = value.parse::<NonZeroU8>() {
|
||||
Some(IndentSize::Spaces(spaces))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let tab_width = pairs
|
||||
.get("tab_width")
|
||||
.and_then(|value| value.parse::<NonZeroU8>().ok())
|
||||
.or(match indent_size {
|
||||
Some(IndentSize::Spaces(spaces)) => Some(spaces),
|
||||
_ => None,
|
||||
});
|
||||
let indent_style = pairs
|
||||
.get("indent_style")
|
||||
.and_then(|value| match value.as_ref() {
|
||||
"tab" => Some(IndentStyle::Tabs),
|
||||
"space" => {
|
||||
let spaces = match indent_size {
|
||||
Some(IndentSize::Spaces(spaces)) => spaces.get(),
|
||||
Some(IndentSize::Tab) => tab_width.map(|n| n.get()).unwrap_or(4),
|
||||
None => 4,
|
||||
};
|
||||
Some(IndentStyle::Spaces(spaces.clamp(1, MAX_INDENT)))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let line_ending = pairs
|
||||
.get("end_of_line")
|
||||
.and_then(|value| match value.as_ref() {
|
||||
"lf" => Some(LineEnding::LF),
|
||||
"crlf" => Some(LineEnding::Crlf),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"cr" => Some(LineEnding::CR),
|
||||
_ => None,
|
||||
});
|
||||
let encoding = pairs.get("charset").and_then(|value| match value.as_ref() {
|
||||
"latin1" => Some(encoding_rs::WINDOWS_1252),
|
||||
"utf-8" => Some(encoding_rs::UTF_8),
|
||||
// `utf-8-bom` is intentionally ignored.
|
||||
// > `utf-8-bom` is discouraged.
|
||||
"utf-16le" => Some(encoding_rs::UTF_16LE),
|
||||
"utf-16be" => Some(encoding_rs::UTF_16BE),
|
||||
_ => None,
|
||||
});
|
||||
let trim_trailing_whitespace =
|
||||
pairs
|
||||
.get("trim_trailing_whitespace")
|
||||
.and_then(|value| match value.as_ref() {
|
||||
"true" => Some(true),
|
||||
"false" => Some(false),
|
||||
_ => None,
|
||||
});
|
||||
let insert_final_newline = pairs
|
||||
.get("insert_final_newline")
|
||||
.and_then(|value| match value.as_ref() {
|
||||
"true" => Some(true),
|
||||
"false" => Some(false),
|
||||
_ => None,
|
||||
});
|
||||
// This option is not in the spec but is supported by some editors.
|
||||
// <https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length>
|
||||
let max_line_length = pairs
|
||||
.get("max_line_length")
|
||||
.and_then(|value| value.parse::<NonZeroU16>().ok());
|
||||
|
||||
Self {
|
||||
indent_style,
|
||||
tab_width,
|
||||
line_ending,
|
||||
encoding,
|
||||
trim_trailing_whitespace,
|
||||
insert_final_newline,
|
||||
max_line_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Pairs = HashMap<Box<str>, Box<str>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Section {
|
||||
glob: GlobMatcher,
|
||||
pairs: Pairs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Ini {
|
||||
pairs: Pairs,
|
||||
sections: Vec<Section>,
|
||||
}
|
||||
|
||||
impl FromStr for Ini {
|
||||
type Err = globset::Error;
|
||||
|
||||
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
||||
// <https://spec.editorconfig.org/#file-format>
|
||||
let mut ini = Ini::default();
|
||||
// > EditorConfig files are in an INI-like file format. To read an EditorConfig file, take
|
||||
// > one line at a time, from beginning to end. For each line:
|
||||
for full_line in source.lines() {
|
||||
// > 1. Remove all leading and trailing whitespace.
|
||||
let line = full_line.trim();
|
||||
// > 2. Process the remaining text as specified for its type below.
|
||||
// > The types of lines are:
|
||||
// > * Blank: contains nothing. Blank lines are ignored.
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// > * Comment: starts with a ';' or '#'. Comment lines are ignored.
|
||||
if line.starts_with([';', '#']) {
|
||||
continue;
|
||||
}
|
||||
if let Some(section) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
|
||||
// > * Section Header: starts with a `[` and ends with a `]`. These lines define
|
||||
// > globs...
|
||||
|
||||
// <https://spec.editorconfig.org/#glob-expressions>
|
||||
// We need to modify the glob string slightly since EditorConfig's glob flavor
|
||||
// doesn't match `globset`'s exactly. `globset` only allows '**' at the beginning
|
||||
// or end of a glob or between two '/'s. (This replacement is not very fancy but
|
||||
// should cover most practical cases.)
|
||||
let mut glob_str = section.replace("**.", "**/*.");
|
||||
if !is_glob_relative(section) {
|
||||
glob_str.insert_str(0, "**/");
|
||||
}
|
||||
let glob = GlobBuilder::new(&glob_str)
|
||||
.literal_separator(true)
|
||||
.backslash_escape(true)
|
||||
.build()?;
|
||||
ini.sections.push(Section {
|
||||
glob: glob.compile_matcher(),
|
||||
pairs: Pairs::new(),
|
||||
});
|
||||
} else if let Some((key, value)) = line.split_once('=') {
|
||||
// > * Key-Value Pair (or Pair): contains a key and a value, separated by an `=`.
|
||||
// > * Key: The part before the first `=` on the line.
|
||||
// > * Value: The part, if any, after the first `=` on the line.
|
||||
// > * Keys and values are trimmed of leading and trailing whitespace, but
|
||||
// > include any whitespace that is between non-whitespace characters.
|
||||
// > * If a value is not provided, then the value is an empty string.
|
||||
let key = key.trim().to_lowercase().into_boxed_str();
|
||||
let value = value.trim().to_lowercase().into_boxed_str();
|
||||
if let Some(section) = ini.sections.last_mut() {
|
||||
section.pairs.insert(key, value);
|
||||
} else {
|
||||
ini.pairs.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ini)
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether a glob is relative to the directory of the config file.
|
||||
fn is_glob_relative(source: &str) -> bool {
|
||||
// > If the glob contains a path separator (a `/` not inside square brackets), then the
|
||||
// > glob is relative to the directory level of the particular `.editorconfig` file itself.
|
||||
let mut idx = 0;
|
||||
while let Some(open) = source[idx..].find('[').map(|open| idx + open) {
|
||||
if source[..open].contains('/') {
|
||||
return true;
|
||||
}
|
||||
idx = source[open..]
|
||||
.find(']')
|
||||
.map_or(source.len(), |close| idx + close);
|
||||
}
|
||||
source[idx..].contains('/')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_glob_relative_test() {
|
||||
assert!(is_glob_relative("subdir/*.c"));
|
||||
assert!(!is_glob_relative("*.txt"));
|
||||
assert!(!is_glob_relative("[a/b].c"));
|
||||
}
|
||||
|
||||
fn editor_config(path: impl AsRef<Path>, source: &str) -> EditorConfig {
|
||||
let path = path.as_ref();
|
||||
let ini = source.parse::<Ini>().unwrap();
|
||||
let pairs = ini
|
||||
.sections
|
||||
.into_iter()
|
||||
.filter(|section| section.glob.is_match(path))
|
||||
.fold(Pairs::new(), |mut acc, section| {
|
||||
acc.extend(section.pairs);
|
||||
acc
|
||||
});
|
||||
EditorConfig::from_pairs(pairs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_test() {
|
||||
let source = r#"
|
||||
[*]
|
||||
indent_style = space
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[docs/**.txt]
|
||||
insert_final_newline = true
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
editor_config("a.txt", source),
|
||||
EditorConfig {
|
||||
indent_style: Some(IndentStyle::Spaces(4)),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
editor_config("pkg/Makefile", source),
|
||||
EditorConfig {
|
||||
indent_style: Some(IndentStyle::Tabs),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
editor_config("docs/config/editor.txt", source),
|
||||
EditorConfig {
|
||||
indent_style: Some(IndentStyle::Spaces(4)),
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
|
||||
//!
|
||||
//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
|
||||
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
|
||||
use ropey::{str_utils::byte_to_char_idx, RopeSlice};
|
||||
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -64,7 +64,7 @@ impl<'a> Grapheme<'a> {
|
||||
}
|
||||
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
!matches!(&self, Grapheme::Other { g } if !g.chars().all(char_is_whitespace))
|
||||
!matches!(&self, Grapheme::Other { g } if !g.chars().next().is_some_and(char_is_whitespace))
|
||||
}
|
||||
|
||||
// TODO currently word boundaries are used for softwrapping.
|
||||
@@ -72,7 +72,7 @@ impl<'a> Grapheme<'a> {
|
||||
// This could however be improved in the future by considering unicode
|
||||
// character classes but
|
||||
pub fn is_word_boundary(&self) -> bool {
|
||||
!matches!(&self, Grapheme::Other { g,.. } if g.chars().all(char_is_word))
|
||||
!matches!(&self, Grapheme::Other { g,.. } if g.chars().next().is_some_and(char_is_word))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,9 @@ pub fn grapheme_width(g: &str) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: for byte indexing versions of these functions see `RopeSliceExt`'s
|
||||
// `floor_grapheme_boundary` and `ceil_grapheme_boundary` and the rope grapheme iterators.
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_prev_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -> usize {
|
||||
// Bounds check
|
||||
@@ -208,43 +211,6 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
|
||||
chunk_char_idx + tmp
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
|
||||
// Bounds check
|
||||
debug_assert!(byte_idx <= slice.len_bytes());
|
||||
|
||||
// Get the chunk with our byte index in it.
|
||||
let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
|
||||
|
||||
// Set up the grapheme cursor.
|
||||
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
|
||||
|
||||
// Find the nth next grapheme cluster boundary.
|
||||
for _ in 0..n {
|
||||
loop {
|
||||
match gc.next_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(None) => return slice.len_bytes(),
|
||||
Ok(Some(n)) => {
|
||||
byte_idx = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
chunk_byte_idx += chunk.len();
|
||||
let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
|
||||
chunk = a;
|
||||
// chunk_char_idx = c;
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
|
||||
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
byte_idx
|
||||
}
|
||||
|
||||
/// Finds the next grapheme boundary after the given char position.
|
||||
#[must_use]
|
||||
#[inline(always)]
|
||||
@@ -252,13 +218,6 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
|
||||
nth_next_grapheme_boundary(slice, char_idx, 1)
|
||||
}
|
||||
|
||||
/// Finds the next grapheme boundary after the given byte position.
|
||||
#[must_use]
|
||||
#[inline(always)]
|
||||
pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
|
||||
nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
|
||||
}
|
||||
|
||||
/// Returns the passed char index if it's already a grapheme boundary,
|
||||
/// or the next grapheme boundary char index if not.
|
||||
#[must_use]
|
||||
@@ -311,187 +270,6 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the given byte position is a grapheme boundary.
|
||||
#[must_use]
|
||||
pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
|
||||
// Bounds check
|
||||
debug_assert!(byte_idx <= slice.len_bytes());
|
||||
|
||||
// Get the chunk with our byte index in it.
|
||||
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
|
||||
|
||||
// Set up the grapheme cursor.
|
||||
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
|
||||
|
||||
// Determine if the given position is a grapheme cluster boundary.
|
||||
loop {
|
||||
match gc.is_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(n) => return n,
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
|
||||
gc.provide_context(ctx_chunk, ctx_byte_start);
|
||||
}
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice`.
|
||||
#[derive(Clone)]
|
||||
pub struct RopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RopeGraphemes<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RopeGraphemes<'_> {
|
||||
#[must_use]
|
||||
pub fn new(slice: RopeSlice) -> RopeGraphemes {
|
||||
let mut chunks = slice.chunks();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
RopeGraphemes {
|
||||
text: slice,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start: 0,
|
||||
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<RopeSlice<'a>> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.next_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
self.cur_chunk_start += self.cur_chunk.len();
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a < self.cur_chunk_start {
|
||||
Some(self.text.byte_slice(a..b))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[a2..b2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice` in reverse.
|
||||
#[derive(Clone)]
|
||||
pub struct RevRopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RevRopeGraphemes<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RevRopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RevRopeGraphemes<'_> {
|
||||
#[must_use]
|
||||
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
|
||||
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
|
||||
chunks.reverse();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
cur_chunk_start -= first_chunk.len();
|
||||
RevRopeGraphemes {
|
||||
text: slice,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start,
|
||||
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RevRopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<RopeSlice<'a>> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
self.cur_chunk_start -= self.cur_chunk.len();
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a >= self.cur_chunk_start + self.cur_chunk.len() {
|
||||
Some(self.text.byte_slice(b..a))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[b2..a2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A highly compressed Cow<'a, str> that holds
|
||||
/// atmost u31::MAX bytes and is readonly
|
||||
pub struct GraphemeStr<'a> {
|
||||
|
@@ -8,7 +8,7 @@ use crate::{
|
||||
graphemes::{grapheme_width, tab_width_at},
|
||||
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
|
||||
tree_sitter::Node,
|
||||
Position, Rope, RopeGraphemes, RopeSlice, Tendril,
|
||||
Position, Rope, RopeSlice, Tendril,
|
||||
};
|
||||
|
||||
/// Enum representing indentation style.
|
||||
@@ -200,7 +200,7 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
|
||||
/// Create a string of tabs & spaces that has the same visual width as the given RopeSlice (independent of the tab width).
|
||||
fn whitespace_with_same_width(text: RopeSlice) -> String {
|
||||
let mut s = String::new();
|
||||
for grapheme in RopeGraphemes::new(text) {
|
||||
for grapheme in text.graphemes() {
|
||||
if grapheme == "\t" {
|
||||
s.push('\t');
|
||||
} else {
|
||||
@@ -456,7 +456,7 @@ struct IndentQueryResult<'a> {
|
||||
fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
|
||||
let mut node_line = node.start_position().row;
|
||||
// Adjust for the new line that will be inserted
|
||||
if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
|
||||
if new_line_byte_pos.is_some_and(|pos| node.start_byte() >= pos) {
|
||||
node_line += 1;
|
||||
}
|
||||
node_line
|
||||
@@ -464,7 +464,7 @@ fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
|
||||
fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
|
||||
let mut node_line = node.end_position().row;
|
||||
// Adjust for the new line that will be inserted (with a strict inequality since end_byte is exclusive)
|
||||
if new_line_byte_pos.map_or(false, |pos| node.end_byte() > pos) {
|
||||
if new_line_byte_pos.is_some_and(|pos| node.end_byte() > pos) {
|
||||
node_line += 1;
|
||||
}
|
||||
node_line
|
||||
|
@@ -3,12 +3,14 @@ pub use encoding_rs as encoding;
|
||||
pub mod auto_pairs;
|
||||
pub mod case_conversion;
|
||||
pub mod chars;
|
||||
pub mod command_line;
|
||||
pub mod comment;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod diagnostic;
|
||||
pub mod diff;
|
||||
pub mod doc_formatter;
|
||||
pub mod editor_config;
|
||||
pub mod fuzzy;
|
||||
pub mod graphemes;
|
||||
pub mod history;
|
||||
@@ -22,7 +24,6 @@ pub mod object;
|
||||
mod position;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
pub mod shellwords;
|
||||
pub mod snippets;
|
||||
pub mod surround;
|
||||
pub mod syntax;
|
||||
@@ -54,7 +55,6 @@ pub type Tendril = SmartString<smartstring::LazyCompact>;
|
||||
#[doc(inline)]
|
||||
pub use {regex, tree_sitter};
|
||||
|
||||
pub use graphemes::RopeGraphemes;
|
||||
pub use position::{
|
||||
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
|
||||
visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError,
|
||||
|
@@ -4,10 +4,12 @@ use std::{
|
||||
ops::{Add, AddAssign, Sub, SubAssign},
|
||||
};
|
||||
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
|
||||
use crate::{
|
||||
chars::char_is_line_ending,
|
||||
doc_formatter::{DocumentFormatter, TextFormat},
|
||||
graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
|
||||
graphemes::{ensure_grapheme_boundary_prev, grapheme_width},
|
||||
line_ending::line_end_char_index,
|
||||
text_annotations::TextAnnotations,
|
||||
RopeSlice,
|
||||
@@ -101,7 +103,7 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
|
||||
|
||||
let line_start = text.line_to_char(line);
|
||||
let pos = ensure_grapheme_boundary_prev(text, pos);
|
||||
let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
|
||||
let col = text.slice(line_start..pos).graphemes().count();
|
||||
|
||||
Position::new(line, col)
|
||||
}
|
||||
@@ -126,7 +128,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
|
||||
|
||||
let mut col = 0;
|
||||
|
||||
for grapheme in RopeGraphemes::new(text.slice(line_start..pos)) {
|
||||
for grapheme in text.slice(line_start..pos).graphemes() {
|
||||
if grapheme == "\t" {
|
||||
col += tab_width - (col % tab_width);
|
||||
} else {
|
||||
@@ -275,7 +277,7 @@ pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending
|
||||
};
|
||||
|
||||
let mut col_char_offset = 0;
|
||||
for (i, g) in RopeGraphemes::new(text.slice(line_start..line_end)).enumerate() {
|
||||
for (i, g) in text.slice(line_start..line_end).graphemes().enumerate() {
|
||||
if i == col {
|
||||
break;
|
||||
}
|
||||
@@ -306,7 +308,7 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
|
||||
|
||||
let mut col_char_offset = 0;
|
||||
let mut cols_remaining = col;
|
||||
for grapheme in RopeGraphemes::new(text.slice(line_start..line_end)) {
|
||||
for grapheme in text.slice(line_start..line_end).graphemes() {
|
||||
let grapheme_width = if grapheme == "\t" {
|
||||
tab_width - ((col - cols_remaining) % tab_width)
|
||||
} else {
|
||||
|
@@ -9,7 +9,7 @@ use crate::{
|
||||
},
|
||||
line_ending::get_line_ending,
|
||||
movement::Direction,
|
||||
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
|
||||
Assoc, ChangeSet, RopeSlice,
|
||||
};
|
||||
use helix_stdx::range::is_subset;
|
||||
use helix_stdx::rope::{self, RopeSliceExt};
|
||||
@@ -379,7 +379,7 @@ impl Range {
|
||||
|
||||
/// Returns true if this Range covers a single grapheme in the given text
|
||||
pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool {
|
||||
let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to()));
|
||||
let mut graphemes = doc.slice(self.from()..self.to()).graphemes();
|
||||
let first = graphemes.next();
|
||||
let second = graphemes.next();
|
||||
first.is_some() && second.is_none()
|
||||
@@ -619,7 +619,6 @@ impl Selection {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: consume an iterator or a vec to reduce allocations?
|
||||
#[must_use]
|
||||
pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
|
||||
assert!(!ranges.is_empty());
|
||||
@@ -721,6 +720,12 @@ impl IntoIterator for Selection {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Range> for Selection {
|
||||
fn from_iter<T: IntoIterator<Item = Range>>(ranges: T) -> Self {
|
||||
Self::new(ranges.into_iter().collect(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range> for Selection {
|
||||
fn from(range: Range) -> Self {
|
||||
Self {
|
||||
|
@@ -1,350 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Auto escape for shellwords usage.
|
||||
pub fn escape(input: Cow<str>) -> Cow<str> {
|
||||
if !input.chars().any(|x| x.is_ascii_whitespace()) {
|
||||
input
|
||||
} else if cfg!(unix) {
|
||||
Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
|
||||
if c.is_ascii_whitespace() {
|
||||
buf.push('\\');
|
||||
}
|
||||
buf.push(c);
|
||||
buf
|
||||
}))
|
||||
} else {
|
||||
Cow::Owned(format!("\"{}\"", input))
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
OnWhitespace,
|
||||
Unquoted,
|
||||
UnquotedEscaped,
|
||||
Quoted,
|
||||
QuoteEscaped,
|
||||
Dquoted,
|
||||
DquoteEscaped,
|
||||
}
|
||||
|
||||
pub struct Shellwords<'a> {
|
||||
state: State,
|
||||
/// Shellwords where whitespace and escapes has been resolved.
|
||||
words: Vec<Cow<'a, str>>,
|
||||
/// The parts of the input that are divided into shellwords. This can be
|
||||
/// used to retrieve the original text for a given word by looking up the
|
||||
/// same index in the Vec as the word in `words`.
|
||||
parts: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Shellwords<'a> {
|
||||
fn from(input: &'a str) -> Self {
|
||||
use State::*;
|
||||
|
||||
let mut state = Unquoted;
|
||||
let mut words = Vec::new();
|
||||
let mut parts = Vec::new();
|
||||
let mut escaped = String::with_capacity(input.len());
|
||||
|
||||
let mut part_start = 0;
|
||||
let mut unescaped_start = 0;
|
||||
let mut end = 0;
|
||||
|
||||
for (i, c) in input.char_indices() {
|
||||
state = match state {
|
||||
OnWhitespace => match c {
|
||||
'"' => {
|
||||
end = i;
|
||||
Dquoted
|
||||
}
|
||||
'\'' => {
|
||||
end = i;
|
||||
Quoted
|
||||
}
|
||||
'\\' => {
|
||||
if cfg!(unix) {
|
||||
escaped.push_str(&input[unescaped_start..i]);
|
||||
unescaped_start = i + 1;
|
||||
UnquotedEscaped
|
||||
} else {
|
||||
OnWhitespace
|
||||
}
|
||||
}
|
||||
c if c.is_ascii_whitespace() => {
|
||||
end = i;
|
||||
OnWhitespace
|
||||
}
|
||||
_ => Unquoted,
|
||||
},
|
||||
Unquoted => match c {
|
||||
'\\' => {
|
||||
if cfg!(unix) {
|
||||
escaped.push_str(&input[unescaped_start..i]);
|
||||
unescaped_start = i + 1;
|
||||
UnquotedEscaped
|
||||
} else {
|
||||
Unquoted
|
||||
}
|
||||
}
|
||||
c if c.is_ascii_whitespace() => {
|
||||
end = i;
|
||||
OnWhitespace
|
||||
}
|
||||
_ => Unquoted,
|
||||
},
|
||||
UnquotedEscaped => Unquoted,
|
||||
Quoted => match c {
|
||||
'\\' => {
|
||||
if cfg!(unix) {
|
||||
escaped.push_str(&input[unescaped_start..i]);
|
||||
unescaped_start = i + 1;
|
||||
QuoteEscaped
|
||||
} else {
|
||||
Quoted
|
||||
}
|
||||
}
|
||||
'\'' => {
|
||||
end = i;
|
||||
OnWhitespace
|
||||
}
|
||||
_ => Quoted,
|
||||
},
|
||||
QuoteEscaped => Quoted,
|
||||
Dquoted => match c {
|
||||
'\\' => {
|
||||
if cfg!(unix) {
|
||||
escaped.push_str(&input[unescaped_start..i]);
|
||||
unescaped_start = i + 1;
|
||||
DquoteEscaped
|
||||
} else {
|
||||
Dquoted
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
end = i;
|
||||
OnWhitespace
|
||||
}
|
||||
_ => Dquoted,
|
||||
},
|
||||
DquoteEscaped => Dquoted,
|
||||
};
|
||||
|
||||
let c_len = c.len_utf8();
|
||||
if i == input.len() - c_len && end == 0 {
|
||||
end = i + c_len;
|
||||
}
|
||||
|
||||
if end > 0 {
|
||||
let esc_trim = escaped.trim();
|
||||
let inp = &input[unescaped_start..end];
|
||||
|
||||
if !(esc_trim.is_empty() && inp.trim().is_empty()) {
|
||||
if esc_trim.is_empty() {
|
||||
words.push(inp.into());
|
||||
parts.push(inp);
|
||||
} else {
|
||||
words.push([escaped, inp.into()].concat().into());
|
||||
parts.push(&input[part_start..end]);
|
||||
escaped = "".to_string();
|
||||
}
|
||||
}
|
||||
unescaped_start = i + 1;
|
||||
part_start = i + 1;
|
||||
end = 0;
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(words.len() == parts.len());
|
||||
|
||||
Self {
|
||||
state,
|
||||
words,
|
||||
parts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Shellwords<'a> {
|
||||
/// Checks that the input ends with a whitespace character which is not escaped.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use helix_core::shellwords::Shellwords;
|
||||
/// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true);
|
||||
/// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true);
|
||||
/// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true);
|
||||
/// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false);
|
||||
/// #[cfg(unix)]
|
||||
/// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), false);
|
||||
/// #[cfg(unix)]
|
||||
/// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false);
|
||||
/// ```
|
||||
pub fn ends_with_whitespace(&self) -> bool {
|
||||
matches!(self.state, State::OnWhitespace)
|
||||
}
|
||||
|
||||
/// Returns the list of shellwords calculated from the input string.
|
||||
pub fn words(&self) -> &[Cow<'a, str>] {
|
||||
&self.words
|
||||
}
|
||||
|
||||
/// Returns a list of strings which correspond to [`Self::words`] but represent the original
|
||||
/// text in the input string - including escape characters - without separating whitespace.
|
||||
pub fn parts(&self) -> &[&'a str] {
|
||||
&self.parts
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_normal() {
|
||||
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
|
||||
let shellwords = Shellwords::from(input);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":o"),
|
||||
Cow::from("single_word"),
|
||||
Cow::from("twó"),
|
||||
Cow::from("wörds"),
|
||||
Cow::from("\\three\\"),
|
||||
Cow::from("\\"),
|
||||
Cow::from("with\\ escaping\\\\"),
|
||||
];
|
||||
// TODO test is_owned and is_borrowed, once they get stabilized.
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_normal() {
|
||||
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
|
||||
let shellwords = Shellwords::from(input);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":o"),
|
||||
Cow::from("single_word"),
|
||||
Cow::from("twó"),
|
||||
Cow::from("wörds"),
|
||||
Cow::from(r#"three "with escaping\"#),
|
||||
];
|
||||
// TODO test is_owned and is_borrowed, once they get stabilized.
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_quoted() {
|
||||
let quoted =
|
||||
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
|
||||
let shellwords = Shellwords::from(quoted);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":o"),
|
||||
Cow::from("single_word"),
|
||||
Cow::from("twó wörds"),
|
||||
Cow::from(r#"three' "with escaping\"#),
|
||||
Cow::from("quote incomplete"),
|
||||
];
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_dquoted() {
|
||||
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
|
||||
let shellwords = Shellwords::from(dquoted);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":o"),
|
||||
Cow::from("single_word"),
|
||||
Cow::from("twó wörds"),
|
||||
Cow::from(r#"three' "with escaping\"#),
|
||||
Cow::from("dquote incomplete"),
|
||||
];
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_mixed() {
|
||||
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
|
||||
let shellwords = Shellwords::from(dquoted);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":o"),
|
||||
Cow::from("single_word"),
|
||||
Cow::from("twó wörds"),
|
||||
Cow::from("three' \"with escaping\\"),
|
||||
Cow::from("no space before"),
|
||||
Cow::from("and after"),
|
||||
Cow::from("$#%^@"),
|
||||
Cow::from("%^&(%^"),
|
||||
Cow::from(")(*&^%"),
|
||||
Cow::from(r#"a\\b"#),
|
||||
//last ' just changes to quoted but since we dont have anything after it, it should be ignored
|
||||
];
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lists() {
|
||||
let input =
|
||||
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#;
|
||||
let shellwords = Shellwords::from(input);
|
||||
let result = shellwords.words().to_vec();
|
||||
let expected = vec![
|
||||
Cow::from(":set"),
|
||||
Cow::from("statusline.center"),
|
||||
Cow::from(r#"["file-type","file-encoding"]"#),
|
||||
Cow::from(r#"["list", "in", "quotes"]"#),
|
||||
];
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_escaping_unix() {
|
||||
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
|
||||
assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar"));
|
||||
assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_escaping_windows() {
|
||||
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
|
||||
assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_parts() {
|
||||
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
|
||||
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\ "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_parts() {
|
||||
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
|
||||
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multibyte_at_end() {
|
||||
assert_eq!(Shellwords::from("𒀀").parts(), &["𒀀"]);
|
||||
assert_eq!(
|
||||
Shellwords::from(":sh echo 𒀀").parts(),
|
||||
&[":sh", "echo", "𒀀"]
|
||||
);
|
||||
assert_eq!(
|
||||
Shellwords::from(":sh echo 𒀀 hello world𒀀").parts(),
|
||||
&[":sh", "echo", "𒀀", "hello", "world𒀀"]
|
||||
);
|
||||
}
|
||||
}
|
@@ -252,4 +252,21 @@ mod tests {
|
||||
snippet.map(edit.changes());
|
||||
assert!(!snippet.is_valid(&Selection::point(4)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabstop_zero_with_placeholder() {
|
||||
// The `$0` tabstop should not have placeholder text. When we receive a snippet like this
|
||||
// (from older versions of clangd for example) we should discard the placeholder text.
|
||||
let snippet = Snippet::parse("sizeof(${0:expression-or-type})").unwrap();
|
||||
let mut doc = Rope::from("\n");
|
||||
let (transaction, _, snippet) = snippet.render(
|
||||
&doc,
|
||||
&Selection::point(0),
|
||||
|_| (0, 0),
|
||||
&mut SnippetRenderCtx::test_ctx(),
|
||||
);
|
||||
assert!(transaction.apply(&mut doc));
|
||||
assert_eq!(doc, "sizeof()\n");
|
||||
assert!(ActiveSnippet::new(snippet).is_none());
|
||||
}
|
||||
}
|
||||
|
@@ -178,9 +178,16 @@ impl Snippet {
|
||||
&mut self,
|
||||
idx: usize,
|
||||
parent: Option<TabstopIdx>,
|
||||
default: Vec<parser::SnippetElement>,
|
||||
mut default: Vec<parser::SnippetElement>,
|
||||
) -> TabstopIdx {
|
||||
let idx = TabstopIdx::elaborate(idx);
|
||||
if idx == LAST_TABSTOP_IDX && !default.is_empty() {
|
||||
// Older versions of clangd for example may send a snippet like `${0:placeholder}`
|
||||
// which is considered by VSCode to be a misuse of the `$0` tabstop.
|
||||
log::warn!("Discarding placeholder text for the `$0` tabstop ({default:?}). \
|
||||
The `$0` tabstop signifies the final cursor position and should not include placeholder text.");
|
||||
default.clear();
|
||||
}
|
||||
let default = self.elaborate(default, Some(idx));
|
||||
self.tabstops.push(Tabstop {
|
||||
idx,
|
||||
|
@@ -361,7 +361,20 @@ mod test {
|
||||
Text(")".into()),
|
||||
]),
|
||||
parse("match(${1:Arg1})")
|
||||
)
|
||||
);
|
||||
// The `$0` tabstop should not have placeholder text. The parser should handle this case
|
||||
// normally and then the placeholder text should be discarded during elaboration.
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Text("sizeof(".into()),
|
||||
Placeholder {
|
||||
tabstop: 0,
|
||||
value: vec![Text("expression-or-type".into())],
|
||||
},
|
||||
Text(")".into()),
|
||||
]),
|
||||
parse("sizeof(${0:expression-or-type})")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@@ -36,12 +36,12 @@ use helix_loader::grammar::{get_language, load_runtime_file};
|
||||
|
||||
pub use tree_cursor::TreeCursor;
|
||||
|
||||
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
|
||||
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<rope::Regex>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer)?
|
||||
.map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom))
|
||||
.map(|buf| rope::Regex::new(&buf).map_err(serde::de::Error::custom))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ pub struct LanguageConfiguration {
|
||||
|
||||
// content_regex
|
||||
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
|
||||
pub injection_regex: Option<Regex>,
|
||||
pub injection_regex: Option<rope::Regex>,
|
||||
// first_line_regex
|
||||
//
|
||||
#[serde(skip)]
|
||||
@@ -334,6 +334,7 @@ pub enum LanguageServerFeature {
|
||||
Diagnostics,
|
||||
RenameSymbol,
|
||||
InlayHints,
|
||||
DocumentColors,
|
||||
}
|
||||
|
||||
impl Display for LanguageServerFeature {
|
||||
@@ -357,6 +358,7 @@ impl Display for LanguageServerFeature {
|
||||
Diagnostics => "diagnostics",
|
||||
RenameSymbol => "rename-symbol",
|
||||
InlayHints => "inlay-hints",
|
||||
DocumentColors => "document-colors",
|
||||
};
|
||||
write!(f, "{feature}",)
|
||||
}
|
||||
@@ -756,7 +758,7 @@ impl LanguageConfiguration {
|
||||
let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id))
|
||||
.map_err(|err| {
|
||||
log::error!(
|
||||
"Failed to load tree-sitter parser for language {:?}: {}",
|
||||
"Failed to load tree-sitter parser for language {:?}: {:#}",
|
||||
self.language_id,
|
||||
err
|
||||
)
|
||||
@@ -994,21 +996,32 @@ impl Loader {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn language_config_for_language_id(&self, id: &str) -> Option<Arc<LanguageConfiguration>> {
|
||||
pub fn language_config_for_language_id(
|
||||
&self,
|
||||
id: impl PartialEq<String>,
|
||||
) -> Option<Arc<LanguageConfiguration>> {
|
||||
self.language_configs
|
||||
.iter()
|
||||
.find(|config| config.language_id == id)
|
||||
.find(|config| id.eq(&config.language_id))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Unlike language_config_for_language_id, which only returns Some for an exact id, this
|
||||
/// Unlike `language_config_for_language_id`, which only returns Some for an exact id, this
|
||||
/// function will perform a regex match on the given string to find the closest language match.
|
||||
pub fn language_config_for_name(&self, name: &str) -> Option<Arc<LanguageConfiguration>> {
|
||||
pub fn language_config_for_name(&self, slice: RopeSlice) -> Option<Arc<LanguageConfiguration>> {
|
||||
// PERF: If the name matches up with the id, then this saves the need to do expensive regex.
|
||||
let shortcircuit = self.language_config_for_language_id(slice);
|
||||
if shortcircuit.is_some() {
|
||||
return shortcircuit;
|
||||
}
|
||||
|
||||
// If the name did not match up with a known id, then match on injection regex.
|
||||
|
||||
let mut best_match_length = 0;
|
||||
let mut best_match_position = None;
|
||||
for (i, configuration) in self.language_configs.iter().enumerate() {
|
||||
if let Some(injection_regex) = &configuration.injection_regex {
|
||||
if let Some(mat) = injection_regex.find(name) {
|
||||
if let Some(mat) = injection_regex.find(slice.regex_input()) {
|
||||
let length = mat.end() - mat.start();
|
||||
if length > best_match_length {
|
||||
best_match_position = Some(i);
|
||||
@@ -1026,12 +1039,18 @@ impl Loader {
|
||||
capture: &InjectionLanguageMarker,
|
||||
) -> Option<Arc<LanguageConfiguration>> {
|
||||
match capture {
|
||||
InjectionLanguageMarker::Name(string) => self.language_config_for_name(string),
|
||||
InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file),
|
||||
InjectionLanguageMarker::Shebang(shebang) => self
|
||||
.language_config_ids_by_shebang
|
||||
.get(shebang)
|
||||
.and_then(|&id| self.language_configs.get(id).cloned()),
|
||||
InjectionLanguageMarker::LanguageId(id) => self.language_config_for_language_id(*id),
|
||||
InjectionLanguageMarker::Name(name) => self.language_config_for_name(*name),
|
||||
InjectionLanguageMarker::Filename(file) => {
|
||||
let path_str: Cow<str> = (*file).into();
|
||||
self.language_config_for_file_name(Path::new(path_str.as_ref()))
|
||||
}
|
||||
InjectionLanguageMarker::Shebang(shebang) => {
|
||||
let shebang_str: Cow<str> = (*shebang).into();
|
||||
self.language_config_ids_by_shebang
|
||||
.get(shebang_str.as_ref())
|
||||
.and_then(|&id| self.language_configs.get(id).cloned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2030,12 +2049,13 @@ impl HighlightConfiguration {
|
||||
for capture in query_match.captures {
|
||||
let index = Some(capture.index);
|
||||
if index == self.injection_language_capture_index {
|
||||
let name = byte_range_to_str(capture.node.byte_range(), source);
|
||||
injection_capture = Some(InjectionLanguageMarker::Name(name));
|
||||
injection_capture = Some(InjectionLanguageMarker::Name(
|
||||
source.byte_slice(capture.node.byte_range()),
|
||||
));
|
||||
} else if index == self.injection_filename_capture_index {
|
||||
let name = byte_range_to_str(capture.node.byte_range(), source);
|
||||
let path = Path::new(name.as_ref()).to_path_buf();
|
||||
injection_capture = Some(InjectionLanguageMarker::Filename(path.into()));
|
||||
injection_capture = Some(InjectionLanguageMarker::Filename(
|
||||
source.byte_slice(capture.node.byte_range()),
|
||||
));
|
||||
} else if index == self.injection_shebang_capture_index {
|
||||
let node_slice = source.byte_slice(capture.node.byte_range());
|
||||
|
||||
@@ -2054,7 +2074,7 @@ impl HighlightConfiguration {
|
||||
.captures_iter(lines.regex_input())
|
||||
.map(|cap| {
|
||||
let cap = lines.byte_slice(cap.get_group(1).unwrap().range());
|
||||
InjectionLanguageMarker::Shebang(cap.into())
|
||||
InjectionLanguageMarker::Shebang(cap)
|
||||
})
|
||||
.next()
|
||||
} else if index == self.injection_content_capture_index {
|
||||
@@ -2085,8 +2105,8 @@ impl HighlightConfiguration {
|
||||
"injection.language" if injection_capture.is_none() => {
|
||||
injection_capture = prop
|
||||
.value
|
||||
.as_ref()
|
||||
.map(|s| InjectionLanguageMarker::Name(s.as_ref().into()));
|
||||
.as_deref()
|
||||
.map(InjectionLanguageMarker::LanguageId);
|
||||
}
|
||||
|
||||
// By default, injections do not include the *children* of an
|
||||
@@ -2484,15 +2504,17 @@ impl Iterator for HighlightIter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// Once a highlighting pattern is found for the current node, skip over
|
||||
// any later highlighting patterns that also match this node. Captures
|
||||
// Use the last capture found for the current node, skipping over any
|
||||
// highlight patterns that also match this node. Captures
|
||||
// for a given node are ordered by pattern index, so these subsequent
|
||||
// captures are guaranteed to be for highlighting, not injections or
|
||||
// local variables.
|
||||
while let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
captures.next();
|
||||
match_.remove();
|
||||
capture = next_capture;
|
||||
match_ = captures.next().unwrap().0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -2521,9 +2543,20 @@ impl Iterator for HighlightIter<'_> {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InjectionLanguageMarker<'a> {
|
||||
Name(Cow<'a, str>),
|
||||
Filename(Cow<'a, Path>),
|
||||
Shebang(String),
|
||||
/// The language is specified by `LanguageConfiguration`'s `language_id` field.
|
||||
///
|
||||
/// This marker is used when a pattern sets the `injection.language` property, for example
|
||||
/// `(#set! injection.language "rust")`.
|
||||
LanguageId(&'a str),
|
||||
/// The language is specified in the document and captured by `@injection.language`.
|
||||
///
|
||||
/// This is used for markdown code fences for example. While the `LanguageId` variant can be
|
||||
/// looked up by finding the language config that sets an `language_id`, this variant contains
|
||||
/// text from the document being highlighted, so the text is checked against each language's
|
||||
/// `injection_regex`.
|
||||
Name(RopeSlice<'a>),
|
||||
Filename(RopeSlice<'a>),
|
||||
Shebang(RopeSlice<'a>),
|
||||
}
|
||||
|
||||
const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";
|
||||
@@ -2666,12 +2699,20 @@ fn node_is_visible(node: &Node) -> bool {
|
||||
node.is_missing() || (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
|
||||
}
|
||||
|
||||
fn format_anonymous_node_kind(kind: &str) -> Cow<str> {
|
||||
if kind.contains('"') {
|
||||
Cow::Owned(kind.replace('"', "\\\""))
|
||||
} else {
|
||||
Cow::Borrowed(kind)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
|
||||
if node.child_count() == 0 {
|
||||
if node_is_visible(&node) {
|
||||
write!(fmt, "({})", node.kind())
|
||||
} else {
|
||||
write!(fmt, "\"{}\"", node.kind())
|
||||
write!(fmt, "\"{}\"", format_anonymous_node_kind(node.kind()))
|
||||
}
|
||||
} else {
|
||||
pretty_print_tree_impl(fmt, &mut node.walk(), 0)
|
||||
@@ -2696,7 +2737,7 @@ fn pretty_print_tree_impl<W: fmt::Write>(
|
||||
|
||||
write!(fmt, "({}", node.kind())?;
|
||||
} else {
|
||||
write!(fmt, " \"{}\"", node.kind())?;
|
||||
write!(fmt, " \"{}\"", format_anonymous_node_kind(node.kind()))?;
|
||||
}
|
||||
|
||||
// Handle children.
|
||||
@@ -2973,8 +3014,8 @@ mod test {
|
||||
" (macro_invocation\n",
|
||||
" macro: (identifier) \"!\"\n",
|
||||
" (token_tree \"(\"\n",
|
||||
" (string_literal \"\"\"\n",
|
||||
" (string_content) \"\"\") \")\")) \";\") \"}\"))",
|
||||
" (string_literal \"\\\"\"\n",
|
||||
" (string_content) \"\\\"\") \")\")) \";\") \"}\"))",
|
||||
),
|
||||
0,
|
||||
source.len(),
|
||||
|
@@ -119,6 +119,7 @@ impl Client {
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
// make sure the process is reaped on drop
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
@@ -128,16 +129,12 @@ impl Client {
|
||||
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
|
||||
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
|
||||
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
|
||||
let errors = process.stderr.take().map(BufReader::new);
|
||||
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
|
||||
|
||||
Self::streams(
|
||||
Box::new(BufReader::new(reader)),
|
||||
Box::new(reader),
|
||||
Box::new(writer),
|
||||
// errors.map(|errors| Box::new(BufReader::new(errors))),
|
||||
match errors {
|
||||
Some(errors) => Some(Box::new(BufReader::new(errors))),
|
||||
None => None,
|
||||
},
|
||||
Some(Box::new(stderr)),
|
||||
id,
|
||||
Some(process),
|
||||
)
|
||||
|
@@ -3,10 +3,11 @@ mod transport;
|
||||
mod types;
|
||||
|
||||
pub use client::{Client, ConnectionType};
|
||||
pub use events::Event;
|
||||
pub use transport::{Payload, Response, Transport};
|
||||
pub use types::*;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use thiserror::Error;
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
@@ -18,9 +19,84 @@ pub enum Error {
|
||||
Timeout(u64),
|
||||
#[error("server closed the stream")]
|
||||
StreamClosed,
|
||||
#[error("Unhandled")]
|
||||
Unhandled,
|
||||
#[error(transparent)]
|
||||
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments),
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn parse(command: &str, arguments: Option<serde_json::Value>) -> Result<Self> {
|
||||
use crate::types::Request as _;
|
||||
|
||||
let arguments = arguments.unwrap_or_default();
|
||||
let request = match command {
|
||||
requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?),
|
||||
_ => return Err(Error::Unhandled),
|
||||
};
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
Initialized(<events::Initialized as events::Event>::Body),
|
||||
Stopped(<events::Stopped as events::Event>::Body),
|
||||
Continued(<events::Continued as events::Event>::Body),
|
||||
Exited(<events::Exited as events::Event>::Body),
|
||||
Terminated(<events::Terminated as events::Event>::Body),
|
||||
Thread(<events::Thread as events::Event>::Body),
|
||||
Output(<events::Output as events::Event>::Body),
|
||||
Breakpoint(<events::Breakpoint as events::Event>::Body),
|
||||
Module(<events::Module as events::Event>::Body),
|
||||
LoadedSource(<events::LoadedSource as events::Event>::Body),
|
||||
Process(<events::Process as events::Event>::Body),
|
||||
Capabilities(<events::Capabilities as events::Event>::Body),
|
||||
// ProgressStart(),
|
||||
// ProgressUpdate(),
|
||||
// ProgressEnd(),
|
||||
// Invalidated(),
|
||||
Memory(<events::Memory as events::Event>::Body),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn parse(event: &str, body: Option<serde_json::Value>) -> Result<Self> {
|
||||
use crate::events::Event as _;
|
||||
|
||||
let body = body.unwrap_or_default();
|
||||
let event = match event {
|
||||
events::Initialized::EVENT => Self::Initialized(parse_value(body)?),
|
||||
events::Stopped::EVENT => Self::Stopped(parse_value(body)?),
|
||||
events::Continued::EVENT => Self::Continued(parse_value(body)?),
|
||||
events::Exited::EVENT => Self::Exited(parse_value(body)?),
|
||||
events::Terminated::EVENT => Self::Terminated(parse_value(body)?),
|
||||
events::Thread::EVENT => Self::Thread(parse_value(body)?),
|
||||
events::Output::EVENT => Self::Output(parse_value(body)?),
|
||||
events::Breakpoint::EVENT => Self::Breakpoint(parse_value(body)?),
|
||||
events::Module::EVENT => Self::Module(parse_value(body)?),
|
||||
events::LoadedSource::EVENT => Self::LoadedSource(parse_value(body)?),
|
||||
events::Process::EVENT => Self::Process(parse_value(body)?),
|
||||
events::Capabilities::EVENT => Self::Capabilities(parse_value(body)?),
|
||||
events::Memory::EVENT => Self::Memory(parse_value(body)?),
|
||||
_ => return Err(Error::Unhandled),
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value<T>(value: serde_json::Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_value(value).map_err(|err| err.into())
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::{Error, Event, Result};
|
||||
use crate::{Error, Result};
|
||||
use anyhow::Context;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -32,11 +32,17 @@ pub struct Response {
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
pub struct Event {
|
||||
pub event: String,
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Payload {
|
||||
// type = "event"
|
||||
Event(Box<Event>),
|
||||
Event(Event),
|
||||
// type = "response"
|
||||
Response(Response),
|
||||
// type = "request"
|
||||
@@ -230,25 +236,37 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_inner(
|
||||
async fn recv(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
client_tx: UnboundedSender<Payload>,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
let msg = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?;
|
||||
transport.process_server_message(&client_tx, msg).await?;
|
||||
}
|
||||
}
|
||||
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
|
||||
Ok(msg) => match transport.process_server_message(&client_tx, msg).await {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
error!("err: <- {err:?}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
if !matches!(err, Error::StreamClosed) {
|
||||
error!("Exiting after unexpected error: {err:?}");
|
||||
}
|
||||
|
||||
async fn recv(
|
||||
transport: Arc<Self>,
|
||||
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
client_tx: UnboundedSender<Payload>,
|
||||
) {
|
||||
if let Err(err) = Self::recv_inner(transport, server_stdout, client_tx).await {
|
||||
error!("err: <- {:?}", err);
|
||||
// Close any outstanding requests.
|
||||
for (id, tx) in transport.pending_requests.lock().await.drain() {
|
||||
match tx.send(Err(Error::StreamClosed)).await {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
error!("Could not close request on a closed channel (id={id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -759,33 +759,30 @@ pub mod requests {
|
||||
pub mod events {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "event", content = "body")]
|
||||
// seq is omitted as unused and is not sent by some implementations
|
||||
pub enum Event {
|
||||
Initialized(Option<DebuggerCapabilities>),
|
||||
Stopped(Stopped),
|
||||
Continued(Continued),
|
||||
Exited(Exited),
|
||||
Terminated(Option<Terminated>),
|
||||
Thread(Thread),
|
||||
Output(Output),
|
||||
Breakpoint(Breakpoint),
|
||||
Module(Module),
|
||||
LoadedSource(LoadedSource),
|
||||
Process(Process),
|
||||
Capabilities(Capabilities),
|
||||
// ProgressStart(),
|
||||
// ProgressUpdate(),
|
||||
// ProgressEnd(),
|
||||
// Invalidated(),
|
||||
Memory(Memory),
|
||||
pub trait Event {
|
||||
type Body: serde::de::DeserializeOwned + serde::Serialize;
|
||||
const EVENT: &'static str;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Initialized {}
|
||||
|
||||
impl Event for Initialized {
|
||||
type Body = Option<DebuggerCapabilities>;
|
||||
const EVENT: &'static str = "initialized";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Stopped {}
|
||||
|
||||
impl Event for Stopped {
|
||||
type Body = StoppedBody;
|
||||
const EVENT: &'static str = "stopped";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Stopped {
|
||||
pub struct StoppedBody {
|
||||
pub reason: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
@@ -801,37 +798,77 @@ pub mod events {
|
||||
pub hit_breakpoint_ids: Option<Vec<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Continued {}
|
||||
|
||||
impl Event for Continued {
|
||||
type Body = ContinuedBody;
|
||||
const EVENT: &'static str = "continued";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Continued {
|
||||
pub struct ContinuedBody {
|
||||
pub thread_id: ThreadId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub all_threads_continued: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Exited {
|
||||
pub exit_code: usize,
|
||||
#[derive(Debug)]
|
||||
pub enum Exited {}
|
||||
|
||||
impl Event for Exited {
|
||||
type Body = ExitedBody;
|
||||
const EVENT: &'static str = "exited";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Terminated {
|
||||
pub struct ExitedBody {
|
||||
pub exit_code: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Terminated {}
|
||||
|
||||
impl Event for Terminated {
|
||||
type Body = Option<TerminatedBody>;
|
||||
const EVENT: &'static str = "terminated";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TerminatedBody {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub restart: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Thread {
|
||||
pub reason: String,
|
||||
pub thread_id: ThreadId,
|
||||
#[derive(Debug)]
|
||||
pub enum Thread {}
|
||||
|
||||
impl Event for Thread {
|
||||
type Body = ThreadBody;
|
||||
const EVENT: &'static str = "thread";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Output {
|
||||
pub struct ThreadBody {
|
||||
pub reason: String,
|
||||
pub thread_id: ThreadId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Output {}
|
||||
|
||||
impl Event for Output {
|
||||
type Body = OutputBody;
|
||||
const EVENT: &'static str = "output";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OutputBody {
|
||||
pub output: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<String>,
|
||||
@@ -849,30 +886,62 @@ pub mod events {
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Breakpoint {}
|
||||
|
||||
impl Event for Breakpoint {
|
||||
type Body = BreakpointBody;
|
||||
const EVENT: &'static str = "breakpoint";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Breakpoint {
|
||||
pub struct BreakpointBody {
|
||||
pub reason: String,
|
||||
pub breakpoint: super::Breakpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Module {}
|
||||
|
||||
impl Event for Module {
|
||||
type Body = ModuleBody;
|
||||
const EVENT: &'static str = "module";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Module {
|
||||
pub struct ModuleBody {
|
||||
pub reason: String,
|
||||
pub module: super::Module,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoadedSource {
|
||||
pub reason: String,
|
||||
pub source: super::Source,
|
||||
#[derive(Debug)]
|
||||
pub enum LoadedSource {}
|
||||
|
||||
impl Event for LoadedSource {
|
||||
type Body = LoadedSourceBody;
|
||||
const EVENT: &'static str = "loadedSource";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Process {
|
||||
pub struct LoadedSourceBody {
|
||||
pub reason: String,
|
||||
pub source: super::Source,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Process {}
|
||||
|
||||
impl Event for Process {
|
||||
type Body = ProcessBody;
|
||||
const EVENT: &'static str = "process";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProcessBody {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_process_id: Option<usize>,
|
||||
@@ -884,39 +953,55 @@ pub mod events {
|
||||
pub pointer_size: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Capabilities {}
|
||||
|
||||
impl Event for Capabilities {
|
||||
type Body = CapabilitiesBody;
|
||||
const EVENT: &'static str = "capabilities";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Capabilities {
|
||||
pub struct CapabilitiesBody {
|
||||
pub capabilities: super::DebuggerCapabilities,
|
||||
}
|
||||
|
||||
// #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
// pub struct Invalidated {
|
||||
// pub struct InvalidatedBody {
|
||||
// pub areas: Vec<InvalidatedArea>,
|
||||
// pub thread_id: Option<ThreadId>,
|
||||
// pub stack_frame_id: Option<usize>,
|
||||
// }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Memory {}
|
||||
|
||||
impl Event for Memory {
|
||||
type Body = MemoryBody;
|
||||
const EVENT: &'static str = "memory";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Memory {
|
||||
pub struct MemoryBody {
|
||||
pub memory_reference: String,
|
||||
pub offset: usize,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_module_id_from_number() {
|
||||
let raw = r#"{"id": 0, "name": "Name"}"#;
|
||||
let module: super::Module = serde_json::from_str(raw).expect("Error!");
|
||||
assert_eq!(module.id, "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_module_id_from_string() {
|
||||
let raw = r#"{"id": "0", "name": "Name"}"#;
|
||||
let module: super::Module = serde_json::from_str(raw).expect("Error!");
|
||||
assert_eq!(module.id, "0");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_module_id_from_number() {
|
||||
let raw = r#"{"id": 0, "name": "Name"}"#;
|
||||
let module: Module = serde_json::from_str(raw).expect("Error!");
|
||||
assert_eq!(module.id, "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_module_id_from_string() {
|
||||
let raw = r#"{"id": "0", "name": "Name"}"#;
|
||||
let module: Module = serde_json::from_str(raw).expect("Error!");
|
||||
assert_eq!(module.id, "0");
|
||||
}
|
||||
|
@@ -12,14 +12,14 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.8.11"
|
||||
hashbrown = "0.14.5"
|
||||
foldhash.workspace = true
|
||||
hashbrown = "0.15"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
|
||||
# the event registry is essentially read only but must be an rwlock so we can
|
||||
# setup new events on initialization, hardware-lock-elision hugely benefits this case
|
||||
# as it essentially makes the lock entirely free as long as there is no writes
|
||||
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
|
||||
once_cell = "1.20"
|
||||
parking_lot = { workspace = true, features = ["hardware-lock-elision"] }
|
||||
once_cell = "1.21"
|
||||
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
|
@@ -14,8 +14,8 @@ use crate::hook::ErasedHook;
|
||||
use crate::runtime_local;
|
||||
|
||||
pub struct Registry {
|
||||
events: HashMap<&'static str, TypeId, ahash::RandomState>,
|
||||
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
|
||||
events: HashMap<&'static str, TypeId, foldhash::fast::FixedState>,
|
||||
handlers: HashMap<&'static str, Vec<ErasedHook>, foldhash::fast::FixedState>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
@@ -105,8 +105,8 @@ runtime_local! {
|
||||
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
|
||||
// hardcoded random number is good enough here we don't care about DOS resistance
|
||||
// and avoids the additional complexity of `Option<Registry>`
|
||||
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
|
||||
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
|
||||
events: HashMap::with_hasher(foldhash::fast::FixedState::with_seed(72536814787)),
|
||||
handlers: HashMap::with_hasher(foldhash::fast::FixedState::with_seed(72536814787)),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -41,8 +41,9 @@ macro_rules! runtime_local {
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
pub struct RuntimeLocal<T: 'static> {
|
||||
data:
|
||||
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
|
||||
data: parking_lot::RwLock<
|
||||
hashbrown::HashMap<tokio::runtime::Id, &'static T, foldhash::fast::FixedState>,
|
||||
>,
|
||||
init: fn() -> T,
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ impl<T> RuntimeLocal<T> {
|
||||
pub const fn __new(init: fn() -> T) -> Self {
|
||||
Self {
|
||||
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
|
||||
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
|
||||
foldhash::fast::FixedState::with_seed(12345678910),
|
||||
)),
|
||||
init,
|
||||
}
|
||||
|
@@ -20,9 +20,9 @@ helix-stdx = { path = "../helix-stdx" }
|
||||
anyhow = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
etcetera = "0.8"
|
||||
etcetera = "0.10"
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.20"
|
||||
once_cell = "1.21"
|
||||
log = "0.4"
|
||||
|
||||
# TODO: these two should be on !wasm32 only
|
||||
@@ -30,8 +30,7 @@ log = "0.4"
|
||||
# cloning/compiling tree-sitter grammars
|
||||
cc = { version = "1" }
|
||||
threadpool = { version = "1.0" }
|
||||
tempfile = "3.14.0"
|
||||
dunce = "1.0.5"
|
||||
tempfile.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
libloading = "0.8"
|
||||
|
@@ -6,23 +6,26 @@ const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
|
||||
const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
|
||||
const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
|
||||
|
||||
fn get_calver() -> String {
|
||||
if PATCH == "0" {
|
||||
format!("{MAJOR}.{MINOR}")
|
||||
} else {
|
||||
format!("{MAJOR}.{MINOR}.{PATCH}")
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let git_hash = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.and_then(|x| String::from_utf8(x.stdout).ok());
|
||||
.and_then(|x| String::from_utf8(x.stdout).ok())
|
||||
.or_else(|| option_env!("HELIX_NIX_BUILD_REV").map(|s| s.to_string()));
|
||||
|
||||
let calver = get_calver();
|
||||
let minor = if MINOR.len() == 1 {
|
||||
// Print single-digit months in '0M' format
|
||||
format!("0{MINOR}")
|
||||
} else {
|
||||
MINOR.to_string()
|
||||
};
|
||||
let calver = if PATCH == "0" {
|
||||
format!("{MAJOR}.{minor}")
|
||||
} else {
|
||||
format!("{MAJOR}.{minor}.{PATCH}")
|
||||
};
|
||||
let version: Cow<_> = match &git_hash {
|
||||
Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(),
|
||||
None => calver.into(),
|
||||
|
@@ -273,12 +273,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
|
||||
}
|
||||
|
||||
// ensure the remote matches the configured remote
|
||||
if get_remote_url(&grammar_dir).map_or(true, |s| s != remote) {
|
||||
if get_remote_url(&grammar_dir).as_ref() != Some(&remote) {
|
||||
set_remote(&grammar_dir, &remote)?;
|
||||
}
|
||||
|
||||
// ensure the revision matches the configured revision
|
||||
if get_revision(&grammar_dir).map_or(true, |s| s != revision) {
|
||||
if get_revision(&grammar_dir).as_ref() != Some(&revision) {
|
||||
// Fetch the exact revision from the remote.
|
||||
// Supported by server-side git since v2.5.0 (July 2015),
|
||||
// enabled by default on major git hosts.
|
||||
@@ -496,9 +496,11 @@ fn build_tree_sitter_library(
|
||||
.arg("/link")
|
||||
.arg(format!("/out:{}", library_path.to_str().unwrap()));
|
||||
} else {
|
||||
#[cfg(not(windows))]
|
||||
command.arg("-fPIC");
|
||||
|
||||
command
|
||||
.arg("-shared")
|
||||
.arg("-fPIC")
|
||||
.arg("-fno-exceptions")
|
||||
.arg("-I")
|
||||
.arg(header_path)
|
||||
@@ -517,8 +519,11 @@ fn build_tree_sitter_library(
|
||||
cpp_command.args(compiler.args());
|
||||
let object_file =
|
||||
library_path.with_file_name(format!("{}_scanner.o", &grammar.grammar_id));
|
||||
|
||||
#[cfg(not(windows))]
|
||||
cpp_command.arg("-fPIC");
|
||||
|
||||
cpp_command
|
||||
.arg("-fPIC")
|
||||
.arg("-fno-exceptions")
|
||||
.arg("-I")
|
||||
.arg(header_path)
|
||||
@@ -592,6 +597,6 @@ fn mtime(path: &Path) -> Result<SystemTime> {
|
||||
/// Gives the contents of a file from a language's `runtime/queries/<lang>`
|
||||
/// directory
|
||||
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||
let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename));
|
||||
let path = crate::runtime_file(PathBuf::new().join("queries").join(language).join(filename));
|
||||
std::fs::read_to_string(path)
|
||||
}
|
||||
|
@@ -107,8 +107,8 @@ fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> {
|
||||
/// The valid runtime directories are searched in priority order and the first
|
||||
/// file found to exist is returned, otherwise the path to the final attempt
|
||||
/// that failed.
|
||||
pub fn runtime_file(rel_path: &Path) -> PathBuf {
|
||||
find_runtime_file(rel_path).unwrap_or_else(|| {
|
||||
pub fn runtime_file(rel_path: impl AsRef<Path>) -> PathBuf {
|
||||
find_runtime_file(rel_path.as_ref()).unwrap_or_else(|| {
|
||||
RUNTIME_DIRS
|
||||
.last()
|
||||
.map(|dir| dir.join(rel_path))
|
||||
|
@@ -21,10 +21,9 @@ keywords = ["language", "server", "lsp", "vscode", "lsif"]
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.6.0"
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
serde_repr = "0.1"
|
||||
bitflags.workspace = true
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
url = {version = "2.5.4", features = ["serde"]}
|
||||
|
||||
[features]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -33,8 +33,13 @@ pub struct DiagnosticClientCapabilities {
|
||||
pub struct DiagnosticOptions {
|
||||
/// An optional identifier under which the diagnostics are
|
||||
/// managed by the client.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub identifier: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_option_arc_str",
|
||||
deserialize_with = "deserialize_option_arc_str"
|
||||
)]
|
||||
pub identifier: Option<Arc<str>>,
|
||||
|
||||
/// Whether the language has inter file dependencies, meaning that editing code in one file can
|
||||
/// result in a different diagnostic set in another file. Inter file dependencies are common
|
||||
@@ -48,6 +53,19 @@ pub struct DiagnosticOptions {
|
||||
pub work_done_progress_options: WorkDoneProgressOptions,
|
||||
}
|
||||
|
||||
fn serialize_option_arc_str<S: serde::Serializer>(
|
||||
val: &Option<Arc<str>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(val.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn deserialize_option_arc_str<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Arc<str>>, D::Error> {
|
||||
Option::<String>::deserialize(deserializer).map(|opt| opt.map(|s| s.into()))
|
||||
}
|
||||
|
||||
/// Diagnostic registration options.
|
||||
///
|
||||
/// @since 3.17.0
|
||||
@@ -81,7 +99,13 @@ pub struct DocumentDiagnosticParams {
|
||||
pub text_document: TextDocumentIdentifier,
|
||||
|
||||
/// The additional identifier provided during registration.
|
||||
pub identifier: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_option_arc_str",
|
||||
deserialize_with = "deserialize_option_arc_str"
|
||||
)]
|
||||
pub identifier: Option<Arc<str>>,
|
||||
|
||||
/// The result ID of a previous response if provided.
|
||||
pub previous_result_id: Option<String>,
|
||||
|
@@ -16,19 +16,18 @@ homepage.workspace = true
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
helix-lsp-types = { path = "../helix-lsp-types" }
|
||||
|
||||
anyhow = "1.0"
|
||||
futures-executor = "0.3"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
globset = "0.4.15"
|
||||
globset = "0.4.16"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
parking_lot = "0.12.3"
|
||||
parking_lot.workspace = true
|
||||
arc-swap = "1"
|
||||
slotmap.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
@@ -16,11 +16,14 @@ use helix_stdx::path;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use std::{future::Future, sync::OnceLock};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use tokio::{
|
||||
@@ -85,7 +88,7 @@ impl Client {
|
||||
.and_then(|root| lsp::Url::from_file_path(root).ok());
|
||||
|
||||
if self.root_path == root.unwrap_or(workspace)
|
||||
|| root_uri.as_ref().map_or(false, |root_uri| {
|
||||
|| root_uri.as_ref().is_some_and(|root_uri| {
|
||||
self.workspace_folders
|
||||
.lock()
|
||||
.iter()
|
||||
@@ -170,7 +173,7 @@ impl Client {
|
||||
// and that we can therefore reuse the client (but are done now)
|
||||
return;
|
||||
}
|
||||
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
|
||||
self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
@@ -178,7 +181,7 @@ impl Client {
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
config: Option<Value>,
|
||||
server_environment: HashMap<String, String>,
|
||||
server_environment: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
|
||||
root_path: PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
id: LanguageServerId,
|
||||
@@ -353,6 +356,7 @@ impl Client {
|
||||
capabilities.inlay_hint_provider,
|
||||
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
|
||||
),
|
||||
LanguageServerFeature::DocumentColors => capabilities.color_provider.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,23 +386,11 @@ impl Client {
|
||||
self.workspace_folders.lock()
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
R::Result: core::fmt::Debug, // TODO: temporary
|
||||
{
|
||||
// a future that resolves into the response
|
||||
let json = self.call::<R>(params).await?;
|
||||
let response = serde_json::from_value(json)?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
fn call<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
) -> impl Future<Output = Result<R::Result>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
@@ -408,7 +400,7 @@ impl Client {
|
||||
fn call_with_ref<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: &R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
) -> impl Future<Output = Result<R::Result>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
@@ -419,66 +411,77 @@ impl Client {
|
||||
&self,
|
||||
params: &R::Params,
|
||||
timeout_secs: u64,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
) -> impl Future<Output = Result<R::Result>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let server_tx = self.server_tx.clone();
|
||||
let id = self.next_request_id();
|
||||
|
||||
let params = serde_json::to_value(params);
|
||||
// It's important that this is not part of the future so that it gets executed right away
|
||||
// and the request order stays consistent.
|
||||
let rx = serde_json::to_value(params)
|
||||
.map_err(Error::from)
|
||||
.and_then(|params| {
|
||||
let request = jsonrpc::MethodCall {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
id: id.clone(),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
let (tx, rx) = channel::<Result<Value>>(1);
|
||||
server_tx
|
||||
.send(Payload::Request {
|
||||
chan: tx,
|
||||
value: request,
|
||||
})
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
Ok(rx)
|
||||
});
|
||||
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
let request = jsonrpc::MethodCall {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
id: id.clone(),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params?),
|
||||
};
|
||||
|
||||
let (tx, mut rx) = channel::<Result<Value>>(1);
|
||||
|
||||
server_tx
|
||||
.send(Payload::Request {
|
||||
chan: tx,
|
||||
value: request,
|
||||
})
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
// TODO: delay other calls until initialize success
|
||||
timeout(Duration::from_secs(timeout_secs), rx.recv())
|
||||
timeout(Duration::from_secs(timeout_secs), rx?.recv())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout(id))? // return Timeout
|
||||
.ok_or(Error::StreamClosed)?
|
||||
.and_then(|value| serde_json::from_value(value).map_err(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a RPC notification to the language server.
|
||||
pub fn notify<R: lsp::notification::Notification>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<()>>
|
||||
pub fn notify<R: lsp::notification::Notification>(&self, params: R::Params)
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
let params = serde_json::to_value(params)?;
|
||||
let params = match serde_json::to_value(params) {
|
||||
Ok(params) => params,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to serialize params for notification '{}' for server '{}': {err}",
|
||||
R::METHOD,
|
||||
self.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let notification = jsonrpc::Notification {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
let notification = jsonrpc::Notification {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
|
||||
server_tx
|
||||
.send(Payload::Notification(notification))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
if let Err(err) = server_tx.send(Payload::Notification(notification)) {
|
||||
log::error!(
|
||||
"Failed to send notification '{}' to server '{}': {err}",
|
||||
R::METHOD,
|
||||
self.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,31 +490,29 @@ impl Client {
|
||||
&self,
|
||||
id: jsonrpc::Id,
|
||||
result: core::result::Result<Value, jsonrpc::Error>,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
) -> Result<()> {
|
||||
use jsonrpc::{Failure, Output, Success, Version};
|
||||
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
let output = match result {
|
||||
Ok(result) => Output::Success(Success {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
result: serde_json::to_value(result)?,
|
||||
}),
|
||||
Err(error) => Output::Failure(Failure {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
error,
|
||||
}),
|
||||
};
|
||||
let output = match result {
|
||||
Ok(result) => Output::Success(Success {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
result,
|
||||
}),
|
||||
Err(error) => Output::Failure(Failure {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
error,
|
||||
}),
|
||||
};
|
||||
|
||||
server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -686,14 +687,14 @@ impl Client {
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
||||
};
|
||||
|
||||
self.request::<lsp::request::Initialize>(params).await
|
||||
self.call::<lsp::request::Initialize>(params).await
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
self.request::<lsp::request::Shutdown>(()).await
|
||||
self.call::<lsp::request::Shutdown>(()).await
|
||||
}
|
||||
|
||||
pub fn exit(&self) -> impl Future<Output = Result<()>> {
|
||||
pub fn exit(&self) {
|
||||
self.notify::<lsp::notification::Exit>(())
|
||||
}
|
||||
|
||||
@@ -701,7 +702,8 @@ impl Client {
|
||||
/// early if server responds with an error.
|
||||
pub async fn shutdown_and_exit(&self) -> Result<()> {
|
||||
self.shutdown().await?;
|
||||
self.exit().await
|
||||
self.exit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Forcefully shuts down the language server ignoring any errors.
|
||||
@@ -709,24 +711,21 @@ impl Client {
|
||||
if let Err(e) = self.shutdown().await {
|
||||
log::warn!("language server failed to terminate gracefully - {}", e);
|
||||
}
|
||||
self.exit().await
|
||||
self.exit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Workspace
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
pub fn did_change_configuration(&self, settings: Value) -> impl Future<Output = Result<()>> {
|
||||
pub fn did_change_configuration(&self, settings: Value) {
|
||||
self.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
}
|
||||
|
||||
pub fn did_change_workspace(
|
||||
&self,
|
||||
added: Vec<WorkspaceFolder>,
|
||||
removed: Vec<WorkspaceFolder>,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
pub fn did_change_workspace(&self, added: Vec<WorkspaceFolder>, removed: Vec<WorkspaceFolder>) {
|
||||
self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
|
||||
event: WorkspaceFoldersChangeEvent { added, removed },
|
||||
})
|
||||
@@ -737,7 +736,7 @@ impl Client {
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.will_rename.has_interest(old_path, is_dir) {
|
||||
return None;
|
||||
@@ -754,24 +753,13 @@ impl Client {
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
|
||||
Some(self.call_with_timeout::<lsp::request::WillRenameFiles>(
|
||||
&lsp::RenameFilesParams { files },
|
||||
5,
|
||||
);
|
||||
|
||||
Some(async move {
|
||||
let json = request.await?;
|
||||
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
|
||||
Ok(response.unwrap_or_default())
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
pub fn did_rename(
|
||||
&self,
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
|
||||
pub fn did_rename(&self, old_path: &Path, new_path: &Path, is_dir: bool) -> Option<()> {
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.did_rename.has_interest(new_path, is_dir) {
|
||||
return None;
|
||||
@@ -789,7 +777,8 @@ impl Client {
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
|
||||
self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files });
|
||||
Some(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -802,7 +791,7 @@ impl Client {
|
||||
version: i32,
|
||||
doc: &Rope,
|
||||
language_id: String,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
) {
|
||||
self.notify::<lsp::notification::DidOpenTextDocument>(lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem {
|
||||
uri,
|
||||
@@ -929,7 +918,7 @@ impl Client {
|
||||
old_text: &Rope,
|
||||
new_text: &Rope,
|
||||
changes: &ChangeSet,
|
||||
) -> Option<impl Future<Output = Result<()>>> {
|
||||
) -> Option<()> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support document sync.
|
||||
@@ -961,18 +950,14 @@ impl Client {
|
||||
kind => unimplemented!("{:?}", kind),
|
||||
};
|
||||
|
||||
Some(self.notify::<lsp::notification::DidChangeTextDocument>(
|
||||
lsp::DidChangeTextDocumentParams {
|
||||
text_document,
|
||||
content_changes: changes,
|
||||
},
|
||||
))
|
||||
self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
|
||||
text_document,
|
||||
content_changes: changes,
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn text_document_did_close(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
pub fn text_document_did_close(&self, text_document: lsp::TextDocumentIdentifier) {
|
||||
self.notify::<lsp::notification::DidCloseTextDocument>(lsp::DidCloseTextDocumentParams {
|
||||
text_document,
|
||||
})
|
||||
@@ -984,7 +969,7 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
text: &Rope,
|
||||
) -> Option<impl Future<Output = Result<()>>> {
|
||||
) -> Option<()> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
let include_text = match &capabilities.text_document_sync.as_ref()? {
|
||||
@@ -1002,12 +987,11 @@ impl Client {
|
||||
lsp::TextDocumentSyncCapability::Kind(..) => false,
|
||||
};
|
||||
|
||||
Some(self.notify::<lsp::notification::DidSaveTextDocument>(
|
||||
lsp::DidSaveTextDocumentParams {
|
||||
text_document,
|
||||
text: include_text.then_some(text.into()),
|
||||
},
|
||||
))
|
||||
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
|
||||
text_document,
|
||||
text: include_text.then_some(text.into()),
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn completion(
|
||||
@@ -1016,7 +1000,7 @@ impl Client {
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
context: lsp::CompletionContext,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::CompletionResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support completion.
|
||||
@@ -1042,14 +1026,13 @@ impl Client {
|
||||
&self,
|
||||
completion_item: &lsp::CompletionItem,
|
||||
) -> impl Future<Output = Result<lsp::CompletionItem>> {
|
||||
let res = self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item);
|
||||
async move { Ok(serde_json::from_value(res.await?)?) }
|
||||
self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item)
|
||||
}
|
||||
|
||||
pub fn resolve_code_action(
|
||||
&self,
|
||||
code_action: lsp::CodeAction,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
code_action: &lsp::CodeAction,
|
||||
) -> Option<impl Future<Output = Result<lsp::CodeAction>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support resolving code actions.
|
||||
@@ -1061,7 +1044,7 @@ impl Client {
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
|
||||
Some(self.call_with_ref::<lsp::request::CodeActionResolveRequest>(code_action))
|
||||
}
|
||||
|
||||
pub fn text_document_signature_help(
|
||||
@@ -1085,8 +1068,7 @@ impl Client {
|
||||
// lsp::SignatureHelpContext
|
||||
};
|
||||
|
||||
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
|
||||
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_range_inlay_hints(
|
||||
@@ -1094,7 +1076,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
range: lsp::Range,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::InlayHint>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
match capabilities.inlay_hint_provider {
|
||||
@@ -1114,12 +1096,31 @@ impl Client {
|
||||
Some(self.call::<lsp::request::InlayHintRequest>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_document_color(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Vec<lsp::ColorInformation>>>> {
|
||||
self.capabilities.get().unwrap().color_provider.as_ref()?;
|
||||
let params = lsp::DocumentColorParams {
|
||||
text_document,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: work_done_token.clone(),
|
||||
},
|
||||
partial_result_params: helix_lsp_types::PartialResultParams {
|
||||
partial_result_token: work_done_token,
|
||||
},
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::DocumentColor>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_hover(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::Hover>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support hover.
|
||||
@@ -1150,7 +1151,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
options: lsp::FormattingOptions,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support formatting.
|
||||
@@ -1183,13 +1184,7 @@ impl Client {
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
};
|
||||
|
||||
let request = self.call::<lsp::request::Formatting>(params);
|
||||
|
||||
Some(async move {
|
||||
let json = request.await?;
|
||||
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
|
||||
Ok(response.unwrap_or_default())
|
||||
})
|
||||
Some(self.call::<lsp::request::Formatting>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_range_formatting(
|
||||
@@ -1198,7 +1193,7 @@ impl Client {
|
||||
range: lsp::Range,
|
||||
options: lsp::FormattingOptions,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support range formatting.
|
||||
@@ -1214,13 +1209,7 @@ impl Client {
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
};
|
||||
|
||||
let request = self.call::<lsp::request::RangeFormatting>(params);
|
||||
|
||||
Some(async move {
|
||||
let json = request.await?;
|
||||
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
|
||||
Ok(response.unwrap_or_default())
|
||||
})
|
||||
Some(self.call::<lsp::request::RangeFormatting>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_document_highlight(
|
||||
@@ -1228,7 +1217,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::DocumentHighlight>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support document highlight.
|
||||
@@ -1261,7 +1250,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
) -> impl Future<Output = Result<T::Result>> {
|
||||
let params = lsp::GotoDefinitionParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document,
|
||||
@@ -1281,7 +1270,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support goto-definition.
|
||||
@@ -1302,7 +1291,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support goto-declaration.
|
||||
@@ -1327,7 +1316,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support goto-type-definition.
|
||||
@@ -1351,7 +1340,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support goto-definition.
|
||||
@@ -1376,7 +1365,7 @@ impl Client {
|
||||
position: lsp::Position,
|
||||
include_declaration: bool,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::Location>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support goto-reference.
|
||||
@@ -1405,7 +1394,7 @@ impl Client {
|
||||
pub fn document_symbols(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::DocumentSymbolResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support document symbols.
|
||||
@@ -1427,7 +1416,7 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::PrepareRenameResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
match capabilities.rename_provider {
|
||||
@@ -1447,7 +1436,10 @@ impl Client {
|
||||
}
|
||||
|
||||
// empty string to get all symbols
|
||||
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
|
||||
pub fn workspace_symbols(
|
||||
&self,
|
||||
query: String,
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceSymbolResponse>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support workspace symbols.
|
||||
@@ -1470,7 +1462,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
range: lsp::Range,
|
||||
context: lsp::CodeActionContext,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<Vec<lsp::CodeActionOrCommand>>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support code actions.
|
||||
@@ -1498,7 +1490,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
new_name: String,
|
||||
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
|
||||
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
|
||||
return None;
|
||||
}
|
||||
@@ -1514,16 +1506,13 @@ impl Client {
|
||||
},
|
||||
};
|
||||
|
||||
let request = self.call::<lsp::request::Rename>(params);
|
||||
|
||||
Some(async move {
|
||||
let json = request.await?;
|
||||
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
|
||||
Ok(response.unwrap_or_default())
|
||||
})
|
||||
Some(self.call::<lsp::request::Rename>(params))
|
||||
}
|
||||
|
||||
pub fn command(&self, command: lsp::Command) -> Option<impl Future<Output = Result<Value>>> {
|
||||
pub fn command(
|
||||
&self,
|
||||
command: lsp::Command,
|
||||
) -> Option<impl Future<Output = Result<Option<Value>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the language server does not support executing commands.
|
||||
@@ -1540,10 +1529,7 @@ impl Client {
|
||||
Some(self.call::<lsp::request::ExecuteCommand>(params))
|
||||
}
|
||||
|
||||
pub fn did_change_watched_files(
|
||||
&self,
|
||||
changes: Vec<lsp::FileEvent>,
|
||||
) -> impl Future<Output = std::result::Result<(), Error>> {
|
||||
pub fn did_change_watched_files(&self, changes: Vec<lsp::FileEvent>) {
|
||||
self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
|
||||
changes,
|
||||
})
|
||||
|
@@ -113,17 +113,13 @@ impl Handler {
|
||||
"Sending didChangeWatchedFiles notification to client '{}'",
|
||||
client.name()
|
||||
);
|
||||
if let Err(err) = crate::block_on(client
|
||||
.did_change_watched_files(vec![lsp::FileEvent {
|
||||
uri,
|
||||
// We currently always send the CHANGED state
|
||||
// since we don't actually have more context at
|
||||
// the moment.
|
||||
typ: lsp::FileChangeType::CHANGED,
|
||||
}]))
|
||||
{
|
||||
log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
|
||||
}
|
||||
client.did_change_watched_files(vec![lsp::FileEvent {
|
||||
uri,
|
||||
// We currently always send the CHANGED state
|
||||
// since we don't actually have more context at
|
||||
// the moment.
|
||||
typ: lsp::FileChangeType::CHANGED,
|
||||
}]);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
@@ -104,10 +104,37 @@ impl std::error::Error for Error {}
|
||||
#[serde(untagged)]
|
||||
pub enum Id {
|
||||
Null,
|
||||
Num(u64),
|
||||
Num(#[serde(deserialize_with = "deserialize_jsonrpc_id_num")] u64),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
fn deserialize_jsonrpc_id_num<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let num = serde_json::Number::deserialize(deserializer)?;
|
||||
|
||||
if let Some(val) = num.as_u64() {
|
||||
return Ok(val);
|
||||
};
|
||||
|
||||
// Accept floats as long as they represent positive whole numbers.
|
||||
// The JSONRPC spec says "Numbers SHOULD NOT contain fractional parts" so we should try to
|
||||
// accept them if possible. The JavaScript type system lumps integers and floats together so
|
||||
// some languages may serialize integer IDs as floats with a zeroed fractional part.
|
||||
// See <https://github.com/helix-editor/helix/issues/12367>.
|
||||
if let Some(val) = num
|
||||
.as_f64()
|
||||
.filter(|f| f.is_sign_positive() && f.fract() == 0.0)
|
||||
{
|
||||
return Ok(val as u64);
|
||||
}
|
||||
|
||||
Err(de::Error::custom(
|
||||
"number must be integer or float representing a whole number in valid u64 range",
|
||||
))
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -375,6 +402,22 @@ fn serialize_skip_none_params() {
|
||||
assert_eq!(serialized, r#"{"jsonrpc":"2.0","method":"exit"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_deserialize() {
|
||||
use serde_json;
|
||||
|
||||
let id = r#"8"#;
|
||||
let deserialized: Id = serde_json::from_str(id).unwrap();
|
||||
assert_eq!(deserialized, Id::Num(8));
|
||||
|
||||
let id = r#"4.0"#;
|
||||
let deserialized: Id = serde_json::from_str(id).unwrap();
|
||||
assert_eq!(deserialized, Id::Num(4));
|
||||
|
||||
let id = r#"0.01"#;
|
||||
assert!(serde_json::from_str::<Id>(id).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_output_deserialize() {
|
||||
use serde_json;
|
||||
|
@@ -618,51 +618,45 @@ impl Registry {
|
||||
Ok(self.inner[id].clone())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// If this method is called, all documents that have a reference to the language server have to refresh their language servers,
|
||||
/// See helix_view::editor::Editor::refresh_language_servers
|
||||
pub fn restart(
|
||||
pub fn restart_server(
|
||||
&mut self,
|
||||
name: &str,
|
||||
language_config: &LanguageConfiguration,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<Vec<Arc<Client>>> {
|
||||
language_config
|
||||
.language_servers
|
||||
.iter()
|
||||
.filter_map(|LanguageServerFeatures { name, .. }| {
|
||||
if let Some(old_clients) = self.inner_by_name.remove(name) {
|
||||
if old_clients.is_empty() {
|
||||
log::info!("restarting client for '{name}' which was manually stopped");
|
||||
} else {
|
||||
log::info!("stopping existing clients for '{name}'");
|
||||
}
|
||||
for old_client in old_clients {
|
||||
self.file_event_handler.remove_client(old_client.id());
|
||||
self.inner.remove(old_client.id());
|
||||
tokio::spawn(async move {
|
||||
let _ = old_client.force_shutdown().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
let client = match self.start_client(
|
||||
name.clone(),
|
||||
language_config,
|
||||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => client,
|
||||
Err(StartupError::NoRequiredRootFound) => return None,
|
||||
Err(StartupError::Error(err)) => return Some(Err(err)),
|
||||
};
|
||||
self.inner_by_name
|
||||
.insert(name.to_owned(), vec![client.clone()]);
|
||||
) -> Option<Result<Arc<Client>>> {
|
||||
if let Some(old_clients) = self.inner_by_name.remove(name) {
|
||||
if old_clients.is_empty() {
|
||||
log::info!("restarting client for '{name}' which was manually stopped");
|
||||
} else {
|
||||
log::info!("stopping existing clients for '{name}'");
|
||||
}
|
||||
for old_client in old_clients {
|
||||
self.file_event_handler.remove_client(old_client.id());
|
||||
self.inner.remove(old_client.id());
|
||||
tokio::spawn(async move {
|
||||
let _ = old_client.force_shutdown().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
let client = match self.start_client(
|
||||
name.to_string(),
|
||||
language_config,
|
||||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => client,
|
||||
Err(StartupError::NoRequiredRootFound) => return None,
|
||||
Err(StartupError::Error(err)) => return Some(Err(err)),
|
||||
};
|
||||
self.inner_by_name
|
||||
.insert(name.to_owned(), vec![client.clone()]);
|
||||
|
||||
Some(Ok(client))
|
||||
})
|
||||
.collect()
|
||||
Some(Ok(client))
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, name: &str) {
|
||||
@@ -701,7 +695,11 @@ impl Registry {
|
||||
}
|
||||
|
||||
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
||||
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
|
||||
let manual_roots = language_config
|
||||
.workspace_lsp_roots
|
||||
.as_deref()
|
||||
.unwrap_or(root_dirs);
|
||||
client.try_add_doc(&language_config.roots, manual_roots, doc_path, *i == 0)
|
||||
}) {
|
||||
return Some((name.to_owned(), Ok(client.clone())));
|
||||
}
|
||||
@@ -873,7 +871,7 @@ fn start_client(
|
||||
&ls_config.command,
|
||||
&ls_config.args,
|
||||
ls_config.config.clone(),
|
||||
ls_config.environment.clone(),
|
||||
&ls_config.environment,
|
||||
root_path,
|
||||
root_uri,
|
||||
id,
|
||||
@@ -902,17 +900,7 @@ fn start_client(
|
||||
}
|
||||
|
||||
// next up, notify<initialized>
|
||||
let notification_result = _client
|
||||
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
|
||||
.await;
|
||||
|
||||
if let Err(e) = notification_result {
|
||||
log::error!(
|
||||
"failed to notify language server of its initialization: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
_client.notify::<lsp::notification::Initialized>(lsp::InitializedParams {});
|
||||
|
||||
initialize_notify.notify_one();
|
||||
});
|
||||
@@ -1044,7 +1032,8 @@ mod tests {
|
||||
|
||||
let mut source = Rope::from_str("[\n\"🇺🇸\",\n\"🎄\",\n]");
|
||||
|
||||
let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf8);
|
||||
let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf16);
|
||||
assert!(transaction.apply(&mut source));
|
||||
assert_eq!(source, "[\n \"🇺🇸\",\n \"🎄\",\n]");
|
||||
}
|
||||
}
|
||||
|
@@ -223,10 +223,7 @@ impl Transport {
|
||||
language_server_name: &str,
|
||||
) -> Result<()> {
|
||||
let (id, result) = match output {
|
||||
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
|
||||
info!("{language_server_name} <- {}", result);
|
||||
(id, Ok(result))
|
||||
}
|
||||
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => (id, Ok(result)),
|
||||
jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
|
||||
error!("{language_server_name} <- {error}");
|
||||
(id, Err(error.into()))
|
||||
|
@@ -13,19 +13,20 @@ homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dunce = "1.0"
|
||||
etcetera = "0.8"
|
||||
ropey = { version = "1.6.1", default-features = false }
|
||||
etcetera = "0.10"
|
||||
ropey.workspace = true
|
||||
which = "7.0"
|
||||
regex-cursor = "0.1.4"
|
||||
bitflags = "2.6"
|
||||
once_cell = "1.19"
|
||||
regex-cursor = "0.1.5"
|
||||
bitflags.workspace = true
|
||||
once_cell = "1.21"
|
||||
regex-automata = "0.4.9"
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { version = "0.38", features = ["fs"] }
|
||||
rustix = { version = "1.0", features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14"
|
||||
tempfile.workspace = true
|
||||
|
@@ -103,6 +103,12 @@ fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>)
|
||||
let mat = captures.get_match().unwrap();
|
||||
let pattern_id = mat.pattern().as_usize();
|
||||
let mut range = mat.range();
|
||||
// A pattern may match multiple times on a single variable, for example `${HOME:-$HOME}`:
|
||||
// `${HOME:-` matches and also the default value (`$HOME`). Skip past any variables which
|
||||
// have already been expanded.
|
||||
if range.start < pos {
|
||||
continue;
|
||||
}
|
||||
let var = &bytes[captures.get_group(1).unwrap().range()];
|
||||
let default = if pattern_id != 5 {
|
||||
let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else {
|
||||
@@ -203,6 +209,7 @@ mod tests {
|
||||
assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${FOO:-$FOO}/foo", "baz/foo/foo");
|
||||
assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo");
|
||||
|
@@ -51,8 +51,8 @@ mod imp {
|
||||
}
|
||||
|
||||
fn chown(p: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
|
||||
let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) });
|
||||
let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) });
|
||||
let uid = uid.map(rustix::fs::Uid::from_raw);
|
||||
let gid = gid.map(rustix::fs::Gid::from_raw);
|
||||
rustix::fs::chown(p, uid, gid)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -33,7 +33,9 @@ where
|
||||
}
|
||||
|
||||
/// Expands tilde `~` into users home directory if available, otherwise returns the path
|
||||
/// unchanged. The tilde will only be expanded when present as the first component of the path
|
||||
/// unchanged.
|
||||
///
|
||||
/// The tilde will only be expanded when present as the first component of the path
|
||||
/// and only slash follows it.
|
||||
pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
|
||||
where
|
||||
@@ -54,11 +56,11 @@ where
|
||||
}
|
||||
|
||||
/// Normalize a path without resolving symlinks.
|
||||
// Strategy: start from the first component and move up. Cannonicalize previous path,
|
||||
// Strategy: start from the first component and move up. Canonicalize previous path,
|
||||
// join component, canonicalize new path, strip prefix and join to the final result.
|
||||
pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
|
||||
let mut components = path.as_ref().components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
@@ -209,7 +211,7 @@ fn path_component_regex(windows: bool) -> String {
|
||||
// TODO: support backslash path escape on windows (when using git bash for example)
|
||||
let space_escape = if windows { r"[\^`]\s" } else { r"[\\]\s" };
|
||||
// partially baesd on what's allowed in an url but with some care to avoid
|
||||
// false positivies (like any kind of brackets or quotes)
|
||||
// false positives (like any kind of brackets or quotes)
|
||||
r"[\w@.\-+#$%?!,;~&]|".to_owned() + space_escape
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
use std::fmt;
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex};
|
||||
pub use regex_cursor::regex_automata::util::syntax::Config;
|
||||
use regex_cursor::{Input as RegexInput, RopeyCursor};
|
||||
use ropey::str_utils::byte_to_char_idx;
|
||||
use ropey::iter::Chunks;
|
||||
use ropey::RopeSlice;
|
||||
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
|
||||
pub trait RopeSliceExt<'a>: Sized {
|
||||
fn ends_with(self, text: &str) -> bool;
|
||||
@@ -17,23 +19,138 @@ pub trait RopeSliceExt<'a>: Sized {
|
||||
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>>;
|
||||
fn first_non_whitespace_char(self) -> Option<usize>;
|
||||
fn last_non_whitespace_char(self) -> Option<usize>;
|
||||
/// returns the char idx of `byte_idx`, if `byte_idx` is a char boundary
|
||||
/// this function behaves the same as `byte_to_char` but if `byte_idx` is
|
||||
/// not a valid char boundary (so within a char) this will return the next
|
||||
/// char index.
|
||||
/// Finds the closest byte index not exceeding `byte_idx` which lies on a character boundary.
|
||||
///
|
||||
/// If `byte_idx` already lies on a character boundary then it is returned as-is. When
|
||||
/// `byte_idx` lies between two character boundaries, this function returns the byte index of
|
||||
/// the lesser / earlier / left-hand-side boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("😆");
|
||||
/// for i in 1..text.len_bytes() {
|
||||
/// assert_eq!(text.byte_to_char(i), 0);
|
||||
/// assert_eq!(text.byte_to_next_char(i), 1);
|
||||
/// }
|
||||
/// let text = RopeSlice::from("⌚"); // three bytes: e2 8c 9a
|
||||
/// assert_eq!(text.floor_char_boundary(0), 0);
|
||||
/// assert_eq!(text.floor_char_boundary(1), 0);
|
||||
/// assert_eq!(text.floor_char_boundary(2), 0);
|
||||
/// assert_eq!(text.floor_char_boundary(3), 3);
|
||||
/// ```
|
||||
fn byte_to_next_char(self, byte_idx: usize) -> usize;
|
||||
fn floor_char_boundary(self, byte_idx: usize) -> usize;
|
||||
/// Finds the closest byte index not below `byte_idx` which lies on a character boundary.
|
||||
///
|
||||
/// If `byte_idx` already lies on a character boundary then it is returned as-is. When
|
||||
/// `byte_idx` lies between two character boundaries, this function returns the byte index of
|
||||
/// the greater / later / right-hand-side boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("⌚"); // three bytes: e2 8c 9a
|
||||
/// assert_eq!(text.ceil_char_boundary(0), 0);
|
||||
/// assert_eq!(text.ceil_char_boundary(1), 3);
|
||||
/// assert_eq!(text.ceil_char_boundary(2), 3);
|
||||
/// assert_eq!(text.ceil_char_boundary(3), 3);
|
||||
/// ```
|
||||
fn ceil_char_boundary(self, byte_idx: usize) -> usize;
|
||||
/// Checks whether the given `byte_idx` lies on a character boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("⌚"); // three bytes: e2 8c 9a
|
||||
/// assert!(text.is_char_boundary(0));
|
||||
/// assert!(!text.is_char_boundary(1));
|
||||
/// assert!(!text.is_char_boundary(2));
|
||||
/// assert!(text.is_char_boundary(3));
|
||||
/// ```
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_char_boundary(self, byte_idx: usize) -> bool;
|
||||
/// Finds the closest byte index not exceeding `byte_idx` which lies on a grapheme cluster
|
||||
/// boundary.
|
||||
///
|
||||
/// If `byte_idx` already lies on a grapheme cluster boundary then it is returned as-is. When
|
||||
/// `byte_idx` lies between two grapheme cluster boundaries, this function returns the byte
|
||||
/// index of the lesser / earlier / left-hand-side boundary.
|
||||
///
|
||||
/// `byte_idx` does not need to be aligned to a character boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
|
||||
/// assert_eq!(text.floor_grapheme_boundary(0), 0);
|
||||
/// assert_eq!(text.floor_grapheme_boundary(1), 0);
|
||||
/// assert_eq!(text.floor_grapheme_boundary(2), 2);
|
||||
/// ```
|
||||
fn floor_grapheme_boundary(self, byte_idx: usize) -> usize;
|
||||
/// Finds the closest byte index not exceeding `byte_idx` which lies on a grapheme cluster
|
||||
/// boundary.
|
||||
///
|
||||
/// If `byte_idx` already lies on a grapheme cluster boundary then it is returned as-is. When
|
||||
/// `byte_idx` lies between two grapheme cluster boundaries, this function returns the byte
|
||||
/// index of the greater / later / right-hand-side boundary.
|
||||
///
|
||||
/// `byte_idx` does not need to be aligned to a character boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
|
||||
/// assert_eq!(text.ceil_grapheme_boundary(0), 0);
|
||||
/// assert_eq!(text.ceil_grapheme_boundary(1), 2);
|
||||
/// assert_eq!(text.ceil_grapheme_boundary(2), 2);
|
||||
/// ```
|
||||
fn ceil_grapheme_boundary(self, byte_idx: usize) -> usize;
|
||||
/// Checks whether the `byte_idx` lies on a grapheme cluster boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
|
||||
/// assert!(text.is_grapheme_boundary(0));
|
||||
/// assert!(!text.is_grapheme_boundary(1));
|
||||
/// assert!(text.is_grapheme_boundary(2));
|
||||
/// ```
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_grapheme_boundary(self, byte_idx: usize) -> bool;
|
||||
/// Returns an iterator over the grapheme clusters in the slice.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("😶🌫️🏴☠️🖼️");
|
||||
/// let graphemes: Vec<_> = text.graphemes().collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["😶🌫️", "🏴☠️", "🖼️"]);
|
||||
/// ```
|
||||
fn graphemes(self) -> RopeGraphemes<'a>;
|
||||
/// Returns an iterator over the grapheme clusters in the slice, reversed.
|
||||
///
|
||||
/// The returned iterator starts at the end of the slice and ends at the beginning of the
|
||||
/// slice.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::RopeSlice;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = RopeSlice::from("😶🌫️🏴☠️🖼️");
|
||||
/// let graphemes: Vec<_> = text.graphemes_rev().collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["🖼️", "🏴☠️", "😶🌫️"]);
|
||||
/// ```
|
||||
fn graphemes_rev(self) -> RevRopeGraphemes<'a>;
|
||||
}
|
||||
|
||||
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
@@ -43,7 +160,7 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(len - text.len()..)
|
||||
.map_or(false, |end| end == text)
|
||||
.is_some_and(|end| end == text)
|
||||
}
|
||||
|
||||
fn starts_with(self, text: &str) -> bool {
|
||||
@@ -52,7 +169,7 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(..text.len())
|
||||
.map_or(false, |start| start == text)
|
||||
.is_some_and(|start| start == text)
|
||||
}
|
||||
|
||||
fn regex_input(self) -> RegexInput<RopeyCursor<'a>> {
|
||||
@@ -94,14 +211,154 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
.map(|pos| self.len_chars() - pos - 1)
|
||||
}
|
||||
|
||||
/// returns the char idx of `byte_idx`, if `byte_idx` is
|
||||
/// a char boundary this function behaves the same as `byte_to_char`
|
||||
fn byte_to_next_char(self, mut byte_idx: usize) -> usize {
|
||||
let (chunk, chunk_byte_off, chunk_char_off, _) = self.chunk_at_byte(byte_idx);
|
||||
byte_idx -= chunk_byte_off;
|
||||
let is_char_boundary =
|
||||
is_utf8_char_boundary(chunk.as_bytes().get(byte_idx).copied().unwrap_or(0));
|
||||
chunk_char_off + byte_to_char_idx(chunk, byte_idx) + !is_char_boundary as usize
|
||||
// These three are adapted from std:
|
||||
|
||||
fn floor_char_boundary(self, byte_idx: usize) -> usize {
|
||||
if byte_idx >= self.len_bytes() {
|
||||
self.len_bytes()
|
||||
} else {
|
||||
let offset = self
|
||||
.bytes_at(byte_idx + 1)
|
||||
.reversed()
|
||||
.take(4)
|
||||
.position(is_utf8_char_boundary)
|
||||
// A char can only be four bytes long so we are guaranteed to find a boundary.
|
||||
.unwrap();
|
||||
|
||||
byte_idx - offset
|
||||
}
|
||||
}
|
||||
|
||||
fn ceil_char_boundary(self, byte_idx: usize) -> usize {
|
||||
if byte_idx > self.len_bytes() {
|
||||
self.len_bytes()
|
||||
} else {
|
||||
let upper_bound = self.len_bytes().min(byte_idx + 4);
|
||||
self.bytes_at(byte_idx)
|
||||
.position(is_utf8_char_boundary)
|
||||
.map_or(upper_bound, |pos| pos + byte_idx)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_char_boundary(self, byte_idx: usize) -> bool {
|
||||
if byte_idx == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if byte_idx >= self.len_bytes() {
|
||||
byte_idx == self.len_bytes()
|
||||
} else {
|
||||
is_utf8_char_boundary(self.bytes_at(byte_idx).next().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn floor_grapheme_boundary(self, mut byte_idx: usize) -> usize {
|
||||
if byte_idx >= self.len_bytes() {
|
||||
return self.len_bytes();
|
||||
}
|
||||
|
||||
byte_idx = self.ceil_char_boundary(byte_idx + 1);
|
||||
|
||||
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
|
||||
|
||||
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
|
||||
|
||||
loop {
|
||||
match cursor.prev_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(None) => return 0,
|
||||
Ok(Some(boundary)) => return boundary,
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
let (ch, ch_byte_idx, _, _) = self.chunk_at_byte(chunk_byte_idx - 1);
|
||||
chunk = ch;
|
||||
chunk_byte_idx = ch_byte_idx;
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let ctx_chunk = self.chunk_at_byte(n - 1).0;
|
||||
cursor.provide_context(ctx_chunk, n - ctx_chunk.len());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ceil_grapheme_boundary(self, mut byte_idx: usize) -> usize {
|
||||
if byte_idx >= self.len_bytes() {
|
||||
return self.len_bytes();
|
||||
}
|
||||
|
||||
if byte_idx == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte_idx = self.floor_char_boundary(byte_idx - 1);
|
||||
|
||||
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
|
||||
|
||||
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
|
||||
|
||||
loop {
|
||||
match cursor.next_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(None) => return self.len_bytes(),
|
||||
Ok(Some(boundary)) => return boundary,
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
chunk_byte_idx += chunk.len();
|
||||
chunk = self.chunk_at_byte(chunk_byte_idx).0;
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let ctx_chunk = self.chunk_at_byte(n - 1).0;
|
||||
cursor.provide_context(ctx_chunk, n - ctx_chunk.len());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grapheme_boundary(self, byte_idx: usize) -> bool {
|
||||
// The byte must lie on a character boundary to lie on a grapheme cluster boundary.
|
||||
if !self.is_char_boundary(byte_idx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (chunk, chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
|
||||
|
||||
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
|
||||
|
||||
loop {
|
||||
match cursor.is_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(n) => return n,
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let (ctx_chunk, ctx_byte_start, _, _) = self.chunk_at_byte(n - 1);
|
||||
cursor.provide_context(ctx_chunk, ctx_byte_start);
|
||||
}
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn graphemes(self) -> RopeGraphemes<'a> {
|
||||
let mut chunks = self.chunks();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
RopeGraphemes {
|
||||
text: self,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start: 0,
|
||||
cursor: GraphemeCursor::new(0, self.len_bytes(), true),
|
||||
}
|
||||
}
|
||||
|
||||
fn graphemes_rev(self) -> RevRopeGraphemes<'a> {
|
||||
let (mut chunks, mut cur_chunk_start, _, _) = self.chunks_at_byte(self.len_bytes());
|
||||
chunks.reverse();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
cur_chunk_start -= first_chunk.len();
|
||||
RevRopeGraphemes {
|
||||
text: self,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start,
|
||||
cursor: GraphemeCursor::new(self.len_bytes(), self.len_bytes(), true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,32 +369,136 @@ const fn is_utf8_char_boundary(b: u8) -> bool {
|
||||
(b as i8) >= -0x40
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice`.
|
||||
#[derive(Clone)]
|
||||
pub struct RopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RopeGraphemes<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.next_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
self.cur_chunk_start += self.cur_chunk.len();
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a < self.cur_chunk_start {
|
||||
Some(self.text.byte_slice(a..b))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[a2..b2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice` in reverse.
|
||||
#[derive(Clone)]
|
||||
pub struct RevRopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RevRopeGraphemes<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RevRopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RevRopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
self.cur_chunk_start -= self.cur_chunk.len();
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a >= self.cur_chunk_start + self.cur_chunk.len() {
|
||||
Some(self.text.byte_slice(b..a))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[b2..a2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ropey::RopeSlice;
|
||||
|
||||
use crate::rope::RopeSliceExt;
|
||||
|
||||
#[test]
|
||||
fn next_char_at_byte() {
|
||||
for i in 0..=6 {
|
||||
assert_eq!(RopeSlice::from("foobar").byte_to_next_char(i), i);
|
||||
}
|
||||
for char_idx in 0..10 {
|
||||
let len = "😆".len();
|
||||
assert_eq!(
|
||||
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len),
|
||||
char_idx
|
||||
);
|
||||
for i in 1..=len {
|
||||
assert_eq!(
|
||||
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len + i),
|
||||
char_idx + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starts_with() {
|
||||
assert!(RopeSlice::from("asdf").starts_with("a"));
|
||||
@@ -147,4 +508,79 @@ mod tests {
|
||||
fn ends_with() {
|
||||
assert!(RopeSlice::from("asdf").ends_with("f"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn char_boundaries() {
|
||||
let ascii = RopeSlice::from("ascii");
|
||||
// When the given index lies on a character boundary, the index should not change.
|
||||
for byte_idx in 0..=ascii.len_bytes() {
|
||||
assert_eq!(ascii.floor_char_boundary(byte_idx), byte_idx);
|
||||
assert_eq!(ascii.ceil_char_boundary(byte_idx), byte_idx);
|
||||
assert!(ascii.is_char_boundary(byte_idx));
|
||||
}
|
||||
|
||||
// This is a polyfill of a method of this trait which was replaced by ceil_char_boundary.
|
||||
// It returns the _character index_ of the given byte index, rounding up if it does not
|
||||
// already lie on a character boundary.
|
||||
fn byte_to_next_char(slice: RopeSlice, byte_idx: usize) -> usize {
|
||||
slice.byte_to_char(slice.ceil_char_boundary(byte_idx))
|
||||
}
|
||||
|
||||
for i in 0..=6 {
|
||||
assert_eq!(byte_to_next_char(RopeSlice::from("foobar"), i), i);
|
||||
}
|
||||
for char_idx in 0..10 {
|
||||
let len = "😆".len();
|
||||
assert_eq!(
|
||||
byte_to_next_char(RopeSlice::from("😆😆😆😆😆😆😆😆😆😆"), char_idx * len),
|
||||
char_idx
|
||||
);
|
||||
for i in 1..=len {
|
||||
assert_eq!(
|
||||
byte_to_next_char(RopeSlice::from("😆😆😆😆😆😆😆😆😆😆"), char_idx * len + i),
|
||||
char_idx + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grapheme_boundaries() {
|
||||
let ascii = RopeSlice::from("ascii");
|
||||
// When the given index lies on a grapheme boundary, the index should not change.
|
||||
for byte_idx in 0..=ascii.len_bytes() {
|
||||
assert_eq!(ascii.floor_char_boundary(byte_idx), byte_idx);
|
||||
assert_eq!(ascii.ceil_char_boundary(byte_idx), byte_idx);
|
||||
assert!(ascii.is_grapheme_boundary(byte_idx));
|
||||
}
|
||||
|
||||
// 🏴☠️: U+1F3F4 U+200D U+2620 U+FE0F
|
||||
// 13 bytes, hex: f0 9f 8f b4 + e2 80 8d + e2 98 a0 + ef b8 8f
|
||||
let g = RopeSlice::from("🏴☠️\r\n");
|
||||
let emoji_len = "🏴☠️".len();
|
||||
let end = g.len_bytes();
|
||||
|
||||
for byte_idx in 0..emoji_len {
|
||||
assert_eq!(g.floor_grapheme_boundary(byte_idx), 0);
|
||||
}
|
||||
for byte_idx in emoji_len..end {
|
||||
assert_eq!(g.floor_grapheme_boundary(byte_idx), emoji_len);
|
||||
}
|
||||
assert_eq!(g.floor_grapheme_boundary(end), end);
|
||||
|
||||
assert_eq!(g.ceil_grapheme_boundary(0), 0);
|
||||
for byte_idx in 1..=emoji_len {
|
||||
assert_eq!(g.ceil_grapheme_boundary(byte_idx), emoji_len);
|
||||
}
|
||||
for byte_idx in emoji_len + 1..=end {
|
||||
assert_eq!(g.ceil_grapheme_boundary(byte_idx), end);
|
||||
}
|
||||
|
||||
assert!(g.is_grapheme_boundary(0));
|
||||
assert!(g.is_grapheme_boundary(emoji_len));
|
||||
assert!(g.is_grapheme_boundary(end));
|
||||
for byte_idx in (1..emoji_len).chain(emoji_len + 1..end) {
|
||||
assert!(!g.is_grapheme_boundary(byte_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -100,7 +100,7 @@ fn test_normalize_path() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(
|
||||
path::normalize(&path),
|
||||
expected,
|
||||
"input {:?} and \"..\" should not erase the simlink that goes ahead",
|
||||
"input {:?} and \"..\" should not erase the symlink that goes ahead",
|
||||
&path
|
||||
);
|
||||
|
||||
|
@@ -12,6 +12,24 @@ categories.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[package.metadata.deb]
|
||||
# generate a .deb in target/debian/ with the command: cargo deb --no-build
|
||||
name = "helix"
|
||||
assets = [
|
||||
{ source = "target/release/hx", dest = "/usr/lib/helix/", mode = "755" },
|
||||
{ source = "../contrib/hx_launcher.sh", dest = "/usr/bin/hx", mode = "755" },
|
||||
{ source = "../runtime/*", dest = "/usr/lib/helix/runtime/", mode = "644" },
|
||||
{ source = "../runtime/grammars/*", dest = "/usr/lib/helix/runtime/grammars/", mode = "644" }, # to avoid sources/
|
||||
{ source = "../runtime/queries/**/*", dest = "/usr/lib/helix/runtime/queries/", mode = "644" },
|
||||
{ source = "../runtime/themes/**/*", dest = "/usr/lib/helix/runtime/themes/", mode = "644" },
|
||||
{ source = "../README.md", dest = "/usr/share/doc/helix/", mode = "644" },
|
||||
{ source = "../contrib/completion/hx.bash", dest = "/usr/share/bash-completion/completions/hx", mode = "644" },
|
||||
{ source = "../contrib/completion/hx.fish", dest = "/usr/share/fish/vendor_completions.d/hx.fish", mode = "644" },
|
||||
{ source = "../contrib/completion/hx.zsh", dest = "/usr/share/zsh/vendor-completions/_hx", mode = "644" },
|
||||
{ source = "../contrib/Helix.desktop", dest = "/usr/share/applications/Helix.desktop", mode = "644" },
|
||||
{ source = "../contrib/helix.png", dest = "/usr/share/icons/hicolor/256x256/apps/helix.png", mode = "644" },
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["git"]
|
||||
unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"]
|
||||
@@ -33,7 +51,7 @@ helix-vcs = { path = "../helix-vcs" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
anyhow = "1"
|
||||
once_cell = "1.20"
|
||||
once_cell = "1.21"
|
||||
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
@@ -43,6 +61,7 @@ tokio-stream = "0.1"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
arc-swap = { version = "1.7.1" }
|
||||
termini = "1"
|
||||
indexmap = "2.8"
|
||||
|
||||
# Logging
|
||||
fern = "0.7"
|
||||
@@ -53,13 +72,13 @@ log = "0.4"
|
||||
nucleo.workspace = true
|
||||
ignore = "0.4"
|
||||
# markdown doc rendering
|
||||
pulldown-cmark = { version = "0.12", default-features = false }
|
||||
pulldown-cmark = { version = "0.13", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
thiserror.workspace = true
|
||||
|
||||
# opening URLs
|
||||
open = "5.3.1"
|
||||
open = "5.3.2"
|
||||
url = "2.5.4"
|
||||
|
||||
# config
|
||||
@@ -74,7 +93,7 @@ grep-searcher = "0.1.14"
|
||||
|
||||
[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.168"
|
||||
libc = "0.2.171"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
|
||||
@@ -83,7 +102,7 @@ crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
[dev-dependencies]
|
||||
smallvec = "1.13"
|
||||
indoc = "2.0.5"
|
||||
tempfile = "3.14.0"
|
||||
smallvec = "1.14"
|
||||
indoc = "2.0.6"
|
||||
tempfile.workspace = true
|
||||
same-file = "1.0.1"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use arc_swap::{access::Map, ArcSwap};
|
||||
use futures_util::Stream;
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, Selection};
|
||||
use helix_lsp::{
|
||||
lsp::{self, notification::Notification},
|
||||
util::lsp_range_to_range,
|
||||
@@ -11,7 +11,6 @@ use helix_view::{
|
||||
align_view,
|
||||
document::{DocumentOpenError, DocumentSavedEventResult},
|
||||
editor::{ConfigEvent, EditorEvent},
|
||||
events::DiagnosticsDidChange,
|
||||
graphics::Rect,
|
||||
theme,
|
||||
tree::Layout,
|
||||
@@ -33,7 +32,7 @@ use crate::{
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use std::io::stdout;
|
||||
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
|
||||
use std::{io::stdin, path::Path, sync::Arc};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use anyhow::Context;
|
||||
@@ -66,11 +65,6 @@ pub struct Application {
|
||||
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
theme_loader: Arc<theme::Loader>,
|
||||
#[allow(dead_code)]
|
||||
syn_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
|
||||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
lsp_progress: LspProgressMap,
|
||||
@@ -107,25 +101,7 @@ impl Application {
|
||||
|
||||
let mut theme_parent_dirs = vec![helix_loader::config_dir()];
|
||||
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
|
||||
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
|
||||
|
||||
let true_color = config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
theme_loader
|
||||
.load(theme)
|
||||
.map_err(|e| {
|
||||
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
.filter(|theme| (true_color || theme.is_16_color()))
|
||||
})
|
||||
.unwrap_or_else(|| theme_loader.default_theme(true_color));
|
||||
|
||||
let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader));
|
||||
let theme_loader = theme::Loader::new(&theme_parent_dirs);
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
@@ -140,13 +116,14 @@ impl Application {
|
||||
let handlers = handlers::setup(config.clone());
|
||||
let mut editor = Editor::new(
|
||||
area,
|
||||
theme_loader.clone(),
|
||||
syn_loader.clone(),
|
||||
Arc::new(theme_loader),
|
||||
Arc::new(ArcSwap::from_pointee(lang_loader)),
|
||||
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.editor
|
||||
})),
|
||||
handlers,
|
||||
);
|
||||
Self::load_configured_theme(&mut editor, &config.load());
|
||||
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.keys
|
||||
@@ -164,7 +141,7 @@ impl Application {
|
||||
|
||||
// If the first file is a directory, skip it and open a picker
|
||||
if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
|
||||
let picker = ui::file_picker(first, &config.load().editor);
|
||||
let picker = ui::file_picker(&editor, first);
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
@@ -210,8 +187,13 @@ impl Application {
|
||||
// opened last is focused on.
|
||||
let view_id = editor.tree.focus;
|
||||
let doc = doc_mut!(editor, &doc_id);
|
||||
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
|
||||
doc.set_selection(view_id, pos);
|
||||
let selection = pos
|
||||
.into_iter()
|
||||
.map(|coords| {
|
||||
Range::point(pos_at_coords(doc.text().slice(..), coords, true))
|
||||
})
|
||||
.collect();
|
||||
doc.set_selection(view_id, selection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +222,6 @@ impl Application {
|
||||
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
|
||||
}
|
||||
|
||||
editor.set_theme(theme);
|
||||
|
||||
#[cfg(windows)]
|
||||
let signals = futures_util::stream::empty();
|
||||
#[cfg(not(windows))]
|
||||
@@ -258,12 +238,7 @@ impl Application {
|
||||
compositor,
|
||||
terminal,
|
||||
editor,
|
||||
|
||||
config,
|
||||
|
||||
theme_loader,
|
||||
syn_loader,
|
||||
|
||||
signals,
|
||||
jobs: Jobs::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
@@ -413,10 +388,9 @@ impl Application {
|
||||
fn refresh_language_config(&mut self) -> Result<(), Error> {
|
||||
let lang_loader = helix_core::config::user_lang_loader()?;
|
||||
|
||||
self.syn_loader.store(Arc::new(lang_loader));
|
||||
self.editor.syn_loader = self.syn_loader.clone();
|
||||
self.editor.syn_loader.store(Arc::new(lang_loader));
|
||||
for document in self.editor.documents.values_mut() {
|
||||
document.detect_language(self.syn_loader.clone());
|
||||
document.detect_language(self.editor.syn_loader.clone());
|
||||
let diagnostics = Editor::doc_diagnostics(
|
||||
&self.editor.language_servers,
|
||||
&self.editor.diagnostics,
|
||||
@@ -428,34 +402,13 @@ impl Application {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refresh theme after config change
|
||||
fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> {
|
||||
let true_color = config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
self.theme_loader
|
||||
.load(theme)
|
||||
.map_err(|e| {
|
||||
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
.filter(|theme| (true_color || theme.is_16_color()))
|
||||
})
|
||||
.unwrap_or_else(|| self.theme_loader.default_theme(true_color));
|
||||
|
||||
self.editor.set_theme(theme);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_config(&mut self) {
|
||||
let mut refresh_config = || -> Result<(), Error> {
|
||||
let default_config = Config::load_default()
|
||||
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
|
||||
self.refresh_language_config()?;
|
||||
self.refresh_theme(&default_config)?;
|
||||
// Refresh theme after config change
|
||||
Self::load_configured_theme(&mut self.editor, &default_config);
|
||||
self.terminal
|
||||
.reconfigure(default_config.editor.clone().into())?;
|
||||
// Store new config
|
||||
@@ -473,6 +426,37 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the theme set in configuration
|
||||
fn load_configured_theme(editor: &mut Editor, config: &Config) {
|
||||
let true_color = config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
editor
|
||||
.theme_loader
|
||||
.load(theme)
|
||||
.map_err(|e| {
|
||||
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
.filter(|theme| {
|
||||
let colors_ok = true_color || theme.is_16_color();
|
||||
if !colors_ok {
|
||||
log::warn!(
|
||||
"loaded theme `{}` but cannot use it because true color \
|
||||
support is not enabled",
|
||||
theme.name()
|
||||
);
|
||||
}
|
||||
colors_ok
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| editor.theme_loader.default_theme(true_color));
|
||||
editor.set_theme(theme);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
// no signal handling available on windows
|
||||
pub async fn handle_signals(&mut self, _signal: ()) -> bool {
|
||||
@@ -717,33 +701,15 @@ impl Application {
|
||||
// This might not be required by the spec but Neovim does this as well, so it's
|
||||
// probably a good idea for compatibility.
|
||||
if let Some(config) = language_server.config() {
|
||||
tokio::spawn(language_server.did_change_configuration(config.clone()));
|
||||
language_server.did_change_configuration(config.clone());
|
||||
}
|
||||
|
||||
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 {
|
||||
let url = match doc.url() {
|
||||
Some(url) => url,
|
||||
None => continue, // skip documents with no path
|
||||
};
|
||||
|
||||
let language_id =
|
||||
doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
|
||||
|
||||
tokio::spawn(language_server.text_document_did_open(
|
||||
url,
|
||||
doc.version(),
|
||||
doc.text(),
|
||||
language_id,
|
||||
));
|
||||
}
|
||||
helix_event::dispatch(helix_view::events::LanguageServerInitialized {
|
||||
editor: &mut self.editor,
|
||||
server_id,
|
||||
});
|
||||
}
|
||||
Notification::PublishDiagnostics(mut params) => {
|
||||
Notification::PublishDiagnostics(params) => {
|
||||
let uri = match helix_core::Uri::try_from(params.uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
@@ -756,100 +722,16 @@ impl Application {
|
||||
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
|
||||
return;
|
||||
}
|
||||
// have to inline the function because of borrow checking...
|
||||
let doc = self.editor.documents.values_mut()
|
||||
.find(|doc| doc.uri().is_some_and(|u| u == uri))
|
||||
.filter(|doc| {
|
||||
if let Some(version) = params.version {
|
||||
if version != doc.version() {
|
||||
log::info!("Version ({version}) is out of date for {uri:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
let mut unchanged_diag_sources = Vec::new();
|
||||
if let Some(doc) = &doc {
|
||||
let lang_conf = doc.language.clone();
|
||||
|
||||
if let Some(lang_conf) = &lang_conf {
|
||||
if let Some(old_diagnostics) = self.editor.diagnostics.get(&uri) {
|
||||
if !lang_conf.persistent_diagnostic_sources.is_empty() {
|
||||
// Sort diagnostics first by severity and then by line numbers.
|
||||
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
||||
params
|
||||
.diagnostics
|
||||
.sort_by_key(|d| (d.severity, d.range.start));
|
||||
}
|
||||
for source in &lang_conf.persistent_diagnostic_sources {
|
||||
let new_diagnostics = params
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|d| d.source.as_ref() == Some(source));
|
||||
let old_diagnostics = old_diagnostics
|
||||
.iter()
|
||||
.filter(|(d, d_server)| {
|
||||
*d_server == server_id
|
||||
&& d.source.as_ref() == Some(source)
|
||||
})
|
||||
.map(|(d, _)| d);
|
||||
if new_diagnostics.eq(old_diagnostics) {
|
||||
unchanged_diag_sources.push(source.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
|
||||
|
||||
// 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.
|
||||
let diagnostics = match self.editor.diagnostics.entry(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.extend(diagnostics);
|
||||
current_diagnostics
|
||||
// Sort diagnostics first by severity and then by line numbers.
|
||||
}
|
||||
Entry::Vacant(v) => v.insert(diagnostics.collect()),
|
||||
let provider = helix_core::diagnostic::DiagnosticProvider::Lsp {
|
||||
server_id,
|
||||
identifier: None,
|
||||
};
|
||||
|
||||
// Sort diagnostics first by severity and then by line numbers.
|
||||
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
||||
diagnostics
|
||||
.sort_by_key(|(d, server_id)| (d.severity, d.range.start, *server_id));
|
||||
|
||||
if let Some(doc) = doc {
|
||||
let diagnostic_of_language_server_and_not_in_unchanged_sources =
|
||||
|diagnostic: &lsp::Diagnostic, ls_id| {
|
||||
ls_id == server_id
|
||||
&& diagnostic.source.as_ref().map_or(true, |source| {
|
||||
!unchanged_diag_sources.contains(source)
|
||||
})
|
||||
};
|
||||
let diagnostics = Editor::doc_diagnostics_with_filter(
|
||||
&self.editor.language_servers,
|
||||
&self.editor.diagnostics,
|
||||
doc,
|
||||
diagnostic_of_language_server_and_not_in_unchanged_sources,
|
||||
);
|
||||
doc.replace_diagnostics(
|
||||
diagnostics,
|
||||
&unchanged_diag_sources,
|
||||
Some(server_id),
|
||||
);
|
||||
|
||||
let doc = doc.id();
|
||||
helix_event::dispatch(DiagnosticsDidChange {
|
||||
editor: &mut self.editor,
|
||||
doc,
|
||||
});
|
||||
}
|
||||
self.editor.handle_lsp_diagnostics(
|
||||
&provider,
|
||||
uri,
|
||||
params.version,
|
||||
params.diagnostics,
|
||||
);
|
||||
}
|
||||
Notification::ShowMessage(params) => {
|
||||
if self.config.load().editor.lsp.display_messages {
|
||||
@@ -958,16 +840,23 @@ impl Application {
|
||||
// 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);
|
||||
diags.retain(|(_, provider)| {
|
||||
provider.language_server_id() != Some(server_id)
|
||||
});
|
||||
}
|
||||
|
||||
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(Some(server_id));
|
||||
doc.clear_diagnostics_for_language_server(server_id);
|
||||
}
|
||||
|
||||
helix_event::dispatch(helix_view::events::LanguageServerExited {
|
||||
editor: &mut self.editor,
|
||||
server_id,
|
||||
});
|
||||
|
||||
// Remove the language server from the registry.
|
||||
self.editor.language_servers.remove_by_id(server_id);
|
||||
}
|
||||
@@ -1126,7 +1015,13 @@ impl Application {
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(language_server!().reply(id, reply));
|
||||
let language_server = language_server!();
|
||||
if let Err(err) = language_server.reply(id.clone(), reply) {
|
||||
log::error!(
|
||||
"Failed to send reply to server '{}' request {id}: {err}",
|
||||
language_server.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use helix_core::Position;
|
||||
use helix_view::tree::Layout;
|
||||
use indexmap::IndexMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -16,7 +17,7 @@ pub struct Args {
|
||||
pub verbosity: u64,
|
||||
pub log_file: Option<PathBuf>,
|
||||
pub config_file: Option<PathBuf>,
|
||||
pub files: Vec<(PathBuf, Position)>,
|
||||
pub files: IndexMap<PathBuf, Vec<Position>>,
|
||||
pub working_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -26,6 +27,18 @@ impl Args {
|
||||
let mut argv = std::env::args().peekable();
|
||||
let mut line_number = 0;
|
||||
|
||||
let mut insert_file_with_position = |file_with_position: &str| {
|
||||
let (filename, position) = parse_file(file_with_position);
|
||||
|
||||
// Before setting the working directory, resolve all the paths in args.files
|
||||
let filename = helix_stdx::path::canonicalize(filename);
|
||||
|
||||
args.files
|
||||
.entry(filename)
|
||||
.and_modify(|positions| positions.push(position))
|
||||
.or_insert_with(|| vec![position]);
|
||||
};
|
||||
|
||||
argv.next(); // skip the program, we don't care about that
|
||||
|
||||
while let Some(arg) = argv.next() {
|
||||
@@ -92,21 +105,25 @@ impl Args {
|
||||
arg if arg.starts_with('+') => {
|
||||
match arg[1..].parse::<usize>() {
|
||||
Ok(n) => line_number = n.saturating_sub(1),
|
||||
_ => args.files.push(parse_file(arg)),
|
||||
_ => insert_file_with_position(arg),
|
||||
};
|
||||
}
|
||||
arg => args.files.push(parse_file(arg)),
|
||||
arg => insert_file_with_position(arg),
|
||||
}
|
||||
}
|
||||
|
||||
// push the remaining args, if any to the files
|
||||
for arg in argv {
|
||||
args.files.push(parse_file(&arg));
|
||||
insert_file_with_position(&arg);
|
||||
}
|
||||
|
||||
if let Some(file) = args.files.first_mut() {
|
||||
if line_number != 0 {
|
||||
file.1.row = line_number;
|
||||
if line_number != 0 {
|
||||
if let Some(first_position) = args
|
||||
.files
|
||||
.first_mut()
|
||||
.and_then(|(_, positions)| positions.first_mut())
|
||||
{
|
||||
first_position.row = line_number;
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -518,15 +518,16 @@ pub fn dap_variables(cx: &mut Context) {
|
||||
Some(thread_frame) => thread_frame,
|
||||
None => {
|
||||
cx.editor
|
||||
.set_error("Failed to get stack frame for thread: {thread_id}");
|
||||
.set_error(format!("Failed to get stack frame for thread: {thread_id}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let stack_frame = match thread_frame.get(frame) {
|
||||
Some(stack_frame) => stack_frame,
|
||||
None => {
|
||||
cx.editor
|
||||
.set_error("Failed to get stack frame for thread {thread_id} and frame {frame}.");
|
||||
cx.editor.set_error(format!(
|
||||
"Failed to get stack frame for thread {thread_id} and frame {frame}."
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@@ -13,17 +13,9 @@ use tui::{text::Span, widgets::Row};
|
||||
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
use helix_core::{
|
||||
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
|
||||
};
|
||||
use helix_core::{diagnostic::DiagnosticProvider, syntax::LanguageServerFeature, Selection, Uri};
|
||||
use helix_stdx::path;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
editor::Action,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
theme::Style,
|
||||
Document, View,
|
||||
};
|
||||
use helix_view::{editor::Action, handlers::lsp::SignatureHelpInvoked, theme::Style};
|
||||
|
||||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
@@ -31,13 +23,7 @@ use crate::{
|
||||
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
|
||||
};
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashSet},
|
||||
fmt::Display,
|
||||
future::Future,
|
||||
path::Path,
|
||||
};
|
||||
use std::{cmp::Ordering, collections::HashSet, fmt::Display, future::Future, path::Path};
|
||||
|
||||
/// 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.
|
||||
@@ -61,14 +47,19 @@ macro_rules! language_server_with_feature {
|
||||
}};
|
||||
}
|
||||
|
||||
/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri`.
|
||||
/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri` and adds
|
||||
/// the server's offset encoding.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Location {
|
||||
uri: Uri,
|
||||
range: lsp::Range,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
fn lsp_location_to_location(location: lsp::Location) -> Option<Location> {
|
||||
fn lsp_location_to_location(
|
||||
location: lsp::Location,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> Option<Location> {
|
||||
let uri = match location.uri.try_into() {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
@@ -79,13 +70,13 @@ fn lsp_location_to_location(location: lsp::Location) -> Option<Location> {
|
||||
Some(Location {
|
||||
uri,
|
||||
range: location.range,
|
||||
offset_encoding,
|
||||
})
|
||||
}
|
||||
|
||||
struct SymbolInformationItem {
|
||||
location: Location,
|
||||
symbol: lsp::SymbolInformation,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
struct DiagnosticStyles {
|
||||
@@ -98,7 +89,6 @@ struct DiagnosticStyles {
|
||||
struct PickerDiagnostic {
|
||||
location: Location,
|
||||
diag: lsp::Diagnostic,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
fn location_to_file_location(location: &Location) -> Option<FileLocation> {
|
||||
@@ -110,12 +100,7 @@ fn location_to_file_location(location: &Location) -> Option<FileLocation> {
|
||||
Some((path.into(), line))
|
||||
}
|
||||
|
||||
fn jump_to_location(
|
||||
editor: &mut Editor,
|
||||
location: &Location,
|
||||
offset_encoding: OffsetEncoding,
|
||||
action: Action,
|
||||
) {
|
||||
fn jump_to_location(editor: &mut Editor, location: &Location, action: Action) {
|
||||
let (view, doc) = current!(editor);
|
||||
push_jump(view, doc);
|
||||
|
||||
@@ -124,7 +109,13 @@ fn jump_to_location(
|
||||
editor.set_error(err);
|
||||
return;
|
||||
};
|
||||
jump_to_position(editor, path, location.range, offset_encoding, action);
|
||||
jump_to_position(
|
||||
editor,
|
||||
path,
|
||||
location.range,
|
||||
location.offset_encoding,
|
||||
action,
|
||||
);
|
||||
}
|
||||
|
||||
fn jump_to_position(
|
||||
@@ -204,7 +195,7 @@ type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
|
||||
|
||||
fn diag_picker(
|
||||
cx: &Context,
|
||||
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
diagnostics: impl IntoIterator<Item = (Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>)>,
|
||||
format: DiagnosticsFormat,
|
||||
) -> DiagnosticsPicker {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
@@ -214,15 +205,18 @@ fn diag_picker(
|
||||
for (uri, diags) in diagnostics {
|
||||
flat_diag.reserve(diags.len());
|
||||
|
||||
for (diag, ls) in diags {
|
||||
if let Some(ls) = cx.editor.language_server_by_id(ls) {
|
||||
for (diag, provider) in diags {
|
||||
if let Some(ls) = provider
|
||||
.language_server_id()
|
||||
.and_then(|id| cx.editor.language_server_by_id(id))
|
||||
{
|
||||
flat_diag.push(PickerDiagnostic {
|
||||
location: Location {
|
||||
uri: uri.clone(),
|
||||
range: diag.range,
|
||||
offset_encoding: ls.offset_encoding(),
|
||||
},
|
||||
diag,
|
||||
offset_encoding: ls.offset_encoding(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -286,7 +280,7 @@ fn diag_picker(
|
||||
flat_diag,
|
||||
styles,
|
||||
move |cx, diag, action| {
|
||||
jump_to_location(cx.editor, &diag.location, diag.offset_encoding, action);
|
||||
jump_to_location(cx.editor, &diag.location, action);
|
||||
let (view, doc) = current!(cx.editor);
|
||||
view.diagnostics_handler
|
||||
.immediately_show_diagnostic(doc, view.id);
|
||||
@@ -314,10 +308,10 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
|
||||
container_name: None,
|
||||
},
|
||||
offset_encoding,
|
||||
location: Location {
|
||||
uri: uri.clone(),
|
||||
range: symbol.selection_range,
|
||||
offset_encoding,
|
||||
},
|
||||
});
|
||||
for child in symbol.children.into_iter().flatten() {
|
||||
@@ -340,9 +334,7 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
.expect("docs with active language servers must be backed by paths");
|
||||
|
||||
async move {
|
||||
let json = request.await?;
|
||||
let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
|
||||
let symbols = match response {
|
||||
let symbols = match request.await? {
|
||||
Some(symbols) => symbols,
|
||||
None => return anyhow::Ok(vec![]),
|
||||
};
|
||||
@@ -355,9 +347,9 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
location: Location {
|
||||
uri: doc_uri.clone(),
|
||||
range: symbol.location.range,
|
||||
offset_encoding,
|
||||
},
|
||||
symbol,
|
||||
offset_encoding,
|
||||
})
|
||||
.collect(),
|
||||
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
||||
@@ -387,9 +379,11 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
|
||||
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);
|
||||
while let Some(response) = futures.next().await {
|
||||
match response {
|
||||
Ok(mut items) => symbols.append(&mut items),
|
||||
Err(err) => log::error!("Error requesting document symbols: {err}"),
|
||||
}
|
||||
}
|
||||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let columns = [
|
||||
@@ -402,6 +396,13 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
item.symbol.name.as_str().into()
|
||||
}),
|
||||
ui::PickerColumn::new("container", |item: &SymbolInformationItem, _| {
|
||||
item.symbol
|
||||
.container_name
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
|
||||
let picker = Picker::new(
|
||||
@@ -410,7 +411,7 @@ pub fn symbol_picker(cx: &mut Context) {
|
||||
symbols,
|
||||
(),
|
||||
move |cx, item, action| {
|
||||
jump_to_location(cx.editor, &item.location, item.offset_encoding, action);
|
||||
jump_to_location(cx.editor, &item.location, action);
|
||||
},
|
||||
)
|
||||
.with_preview(move |_editor, item| location_to_file_location(&item.location))
|
||||
@@ -449,30 +450,34 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
.unwrap();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
async move {
|
||||
let json = request.await?;
|
||||
let symbols = request
|
||||
.await?
|
||||
.and_then(|resp| match resp {
|
||||
lsp::WorkspaceSymbolResponse::Flat(symbols) => Some(symbols),
|
||||
lsp::WorkspaceSymbolResponse::Nested(_) => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let response: Vec<_> =
|
||||
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|symbol| {
|
||||
let uri = match Uri::try_from(&symbol.location.uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::warn!("discarding symbol with invalid URI: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SymbolInformationItem {
|
||||
location: Location {
|
||||
uri,
|
||||
range: symbol.location.range,
|
||||
},
|
||||
symbol,
|
||||
let response: Vec<_> = symbols
|
||||
.into_iter()
|
||||
.filter_map(|symbol| {
|
||||
let uri = match Uri::try_from(&symbol.location.uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::warn!("discarding symbol with invalid URI: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SymbolInformationItem {
|
||||
location: Location {
|
||||
uri,
|
||||
range: symbol.location.range,
|
||||
offset_encoding,
|
||||
})
|
||||
},
|
||||
symbol,
|
||||
})
|
||||
.collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
anyhow::Ok(response)
|
||||
}
|
||||
@@ -485,10 +490,14 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
|
||||
let injector = injector.clone();
|
||||
async move {
|
||||
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
|
||||
while let Some(lsp_items) = futures.try_next().await? {
|
||||
for item in lsp_items {
|
||||
injector.push(item)?;
|
||||
while let Some(response) = futures.next().await {
|
||||
match response {
|
||||
Ok(items) => {
|
||||
for item in items {
|
||||
injector.push(item)?;
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!("Error requesting workspace symbols: {err}"),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -503,6 +512,13 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
item.symbol.name.as_str().into()
|
||||
})
|
||||
.without_filtering(),
|
||||
ui::PickerColumn::new("container", |item: &SymbolInformationItem, _| {
|
||||
item.symbol
|
||||
.container_name
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
}),
|
||||
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
|
||||
if let Some(path) = item.location.uri.as_path() {
|
||||
path::get_relative_path(path)
|
||||
@@ -521,7 +537,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
[],
|
||||
(),
|
||||
move |cx, item, action| {
|
||||
jump_to_location(cx.editor, &item.location, item.offset_encoding, action);
|
||||
jump_to_location(cx.editor, &item.location, action);
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, item| location_to_file_location(&item.location))
|
||||
@@ -535,11 +551,7 @@ pub fn diagnostics_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
if let Some(uri) = doc.uri() {
|
||||
let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default();
|
||||
let picker = diag_picker(
|
||||
cx,
|
||||
[(uri, diagnostics)].into(),
|
||||
DiagnosticsFormat::HideSourcePath,
|
||||
);
|
||||
let picker = diag_picker(cx, [(uri, diagnostics)], DiagnosticsFormat::HideSourcePath);
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
}
|
||||
@@ -657,11 +669,8 @@ pub fn code_action(cx: &mut Context) {
|
||||
Some((code_action_request, language_server_id))
|
||||
})
|
||||
.map(|(request, ls_id)| async move {
|
||||
let json = request.await?;
|
||||
let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
|
||||
let mut actions = match response {
|
||||
Some(a) => a,
|
||||
None => return anyhow::Ok(Vec::new()),
|
||||
let Some(mut actions) = request.await? else {
|
||||
return anyhow::Ok(Vec::new());
|
||||
};
|
||||
|
||||
// remove disabled code actions
|
||||
@@ -726,9 +735,12 @@ pub fn code_action(cx: &mut Context) {
|
||||
|
||||
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);
|
||||
|
||||
while let Some(output) = futures.next().await {
|
||||
match output {
|
||||
Ok(mut lsp_items) => actions.append(&mut lsp_items),
|
||||
Err(err) => log::error!("while gathering code actions: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
@@ -753,22 +765,16 @@ pub fn code_action(cx: &mut Context) {
|
||||
match &action.lsp_item {
|
||||
lsp::CodeActionOrCommand::Command(command) => {
|
||||
log::debug!("code action command: {:?}", command);
|
||||
execute_lsp_command(editor, action.language_server_id, command.clone());
|
||||
editor.execute_lsp_command(command.clone(), action.language_server_id);
|
||||
}
|
||||
lsp::CodeActionOrCommand::CodeAction(code_action) => {
|
||||
log::debug!("code action: {:?}", code_action);
|
||||
// we support lsp "codeAction/resolve" for `edit` and `command` fields
|
||||
let mut resolved_code_action = None;
|
||||
if code_action.edit.is_none() || code_action.command.is_none() {
|
||||
if let Some(future) =
|
||||
language_server.resolve_code_action(code_action.clone())
|
||||
{
|
||||
if let Ok(response) = helix_lsp::block_on(future) {
|
||||
if let Ok(code_action) =
|
||||
serde_json::from_value::<CodeAction>(response)
|
||||
{
|
||||
resolved_code_action = Some(code_action);
|
||||
}
|
||||
if let Some(future) = language_server.resolve_code_action(code_action) {
|
||||
if let Ok(code_action) = helix_lsp::block_on(future) {
|
||||
resolved_code_action = Some(code_action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -782,7 +788,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, action.language_server_id, command.clone());
|
||||
editor.execute_lsp_command(command.clone(), action.language_server_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,33 +804,6 @@ pub fn code_action(cx: &mut Context) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn execute_lsp_command(
|
||||
editor: &mut Editor,
|
||||
language_server_id: LanguageServerId,
|
||||
cmd: lsp::Command,
|
||||
) {
|
||||
// the command is executed on the server and communicated back
|
||||
// to the client asynchronously using workspace edits
|
||||
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");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
let res = future.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
log::error!("execute LSP command: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyEditError {
|
||||
pub kind: ApplyEditErrorKind,
|
||||
@@ -853,17 +832,12 @@ impl Display for ApplyEditErrorKind {
|
||||
}
|
||||
|
||||
/// Precondition: `locations` should be non-empty.
|
||||
fn goto_impl(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
locations: Vec<Location>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) {
|
||||
fn goto_impl(editor: &mut Editor, compositor: &mut Compositor, locations: Vec<Location>) {
|
||||
let cwdir = helix_stdx::env::current_working_dir();
|
||||
|
||||
match locations.as_slice() {
|
||||
[location] => {
|
||||
jump_to_location(editor, location, offset_encoding, Action::Replace);
|
||||
jump_to_location(editor, location, Action::Replace);
|
||||
}
|
||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||
_locations => {
|
||||
@@ -880,58 +854,73 @@ fn goto_impl(
|
||||
},
|
||||
)];
|
||||
|
||||
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
let picker = Picker::new(columns, 0, locations, cwdir, |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, action)
|
||||
})
|
||||
.with_preview(move |_editor, location| location_to_file_location(location));
|
||||
.with_preview(|_editor, location| location_to_file_location(location));
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<Location> {
|
||||
match definitions {
|
||||
Some(lsp::GotoDefinitionResponse::Scalar(location)) => {
|
||||
lsp_location_to_location(location).into_iter().collect()
|
||||
}
|
||||
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations
|
||||
.into_iter()
|
||||
.flat_map(lsp_location_to_location)
|
||||
.collect(),
|
||||
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
|
||||
.into_iter()
|
||||
.map(|location_link| {
|
||||
lsp::Location::new(location_link.target_uri, location_link.target_range)
|
||||
})
|
||||
.flat_map(lsp_location_to_location)
|
||||
.collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto_single_impl<P, F>(cx: &mut Context, feature: LanguageServerFeature, request_provider: P)
|
||||
where
|
||||
P: Fn(&Client, lsp::Position, lsp::TextDocumentIdentifier) -> Option<F>,
|
||||
F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
|
||||
F: Future<Output = helix_lsp::Result<Option<lsp::GotoDefinitionResponse>>> + 'static + Send,
|
||||
{
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(feature)
|
||||
.map(|language_server| {
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let pos = doc.position(view.id, offset_encoding);
|
||||
let future = request_provider(language_server, pos, doc.identifier()).unwrap();
|
||||
async move { anyhow::Ok((future.await?, offset_encoding)) }
|
||||
})
|
||||
.collect();
|
||||
|
||||
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 = request_provider(language_server, pos, doc.identifier()).unwrap();
|
||||
|
||||
cx.callback(
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
|
||||
let items = to_locations(response);
|
||||
if items.is_empty() {
|
||||
cx.jobs.callback(async move {
|
||||
let mut locations = Vec::new();
|
||||
while let Some(response) = futures.next().await {
|
||||
match response {
|
||||
Ok((response, offset_encoding)) => match response {
|
||||
Some(lsp::GotoDefinitionResponse::Scalar(lsp_location)) => {
|
||||
locations.extend(lsp_location_to_location(lsp_location, offset_encoding));
|
||||
}
|
||||
Some(lsp::GotoDefinitionResponse::Array(lsp_locations)) => {
|
||||
locations.extend(lsp_locations.into_iter().flat_map(|location| {
|
||||
lsp_location_to_location(location, offset_encoding)
|
||||
}));
|
||||
}
|
||||
Some(lsp::GotoDefinitionResponse::Link(lsp_locations)) => {
|
||||
locations.extend(
|
||||
lsp_locations
|
||||
.into_iter()
|
||||
.map(|location_link| {
|
||||
lsp::Location::new(
|
||||
location_link.target_uri,
|
||||
location_link.target_range,
|
||||
)
|
||||
})
|
||||
.flat_map(|location| {
|
||||
lsp_location_to_location(location, offset_encoding)
|
||||
}),
|
||||
);
|
||||
}
|
||||
None => (),
|
||||
},
|
||||
Err(err) => log::error!("Error requesting locations: {err}"),
|
||||
}
|
||||
}
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if locations.is_empty() {
|
||||
editor.set_error("No definition found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
goto_impl(editor, compositor, locations);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
}
|
||||
|
||||
pub fn goto_declaration(cx: &mut Context) {
|
||||
@@ -968,38 +957,47 @@ pub fn goto_implementation(cx: &mut Context) {
|
||||
|
||||
pub fn goto_reference(cx: &mut Context) {
|
||||
let config = cx.editor.config();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
|
||||
// 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 = language_server
|
||||
.goto_reference(
|
||||
doc.identifier(),
|
||||
pos,
|
||||
config.lsp.goto_reference_include_declaration,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::GotoReference)
|
||||
.map(|language_server| {
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let pos = doc.position(view.id, offset_encoding);
|
||||
let future = language_server
|
||||
.goto_reference(
|
||||
doc.identifier(),
|
||||
pos,
|
||||
config.lsp.goto_reference_include_declaration,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
async move { anyhow::Ok((future.await?, offset_encoding)) }
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.callback(
|
||||
future,
|
||||
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
|
||||
let items: Vec<Location> = response
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(lsp_location_to_location)
|
||||
.collect();
|
||||
if items.is_empty() {
|
||||
cx.jobs.callback(async move {
|
||||
let mut locations = Vec::new();
|
||||
while let Some(response) = futures.next().await {
|
||||
match response {
|
||||
Ok((lsp_locations, offset_encoding)) => locations.extend(
|
||||
lsp_locations
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|location| lsp_location_to_location(location, offset_encoding)),
|
||||
),
|
||||
Err(err) => log::error!("Error requesting references: {err}"),
|
||||
}
|
||||
}
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if locations.is_empty() {
|
||||
editor.set_error("No references found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
goto_impl(editor, compositor, locations);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
}
|
||||
|
||||
pub fn signature_help(cx: &mut Context) {
|
||||
@@ -1009,54 +1007,59 @@ pub fn signature_help(cx: &mut Context) {
|
||||
}
|
||||
|
||||
pub fn hover(cx: &mut Context) {
|
||||
use ui::lsp::hover::Hover;
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Hover)
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
cx.editor
|
||||
.set_error("No configured language server supports hover");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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, language_server.offset_encoding());
|
||||
let future = language_server
|
||||
.text_document_hover(doc.identifier(), pos, None)
|
||||
.unwrap();
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Hover)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|language_server| {
|
||||
let server_name = language_server.name().to_string();
|
||||
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
|
||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||
let request = language_server
|
||||
.text_document_hover(doc.identifier(), pos, None)
|
||||
.unwrap();
|
||||
|
||||
cx.callback(
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::Hover>| {
|
||||
if let Some(hover) = response {
|
||||
// hover.contents / .range <- used for visualizing
|
||||
async move { anyhow::Ok((server_name, request.await?)) }
|
||||
})
|
||||
.collect();
|
||||
|
||||
fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
|
||||
match contents {
|
||||
lsp::MarkedString::String(contents) => contents,
|
||||
lsp::MarkedString::LanguageString(string) => {
|
||||
if string.language == "markdown" {
|
||||
string.value
|
||||
} else {
|
||||
format!("```{}\n{}\n```", string.language, string.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.jobs.callback(async move {
|
||||
let mut hovers: Vec<(String, lsp::Hover)> = Vec::new();
|
||||
|
||||
let contents = match hover.contents {
|
||||
lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
|
||||
lsp::HoverContents::Array(contents) => contents
|
||||
.into_iter()
|
||||
.map(marked_string_to_markdown)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n"),
|
||||
lsp::HoverContents::Markup(contents) => contents.value,
|
||||
};
|
||||
|
||||
// skip if contents empty
|
||||
|
||||
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
|
||||
let popup = Popup::new("hover", contents).auto_close(true);
|
||||
compositor.replace_or_push("hover", popup);
|
||||
while let Some(response) = futures.next().await {
|
||||
match response {
|
||||
Ok((server_name, Some(hover))) => hovers.push((server_name, hover)),
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("Error requesting hover: {err}"),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if hovers.is_empty() {
|
||||
editor.set_status("No hover results available.");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new popup
|
||||
let contents = Hover::new(hovers, editor.syn_loader.clone());
|
||||
let popup = Popup::new(Hover::ID, contents).auto_close(true);
|
||||
compositor.replace_or_push(Hover::ID, popup);
|
||||
};
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
}
|
||||
|
||||
pub fn rename_symbol(cx: &mut Context) {
|
||||
@@ -1131,7 +1134,9 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||
|
||||
match block_on(future) {
|
||||
Ok(edits) => {
|
||||
let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
|
||||
let _ = cx
|
||||
.editor
|
||||
.apply_workspace_edit(offset_encoding, &edits.unwrap_or_default());
|
||||
}
|
||||
Err(err) => cx.editor.set_error(err.to_string()),
|
||||
}
|
||||
@@ -1237,164 +1242,3 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
|
||||
if !editor.config().lsp.display_inlay_hints {
|
||||
return;
|
||||
}
|
||||
|
||||
for (view, _) in editor.tree.views() {
|
||||
let doc = match editor.documents.get(&view.doc) {
|
||||
Some(doc) => doc,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(callback) = compute_inlay_hints_for_view(view, doc) {
|
||||
jobs.callback(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inlay_hints_for_view(
|
||||
view: &View,
|
||||
doc: &Document,
|
||||
) -> Option<std::pin::Pin<Box<impl Future<Output = Result<crate::job::Callback, anyhow::Error>>>>> {
|
||||
let view_id = view.id;
|
||||
let doc_id = view.doc;
|
||||
|
||||
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(doc.view_offset(view_id).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 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 offset_encoding = language_server.offset_encoding();
|
||||
|
||||
let callback = super::make_job_callback(
|
||||
language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
|
||||
move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
|
||||
// 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() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add annotations to relevant document, not the current one (it may have changed in between)
|
||||
let doc = match editor.documents.get_mut(&doc_id) {
|
||||
Some(doc) => doc,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
|
||||
let mut hints = match response {
|
||||
Some(hints) if !hints.is_empty() => hints,
|
||||
_ => {
|
||||
doc.set_inlay_hints(
|
||||
view_id,
|
||||
DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id),
|
||||
);
|
||||
doc.inlay_hints_oudated = false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Most language servers will already send them sorted but ensure this is the case to
|
||||
// avoid errors on our end.
|
||||
hints.sort_by_key(|inlay_hint| inlay_hint.position);
|
||||
|
||||
let mut padding_before_inlay_hints = Vec::new();
|
||||
let mut type_inlay_hints = Vec::new();
|
||||
let mut parameter_inlay_hints = Vec::new();
|
||||
let mut other_inlay_hints = Vec::new();
|
||||
let mut padding_after_inlay_hints = Vec::new();
|
||||
|
||||
let doc_text = doc.text();
|
||||
|
||||
for hint in hints {
|
||||
let char_idx =
|
||||
match helix_lsp::util::lsp_pos_to_pos(doc_text, hint.position, offset_encoding)
|
||||
{
|
||||
Some(pos) => pos,
|
||||
// Skip inlay hints that have no "real" position
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let label = match hint.label {
|
||||
lsp::InlayHintLabel::String(s) => s,
|
||||
lsp::InlayHintLabel::LabelParts(parts) => parts
|
||||
.into_iter()
|
||||
.map(|p| p.value)
|
||||
.collect::<Vec<_>>()
|
||||
.join(""),
|
||||
};
|
||||
|
||||
let inlay_hints_vec = match hint.kind {
|
||||
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,
|
||||
Some(lsp::InlayHintKind::PARAMETER) => &mut parameter_inlay_hints,
|
||||
// We can't warn on unknown kind here since LSPs are free to set it or not, for
|
||||
// example Rust Analyzer does not: every kind will be `None`.
|
||||
_ => &mut other_inlay_hints,
|
||||
};
|
||||
|
||||
if let Some(true) = hint.padding_left {
|
||||
padding_before_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
|
||||
}
|
||||
|
||||
inlay_hints_vec.push(InlineAnnotation::new(char_idx, label));
|
||||
|
||||
if let Some(true) = hint.padding_right {
|
||||
padding_after_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
|
||||
}
|
||||
}
|
||||
|
||||
doc.set_inlay_hints(
|
||||
view_id,
|
||||
DocumentInlayHints {
|
||||
id: new_doc_inlay_hints_id,
|
||||
type_inlay_hints,
|
||||
parameter_inlay_hints,
|
||||
other_inlay_hints,
|
||||
padding_before_inlay_hints,
|
||||
padding_after_inlay_hints,
|
||||
},
|
||||
);
|
||||
doc.inlay_hints_oudated = false;
|
||||
},
|
||||
);
|
||||
|
||||
Some(callback)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -137,9 +137,12 @@ impl Compositor {
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: &Event, cx: &mut Context) -> bool {
|
||||
// If it is a key event and a macro is being recorded, push the key event to the recording.
|
||||
// If it is a key event, a macro is being recorded, and a macro isn't being replayed,
|
||||
// push the key event to the recording.
|
||||
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
|
||||
keys.push(*key);
|
||||
if cx.editor.macro_replaying.is_empty() {
|
||||
keys.push(*key);
|
||||
}
|
||||
}
|
||||
|
||||
let mut callbacks = Vec::new();
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use helix_event::{events, register_event};
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{
|
||||
DiagnosticsDidChange, DocumentDidChange, DocumentFocusLost, SelectionDidChange,
|
||||
DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, DocumentDidOpen, DocumentFocusLost,
|
||||
LanguageServerExited, LanguageServerInitialized, SelectionDidChange,
|
||||
};
|
||||
|
||||
use crate::commands;
|
||||
@@ -17,8 +18,12 @@ pub fn register() {
|
||||
register_event::<OnModeSwitch>();
|
||||
register_event::<PostInsertChar>();
|
||||
register_event::<PostCommand>();
|
||||
register_event::<DocumentDidOpen>();
|
||||
register_event::<DocumentDidChange>();
|
||||
register_event::<DocumentDidClose>();
|
||||
register_event::<DocumentFocusLost>();
|
||||
register_event::<SelectionDidChange>();
|
||||
register_event::<DiagnosticsDidChange>();
|
||||
register_event::<LanguageServerInitialized>();
|
||||
register_event::<LanguageServerExited>();
|
||||
}
|
||||
|
@@ -6,35 +6,45 @@ use helix_event::AsyncHook;
|
||||
use crate::config::Config;
|
||||
use crate::events;
|
||||
use crate::handlers::auto_save::AutoSaveHandler;
|
||||
use crate::handlers::completion::CompletionHandler;
|
||||
use crate::handlers::inlay_hints::InlayHintHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use completion::trigger_auto_completion;
|
||||
pub use helix_view::handlers::Handlers;
|
||||
|
||||
use self::document_colors::DocumentColorsHandler;
|
||||
|
||||
mod auto_save;
|
||||
pub mod completion;
|
||||
mod diagnostics;
|
||||
mod document_colors;
|
||||
mod inlay_hints;
|
||||
mod signature_help;
|
||||
mod snippet;
|
||||
|
||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
events::register();
|
||||
|
||||
let completions = CompletionHandler::new(config).spawn();
|
||||
let event_tx = completion::CompletionHandler::new(config).spawn();
|
||||
let signature_hints = SignatureHelpHandler::new().spawn();
|
||||
let inlay_hints = InlayHintHandler::default().spawn();
|
||||
let auto_save = AutoSaveHandler::new().spawn();
|
||||
let document_colors = DocumentColorsHandler::default().spawn();
|
||||
|
||||
let handlers = Handlers {
|
||||
completions,
|
||||
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
|
||||
signature_hints,
|
||||
inlay_hints,
|
||||
auto_save,
|
||||
document_colors,
|
||||
};
|
||||
|
||||
helix_view::handlers::register_hooks(&handlers);
|
||||
completion::register_hooks(&handlers);
|
||||
signature_help::register_hooks(&handlers);
|
||||
auto_save::register_hooks(&handlers);
|
||||
diagnostics::register_hooks(&handlers);
|
||||
snippet::register_hooks(&handlers);
|
||||
document_colors::register_hooks(&handlers);
|
||||
inlay_hints::register_hooks(&handlers);
|
||||
handlers
|
||||
}
|
||||
|
@@ -87,7 +87,13 @@ fn request_auto_save(editor: &mut Editor) {
|
||||
jobs: &mut Jobs::new(),
|
||||
};
|
||||
|
||||
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
|
||||
let options = commands::WriteAllOptions {
|
||||
force: false,
|
||||
write_scratch: false,
|
||||
auto_format: false,
|
||||
};
|
||||
|
||||
if let Err(e) = commands::typed::write_all_impl(context, options) {
|
||||
context.editor.set_error(format!("{}", e));
|
||||
}
|
||||
}
|
||||
|
@@ -1,309 +1,90 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use futures_util::FutureExt;
|
||||
use helix_core::chars::char_is_word;
|
||||
use helix_core::completion::CompletionProvider;
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
|
||||
use helix_event::{register_hook, TaskHandle};
|
||||
use helix_lsp::lsp;
|
||||
use helix_lsp::util::pos_to_lsp_pos;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::{Mode, SavePoint};
|
||||
use helix_view::handlers::lsp::CompletionEvent;
|
||||
use helix_view::{DocumentId, Editor, ViewId};
|
||||
use path::path_completion;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt as _;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::handlers::completion::{CompletionEvent, ResponseContext};
|
||||
use helix_view::Editor;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::commands;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::config::Config;
|
||||
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
|
||||
use crate::job::{dispatch, dispatch_blocking};
|
||||
use crate::handlers::completion::request::{request_incomplete_completion_list, Trigger};
|
||||
use crate::job::dispatch;
|
||||
use crate::keymap::MappableCommand;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::lsp::signature_help::SignatureHelp;
|
||||
use crate::ui::{self, Popup};
|
||||
|
||||
use super::Handlers;
|
||||
pub use item::{CompletionItem, LspCompletionItem};
|
||||
|
||||
pub use item::{CompletionItem, CompletionItems, CompletionResponse, LspCompletionItem};
|
||||
pub use request::CompletionHandler;
|
||||
pub use resolve::ResolveHandler;
|
||||
|
||||
mod item;
|
||||
mod path;
|
||||
mod request;
|
||||
mod resolve;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum TriggerKind {
|
||||
Auto,
|
||||
TriggerChar,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Trigger {
|
||||
pos: usize,
|
||||
view: ViewId,
|
||||
doc: DocumentId,
|
||||
kind: TriggerKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct CompletionHandler {
|
||||
/// currently active trigger which will cause a
|
||||
/// completion request after the timeout
|
||||
trigger: Option<Trigger>,
|
||||
in_flight: Option<Trigger>,
|
||||
task_controller: TaskController,
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
}
|
||||
|
||||
impl CompletionHandler {
|
||||
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||
Self {
|
||||
config,
|
||||
task_controller: TaskController::new(),
|
||||
trigger: None,
|
||||
in_flight: None,
|
||||
async fn handle_response(
|
||||
requests: &mut JoinSet<CompletionResponse>,
|
||||
is_incomplete: bool,
|
||||
) -> Option<CompletionResponse> {
|
||||
loop {
|
||||
let response = requests.join_next().await?.unwrap();
|
||||
if !is_incomplete && !response.context.is_incomplete && response.items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for CompletionHandler {
|
||||
type Event = CompletionEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_old_timeout: Option<Instant>,
|
||||
) -> Option<Instant> {
|
||||
if self.in_flight.is_some() && !self.task_controller.is_running() {
|
||||
self.in_flight = None;
|
||||
}
|
||||
match event {
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor: trigger_pos,
|
||||
doc,
|
||||
view,
|
||||
} => {
|
||||
// techically it shouldn't be possible to switch views/documents in insert mode
|
||||
// but people may create weird keymaps/use the mouse so lets be extra careful
|
||||
if self
|
||||
.trigger
|
||||
.or(self.in_flight)
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
pos: trigger_pos,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Auto,
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.task_controller.cancel();
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::TriggerChar,
|
||||
});
|
||||
}
|
||||
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Manual,
|
||||
});
|
||||
// stop debouncing immediately and request the completion
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
CompletionEvent::Cancel => {
|
||||
self.trigger = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
CompletionEvent::DeleteText { cursor } => {
|
||||
// if we deleted the original trigger, abort the completion
|
||||
if matches!(self.trigger.or(self.in_flight), Some(Trigger{ pos, .. }) if cursor < pos)
|
||||
{
|
||||
self.trigger = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.trigger.map(|trigger| {
|
||||
// if the current request was closed forget about it
|
||||
// otherwise immediately restart the completion request
|
||||
let timeout = if trigger.kind == TriggerKind::Auto {
|
||||
self.config.load().editor.completion_timeout
|
||||
} else {
|
||||
// we want almost instant completions for trigger chars
|
||||
// and restarting completion requests. The small timeout here mainly
|
||||
// serves to better handle cases where the completion handler
|
||||
// may fall behind (so multiple events in the channel) and macros
|
||||
Duration::from_millis(5)
|
||||
};
|
||||
Instant::now() + timeout
|
||||
})
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||
self.in_flight = Some(trigger);
|
||||
let handle = self.task_controller.restart();
|
||||
dispatch_blocking(move |editor, compositor| {
|
||||
request_completion(trigger, handle, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completion(
|
||||
mut trigger: Trigger,
|
||||
async fn replace_completions(
|
||||
handle: TaskHandle,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
mut requests: JoinSet<CompletionResponse>,
|
||||
is_incomplete: bool,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.is_some()
|
||||
|| editor.mode != Mode::Insert
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text();
|
||||
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
|
||||
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
|
||||
return;
|
||||
}
|
||||
// this looks odd... Why are we not using the trigger position from
|
||||
// the `trigger` here? Won't that mean that the trigger char doesn't get
|
||||
// send to the LS if we type fast enougn? Yes that is true but it's
|
||||
// not actually a problem. The LSP will resolve the completion to the identifier
|
||||
// anyway (in fact sending the later position is necessary to get the right results
|
||||
// from LSPs that provide incomplete completion list). We rely on trigger offset
|
||||
// and primary cursor matching for multi-cursor completions so this is definitely
|
||||
// necessary from our side too.
|
||||
trigger.pos = cursor;
|
||||
let trigger_text = text.slice(..cursor);
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesUnordered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|ls| {
|
||||
let language_server_id = ls.id();
|
||||
let offset_encoding = ls.offset_encoding();
|
||||
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
|
||||
let doc_id = doc.identifier();
|
||||
let context = if trigger.kind == TriggerKind::Manual {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
} else {
|
||||
let trigger_char =
|
||||
ls.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| {
|
||||
provider
|
||||
.trigger_characters
|
||||
.as_deref()?
|
||||
.iter()
|
||||
.find(|&trigger| trigger_text.ends_with(trigger))
|
||||
});
|
||||
|
||||
if trigger_char.is_some() {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: trigger_char.cloned(),
|
||||
}
|
||||
} else {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
|
||||
async move {
|
||||
let json = completion_response.await?;
|
||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||
let items = match response {
|
||||
Some(lsp::CompletionResponse::Array(items)) => items,
|
||||
// TODO: do something with is_incomplete
|
||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: _is_incomplete,
|
||||
items,
|
||||
})) => items,
|
||||
None => Vec::new(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
CompletionItem::Lsp(LspCompletionItem {
|
||||
item,
|
||||
provider: language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
anyhow::Ok(items)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.chain(path_completion(cursor, text.clone(), doc, handle.clone()))
|
||||
.collect();
|
||||
|
||||
let future = async move {
|
||||
let mut items = Vec::new();
|
||||
while let Some(lsp_items) = futures.next().await {
|
||||
match lsp_items {
|
||||
Ok(mut lsp_items) => items.append(&mut lsp_items),
|
||||
Err(err) => {
|
||||
log::debug!("completion request failed: {err:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
items
|
||||
};
|
||||
|
||||
let savepoint = doc.savepoint(view);
|
||||
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
tokio::spawn(async move {
|
||||
let items = cancelable_future(future, &handle).await;
|
||||
let Some(items) = items.filter(|items| !items.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
while let Some(mut response) = handle_response(&mut requests, is_incomplete).await {
|
||||
let handle = handle.clone();
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, trigger, savepoint);
|
||||
drop(handle)
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
let Some(completion) = &mut editor_view.completion else {
|
||||
return;
|
||||
};
|
||||
if handle.is_canceled() {
|
||||
log::info!("dropping outdated completion response");
|
||||
return;
|
||||
}
|
||||
|
||||
completion.replace_provider_completions(&mut response, is_incomplete);
|
||||
if completion.is_empty() {
|
||||
editor_view.clear_completion(editor);
|
||||
// clearing completions might mean we want to immediately re-request them (usually
|
||||
// this occurs if typing a trigger char)
|
||||
trigger_auto_completion(editor, false);
|
||||
} else {
|
||||
editor
|
||||
.handlers
|
||||
.completions
|
||||
.active_completions
|
||||
.insert(response.provider, response.context);
|
||||
}
|
||||
})
|
||||
.await
|
||||
});
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn show_completion(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
items: Vec<CompletionItem>,
|
||||
context: HashMap<CompletionProvider, ResponseContext>,
|
||||
trigger: Trigger,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) {
|
||||
let (view, doc) = current_ref!(editor);
|
||||
// check if the completion request is stale.
|
||||
@@ -320,8 +101,9 @@ fn show_completion(
|
||||
if ui.completion.is_some() {
|
||||
return;
|
||||
}
|
||||
editor.handlers.completions.active_completions = context;
|
||||
|
||||
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
|
||||
let completion_area = ui.set_completion(editor, items, trigger.pos, size);
|
||||
let signature_help_area = compositor
|
||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||
.map(|signature_help| signature_help.area(size, editor));
|
||||
@@ -331,11 +113,7 @@ fn show_completion(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_auto_completion(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
editor: &Editor,
|
||||
trigger_char_only: bool,
|
||||
) {
|
||||
pub fn trigger_auto_completion(editor: &Editor, trigger_char_only: bool) {
|
||||
let config = editor.config.load();
|
||||
if !config.auto_completion {
|
||||
return;
|
||||
@@ -363,15 +141,13 @@ pub fn trigger_auto_completion(
|
||||
#[cfg(not(windows))]
|
||||
let is_path_completion_trigger = matches!(cursor_char, Some(b'/'));
|
||||
|
||||
let handler = &editor.handlers.completions;
|
||||
if is_trigger_char || (is_path_completion_trigger && doc.path_completion_enabled()) {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::TriggerChar {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
handler.event(CompletionEvent::TriggerChar {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -384,29 +160,29 @@ pub fn trigger_auto_completion(
|
||||
.all(char_is_word);
|
||||
|
||||
if is_auto_trigger {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
handler.event(CompletionEvent::AutoTrigger {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
|
||||
fn update_completion_filter(cx: &mut commands::Context, c: Option<char>) {
|
||||
cx.callback.push(Box::new(move |compositor, cx| {
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
if let Some(completion) = &mut editor_view.completion {
|
||||
completion.update_filter(c);
|
||||
if completion.is_empty() {
|
||||
if completion.is_empty() || c.is_some_and(|c| !char_is_word(c)) {
|
||||
editor_view.clear_completion(cx.editor);
|
||||
// clearing completions might mean we want to immediately rerequest them (usually
|
||||
// this occurs if typing a trigger char)
|
||||
if c.is_some() {
|
||||
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
|
||||
trigger_auto_completion(cx.editor, false);
|
||||
}
|
||||
} else {
|
||||
let handle = cx.editor.handlers.completions.request_controller.restart();
|
||||
request_incomplete_completion_list(cx.editor, handle)
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -420,7 +196,6 @@ fn clear_completions(cx: &mut commands::Context) {
|
||||
}
|
||||
|
||||
fn completion_post_command_hook(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
if cx.editor.mode == Mode::Insert {
|
||||
@@ -433,7 +208,7 @@ fn completion_post_command_hook(
|
||||
MappableCommand::Static {
|
||||
name: "delete_char_backward",
|
||||
..
|
||||
} => update_completions(cx, None),
|
||||
} => update_completion_filter(cx, None),
|
||||
_ => clear_completions(cx),
|
||||
}
|
||||
} else {
|
||||
@@ -459,33 +234,35 @@ fn completion_post_command_hook(
|
||||
} => return Ok(()),
|
||||
_ => CompletionEvent::Cancel,
|
||||
};
|
||||
send_blocking(tx, event);
|
||||
cx.editor.handlers.completions.event(event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
|
||||
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(event));
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||
if event.old_mode == Mode::Insert {
|
||||
send_blocking(&tx, CompletionEvent::Cancel);
|
||||
event
|
||||
.cx
|
||||
.editor
|
||||
.handlers
|
||||
.completions
|
||||
.event(CompletionEvent::Cancel);
|
||||
clear_completions(event.cx);
|
||||
} else if event.new_mode == Mode::Insert {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false)
|
||||
trigger_auto_completion(event.cx.editor, false)
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
|
||||
if event.cx.editor.last_completion.is_some() {
|
||||
update_completions(event.cx, Some(event.c))
|
||||
update_completion_filter(event.cx, Some(event.c))
|
||||
} else {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false);
|
||||
trigger_auto_completion(event.cx.editor, false);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
@@ -1,10 +1,70 @@
|
||||
use std::mem;
|
||||
|
||||
use helix_core::completion::CompletionProvider;
|
||||
use helix_lsp::{lsp, LanguageServerId};
|
||||
use helix_view::handlers::completion::ResponseContext;
|
||||
|
||||
pub struct CompletionResponse {
|
||||
pub items: CompletionItems,
|
||||
pub provider: CompletionProvider,
|
||||
pub context: ResponseContext,
|
||||
}
|
||||
|
||||
pub enum CompletionItems {
|
||||
Lsp(Vec<lsp::CompletionItem>),
|
||||
Other(Vec<CompletionItem>),
|
||||
}
|
||||
|
||||
impl CompletionItems {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
CompletionItems::Lsp(items) => items.is_empty(),
|
||||
CompletionItems::Other(items) => items.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionResponse {
|
||||
pub fn take_items(&mut self, dst: &mut Vec<CompletionItem>) {
|
||||
match &mut self.items {
|
||||
CompletionItems::Lsp(items) => dst.extend(items.drain(..).map(|item| {
|
||||
CompletionItem::Lsp(LspCompletionItem {
|
||||
item,
|
||||
provider: match self.provider {
|
||||
CompletionProvider::Lsp(provider) => provider,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
resolved: false,
|
||||
provider_priority: self.context.priority,
|
||||
})
|
||||
})),
|
||||
CompletionItems::Other(items) if dst.is_empty() => mem::swap(dst, items),
|
||||
CompletionItems::Other(items) => dst.append(items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LspCompletionItem {
|
||||
pub item: lsp::CompletionItem,
|
||||
pub provider: LanguageServerId,
|
||||
pub resolved: bool,
|
||||
// TODO: we should not be filtering and sorting incomplete completion list
|
||||
// according to the spec but vscode does that anyway and most servers (
|
||||
// including rust-analyzer) rely on that.. so we can't do that without
|
||||
// breaking completions.
|
||||
pub provider_priority: i8,
|
||||
}
|
||||
|
||||
impl LspCompletionItem {
|
||||
#[inline]
|
||||
pub fn filter_text(&self) -> &str {
|
||||
self.item
|
||||
.filter_text
|
||||
.as_ref()
|
||||
.unwrap_or(&self.item.label)
|
||||
.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@@ -13,6 +73,16 @@ pub enum CompletionItem {
|
||||
Other(helix_core::CompletionItem),
|
||||
}
|
||||
|
||||
impl CompletionItem {
|
||||
#[inline]
|
||||
pub fn filter_text(&self) -> &str {
|
||||
match self {
|
||||
CompletionItem::Lsp(item) => item.filter_text(),
|
||||
CompletionItem::Other(item) => &item.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<CompletionItem> for LspCompletionItem {
|
||||
fn eq(&self, other: &CompletionItem) -> bool {
|
||||
match other {
|
||||
@@ -32,6 +102,21 @@ impl PartialEq<CompletionItem> for helix_core::CompletionItem {
|
||||
}
|
||||
|
||||
impl CompletionItem {
|
||||
pub fn provider_priority(&self) -> i8 {
|
||||
match self {
|
||||
CompletionItem::Lsp(item) => item.provider_priority,
|
||||
// sorting path completions after LSP for now
|
||||
CompletionItem::Other(_) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> CompletionProvider {
|
||||
match self {
|
||||
CompletionItem::Lsp(item) => CompletionProvider::Lsp(item.provider),
|
||||
CompletionItem::Other(item) => item.provider,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preselect(&self) -> bool {
|
||||
match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
|
||||
|
@@ -3,28 +3,29 @@ use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures_util::{future::BoxFuture, FutureExt as _};
|
||||
use helix_core as core;
|
||||
use helix_core::Transaction;
|
||||
use helix_core::{self as core, completion::CompletionProvider, Selection, Transaction};
|
||||
use helix_event::TaskHandle;
|
||||
use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix};
|
||||
use helix_view::Document;
|
||||
use helix_view::{document::SavePoint, handlers::completion::ResponseContext, Document};
|
||||
use url::Url;
|
||||
|
||||
use super::item::CompletionItem;
|
||||
use crate::handlers::completion::{item::CompletionResponse, CompletionItem, CompletionItems};
|
||||
|
||||
pub(crate) fn path_completion(
|
||||
cursor: usize,
|
||||
text: core::Rope,
|
||||
selection: Selection,
|
||||
doc: &Document,
|
||||
handle: TaskHandle,
|
||||
) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> {
|
||||
savepoint: Arc<SavePoint>,
|
||||
) -> Option<impl FnOnce() -> CompletionResponse> {
|
||||
if !doc.path_completion_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = doc.text().clone();
|
||||
let cursor = selection.primary().cursor(text.slice(..));
|
||||
let cur_line = text.char_to_line(cursor);
|
||||
let start = text.line_to_char(cur_line).max(cursor.saturating_sub(1000));
|
||||
let line_until_cursor = text.slice(start..cursor);
|
||||
@@ -67,12 +68,27 @@ pub(crate) fn path_completion(
|
||||
return None;
|
||||
}
|
||||
|
||||
let future = tokio::task::spawn_blocking(move || {
|
||||
// TODO: handle properly in the future
|
||||
const PRIORITY: i8 = 1;
|
||||
let future = move || {
|
||||
let Ok(read_dir) = std::fs::read_dir(&dir_path) else {
|
||||
return Vec::new();
|
||||
return CompletionResponse {
|
||||
items: CompletionItems::Other(Vec::new()),
|
||||
provider: CompletionProvider::Path,
|
||||
context: ResponseContext {
|
||||
is_incomplete: false,
|
||||
priority: PRIORITY,
|
||||
savepoint,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
read_dir
|
||||
let edit_diff = typed_file_name
|
||||
.as_ref()
|
||||
.map(|s| s.chars().count())
|
||||
.unwrap_or_default();
|
||||
|
||||
let res: Vec<_> = read_dir
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|dir_entry| {
|
||||
dir_entry
|
||||
@@ -88,27 +104,32 @@ pub(crate) fn path_completion(
|
||||
let kind = path_kind(&md);
|
||||
let documentation = path_documentation(&md, &dir_path.join(&file_name), kind);
|
||||
|
||||
let edit_diff = typed_file_name
|
||||
.as_ref()
|
||||
.map(|f| f.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
let transaction = Transaction::change(
|
||||
&text,
|
||||
std::iter::once((cursor - edit_diff, cursor, Some((&file_name).into()))),
|
||||
);
|
||||
let transaction = Transaction::change_by_selection(&text, &selection, |range| {
|
||||
let cursor = range.cursor(text.slice(..));
|
||||
(cursor - edit_diff, cursor, Some((&file_name).into()))
|
||||
});
|
||||
|
||||
Some(CompletionItem::Other(core::CompletionItem {
|
||||
kind: Cow::Borrowed(kind),
|
||||
label: file_name.into(),
|
||||
transaction,
|
||||
documentation,
|
||||
documentation: Some(documentation),
|
||||
provider: CompletionProvider::Path,
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
.collect();
|
||||
CompletionResponse {
|
||||
items: CompletionItems::Other(res),
|
||||
provider: CompletionProvider::Path,
|
||||
context: ResponseContext {
|
||||
is_incomplete: false,
|
||||
priority: PRIORITY,
|
||||
savepoint,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Some(async move { Ok(future.await?) }.boxed())
|
||||
Some(future)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
367
helix-term/src/handlers/completion/request.rs
Normal file
367
helix-term/src/handlers/completion/request.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::Future;
|
||||
use helix_core::completion::CompletionProvider;
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{cancelable_future, TaskController, TaskHandle};
|
||||
use helix_lsp::lsp;
|
||||
use helix_lsp::lsp::{CompletionContext, CompletionTriggerKind};
|
||||
use helix_lsp::util::pos_to_lsp_pos;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::{Mode, SavePoint};
|
||||
use helix_view::handlers::completion::{CompletionEvent, ResponseContext};
|
||||
use helix_view::{Document, DocumentId, Editor, ViewId};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::{timeout_at, Instant};
|
||||
|
||||
use crate::compositor::Compositor;
|
||||
use crate::config::Config;
|
||||
use crate::handlers::completion::item::CompletionResponse;
|
||||
use crate::handlers::completion::path::path_completion;
|
||||
use crate::handlers::completion::{
|
||||
handle_response, replace_completions, show_completion, CompletionItems,
|
||||
};
|
||||
use crate::job::{dispatch, dispatch_blocking};
|
||||
use crate::ui;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub(super) enum TriggerKind {
|
||||
Auto,
|
||||
TriggerChar,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct Trigger {
|
||||
pub(super) pos: usize,
|
||||
pub(super) view: ViewId,
|
||||
pub(super) doc: DocumentId,
|
||||
pub(super) kind: TriggerKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CompletionHandler {
|
||||
/// The currently active trigger which will cause a completion request after the timeout.
|
||||
trigger: Option<Trigger>,
|
||||
in_flight: Option<Trigger>,
|
||||
task_controller: TaskController,
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
}
|
||||
|
||||
impl CompletionHandler {
|
||||
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||
Self {
|
||||
config,
|
||||
task_controller: TaskController::new(),
|
||||
trigger: None,
|
||||
in_flight: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for CompletionHandler {
|
||||
type Event = CompletionEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_old_timeout: Option<Instant>,
|
||||
) -> Option<Instant> {
|
||||
if self.in_flight.is_some() && !self.task_controller.is_running() {
|
||||
self.in_flight = None;
|
||||
}
|
||||
match event {
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor: trigger_pos,
|
||||
doc,
|
||||
view,
|
||||
} => {
|
||||
// Technically it shouldn't be possible to switch views/documents in insert mode
|
||||
// but people may create weird keymaps/use the mouse so let's be extra careful.
|
||||
if self
|
||||
.trigger
|
||||
.or(self.in_flight)
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
pos: trigger_pos,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Auto,
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.task_controller.cancel();
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::TriggerChar,
|
||||
});
|
||||
}
|
||||
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Manual,
|
||||
});
|
||||
// stop debouncing immediately and request the completion
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
CompletionEvent::Cancel => {
|
||||
self.trigger = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
CompletionEvent::DeleteText { cursor } => {
|
||||
// if we deleted the original trigger, abort the completion
|
||||
if matches!(self.trigger.or(self.in_flight), Some(Trigger{ pos, .. }) if cursor < pos)
|
||||
{
|
||||
self.trigger = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.trigger.map(|trigger| {
|
||||
// if the current request was closed forget about it
|
||||
// otherwise immediately restart the completion request
|
||||
let timeout = if trigger.kind == TriggerKind::Auto {
|
||||
self.config.load().editor.completion_timeout
|
||||
} else {
|
||||
// we want almost instant completions for trigger chars
|
||||
// and restarting completion requests. The small timeout here mainly
|
||||
// serves to better handle cases where the completion handler
|
||||
// may fall behind (so multiple events in the channel) and macros
|
||||
Duration::from_millis(5)
|
||||
};
|
||||
Instant::now() + timeout
|
||||
})
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||
self.in_flight = Some(trigger);
|
||||
let handle = self.task_controller.restart();
|
||||
dispatch_blocking(move |editor, compositor| {
|
||||
request_completions(trigger, handle, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completions(
|
||||
mut trigger: Trigger,
|
||||
handle: TaskHandle,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
) {
|
||||
let (view, doc) = current_ref!(editor);
|
||||
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.is_some()
|
||||
|| editor.mode != Mode::Insert
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text();
|
||||
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
|
||||
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
|
||||
return;
|
||||
}
|
||||
// This looks odd... Why are we not using the trigger position from the `trigger` here? Won't
|
||||
// that mean that the trigger char doesn't get send to the language server if we type fast
|
||||
// enough? Yes that is true but it's not actually a problem. The language server will resolve
|
||||
// the completion to the identifier anyway (in fact sending the later position is necessary to
|
||||
// get the right results from language servers that provide incomplete completion list). We
|
||||
// rely on the trigger offset and primary cursor matching for multi-cursor completions so this
|
||||
// is definitely necessary from our side too.
|
||||
trigger.pos = cursor;
|
||||
let doc = doc_mut!(editor, &doc.id());
|
||||
let savepoint = doc.savepoint(view);
|
||||
let text = doc.text();
|
||||
let trigger_text = text.slice(..cursor);
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let language_servers: Vec<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.collect();
|
||||
let mut requests = JoinSet::new();
|
||||
for (priority, ls) in language_servers.iter().enumerate() {
|
||||
let context = if trigger.kind == TriggerKind::Manual {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
} else {
|
||||
let trigger_char =
|
||||
ls.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| {
|
||||
provider
|
||||
.trigger_characters
|
||||
.as_deref()?
|
||||
.iter()
|
||||
.find(|&trigger| trigger_text.ends_with(trigger))
|
||||
});
|
||||
|
||||
if trigger_char.is_some() {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: trigger_char.cloned(),
|
||||
}
|
||||
} else {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
requests.spawn(request_completions_from_language_server(
|
||||
ls,
|
||||
doc,
|
||||
view.id,
|
||||
context,
|
||||
-(priority as i8),
|
||||
savepoint.clone(),
|
||||
));
|
||||
}
|
||||
if let Some(path_completion_request) = path_completion(
|
||||
doc.selection(view.id).clone(),
|
||||
doc,
|
||||
handle.clone(),
|
||||
savepoint,
|
||||
) {
|
||||
requests.spawn_blocking(path_completion_request);
|
||||
}
|
||||
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
let handle_ = handle.clone();
|
||||
let request_completions = async move {
|
||||
let mut context = HashMap::new();
|
||||
let Some(mut response) = handle_response(&mut requests, false).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut items: Vec<_> = Vec::new();
|
||||
response.take_items(&mut items);
|
||||
context.insert(response.provider, response.context);
|
||||
let deadline = Instant::now() + Duration::from_millis(100);
|
||||
loop {
|
||||
let Some(mut response) = timeout_at(deadline, handle_response(&mut requests, false))
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
break;
|
||||
};
|
||||
response.take_items(&mut items);
|
||||
context.insert(response.provider, response.context);
|
||||
}
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, context, trigger)
|
||||
})
|
||||
.await;
|
||||
if !requests.is_empty() {
|
||||
replace_completions(handle_, requests, false).await;
|
||||
}
|
||||
};
|
||||
tokio::spawn(cancelable_future(request_completions, handle));
|
||||
}
|
||||
|
||||
fn request_completions_from_language_server(
|
||||
ls: &helix_lsp::Client,
|
||||
doc: &Document,
|
||||
view: ViewId,
|
||||
context: lsp::CompletionContext,
|
||||
priority: i8,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) -> impl Future<Output = CompletionResponse> {
|
||||
let provider = ls.id();
|
||||
let offset_encoding = ls.offset_encoding();
|
||||
let text = doc.text();
|
||||
let cursor = doc.selection(view).primary().cursor(text.slice(..));
|
||||
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
|
||||
let doc_id = doc.identifier();
|
||||
|
||||
// it's important that this is before the async block (and that this is not an async function)
|
||||
// to ensure the request is dispatched right away before any new edit notifications
|
||||
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
|
||||
async move {
|
||||
let response: Option<lsp::CompletionResponse> = completion_response
|
||||
.await
|
||||
.inspect_err(|err| log::error!("completion request failed: {err}"))
|
||||
.ok()
|
||||
.flatten();
|
||||
let (mut items, is_incomplete) = match response {
|
||||
Some(lsp::CompletionResponse::Array(items)) => (items, false),
|
||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete,
|
||||
items,
|
||||
})) => (items, is_incomplete),
|
||||
None => (Vec::new(), false),
|
||||
};
|
||||
items.sort_by(|item1, item2| {
|
||||
let sort_text1 = item1.sort_text.as_deref().unwrap_or(&item1.label);
|
||||
let sort_text2 = item2.sort_text.as_deref().unwrap_or(&item2.label);
|
||||
sort_text1.cmp(sort_text2)
|
||||
});
|
||||
CompletionResponse {
|
||||
items: CompletionItems::Lsp(items),
|
||||
context: ResponseContext {
|
||||
is_incomplete,
|
||||
priority,
|
||||
savepoint,
|
||||
},
|
||||
provider: CompletionProvider::Lsp(provider),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_incomplete_completion_list(editor: &mut Editor, handle: TaskHandle) {
|
||||
let handler = &mut editor.handlers.completions;
|
||||
let mut requests = JoinSet::new();
|
||||
let mut savepoint = None;
|
||||
for (&provider, context) in &handler.active_completions {
|
||||
if !context.is_incomplete {
|
||||
continue;
|
||||
}
|
||||
let CompletionProvider::Lsp(ls_id) = provider else {
|
||||
log::error!("non-lsp incomplete completion lists");
|
||||
continue;
|
||||
};
|
||||
let Some(ls) = editor.language_servers.get_by_id(ls_id) else {
|
||||
continue;
|
||||
};
|
||||
let (view, doc) = current!(editor);
|
||||
let savepoint = savepoint.get_or_insert_with(|| doc.savepoint(view)).clone();
|
||||
let request = request_completions_from_language_server(
|
||||
ls,
|
||||
doc,
|
||||
view.id,
|
||||
CompletionContext {
|
||||
trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
|
||||
trigger_character: None,
|
||||
},
|
||||
context.priority,
|
||||
savepoint,
|
||||
);
|
||||
requests.spawn(request);
|
||||
}
|
||||
if !requests.is_empty() {
|
||||
tokio::spawn(replace_completions(handle, requests, true));
|
||||
}
|
||||
}
|
208
helix-term/src/handlers/document_colors.rs
Normal file
208
helix-term/src/handlers/document_colors.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use futures_util::{stream::FuturesOrdered, StreamExt};
|
||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation};
|
||||
use helix_event::{cancelable_future, register_hook};
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::{
|
||||
document::DocumentColorSwatches,
|
||||
events::{DocumentDidChange, DocumentDidOpen, LanguageServerExited, LanguageServerInitialized},
|
||||
handlers::{lsp::DocumentColorsEvent, Handlers},
|
||||
DocumentId, Editor, Theme,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::job;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct DocumentColorsHandler {
|
||||
docs: HashSet<DocumentId>,
|
||||
}
|
||||
|
||||
const DOCUMENT_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250);
|
||||
|
||||
impl helix_event::AsyncHook for DocumentColorsHandler {
|
||||
type Event = DocumentColorsEvent;
|
||||
|
||||
fn handle_event(&mut self, event: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
|
||||
let DocumentColorsEvent(doc_id) = event;
|
||||
self.docs.insert(doc_id);
|
||||
Some(Instant::now() + DOCUMENT_CHANGE_DEBOUNCE)
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let docs = std::mem::take(&mut self.docs);
|
||||
|
||||
job::dispatch_blocking(move |editor, _compositor| {
|
||||
for doc in docs {
|
||||
request_document_colors(editor, doc);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
|
||||
if !editor.config().lsp.display_color_swatches {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(doc) = editor.document_mut(doc_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cancel = doc.color_swatch_controller.restart();
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::DocumentColors)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|language_server| {
|
||||
let text = doc.text().clone();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let future = language_server
|
||||
.text_document_document_color(doc.identifier(), None)
|
||||
.unwrap();
|
||||
|
||||
async move {
|
||||
let colors: Vec<_> = future
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|color_info| {
|
||||
let pos = helix_lsp::util::lsp_pos_to_pos(
|
||||
&text,
|
||||
color_info.range.start,
|
||||
offset_encoding,
|
||||
)?;
|
||||
Some((pos, color_info.color))
|
||||
})
|
||||
.collect();
|
||||
anyhow::Ok(colors)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut all_colors = Vec::new();
|
||||
loop {
|
||||
match cancelable_future(futures.next(), &cancel).await {
|
||||
Some(Some(Ok(items))) => all_colors.extend(items),
|
||||
Some(Some(Err(err))) => log::error!("document color request failed: {err}"),
|
||||
Some(None) => break,
|
||||
// The request was cancelled.
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
job::dispatch(move |editor, _| attach_document_colors(editor, doc_id, all_colors)).await;
|
||||
});
|
||||
}
|
||||
|
||||
fn attach_document_colors(
|
||||
editor: &mut Editor,
|
||||
doc_id: DocumentId,
|
||||
mut doc_colors: Vec<(usize, lsp::Color)>,
|
||||
) {
|
||||
if !editor.config().lsp.display_color_swatches {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(doc) = editor.documents.get_mut(&doc_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if doc_colors.is_empty() {
|
||||
doc.color_swatches.take();
|
||||
return;
|
||||
}
|
||||
|
||||
doc_colors.sort_by_key(|(pos, _)| *pos);
|
||||
|
||||
let mut color_swatches = Vec::with_capacity(doc_colors.len());
|
||||
let mut color_swatches_padding = Vec::with_capacity(doc_colors.len());
|
||||
let mut colors = Vec::with_capacity(doc_colors.len());
|
||||
|
||||
for (pos, color) in doc_colors {
|
||||
color_swatches_padding.push(InlineAnnotation::new(pos, " "));
|
||||
color_swatches.push(InlineAnnotation::new(pos, "■"));
|
||||
colors.push(Theme::rgb_highlight(
|
||||
(color.red * 255.) as u8,
|
||||
(color.green * 255.) as u8,
|
||||
(color.blue * 255.) as u8,
|
||||
));
|
||||
}
|
||||
|
||||
doc.color_swatches = Some(DocumentColorSwatches {
|
||||
color_swatches,
|
||||
colors,
|
||||
color_swatches_padding,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
// when a document is initially opened, request colors for it
|
||||
request_document_colors(event.editor, event.doc);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.document_colors.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
// Update the color swatch' positions, helping ensure they are displayed in the
|
||||
// proper place.
|
||||
let apply_color_swatch_changes = |annotations: &mut Vec<InlineAnnotation>| {
|
||||
event.changes.update_positions(
|
||||
annotations
|
||||
.iter_mut()
|
||||
.map(|annotation| (&mut annotation.char_idx, helix_core::Assoc::After)),
|
||||
);
|
||||
};
|
||||
|
||||
if let Some(DocumentColorSwatches {
|
||||
color_swatches,
|
||||
colors: _colors,
|
||||
color_swatches_padding,
|
||||
}) = &mut event.doc.color_swatches
|
||||
{
|
||||
apply_color_swatch_changes(color_swatches);
|
||||
apply_color_swatch_changes(color_swatches_padding);
|
||||
}
|
||||
|
||||
// Avoid re-requesting document colors if the change is a ghost transaction (completion)
|
||||
// because the language server will not know about the updates to the document and will
|
||||
// give out-of-date locations.
|
||||
if !event.ghost_transaction {
|
||||
// Cancel the ongoing request, if present.
|
||||
event.doc.color_swatch_controller.cancel();
|
||||
helix_event::send_blocking(&tx, DocumentColorsEvent(event.doc.id()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
|
||||
let doc_ids: Vec<_> = event.editor.documents().map(|doc| doc.id()).collect();
|
||||
|
||||
for doc_id in doc_ids {
|
||||
request_document_colors(event.editor, doc_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerExited<'_>| {
|
||||
// Clear and re-request all color swatches when a server exits.
|
||||
for doc in event.editor.documents_mut() {
|
||||
if doc.supports_language_server(event.server_id) {
|
||||
doc.color_swatches.take();
|
||||
}
|
||||
}
|
||||
|
||||
let doc_ids: Vec<_> = event.editor.documents().map(|doc| doc.id()).collect();
|
||||
|
||||
for doc_id in doc_ids {
|
||||
request_document_colors(event.editor, doc_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
338
helix-term/src/handlers/inlay_hints.rs
Normal file
338
helix-term/src/handlers/inlay_hints.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
use std::{collections::HashSet, mem, time::Duration};
|
||||
|
||||
use crate::job;
|
||||
|
||||
use super::Handlers;
|
||||
|
||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation};
|
||||
use helix_event::{cancelable_future, register_hook, send_blocking};
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
events::{
|
||||
DocumentDidChange, DocumentDidOpen, LanguageServerExited, LanguageServerInitialized,
|
||||
SelectionDidChange,
|
||||
},
|
||||
handlers::lsp::InlayHintEvent,
|
||||
DocumentId, Editor, ViewId,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct InlayHintHandler {
|
||||
views: HashSet<ViewId>,
|
||||
docs: HashSet<DocumentId>,
|
||||
}
|
||||
|
||||
const DOCUMENT_CHANGE_DEBOUNCE: Duration = Duration::from_millis(500);
|
||||
const VIEWPORT_SCROLL_DEBOUNCE: Duration = Duration::from_millis(100);
|
||||
|
||||
impl helix_event::AsyncHook for InlayHintHandler {
|
||||
type Event = InlayHintEvent;
|
||||
|
||||
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant> {
|
||||
match event {
|
||||
InlayHintEvent::DocumentChanged(doc) => {
|
||||
self.docs.insert(doc);
|
||||
Some(Instant::now() + DOCUMENT_CHANGE_DEBOUNCE)
|
||||
}
|
||||
InlayHintEvent::ViewportScrolled(view) => {
|
||||
self.views.insert(view);
|
||||
let mut new_timeout = Instant::now() + VIEWPORT_SCROLL_DEBOUNCE;
|
||||
if let Some(timeout) = timeout {
|
||||
new_timeout = new_timeout.max(timeout);
|
||||
}
|
||||
Some(new_timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let mut views = mem::take(&mut self.views);
|
||||
let docs = mem::take(&mut self.docs);
|
||||
|
||||
job::dispatch_blocking(move |editor, _compositor| {
|
||||
// Drop any views which have been closed.
|
||||
views.retain(|&view| editor.tree.contains(view));
|
||||
// Add any views that show documents which changed.
|
||||
views.extend(
|
||||
editor
|
||||
.tree
|
||||
.views()
|
||||
.filter_map(|(view, _)| docs.contains(&view.doc).then_some(view.id)),
|
||||
);
|
||||
|
||||
for view in views {
|
||||
let doc = editor.tree.get(view).doc;
|
||||
let is_scroll = !docs.contains(&doc);
|
||||
request_inlay_hints_for_view(editor, view, doc, is_scroll);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_inlay_hints_for_view(
|
||||
editor: &mut Editor,
|
||||
view_id: ViewId,
|
||||
doc_id: DocumentId,
|
||||
is_scroll: bool,
|
||||
) {
|
||||
if !editor.config().lsp.display_inlay_hints {
|
||||
return;
|
||||
}
|
||||
let Some(doc) = editor.documents.get_mut(&doc_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(view) = editor.tree.try_get(view_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(language_server) = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::InlayHints)
|
||||
.next()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let rope = doc.text();
|
||||
let text = rope.slice(..);
|
||||
let len_lines = text.len_lines();
|
||||
let view_height = view.inner_height();
|
||||
let first_visible_line =
|
||||
text.char_to_line(doc.view_offset(view_id).anchor.min(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,
|
||||
};
|
||||
// If the view was updated by scrolling (rather than changing) and the viewport still has the
|
||||
// the same position, we can reuse the hints.
|
||||
if is_scroll
|
||||
&& doc
|
||||
.inlay_hints(view_id)
|
||||
.is_some_and(|hint| hint.id == new_doc_inlay_hints_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let range = helix_lsp::util::range_to_lsp_range(
|
||||
rope,
|
||||
helix_core::Range::new(text.line_to_char(first_line), text.line_to_char(last_line)),
|
||||
offset_encoding,
|
||||
);
|
||||
let future = language_server
|
||||
.text_document_range_inlay_hints(doc.identifier(), range, None)
|
||||
.expect("language server must return Some if it supports inlay hints");
|
||||
let controller = doc.inlay_hint_controllers.entry(view_id).or_default();
|
||||
let cancel = controller.restart();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match cancelable_future(future, cancel).await {
|
||||
Some(Ok(res)) => {
|
||||
job::dispatch(move |editor, _compositor| {
|
||||
attach_inlay_hints(
|
||||
editor,
|
||||
view_id,
|
||||
doc_id,
|
||||
new_doc_inlay_hints_id,
|
||||
offset_encoding,
|
||||
res,
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(Err(err)) => log::error!("inlay hint request failed: {err}"),
|
||||
None => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn attach_inlay_hints(
|
||||
editor: &mut Editor,
|
||||
view_id: ViewId,
|
||||
doc_id: DocumentId,
|
||||
id: DocumentInlayHintsId,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
response: Option<Vec<lsp::InlayHint>>,
|
||||
) {
|
||||
if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(doc) = editor.documents.get_mut(&doc_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut hints = match response {
|
||||
Some(hints) if !hints.is_empty() => hints,
|
||||
_ => {
|
||||
doc.set_inlay_hints(view_id, DocumentInlayHints::empty_with_id(id));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Most language servers will already send them sorted but ensure this is the case to
|
||||
// avoid errors on our end.
|
||||
hints.sort_by_key(|inlay_hint| inlay_hint.position);
|
||||
|
||||
let mut padding_before_inlay_hints = Vec::new();
|
||||
let mut type_inlay_hints = Vec::new();
|
||||
let mut parameter_inlay_hints = Vec::new();
|
||||
let mut other_inlay_hints = Vec::new();
|
||||
let mut padding_after_inlay_hints = Vec::new();
|
||||
|
||||
let doc_text = doc.text();
|
||||
|
||||
for hint in hints {
|
||||
let char_idx =
|
||||
match helix_lsp::util::lsp_pos_to_pos(doc_text, hint.position, offset_encoding) {
|
||||
Some(pos) => pos,
|
||||
// Skip inlay hints that have no "real" position
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let label = match hint.label {
|
||||
lsp::InlayHintLabel::String(s) => s,
|
||||
lsp::InlayHintLabel::LabelParts(parts) => parts
|
||||
.into_iter()
|
||||
.map(|p| p.value)
|
||||
.collect::<Vec<_>>()
|
||||
.join(""),
|
||||
};
|
||||
|
||||
let inlay_hints_vec = match hint.kind {
|
||||
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,
|
||||
Some(lsp::InlayHintKind::PARAMETER) => &mut parameter_inlay_hints,
|
||||
// We can't warn on unknown kind here since LSPs are free to set it or not, for
|
||||
// example Rust Analyzer does not: every kind will be `None`.
|
||||
_ => &mut other_inlay_hints,
|
||||
};
|
||||
|
||||
if let Some(true) = hint.padding_left {
|
||||
padding_before_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
|
||||
}
|
||||
|
||||
inlay_hints_vec.push(InlineAnnotation::new(char_idx, label));
|
||||
|
||||
if let Some(true) = hint.padding_right {
|
||||
padding_after_inlay_hints.push(InlineAnnotation::new(char_idx, " "));
|
||||
}
|
||||
}
|
||||
|
||||
doc.set_inlay_hints(
|
||||
view_id,
|
||||
DocumentInlayHints {
|
||||
id,
|
||||
type_inlay_hints,
|
||||
parameter_inlay_hints,
|
||||
other_inlay_hints,
|
||||
padding_before_inlay_hints,
|
||||
padding_after_inlay_hints,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
// When a document is initially opened, request inlay hints for it.
|
||||
let views: Vec<_> = event
|
||||
.editor
|
||||
.tree
|
||||
.views()
|
||||
.filter_map(|(view, _)| (view.doc == event.doc).then_some(view.id))
|
||||
.collect();
|
||||
for view in views {
|
||||
request_inlay_hints_for_view(event.editor, view, event.doc, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.inlay_hints.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
// Update the inlay hint annotations' positions, helping ensure they are displayed in the
|
||||
// proper place.
|
||||
let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| {
|
||||
event.changes.update_positions(
|
||||
annotations
|
||||
.iter_mut()
|
||||
.map(|annotation| (&mut annotation.char_idx, helix_core::Assoc::After)),
|
||||
);
|
||||
};
|
||||
|
||||
for (_view_id, text_annotation) in event.doc.inlay_hints_mut() {
|
||||
let DocumentInlayHints {
|
||||
id: _,
|
||||
type_inlay_hints,
|
||||
parameter_inlay_hints,
|
||||
other_inlay_hints,
|
||||
padding_before_inlay_hints,
|
||||
padding_after_inlay_hints,
|
||||
} = text_annotation;
|
||||
|
||||
apply_inlay_hint_changes(padding_before_inlay_hints);
|
||||
apply_inlay_hint_changes(type_inlay_hints);
|
||||
apply_inlay_hint_changes(parameter_inlay_hints);
|
||||
apply_inlay_hint_changes(other_inlay_hints);
|
||||
apply_inlay_hint_changes(padding_after_inlay_hints);
|
||||
}
|
||||
|
||||
if !event.ghost_transaction {
|
||||
if let Some(controller) = event.doc.inlay_hint_controllers.get_mut(&event.view) {
|
||||
controller.cancel();
|
||||
}
|
||||
// TODO: ideally we should only send this if the document is visible.
|
||||
send_blocking(&tx, InlayHintEvent::DocumentChanged(event.doc.id()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.inlay_hints.clone();
|
||||
register_hook!(move |event: &mut SelectionDidChange<'_>| {
|
||||
if let Some(controller) = event.doc.inlay_hint_controllers.get_mut(&event.view) {
|
||||
controller.cancel();
|
||||
}
|
||||
// Ideally this would only trigger an update if the viewport changed...
|
||||
send_blocking(&tx, InlayHintEvent::ViewportScrolled(event.view));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
|
||||
let views: Vec<_> = event
|
||||
.editor
|
||||
.tree
|
||||
.views()
|
||||
.map(|(view, _)| (view.id, view.doc))
|
||||
.collect();
|
||||
for (view, doc) in views {
|
||||
request_inlay_hints_for_view(event.editor, view, doc, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerExited<'_>| {
|
||||
// Clear and re-request all annotations when a server exits.
|
||||
for doc in event.editor.documents_mut() {
|
||||
if doc.supports_language_server(event.server_id) {
|
||||
doc.reset_all_inlay_hints();
|
||||
}
|
||||
}
|
||||
|
||||
let views: Vec<_> = event
|
||||
.editor
|
||||
.tree
|
||||
.views()
|
||||
.map(|(view, _)| (view.id, view.doc))
|
||||
.collect();
|
||||
for (view, doc) in views {
|
||||
request_inlay_hints_for_view(event.editor, view, doc, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
@@ -16,7 +16,7 @@ use crate::commands::Open;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::events::{OnModeSwitch, PostInsertChar};
|
||||
use crate::handlers::Handlers;
|
||||
use crate::ui::lsp::{Signature, SignatureHelp};
|
||||
use crate::ui::lsp::signature_help::{Signature, SignatureHelp};
|
||||
use crate::ui::Popup;
|
||||
use crate::{job, ui};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use crate::config::{Config, ConfigLoadError};
|
||||
use crossterm::{
|
||||
style::{Color, Print, Stylize},
|
||||
style::{Color, StyledContent, Stylize},
|
||||
tty::IsTty,
|
||||
};
|
||||
use helix_core::config::{default_lang_config, user_lang_config};
|
||||
@@ -164,25 +164,20 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
let column_width = terminal_cols as usize / headings.len();
|
||||
let is_terminal = std::io::stdout().is_tty();
|
||||
|
||||
let column = |item: &str, color: Color| {
|
||||
let mut data = format!(
|
||||
"{:width$}",
|
||||
item.get(..column_width - 2)
|
||||
let fit = |s: &str| -> StyledContent<String> {
|
||||
format!(
|
||||
"{:column_width$}",
|
||||
s.get(..column_width - 2)
|
||||
.map(|s| format!("{}…", s))
|
||||
.unwrap_or_else(|| item.to_string()),
|
||||
width = column_width,
|
||||
);
|
||||
if is_terminal {
|
||||
data = data.stylize().with(color).to_string();
|
||||
}
|
||||
|
||||
// We can't directly use println!() because of
|
||||
// https://github.com/crossterm-rs/crossterm/issues/589
|
||||
let _ = crossterm::execute!(std::io::stdout(), Print(data));
|
||||
.unwrap_or_else(|| s.to_string())
|
||||
)
|
||||
.stylize()
|
||||
};
|
||||
let color = |s: StyledContent<String>, c: Color| if is_terminal { s.with(c) } else { s };
|
||||
let bold = |s: StyledContent<String>| if is_terminal { s.bold() } else { s };
|
||||
|
||||
for heading in headings {
|
||||
column(heading, Color::White);
|
||||
write!(stdout, "{}", bold(fit(heading)))?;
|
||||
}
|
||||
writeln!(stdout)?;
|
||||
|
||||
@@ -192,14 +187,14 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
|
||||
let check_binary = |cmd: Option<&str>| match cmd {
|
||||
Some(cmd) => match helix_stdx::env::which(cmd) {
|
||||
Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
|
||||
Err(_) => column(&format!("✘ {}", cmd), Color::Red),
|
||||
Ok(_) => color(fit(&format!("✓ {}", cmd)), Color::Green),
|
||||
Err(_) => color(fit(&format!("✘ {}", cmd)), Color::Red),
|
||||
},
|
||||
None => column("None", Color::Yellow),
|
||||
None => color(fit("None"), Color::Yellow),
|
||||
};
|
||||
|
||||
for lang in &syn_loader_conf.language {
|
||||
column(&lang.language_id, Color::Reset);
|
||||
write!(stdout, "{}", fit(&lang.language_id))?;
|
||||
|
||||
let mut cmds = lang.language_servers.iter().filter_map(|ls| {
|
||||
syn_loader_conf
|
||||
@@ -207,30 +202,29 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
.get(&ls.name)
|
||||
.map(|config| config.command.as_str())
|
||||
});
|
||||
check_binary(cmds.next());
|
||||
write!(stdout, "{}", check_binary(cmds.next()))?;
|
||||
|
||||
let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
|
||||
check_binary(dap);
|
||||
write!(stdout, "{}", check_binary(dap))?;
|
||||
|
||||
let formatter = lang
|
||||
.formatter
|
||||
.as_ref()
|
||||
.map(|formatter| formatter.command.as_str());
|
||||
check_binary(formatter);
|
||||
write!(stdout, "{}", check_binary(formatter))?;
|
||||
|
||||
for ts_feat in TsFeature::all() {
|
||||
match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
|
||||
true => column("✓", Color::Green),
|
||||
false => column("✘", Color::Red),
|
||||
true => write!(stdout, "{}", color(fit("✓"), Color::Green))?,
|
||||
false => write!(stdout, "{}", color(fit("✘"), Color::Red))?,
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(stdout)?;
|
||||
|
||||
for cmd in cmds {
|
||||
column("", Color::Reset);
|
||||
check_binary(Some(cmd));
|
||||
writeln!(stdout)?;
|
||||
write!(stdout, "{}", fit(""))?;
|
||||
writeln!(stdout, "{}", check_binary(Some(cmd)))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -106,7 +106,7 @@ impl KeyTrieNode {
|
||||
(events.join(", "), desc)
|
||||
})
|
||||
.collect();
|
||||
Info::new(&self.name, &body)
|
||||
Info::new(self.name.clone(), &body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,11 +601,7 @@ mod tests {
|
||||
MappableCommand::select_all,
|
||||
MappableCommand::Typable {
|
||||
name: "pipe".to_string(),
|
||||
args: vec!{
|
||||
"sed".to_string(),
|
||||
"-E".to_string(),
|
||||
"'s/\\s+$//g'".to_string()
|
||||
},
|
||||
args: "sed -E 's/\\s+$//g'".to_string(),
|
||||
doc: "".to_string(),
|
||||
},
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user