Compare commits
372 commits
Author | SHA1 | Date | |
---|---|---|---|
d26f1b9d41 | |||
1974fe9033 | |||
2c19d0934a | |||
8229e746ae | |||
![]() |
7c7bd20159 | ||
![]() |
761a62df86 | ||
![]() |
3c0fcb0679 | ||
![]() |
b1c7bd91b9 | ||
![]() |
f32f5fddba | ||
![]() |
c74fec4c6e | ||
53ed151a9f | |||
7db2913019 | |||
![]() |
e8d7e76c73 | ||
![]() |
7ebf650029 | ||
![]() |
db187c4870 | ||
![]() |
e148d8b311 | ||
![]() |
1a0dad36b7 | ||
![]() |
95344a9585 | ||
![]() |
fb815e2c6f | ||
![]() |
e735485277 | ||
![]() |
bb96a535fc | ||
![]() |
01fce51c45 | ||
f79099a418 | |||
6ad3624f84 | |||
2ea120d205 | |||
096201540b | |||
bf032afa79 | |||
b4ba2dc83d | |||
![]() |
7929c0719d | ||
![]() |
a51334f00b | ||
![]() |
55ef555f87 | ||
![]() |
603d95f5d3 | ||
![]() |
68d7308e25 | ||
![]() |
a2c580c4ae | ||
![]() |
cf7eb5517f | ||
![]() |
593504bb27 | ||
![]() |
3471d82f87 | ||
![]() |
1e2f1363bc | ||
![]() |
b68aa35bf4 | ||
![]() |
871f12b751 | ||
![]() |
7e9bf8a3bb | ||
![]() |
810ac6023e | ||
![]() |
5640161e38 | ||
![]() |
af3b670de6 | ||
![]() |
b3b1c88d27 | ||
![]() |
c101f37298 | ||
![]() |
082ba4d741 | ||
![]() |
ab5663891c | ||
![]() |
00d168a78d | ||
![]() |
a8097f1cdc | ||
![]() |
22f9571687 | ||
![]() |
388a3b78e3 | ||
![]() |
b9f8226208 | ||
![]() |
d43de14807 | ||
![]() |
d34074af1b | ||
![]() |
04d1180a0c | ||
![]() |
ac0e677fae | ||
![]() |
76a92aff2f | ||
![]() |
7478d9e688 | ||
![]() |
f54fdef099 | ||
![]() |
5adb4b7413 | ||
![]() |
8f0721f00a | ||
![]() |
13b2dc31f5 | ||
![]() |
8bce378bad | ||
![]() |
07c69c1e74 | ||
![]() |
647615ddec | ||
![]() |
29f442887a | ||
![]() |
578a81f146 | ||
![]() |
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 | ||
![]() |
da577cf1ac | ||
![]() |
1b89f998e8 | ||
![]() |
df96f0c122 | ||
![]() |
355402e210 | ||
![]() |
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 | ||
![]() |
f8e9a75633 | ||
![]() |
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 | ||
![]() |
f44be74435 | ||
![]() |
13d2cd1293 |
373 changed files with 19140 additions and 7414 deletions
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
|
||||
|
|
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -20,10 +20,67 @@ 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))
|
||||
|
@ -34,7 +91,7 @@ Features:
|
|||
* 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 (Cwd/Win/Meta) modifier ([#6592](https://github.com/helix-editor/helix/pull/6592))
|
||||
* 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))
|
||||
|
||||
|
@ -678,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))
|
||||
|
@ -885,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))
|
||||
|
@ -1372,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))
|
||||
|
@ -1527,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))
|
||||
|
@ -1694,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))
|
||||
|
|
720
Cargo.lock
generated
720
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 = "25.1.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,14 @@
|
|||
## Editor
|
||||
|
||||
- [`[editor]` Section](#editor-section)
|
||||
- [`[editor.clipboard-provider]` Section](#editorclipboard-provider-section)
|
||||
- [`[editor.statusline]` Section](#editorstatusline-section)
|
||||
- [`[editor.lsp]` Section](#editorlsp-section)
|
||||
- [`[editor.inline-blame]` Section](#editorinlineblame-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 +54,16 @@
|
|||
| `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` |
|
||||
| `rounded-corners` | Set to `true` to draw rounded border corners | `false` |
|
||||
| `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` |
|
||||
| `welcome-screen` | Whether to enable the welcome screen | `true` |
|
||||
|
||||
### `[editor.clipboard-provider]` Section
|
||||
|
||||
|
@ -68,7 +76,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]
|
||||
|
@ -111,6 +119,7 @@ The `[editor.statusline]` key takes the following sub-keys:
|
|||
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
|
||||
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
|
||||
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
|
||||
| `merge-with-commandline` | If set, the command line and statusline will merge into a single line. Status text will replace the statusline briefly | `false` |
|
||||
|
||||
The following statusline elements can be configured:
|
||||
|
||||
|
@ -143,9 +152,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` |
|
||||
|
@ -154,6 +165,50 @@ The following statusline elements can be configured:
|
|||
|
||||
[^2]: You may also have to activate them in the language server config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
|
||||
|
||||
### `[editor.inline-blame]` Section
|
||||
|
||||
Inline blame is virtual text that appears at the end of a line, displaying information about the most recent commit that affected this line.
|
||||
|
||||
| Key | Description | Default |
|
||||
| ------- | ------------------------------------------ | ------- |
|
||||
| `behaviour` | Choose when to show inline blame | `"hidden"` |
|
||||
| `compute` | Choose when inline blame should be computed | `"on-demand"` |
|
||||
| `format` | The format in which to show the inline blame | `"{author}, {time-ago} • {message} • {commit}"` |
|
||||
|
||||
The `behaviour` can be one of the following:
|
||||
- `"all-lines"`: Inline blame is on every line.
|
||||
- `"cursor-line"`: Inline blame is only on the line of the primary cursor.
|
||||
- `"hidden"`: Inline blame is not shown.
|
||||
|
||||
Inline blame will only show if the blame for the file has already been computed.
|
||||
|
||||
The `compute` key determines under which circumstances the blame is computed, and can be one of the following:
|
||||
- `"on-demand"`: Blame for the file is computed only when explicitly requested, such as when using `space + B` to blame the line of the cursor. There may be a little delay when loading the blame. When opening new files, even with `behaviour` not set to `"hidden"`, the inline blame won't show. It needs to be computed first in order to become available. This computation can be manually triggered by requesting it with `space + B`.
|
||||
- `"background"`: Blame for the file is loaded in the background. This will have zero effect on performance of the Editor, but will use a little bit extra resources. Directly requesting the blame with `space + B` will be instant. Inline blame will show as soon as the blame is available when loading new files.
|
||||
|
||||
`inline-blame-format` allows customization of the blame message, and can be set to any string. Variables can be used like so: `{variable}`. These are the available variables:
|
||||
|
||||
- `author`: The author of the commit
|
||||
- `date`: When the commit was made
|
||||
- `time-ago`: How long ago the commit was made
|
||||
- `message`: The message of the commit, excluding the body
|
||||
- `body`: The body of the commit
|
||||
- `commit`: The short hex SHA1 hash of the commit
|
||||
- `email`: The email of the author of the commit
|
||||
|
||||
Any of the variables can potentially be empty.
|
||||
In this case, the content before the variable will not be included in the string.
|
||||
If the variable is at the beginning of the string, the content after the variable will not be included.
|
||||
|
||||
Some examples, using the default value `format` value:
|
||||
|
||||
- If `author` is empty: `"{time-ago} • {message} • {commit}"`
|
||||
- If `time-ago` is empty: `"{author} • {message} • {commit}"`
|
||||
- If `message` is empty: `"{author}, {time-ago} • {commit}"`
|
||||
- If `commit` is empty: `"{author}, {time-ago} • {message}"`
|
||||
- If `time-ago` and `message` is empty: `"{author} • {commit}"`
|
||||
- If `author` and `message` is empty: `"{time-ago} • {commit}"`
|
||||
|
||||
### `[editor.cursor-shape]` Section
|
||||
|
||||
Defines the shape of cursor in each mode.
|
||||
|
|
|
@ -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,12 +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` |
|
||||
|
@ -160,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` |
|
||||
|
@ -183,7 +191,7 @@
|
|||
| robot | ✓ | | | `robotframework_ls` |
|
||||
| ron | ✓ | | ✓ | |
|
||||
| rst | ✓ | | | |
|
||||
| ruby | ✓ | ✓ | ✓ | `solargraph` |
|
||||
| ruby | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
| sage | ✓ | ✓ | | |
|
||||
| scala | ✓ | ✓ | ✓ | `metals` |
|
||||
|
@ -195,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` |
|
||||
|
@ -210,11 +219,13 @@
|
|||
| tact | ✓ | ✓ | ✓ | |
|
||||
| task | ✓ | | | |
|
||||
| tcl | ✓ | | ✓ | |
|
||||
| teal | ✓ | | | |
|
||||
| teal | ✓ | | | `teal-language-server` |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| tera | ✓ | | | |
|
||||
| textproto | ✓ | ✓ | ✓ | |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
| thrift | ✓ | | | |
|
||||
| tlaplus | ✓ | | | |
|
||||
| todotxt | ✓ | | | |
|
||||
| toml | ✓ | ✓ | | `taplo` |
|
||||
| tsq | ✓ | | | `ts_query_ls` |
|
||||
|
@ -222,7 +233,7 @@
|
|||
| twig | ✓ | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typespec | ✓ | ✓ | ✓ | `tsp-server` |
|
||||
| typst | ✓ | | | `tinymist`, `typst-lsp` |
|
||||
| typst | ✓ | | | `tinymist` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | ✓ | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
|
@ -234,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 `` |
|
||||
|
@ -294,3 +298,4 @@
|
|||
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
|
||||
| `goto_next_tabstop` | goto next snippet placeholder | |
|
||||
| `goto_prev_tabstop` | goto next snippet placeholder | |
|
||||
| `blame_line` | Show blame for the current line | normal: `` <space>B ``, select: `` <space>B `` |
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -38,12 +38,6 @@ below.
|
|||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -309,6 +309,7 @@ This layer is a kludge of mappings, mostly pickers.
|
|||
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
|
||||
| `/` | Global search in workspace folder | `global_search` |
|
||||
| `?` | Open command palette | `command_palette` |
|
||||
| `B` | Show blame for the current line | `blame_line` |
|
||||
|
||||
> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
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:
|
||||
|
||||
|
|
|
@ -314,6 +314,7 @@ These scopes are used for theming the editor interface:
|
|||
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (language servers are not required to set a kind) |
|
||||
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
|
||||
| `ui.virtual.jump-label` | Style for virtual jump labels |
|
||||
| `ui.virtual.inline-blame` | Inline blame indicator (see the [`editor.inline-blame` config][editor-section]) |
|
||||
| `ui.menu` | Code and command completion menus |
|
||||
| `ui.menu.selected` | Selected autocomplete item |
|
||||
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
|
||||
|
@ -336,5 +337,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
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
<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>
|
||||
|
|
|
@ -86,6 +86,6 @@ Keywords[ru]=текст;текстовый редактор;
|
|||
Keywords[sr]=Текст;едитор;
|
||||
Keywords[tr]=Metin;düzenleyici;
|
||||
Icon=helix
|
||||
Categories=Utility;TextEditor;
|
||||
Categories=Utility;TextEditor;ConsoleOnly
|
||||
StartupNotify=false
|
||||
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
|
||||
|
|
|
@ -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.
|
||||
|
|
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": {
|
||||
|
|
206
flake.nix
206
flake.nix
|
@ -8,183 +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}
|
||||
'';
|
||||
# set git revision for nix flake builds, see 'git_hash' in helix-loader/build.rs
|
||||
HELIX_NIX_BUILD_REV = self.rev or self.dirtyRev or null;
|
||||
});
|
||||
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)]
|
||||
|
@ -332,8 +332,10 @@ pub enum LanguageServerFeature {
|
|||
WorkspaceSymbols,
|
||||
// Symbols, use bitflags, see above?
|
||||
Diagnostics,
|
||||
PullDiagnostics,
|
||||
RenameSymbol,
|
||||
InlayHints,
|
||||
DocumentColors,
|
||||
}
|
||||
|
||||
impl Display for LanguageServerFeature {
|
||||
|
@ -355,8 +357,10 @@ impl Display for LanguageServerFeature {
|
|||
DocumentSymbols => "document-symbols",
|
||||
WorkspaceSymbols => "workspace-symbols",
|
||||
Diagnostics => "diagnostics",
|
||||
PullDiagnostics => "pull-diagnostics",
|
||||
RenameSymbol => "rename-symbol",
|
||||
InlayHints => "inlay-hints",
|
||||
DocumentColors => "document-colors",
|
||||
};
|
||||
write!(f, "{feature}",)
|
||||
}
|
||||
|
@ -756,7 +760,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 +998,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 +1041,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 +2051,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 +2076,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 +2107,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 +2506,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 +2545,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]+)";
|
||||
|
|
|
@ -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,14 +6,6 @@ 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"])
|
||||
|
@ -23,7 +15,17 @@ fn main() {
|
|||
.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.217", features = ["derive"] }
|
||||
serde_json = "1.0.134"
|
||||
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,
|
||||
|
@ -345,6 +348,7 @@ impl Client {
|
|||
Some(OneOf::Left(true) | OneOf::Right(_))
|
||||
),
|
||||
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
|
||||
LanguageServerFeature::PullDiagnostics => capabilities.diagnostic_provider.is_some(),
|
||||
LanguageServerFeature::RenameSymbol => matches!(
|
||||
capabilities.rename_provider,
|
||||
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
|
||||
|
@ -353,6 +357,7 @@ impl Client {
|
|||
capabilities.inlay_hint_provider,
|
||||
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
|
||||
),
|
||||
LanguageServerFeature::DocumentColors => capabilities.color_provider.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,23 +387,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 +401,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 +412,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 +491,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(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
@ -570,6 +572,9 @@ impl Client {
|
|||
did_rename: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities {
|
||||
refresh_support: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(lsp::TextDocumentClientCapabilities {
|
||||
|
@ -647,6 +652,10 @@ impl Client {
|
|||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic: Some(lsp::DiagnosticClientCapabilities {
|
||||
dynamic_registration: Some(false),
|
||||
related_document_support: Some(true),
|
||||
}),
|
||||
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
|
||||
version_support: Some(true),
|
||||
tag_support: Some(lsp::TagSupport {
|
||||
|
@ -686,14 +695,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 +710,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 +719,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 +744,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 +761,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 +785,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 +799,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 +926,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 +958,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 +977,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 +995,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 +1008,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 +1034,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 +1052,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 +1076,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 +1084,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 +1104,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 +1159,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 +1192,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 +1201,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 +1217,33 @@ impl Client {
|
|||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
};
|
||||
|
||||
let request = self.call::<lsp::request::RangeFormatting>(params);
|
||||
Some(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())
|
||||
})
|
||||
pub fn text_document_diagnostic(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
previous_result_id: Option<String>,
|
||||
) -> Option<impl Future<Output = Result<lsp::DocumentDiagnosticReportResult>>> {
|
||||
let capabilities = self.capabilities();
|
||||
|
||||
// Return early if the server does not support pull diagnostic.
|
||||
let identifier = match capabilities.diagnostic_provider.as_ref()? {
|
||||
lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(),
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => {
|
||||
cap.diagnostic_options.identifier.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let params = lsp::DocumentDiagnosticParams {
|
||||
text_document,
|
||||
identifier,
|
||||
previous_result_id,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
||||
partial_result_params: lsp::PartialResultParams::default(),
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::DocumentDiagnosticRequest>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_document_highlight(
|
||||
|
@ -1228,7 +1251,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 +1284,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 +1304,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 +1325,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 +1350,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 +1374,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 +1399,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 +1428,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 +1450,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 +1470,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 +1496,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 +1524,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 +1540,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 +1563,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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -463,6 +463,7 @@ pub enum MethodCall {
|
|||
RegisterCapability(lsp::RegistrationParams),
|
||||
UnregisterCapability(lsp::UnregistrationParams),
|
||||
ShowDocument(lsp::ShowDocumentParams),
|
||||
WorkspaceDiagnosticRefresh,
|
||||
}
|
||||
|
||||
impl MethodCall {
|
||||
|
@ -494,6 +495,7 @@ impl MethodCall {
|
|||
let params: lsp::ShowDocumentParams = params.parse()?;
|
||||
Self::ShowDocument(params)
|
||||
}
|
||||
lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh,
|
||||
_ => {
|
||||
return Err(Error::Unhandled);
|
||||
}
|
||||
|
@ -618,51 +620,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) {
|
||||
|
@ -739,14 +735,17 @@ impl Registry {
|
|||
#[derive(Debug)]
|
||||
pub enum ProgressStatus {
|
||||
Created,
|
||||
Started(lsp::WorkDoneProgress),
|
||||
Started {
|
||||
title: String,
|
||||
progress: lsp::WorkDoneProgress,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProgressStatus {
|
||||
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
|
||||
match &self {
|
||||
ProgressStatus::Created => None,
|
||||
ProgressStatus::Started(progress) => Some(progress),
|
||||
ProgressStatus::Started { title: _, progress } => Some(progress),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -783,6 +782,13 @@ impl LspProgressMap {
|
|||
self.0.get(&id).and_then(|values| values.get(token))
|
||||
}
|
||||
|
||||
pub fn title(&self, id: LanguageServerId, token: &lsp::ProgressToken) -> Option<&String> {
|
||||
self.progress(id, token).and_then(|p| match p {
|
||||
ProgressStatus::Created => None,
|
||||
ProgressStatus::Started { title, .. } => Some(title),
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if progress `token` for server with `id` is created.
|
||||
pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool {
|
||||
self.0
|
||||
|
@ -807,17 +813,39 @@ impl LspProgressMap {
|
|||
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
|
||||
}
|
||||
|
||||
/// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
|
||||
/// Updates the progress of `token` for server with `id` to begin state `status`
|
||||
pub fn begin(
|
||||
&mut self,
|
||||
id: LanguageServerId,
|
||||
token: lsp::ProgressToken,
|
||||
status: lsp::WorkDoneProgressBegin,
|
||||
) {
|
||||
self.0.entry(id).or_default().insert(
|
||||
token,
|
||||
ProgressStatus::Started {
|
||||
title: status.title.clone(),
|
||||
progress: lsp::WorkDoneProgress::Begin(status),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates the progress of `token` for server with `id` to report state `status`.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
id: LanguageServerId,
|
||||
token: lsp::ProgressToken,
|
||||
status: lsp::WorkDoneProgress,
|
||||
) -> Option<ProgressStatus> {
|
||||
status: lsp::WorkDoneProgressReport,
|
||||
) {
|
||||
self.0
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.insert(token, ProgressStatus::Started(status))
|
||||
.entry(token)
|
||||
.and_modify(|e| match e {
|
||||
ProgressStatus::Created => (),
|
||||
ProgressStatus::Started { progress, .. } => {
|
||||
*progress = lsp::WorkDoneProgress::Report(status)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -877,7 +905,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,
|
||||
|
@ -906,17 +934,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();
|
||||
});
|
||||
|
@ -1048,7 +1066,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(())
|
||||
}
|
||||
|
|
|
@ -3,5 +3,7 @@ pub mod faccess;
|
|||
pub mod path;
|
||||
pub mod range;
|
||||
pub mod rope;
|
||||
pub mod str;
|
||||
pub mod time;
|
||||
|
||||
pub use range::Range;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
18
helix-stdx/src/str.rs
Normal file
18
helix-stdx/src/str.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
/// Concatenates strings together.
|
||||
///
|
||||
/// `concat!(a, " ", b, " ", c)` is:
|
||||
/// - more performant than `format!("{a} {b} {c}")`
|
||||
/// - more ergonomic than using `String::with_capacity` followed by a series of `String::push_str`
|
||||
#[macro_export]
|
||||
macro_rules! concat {
|
||||
($($value:expr),*) => {{
|
||||
// Rust does not allow using `+` as separator between value
|
||||
// so we must add that at the end of everything. The `0` is necessary
|
||||
// at the end so it does not end with "+ " (which would be invalid syntax)
|
||||
let mut buf = String::with_capacity($($value.len() + )* 0);
|
||||
$(
|
||||
buf.push_str(&$value);
|
||||
)*
|
||||
buf
|
||||
}}
|
||||
}
|
75
helix-stdx/src/time.rs
Normal file
75
helix-stdx/src/time.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::time::{Instant, SystemTime};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const SECOND: i64 = 1;
|
||||
const MINUTE: i64 = 60 * SECOND;
|
||||
const HOUR: i64 = 60 * MINUTE;
|
||||
const DAY: i64 = 24 * HOUR;
|
||||
const MONTH: i64 = 30 * DAY;
|
||||
const YEAR: i64 = 365 * DAY;
|
||||
|
||||
/// Like `std::time::SystemTime::now()` but does not cause a syscall on every invocation.
|
||||
///
|
||||
/// There is just one syscall at the start of the program, subsequent invocations are
|
||||
/// much cheaper and use the monotonic clock instead of trigerring a syscall.
|
||||
#[inline]
|
||||
fn now() -> SystemTime {
|
||||
static START_INSTANT: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
static START_SYSTEM_TIME: Lazy<SystemTime> = Lazy::new(SystemTime::now);
|
||||
|
||||
*START_SYSTEM_TIME + START_INSTANT.elapsed()
|
||||
}
|
||||
|
||||
/// Formats a timestamp into a human-readable relative time string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timestamp` - A point in history. Seconds since UNIX epoch (UTC)
|
||||
/// * `timezone_offset` - Timezone offset in seconds
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A String representing the relative time (e.g., "4 years ago", "11 months from now")
|
||||
#[inline]
|
||||
pub fn format_relative_time(timestamp: i64, timezone_offset: i32) -> String {
|
||||
let timestamp = timestamp + timezone_offset as i64;
|
||||
let now = now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64
|
||||
+ timezone_offset as i64;
|
||||
|
||||
let time_passed = now - timestamp;
|
||||
|
||||
let time_difference = time_passed.abs();
|
||||
|
||||
let (value, unit) = if time_difference >= YEAR {
|
||||
let years = time_difference / YEAR;
|
||||
(years, if years == 1 { "year" } else { "years" })
|
||||
} else if time_difference >= MONTH {
|
||||
let months = time_difference / MONTH;
|
||||
(months, if months == 1 { "month" } else { "months" })
|
||||
} else if time_difference >= DAY {
|
||||
let days = time_difference / DAY;
|
||||
(days, if days == 1 { "day" } else { "days" })
|
||||
} else if time_difference >= HOUR {
|
||||
let hours = time_difference / HOUR;
|
||||
(hours, if hours == 1 { "hour" } else { "hours" })
|
||||
} else if time_difference >= MINUTE {
|
||||
let minutes = time_difference / MINUTE;
|
||||
(minutes, if minutes == 1 { "minute" } else { "minutes" })
|
||||
} else {
|
||||
let seconds = time_difference / SECOND;
|
||||
(seconds, if seconds == 1 { "second" } else { "seconds" })
|
||||
};
|
||||
let value = value.to_string();
|
||||
|
||||
let label = if time_passed.is_positive() {
|
||||
"ago"
|
||||
} else {
|
||||
"from now"
|
||||
};
|
||||
|
||||
crate::concat!(value, " ", unit, " ", label)
|
||||
}
|
|
@ -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.169"
|
||||
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,7 @@ use helix_view::{
|
|||
align_view,
|
||||
document::{DocumentOpenError, DocumentSavedEventResult},
|
||||
editor::{ConfigEvent, EditorEvent},
|
||||
events::DiagnosticsDidChange,
|
||||
events::EditorConfigDidChange,
|
||||
graphics::Rect,
|
||||
theme,
|
||||
tree::Layout,
|
||||
|
@ -33,7 +33,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 +66,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 +102,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 +117,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 +142,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 +188,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,15 +216,13 @@ impl Application {
|
|||
editor.new_file(Action::VerticalSplit);
|
||||
}
|
||||
} else if stdin().is_tty() || cfg!(feature = "integration") {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
editor.new_file_welcome();
|
||||
} else {
|
||||
editor
|
||||
.new_file_from_stdin(Action::VerticalSplit)
|
||||
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
|
||||
.unwrap_or_else(|_| editor.new_file_welcome());
|
||||
}
|
||||
|
||||
editor.set_theme(theme);
|
||||
|
||||
#[cfg(windows)]
|
||||
let signals = futures_util::stream::empty();
|
||||
#[cfg(not(windows))]
|
||||
|
@ -258,12 +239,7 @@ impl Application {
|
|||
compositor,
|
||||
terminal,
|
||||
editor,
|
||||
|
||||
config,
|
||||
|
||||
theme_loader,
|
||||
syn_loader,
|
||||
|
||||
signals,
|
||||
jobs: Jobs::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
|
@ -389,6 +365,10 @@ impl Application {
|
|||
// the Application can apply it.
|
||||
ConfigEvent::Update(editor_config) => {
|
||||
let mut app_config = (*self.config.load().clone()).clone();
|
||||
helix_event::dispatch(EditorConfigDidChange {
|
||||
old_config: &app_config.editor,
|
||||
editor: &mut self.editor,
|
||||
});
|
||||
app_config.editor = *editor_config;
|
||||
if let Err(err) = self.terminal.reconfigure(app_config.editor.clone().into()) {
|
||||
self.editor.set_error(err.to_string());
|
||||
|
@ -413,10 +393,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 +407,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 +431,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 +706,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 +727,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 {
|
||||
|
@ -874,10 +761,11 @@ impl Application {
|
|||
.compositor
|
||||
.find::<ui::EditorView>()
|
||||
.expect("expected at least one EditorView");
|
||||
let lsp::ProgressParams { token, value } = params;
|
||||
|
||||
let lsp::ProgressParamsValue::WorkDone(work) = value;
|
||||
let parts = match &work {
|
||||
let lsp::ProgressParams {
|
||||
token,
|
||||
value: lsp::ProgressParamsValue::WorkDone(work),
|
||||
} = params;
|
||||
let (title, message, percentage) = match &work {
|
||||
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
|
||||
title,
|
||||
message,
|
||||
|
@ -905,47 +793,43 @@ impl Application {
|
|||
}
|
||||
};
|
||||
|
||||
let token_d: &dyn std::fmt::Display = match &token {
|
||||
lsp::NumberOrString::Number(n) => n,
|
||||
lsp::NumberOrString::String(s) => s,
|
||||
};
|
||||
|
||||
let status = match parts {
|
||||
(Some(title), Some(message), Some(percentage)) => {
|
||||
format!("[{}] {}% {} - {}", token_d, percentage, title, message)
|
||||
if self.editor.config().lsp.display_progress_messages {
|
||||
let title =
|
||||
title.or_else(|| self.lsp_progress.title(server_id, &token));
|
||||
if title.is_some() || percentage.is_some() || message.is_some() {
|
||||
use std::fmt::Write as _;
|
||||
let mut status = format!("{}: ", language_server!().name());
|
||||
if let Some(percentage) = percentage {
|
||||
write!(status, "{percentage:>2}% ").unwrap();
|
||||
}
|
||||
if let Some(title) = title {
|
||||
status.push_str(title);
|
||||
}
|
||||
if title.is_some() && message.is_some() {
|
||||
status.push_str(" ⋅ ");
|
||||
}
|
||||
if let Some(message) = message {
|
||||
status.push_str(message);
|
||||
}
|
||||
self.editor.set_status(status);
|
||||
}
|
||||
(Some(title), None, Some(percentage)) => {
|
||||
format!("[{}] {}% {}", token_d, percentage, title)
|
||||
}
|
||||
(Some(title), Some(message), None) => {
|
||||
format!("[{}] {} - {}", token_d, title, message)
|
||||
}
|
||||
(None, Some(message), Some(percentage)) => {
|
||||
format!("[{}] {}% {}", token_d, percentage, message)
|
||||
}
|
||||
(Some(title), None, None) => {
|
||||
format!("[{}] {}", token_d, title)
|
||||
}
|
||||
(None, Some(message), None) => {
|
||||
format!("[{}] {}", token_d, message)
|
||||
}
|
||||
(None, None, Some(percentage)) => {
|
||||
format!("[{}] {}%", token_d, percentage)
|
||||
}
|
||||
(None, None, None) => format!("[{}]", token_d),
|
||||
};
|
||||
|
||||
if let lsp::WorkDoneProgress::End(_) = work {
|
||||
self.lsp_progress.end_progress(server_id, &token);
|
||||
if !self.lsp_progress.is_progressing(server_id) {
|
||||
editor_view.spinners_mut().get_or_create(server_id).stop();
|
||||
}
|
||||
} else {
|
||||
self.lsp_progress.update(server_id, token, work);
|
||||
}
|
||||
|
||||
if self.config.load().editor.lsp.display_progress_messages {
|
||||
self.editor.set_status(status);
|
||||
match work {
|
||||
lsp::WorkDoneProgress::Begin(begin_status) => {
|
||||
self.lsp_progress
|
||||
.begin(server_id, token.clone(), begin_status);
|
||||
}
|
||||
lsp::WorkDoneProgress::Report(report_status) => {
|
||||
self.lsp_progress
|
||||
.update(server_id, token.clone(), report_status);
|
||||
}
|
||||
lsp::WorkDoneProgress::End(_) => {
|
||||
self.lsp_progress.end_progress(server_id, &token);
|
||||
if !self.lsp_progress.is_progressing(server_id) {
|
||||
editor_view.spinners_mut().get_or_create(server_id).stop();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::ProgressMessage(_params) => {
|
||||
|
@ -958,16 +842,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);
|
||||
}
|
||||
|
@ -1124,9 +1015,25 @@ impl Application {
|
|||
let result = self.handle_show_document(params, offset_encoding);
|
||||
Ok(json!(result))
|
||||
}
|
||||
Ok(MethodCall::WorkspaceDiagnosticRefresh) => {
|
||||
for document in self.editor.documents() {
|
||||
let language_server = language_server!();
|
||||
handlers::diagnostics::pull_diagnostics_for_document(
|
||||
document,
|
||||
language_server,
|
||||
);
|
||||
}
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
};
|
||||
|
||||
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
|
@ -14,13 +14,15 @@ 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,
|
||||
diagnostic::DiagnosticProvider, syntax::LanguageServerFeature,
|
||||
text_annotations::InlineAnnotation, Selection, Uri,
|
||||
};
|
||||
use helix_stdx::path;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
editor::Action,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
icons::ICONS,
|
||||
theme::Style,
|
||||
Document, View,
|
||||
};
|
||||
|
@ -31,13 +33,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 +57,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 +80,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 +99,6 @@ struct DiagnosticStyles {
|
|||
struct PickerDiagnostic {
|
||||
location: Location,
|
||||
diag: lsp::Diagnostic,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
fn location_to_file_location(location: &Location) -> Option<FileLocation> {
|
||||
|
@ -110,12 +110,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 +119,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 +205,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 +215,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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -239,11 +243,22 @@ fn diag_picker(
|
|||
ui::PickerColumn::new(
|
||||
"severity",
|
||||
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
|
||||
let icons = ICONS.load();
|
||||
match item.diag.severity {
|
||||
Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
|
||||
Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
|
||||
Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
|
||||
Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
|
||||
Some(DiagnosticSeverity::HINT) => {
|
||||
Span::styled(format!("{} HINT", icons.diagnostic().hint()), styles.hint)
|
||||
}
|
||||
Some(DiagnosticSeverity::INFORMATION) => {
|
||||
Span::styled(format!("{} INFO", icons.diagnostic().info()), styles.info)
|
||||
}
|
||||
Some(DiagnosticSeverity::WARNING) => Span::styled(
|
||||
format!("{} WARN", icons.diagnostic().warning()),
|
||||
styles.warning,
|
||||
),
|
||||
Some(DiagnosticSeverity::ERROR) => Span::styled(
|
||||
format!("{} ERROR", icons.diagnostic().error()),
|
||||
styles.error,
|
||||
),
|
||||
_ => Span::raw(""),
|
||||
}
|
||||
.into()
|
||||
|
@ -286,7 +301,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 +329,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 +355,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 +368,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,14 +400,21 @@ 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 = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
let icons = ICONS.load();
|
||||
let name = display_symbol_kind(item.symbol.kind);
|
||||
icons
|
||||
.lsp()
|
||||
.get(name)
|
||||
.map_or_else(|| name.into(), |symbol| format!("{symbol} {name}").into())
|
||||
}),
|
||||
// Some symbols in the document symbol picker may have a URI that isn't
|
||||
// the current file. It should be rare though, so we concatenate that
|
||||
|
@ -402,6 +422,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 +437,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 +476,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 +516,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(())
|
||||
|
@ -497,12 +532,24 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||
};
|
||||
let columns = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
let icons = ICONS.load();
|
||||
let name = display_symbol_kind(item.symbol.kind);
|
||||
icons
|
||||
.lsp()
|
||||
.get(name)
|
||||
.map_or_else(|| name.into(), |symbol| format!("{symbol} {name}").into())
|
||||
}),
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
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 +568,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 +582,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 +700,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 +766,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 +796,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 +819,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 +835,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 +863,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 +885,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 +988,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 +1038,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 +1165,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()),
|
||||
}
|
||||
|
@ -1288,7 +1324,7 @@ fn compute_inlay_hints_for_view(
|
|||
if !doc.inlay_hints_oudated
|
||||
&& doc
|
||||
.inlay_hints(view_id)
|
||||
.map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
|
||||
.is_some_and(|dih| dih.id == new_doc_inlay_hints_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
|
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();
|
||||
|
|
|
@ -2,11 +2,13 @@ use crate::keymap;
|
|||
use crate::keymap::{merge_keys, KeyTrie};
|
||||
use helix_loader::merge_toml_values;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::icons::{Icons, ICONS};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::io::Error as IOError;
|
||||
use std::sync::Arc;
|
||||
use toml::de::Error as TomlError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -22,6 +24,7 @@ pub struct ConfigRaw {
|
|||
pub theme: Option<String>,
|
||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||
pub editor: Option<toml::Value>,
|
||||
pub icons: Option<toml::Value>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -64,6 +67,7 @@ impl Config {
|
|||
global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
|
||||
let local_config: Result<ConfigRaw, ConfigLoadError> =
|
||||
local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
|
||||
|
||||
let res = match (global_config, local_config) {
|
||||
(Ok(global), Ok(local)) => {
|
||||
let mut keys = keymap::default();
|
||||
|
@ -84,6 +88,18 @@ impl Config {
|
|||
.map_err(ConfigLoadError::BadConfig)?,
|
||||
};
|
||||
|
||||
let icons: Icons = match (global.icons, local.icons) {
|
||||
(None, None) => Icons::default(),
|
||||
(None, Some(val)) | (Some(val), None) => {
|
||||
val.try_into().map_err(ConfigLoadError::BadConfig)?
|
||||
}
|
||||
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
|
||||
.try_into()
|
||||
.map_err(ConfigLoadError::BadConfig)?,
|
||||
};
|
||||
|
||||
ICONS.store(Arc::new(icons));
|
||||
|
||||
Config {
|
||||
theme: local.theme.or(global.theme),
|
||||
keys,
|
||||
|
@ -100,6 +116,14 @@ impl Config {
|
|||
if let Some(keymap) = config.keys {
|
||||
merge_keys(&mut keys, keymap);
|
||||
}
|
||||
|
||||
let icons = config.icons.map_or_else(
|
||||
|| Ok(Icons::default()),
|
||||
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
|
||||
)?;
|
||||
|
||||
ICONS.store(Arc::new(icons));
|
||||
|
||||
Config {
|
||||
theme: config.theme,
|
||||
keys,
|
||||
|
|
|
@ -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,
|
||||
EditorConfigDidChange, LanguageServerExited, LanguageServerInitialized, SelectionDidChange,
|
||||
};
|
||||
|
||||
use crate::commands;
|
||||
|
@ -17,8 +18,13 @@ pub fn register() {
|
|||
register_event::<OnModeSwitch>();
|
||||
register_event::<PostInsertChar>();
|
||||
register_event::<PostCommand>();
|
||||
register_event::<DocumentDidOpen>();
|
||||
register_event::<DocumentDidChange>();
|
||||
register_event::<EditorConfigDidChange>();
|
||||
register_event::<DocumentDidClose>();
|
||||
register_event::<DocumentFocusLost>();
|
||||
register_event::<SelectionDidChange>();
|
||||
register_event::<DiagnosticsDidChange>();
|
||||
register_event::<LanguageServerInitialized>();
|
||||
register_event::<LanguageServerExited>();
|
||||
}
|
||||
|
|
|
@ -6,35 +6,48 @@ 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::diagnostics::PullDiagnosticsHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use completion::trigger_auto_completion;
|
||||
pub use helix_view::handlers::Handlers;
|
||||
|
||||
use self::blame::BlameHandler;
|
||||
use self::document_colors::DocumentColorsHandler;
|
||||
|
||||
mod auto_save;
|
||||
pub mod blame;
|
||||
pub mod completion;
|
||||
mod diagnostics;
|
||||
pub mod diagnostics;
|
||||
mod document_colors;
|
||||
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 auto_save = AutoSaveHandler::new().spawn();
|
||||
let document_colors = DocumentColorsHandler::default().spawn();
|
||||
let blame = BlameHandler::default().spawn();
|
||||
let pull_diagnostics = PullDiagnosticsHandler::new().spawn();
|
||||
|
||||
let handlers = Handlers {
|
||||
completions,
|
||||
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
|
||||
signature_hints,
|
||||
auto_save,
|
||||
document_colors,
|
||||
blame,
|
||||
pull_diagnostics,
|
||||
};
|
||||
|
||||
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);
|
||||
blame::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));
|
||||
}
|
||||
}
|
||||
|
|
100
helix-term/src/handlers/blame.rs
Normal file
100
helix-term/src/handlers/blame.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use std::{mem, time::Duration};
|
||||
|
||||
use helix_event::register_hook;
|
||||
use helix_vcs::FileBlame;
|
||||
use helix_view::{
|
||||
editor::InlineBlameCompute,
|
||||
events::{DocumentDidOpen, EditorConfigDidChange},
|
||||
handlers::{BlameEvent, Handlers},
|
||||
DocumentId,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::job;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BlameHandler {
|
||||
file_blame: Option<anyhow::Result<FileBlame>>,
|
||||
doc_id: DocumentId,
|
||||
show_blame_for_line_in_statusline: Option<u32>,
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for BlameHandler {
|
||||
type Event = BlameEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
self.doc_id = event.doc_id;
|
||||
self.show_blame_for_line_in_statusline = event.line;
|
||||
self.file_blame = Some(FileBlame::try_new(event.path));
|
||||
Some(Instant::now() + Duration::from_millis(50))
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let doc_id = self.doc_id;
|
||||
let line_blame = self.show_blame_for_line_in_statusline;
|
||||
let result = mem::take(&mut self.file_blame);
|
||||
if let Some(result) = result {
|
||||
tokio::spawn(async move {
|
||||
job::dispatch(move |editor, _| {
|
||||
let Some(doc) = editor.document_mut(doc_id) else {
|
||||
return;
|
||||
};
|
||||
doc.file_blame = Some(result);
|
||||
if editor.config().inline_blame.compute == InlineBlameCompute::OnDemand {
|
||||
if let Some(line) = line_blame {
|
||||
crate::commands::blame_line_impl(editor, doc_id, line);
|
||||
} else {
|
||||
editor.set_status("Blame for this file is now available")
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.blame.clone();
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
if event.editor.config().inline_blame.compute != InlineBlameCompute::OnDemand {
|
||||
helix_event::send_blocking(
|
||||
&tx,
|
||||
BlameEvent {
|
||||
path: event.path.to_path_buf(),
|
||||
doc_id: event.doc,
|
||||
line: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
let tx = handlers.blame.clone();
|
||||
register_hook!(move |event: &mut EditorConfigDidChange<'_>| {
|
||||
let has_enabled_inline_blame = event.old_config.inline_blame.compute
|
||||
== InlineBlameCompute::OnDemand
|
||||
&& event.editor.config().inline_blame.compute == InlineBlameCompute::Background;
|
||||
|
||||
if has_enabled_inline_blame {
|
||||
// request blame for all documents, since any of them could have
|
||||
// outdated blame
|
||||
for doc in event.editor.documents() {
|
||||
if let Some(path) = doc.path() {
|
||||
helix_event::send_blocking(
|
||||
&tx,
|
||||
BlameEvent {
|
||||
path: path.to_path_buf(),
|
||||
doc_id: doc.id(),
|
||||
line: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,24 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use helix_core::diagnostic::DiagnosticProvider;
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_core::Uri;
|
||||
use helix_event::{register_hook, send_blocking};
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::DiagnosticsDidChange;
|
||||
use helix_view::events::{
|
||||
DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen, LanguageServerInitialized,
|
||||
};
|
||||
use helix_view::handlers::diagnostics::DiagnosticEvent;
|
||||
use helix_view::handlers::lsp::PullDiagnosticsEvent;
|
||||
use helix_view::handlers::Handlers;
|
||||
use helix_view::{DocumentId, Editor};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::events::OnModeSwitch;
|
||||
use crate::job;
|
||||
|
||||
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
|
||||
if event.editor.mode != Mode::Insert {
|
||||
for (view, _) in event.editor.tree.views_mut() {
|
||||
|
@ -21,4 +33,267 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
|
|||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.pull_diagnostics.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
if event
|
||||
.doc
|
||||
.has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
{
|
||||
let document_id = event.doc.id();
|
||||
send_blocking(&tx, PullDiagnosticsEvent { document_id });
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
let doc = doc!(event.editor, &event.doc);
|
||||
for language_server in
|
||||
doc.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
{
|
||||
pull_diagnostics_for_document(doc, language_server);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
|
||||
let language_server = event.editor.language_server_by_id(event.server_id).unwrap();
|
||||
if language_server.supports_feature(LanguageServerFeature::PullDiagnostics) {
|
||||
for doc in event
|
||||
.editor
|
||||
.documents()
|
||||
.filter(|doc| doc.supports_language_server(event.server_id))
|
||||
{
|
||||
pull_diagnostics_for_document(doc, language_server);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct PullDiagnosticsHandler {
|
||||
no_inter_file_dependency_timeout: Option<tokio::time::Instant>,
|
||||
}
|
||||
|
||||
impl PullDiagnosticsHandler {
|
||||
pub fn new() -> PullDiagnosticsHandler {
|
||||
PullDiagnosticsHandler {
|
||||
no_inter_file_dependency_timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_millis(500);
|
||||
const TIMEOUT_NO_INTER_FILE_DEPENDENCY: Duration = Duration::from_millis(125);
|
||||
|
||||
impl helix_event::AsyncHook for PullDiagnosticsHandler {
|
||||
type Event = PullDiagnosticsEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
if timeout.is_none() {
|
||||
dispatch_pull_diagnostic_for_document(event.document_id, false);
|
||||
self.no_inter_file_dependency_timeout = Some(Instant::now());
|
||||
}
|
||||
|
||||
if self
|
||||
.no_inter_file_dependency_timeout
|
||||
.is_some_and(|nifd_timeout| {
|
||||
nifd_timeout.duration_since(Instant::now()) > TIMEOUT_NO_INTER_FILE_DEPENDENCY
|
||||
})
|
||||
{
|
||||
dispatch_pull_diagnostic_for_document(event.document_id, true);
|
||||
self.no_inter_file_dependency_timeout = Some(Instant::now());
|
||||
};
|
||||
|
||||
Some(Instant::now() + TIMEOUT)
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
dispatch_pull_diagnostic_for_open_documents();
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_pull_diagnostic_for_document(
|
||||
document_id: DocumentId,
|
||||
exclude_language_servers_without_inter_file_dependency: bool,
|
||||
) {
|
||||
job::dispatch_blocking(move |editor, _| {
|
||||
let Some(doc) = editor.document(document_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let language_servers = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
.filter(|ls| ls.is_initialized())
|
||||
.filter(|ls| {
|
||||
if !exclude_language_servers_without_inter_file_dependency {
|
||||
return true;
|
||||
};
|
||||
ls.capabilities()
|
||||
.diagnostic_provider
|
||||
.as_ref()
|
||||
.is_some_and(|dp| match dp {
|
||||
lsp::DiagnosticServerCapabilities::Options(options) => {
|
||||
options.inter_file_dependencies
|
||||
}
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
|
||||
options.diagnostic_options.inter_file_dependencies
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
for language_server in language_servers {
|
||||
pull_diagnostics_for_document(doc, language_server);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn dispatch_pull_diagnostic_for_open_documents() {
|
||||
job::dispatch_blocking(move |editor, _| {
|
||||
let documents = editor.documents.values();
|
||||
|
||||
for document in documents {
|
||||
let language_servers = document
|
||||
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
.filter(|ls| ls.is_initialized());
|
||||
|
||||
for language_server in language_servers {
|
||||
pull_diagnostics_for_document(document, language_server);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pull_diagnostics_for_document(
|
||||
doc: &helix_view::Document,
|
||||
language_server: &helix_lsp::Client,
|
||||
) {
|
||||
let Some(future) = language_server
|
||||
.text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(uri) = doc.uri() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let identifier = language_server
|
||||
.capabilities()
|
||||
.diagnostic_provider
|
||||
.as_ref()
|
||||
.and_then(|diagnostic_provider| match diagnostic_provider {
|
||||
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
|
||||
options.diagnostic_options.identifier.clone()
|
||||
}
|
||||
});
|
||||
|
||||
let language_server_id = language_server.id();
|
||||
let provider = DiagnosticProvider::Lsp {
|
||||
server_id: language_server_id,
|
||||
identifier,
|
||||
};
|
||||
let document_id = doc.id();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match future.await {
|
||||
Ok(result) => {
|
||||
job::dispatch(move |editor, _| {
|
||||
handle_pull_diagnostics_response(editor, result, provider, uri, document_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Err(err) => {
|
||||
let parsed_cancellation_data = if let helix_lsp::Error::Rpc(error) = err {
|
||||
error.data.and_then(|data| {
|
||||
serde_json::from_value::<lsp::DiagnosticServerCancellationData>(data).ok()
|
||||
})
|
||||
} else {
|
||||
log::error!("Pull diagnostic request failed: {err}");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parsed_cancellation_data) = parsed_cancellation_data {
|
||||
if parsed_cancellation_data.retrigger_request {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
job::dispatch(move |editor, _| {
|
||||
if let (Some(doc), Some(language_server)) = (
|
||||
editor.document(document_id),
|
||||
editor.language_server_by_id(language_server_id),
|
||||
) {
|
||||
pull_diagnostics_for_document(doc, language_server);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_pull_diagnostics_response(
|
||||
editor: &mut Editor,
|
||||
result: lsp::DocumentDiagnosticReportResult,
|
||||
provider: DiagnosticProvider,
|
||||
uri: Uri,
|
||||
document_id: DocumentId,
|
||||
) {
|
||||
let related_documents = match result {
|
||||
lsp::DocumentDiagnosticReportResult::Report(report) => {
|
||||
let (result_id, related_documents) = match report {
|
||||
lsp::DocumentDiagnosticReport::Full(report) => {
|
||||
editor.handle_lsp_diagnostics(
|
||||
&provider,
|
||||
uri,
|
||||
None,
|
||||
report.full_document_diagnostic_report.items,
|
||||
);
|
||||
|
||||
(
|
||||
report.full_document_diagnostic_report.result_id,
|
||||
report.related_documents,
|
||||
)
|
||||
}
|
||||
lsp::DocumentDiagnosticReport::Unchanged(report) => (
|
||||
Some(report.unchanged_document_diagnostic_report.result_id),
|
||||
report.related_documents,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(doc) = editor.document_mut(document_id) {
|
||||
doc.previous_diagnostic_id = result_id;
|
||||
};
|
||||
|
||||
related_documents
|
||||
}
|
||||
lsp::DocumentDiagnosticReportResult::Partial(report) => report.related_documents,
|
||||
};
|
||||
|
||||
for (url, report) in related_documents.into_iter().flatten() {
|
||||
let result_id = match report {
|
||||
lsp::DocumentDiagnosticReportKind::Full(report) => {
|
||||
let Ok(uri) = Uri::try_from(url) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
editor.handle_lsp_diagnostics(&provider, uri, None, report.items);
|
||||
report.result_id
|
||||
}
|
||||
lsp::DocumentDiagnosticReportKind::Unchanged(report) => Some(report.result_id),
|
||||
};
|
||||
|
||||
if let Some(doc) = editor.document_mut(document_id) {
|
||||
doc.previous_diagnostic_id = result_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
211
helix-term/src/handlers/document_colors.rs
Normal file
211
helix-term/src/handlers/document_colors.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
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},
|
||||
icons::ICONS,
|
||||
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());
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
for (pos, color) in doc_colors {
|
||||
color_swatches_padding.push(InlineAnnotation::new(pos, " "));
|
||||
color_swatches.push(InlineAnnotation::new(pos, icons.lsp().color()));
|
||||
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(())
|
||||
});
|
||||
}
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -224,8 +224,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
|||
|
||||
for cmd in cmds {
|
||||
write!(stdout, "{}", fit(""))?;
|
||||
check_binary(Some(cmd));
|
||||
writeln!(stdout)?;
|
||||
writeln!(stdout, "{}", check_binary(Some(cmd)))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue