on master help sign in
292 files changed+14974−5232
since splitsnapshots
unifiedsplit
.envrc+1 .github/ISSUE_TEMPLATE/bug_report.yaml+10 .github/workflows/build.yml+15−7 .github/workflows/cachix.yml+3−3 .github/workflows/gh-pages.yml+5−5 .github/workflows/release.yml+15−15 CHANGELOG.md+260 Cargo.lock+538−615 Cargo.toml+16 README.md+1−1 VERSION−1 book/book.toml+3 book/custom.css+1−1 book/src/configuration.md+38−11 book/src/generated/lang-support.md+34−15 book/src/generated/typable-cmd.md+4−1 book/src/guides/adding_languages.md+2 book/src/guides/indent.md+319−43 book/src/guides/injection.md+1−1 book/src/install.md+30−6 book/src/keymap.md+16−15 book/src/languages.md+3−1 book/src/themes.md+9−1 book/src/usage.md+21−5 book/theme/index.hbs+18−15 contrib/Helix.appdata.xml+15 contrib/Helix.desktop+3−1 docs/CONTRIBUTING.md+10−3 docs/releases.md+12−5 flake.lock+21−237 flake.nix+170−143 grammars.nix+24−21 helix-core/Cargo.toml+19−15 helix-core/src/diagnostic.rs+4 helix-core/src/fuzzy.rs+43 helix-core/src/graphemes.rs+1−1 helix-core/src/history.rs+2−2 helix-core/src/indent.rs+586−127 helix-core/src/lib.rs+1−1 helix-core/src/match_brackets.rs+6−2 helix-core/src/movement.rs+80−1 helix-core/src/path.rs+2−2 helix-core/src/register.rs−89 helix-core/src/selection.rs+10−2 helix-core/src/syntax.rs+40−21 helix-core/src/transaction.rs+84−21 helix-core/src/wrap.rs+3−1 helix-core/tests/data/indent/cpp.cpp+48 helix-core/tests/data/indent/indent.rs−1 helix-core/tests/data/indent/languages.toml+13 helix-core/tests/indent.rs+153−25 helix-dap/Cargo.toml+11−9 helix-event/Cargo.toml+16 helix-event/src/lib.rs+8 helix-event/src/redraw.rs+49 helix-loader/Cargo.toml+13−12 helix-loader/build.rs+20−6 helix-loader/src/lib.rs+13−4 helix-lsp/Cargo.toml+15−13 helix-lsp/src/client.rs+123−11 helix-lsp/src/file_event.rs+193 helix-lsp/src/lib.rs+44−29 helix-parsec/Cargo.toml+8−7 helix-term/Cargo.toml+30−25 helix-term/src/application.rs+175−90 helix-term/src/args.rs+29 helix-term/src/commands.rs+545−351 helix-term/src/commands/dap.rs+13−4 helix-term/src/commands/lsp.rs+59−61 helix-term/src/commands/typed.rs+905−698 helix-term/src/compositor.rs+12 helix-term/src/config.rs+1 helix-term/src/health.rs+44−16 helix-term/src/keymap.rs+14−14 helix-term/src/keymap/default.rs+8−2 helix-term/src/main.rs+17−1 helix-term/src/ui/completion.rs+32−4 helix-term/src/ui/document.rs+60−24 helix-term/src/ui/editor.rs+102−54 helix-term/src/ui/fuzzy_match.rs−239 helix-term/src/ui/fuzzy_match/test.rs−47 helix-term/src/ui/lsp.rs+5−3 helix-term/src/ui/markdown.rs+19−12 helix-term/src/ui/menu.rs+73−40 helix-term/src/ui/mod.rs+90−149 helix-term/src/ui/picker.rs+362−325 helix-term/src/ui/popup.rs+27−7 helix-term/src/ui/prompt.rs+53−48 helix-term/src/ui/statusline.rs+14 helix-term/tests/integration.rs+1 helix-term/tests/test/commands.rs+47 helix-term/tests/test/commands/movement.rs+452 helix-term/tests/test/commands/write.rs+122−3 helix-term/tests/test/helpers.rs+9−1 helix-term/tests/test/languages/go.rs+41 helix-term/tests/test/languages/mod.rs+4 helix-term/tests/test/languages/yaml.rs+819 helix-term/tests/test/movement.rs+39 helix-term/tests/test/splits.rs+3−3 helix-tui/Cargo.toml+15−15 helix-tui/src/backend/crossterm.rs+51−33 helix-tui/src/buffer.rs+2−2 helix-tui/src/terminal.rs−14 helix-vcs/Cargo.toml+12−10 helix-vcs/src/diff.rs+6−19 helix-vcs/src/diff/worker.rs+3−9 helix-vcs/src/diff/worker/test.rs+1−5 helix-vcs/src/git.rs+5−5 helix-view/Cargo.toml+22−18 helix-view/src/clipboard.rs+7−2 helix-view/src/document.rs+122−46 helix-view/src/editor.rs+154−77 helix-view/src/gutter.rs+4−5 helix-view/src/handlers/dap.rs+1−3 helix-view/src/info.rs+4−11 helix-view/src/input.rs+67−3 helix-view/src/lib.rs+1 helix-view/src/register.rs+333 helix-view/src/theme.rs+6 helix-view/src/view.rs+8−2 languages.toml+514−206 logo.svg+1−1 runtime/queries/_jsx/highlights.scm+3−15 runtime/queries/_jsx/indents.scm−1 runtime/queries/_typescript/highlights.scm+5−3 runtime/queries/_typescript/locals.scm+17 runtime/queries/agda/highlights.scm+124 runtime/queries/bash/indents.scm+1 runtime/queries/bash/textobjects.scm+9 runtime/queries/blueprint/highlights.scm+5 runtime/queries/c/indents.scm+18−1 runtime/queries/comment/highlights.scm+8−8 runtime/queries/common-lisp/indents.scm+1 runtime/queries/dart/highlights.scm+3−1 runtime/queries/dbml/highlights.scm+44 runtime/queries/ecma/highlights.scm+1−1 runtime/queries/fsharp/locals.scm+25 runtime/queries/gas/highlights.scm+21 runtime/queries/gas/injections.scm+2 runtime/queries/gas/textobjects.scm+2 runtime/queries/gemini/highlights.scm+26 runtime/queries/gleam/highlights.scm+16−5 runtime/queries/gn/highlights.scm+42 runtime/queries/gn/injections.scm+15 runtime/queries/gn/locals.scm+16 runtime/queries/go/highlights.scm+13−5 runtime/queries/go/locals.scm+10−2 runtime/queries/hare/highlights.scm+4−1 runtime/queries/haskell/highlights.scm+24−11 runtime/queries/janet/highlights.scm+1 runtime/queries/java/indents.scm+35 runtime/queries/jinja/highlights.scm+7 runtime/queries/jinja/injections.scm+4 runtime/queries/json5/highlights.scm+11 runtime/queries/kdl/highlights.scm+4−4 runtime/queries/kdl/indents.scm+3 runtime/queries/kdl/textobjects.scm+27 runtime/queries/kotlin/highlights.scm+4−1 runtime/queries/kotlin/locals.scm+15 runtime/queries/log/highlights.scm+10 runtime/queries/lpf/highlights.scm+19 runtime/queries/markdown.inline/injections.scm+4−1 runtime/queries/nix/injections.scm+14 runtime/queries/nu/folds.scm−4 runtime/queries/nu/highlights.scm+276−44 runtime/queries/nu/injections.scm+1−1 runtime/queries/nu/locals.scm−13 runtime/queries/nunjucks/highlights.scm+1 runtime/queries/nunjucks/injections.scm+1 runtime/queries/ocaml/highlights.scm+2 runtime/queries/perl/highlights.scm−2 runtime/queries/perl/indents.scm+29 runtime/queries/perl/injections.scm+2 runtime/queries/perl/textobjects.scm+14 runtime/queries/pod/highlights.scm+61 runtime/queries/prql/highlights.scm+19−12 runtime/queries/purescript/highlights.scm+164−1 runtime/queries/purescript/injections.scm+2−1 runtime/queries/purescript/locals.scm+4−1 runtime/queries/purescript/textobjects.scm+13 runtime/queries/python/highlights.scm+6 runtime/queries/python/indents.scm+9 runtime/queries/racket/indents.scm+1 runtime/queries/regex/highlights.scm+1−1 runtime/queries/robot/highlights.scm+51−13 runtime/queries/ron/highlights.scm+43−1 runtime/queries/ron/indents.scm+12−1 runtime/queries/ron/injections.scm+2−1 runtime/queries/rust/highlights.scm+7 runtime/queries/rust/indents.scm+69−2 runtime/queries/rust/injections.scm+6 runtime/queries/rust/locals.scm+14−1 runtime/queries/rust/textobjects.scm+4−1 runtime/queries/scheme/highlights.scm+5 runtime/queries/scheme/indents.scm+43 runtime/queries/slint/highlights.scm+118−72 runtime/queries/slint/indents.scm+6−5 runtime/queries/slint/injections.scm+2 runtime/queries/slint/locals.scm+1−1 runtime/queries/smali/folds.scm+12 runtime/queries/smali/highlights.scm+216 runtime/queries/smali/indents.scm+31 runtime/queries/smali/injections.scm+2 runtime/queries/smali/locals.scm+42 runtime/queries/sql/highlights.scm+317−231 runtime/queries/strace/highlights.scm+8 runtime/queries/t32/highlights.scm+108−26 runtime/queries/templ/highlights.scm+99 runtime/queries/templ/injections.scm+4 runtime/queries/todotxt/highlights.scm+6 runtime/queries/typst/highlights.scm+77 runtime/queries/typst/injections.scm+6 runtime/queries/unison/highlights.scm+87 runtime/queries/unison/injections.scm+1 runtime/queries/v/highlights.scm+85−51 runtime/queries/v/indents.scm+2−3 runtime/queries/v/textobjects.scm+3−3 runtime/queries/wren/highlights.scm+82 runtime/queries/wren/indents.scm+29 runtime/queries/wren/injections.scm+3 runtime/queries/wren/locals.scm+21 runtime/queries/wren/textobjects.scm+16 runtime/queries/yaml/indents.scm+35−1 runtime/queries/yaml/injections.scm+52 runtime/queries/yuck/highlights.scm+1−1 runtime/queries/zig/highlights.scm+5−2 runtime/queries/zig/textobjects.scm+3−3 runtime/themes/README.md+8 runtime/themes/autumn.toml+62−65 runtime/themes/autumn_night.toml+6−6 runtime/themes/catppuccin_frappe.toml+2 runtime/themes/catppuccin_latte.toml+2 runtime/themes/catppuccin_macchiato.toml+2 runtime/themes/catppuccin_mocha.toml+11 runtime/themes/cyan_light.toml+154 runtime/themes/darcula-solid.toml+1−4 runtime/themes/darcula.toml+10−4 runtime/themes/dracula.toml+2 runtime/themes/everblush.toml+1 runtime/themes/everforest_dark.toml+4 runtime/themes/gruber-darker.toml+114 runtime/themes/gruvbox.toml+1 runtime/themes/gruvbox_dark_hard.toml−2 runtime/themes/gruvbox_light.toml−2 runtime/themes/horizon-dark.toml+80 runtime/themes/jellybeans.toml+1−1 runtime/themes/kaolin-dark.toml+108 runtime/themes/kaolin-light.toml+60 runtime/themes/kaolin-valley-dark.toml+73 runtime/themes/licenses/everforest.LICENSE+21 runtime/themes/licenses/modus_vivendi.LICENSE+5 runtime/themes/licenses/modus_vivendi_deuteranopia.LICENSE+5 runtime/themes/licenses/modus_vivendi_tinted.LICENSE+5 runtime/themes/licenses/modus_vivendi_tritanopia.LICENSE+5 runtime/themes/licenses/poimandres.LICENSE+21 runtime/themes/licenses/serika-syntax.LICENSE+21 runtime/themes/licenses/sonokai.LICENSE+21 runtime/themes/licenses/starlight.LICENSE+24 runtime/themes/material_darker.toml+20 runtime/themes/material_deep_ocean.toml+123 runtime/themes/material_oceanic.toml+21 runtime/themes/material_palenight.toml+19 runtime/themes/modus_operandi.toml+194 runtime/themes/modus_operandi_deuteranopia.toml+46 runtime/themes/modus_operandi_tinted.toml+45 runtime/themes/modus_operandi_tritanopia.toml+73 runtime/themes/modus_vivendi.toml+242 runtime/themes/modus_vivendi_deuteranopia.toml+158 runtime/themes/modus_vivendi_tinted.toml+144 runtime/themes/modus_vivendi_tritanopia.toml+186 runtime/themes/molokai.toml+1−1 runtime/themes/monokai.toml+5 runtime/themes/naysayer.toml+2−2 runtime/themes/new_moon.toml+124 runtime/themes/nightfox.toml+2−2 runtime/themes/noctis.toml+26−26 runtime/themes/nord-night.toml+23 runtime/themes/nord.toml+23−13 runtime/themes/onedark.toml+1 runtime/themes/papercolor-dark.toml+67−116 runtime/themes/papercolor-light.toml+194−103 runtime/themes/poimandres.toml+113 runtime/themes/poimandres_storm.toml+19 runtime/themes/pop-dark.toml+4−4 runtime/themes/rose_pine.toml+31−29 runtime/themes/starlight.toml+113 runtime/themes/vim_dark_high_contrast.toml+72 runtime/themes/yellowed.toml+135 runtime/themes/zenburn.toml+1−1 runtime/tutor+157−3 rust-toolchain.toml+2−2 xtask/Cargo.toml+12−6
.envrc+1
@@ -1,5 +1,6 @@
11watch_file shell.nix
22watch_file flake.lock
3+watch_file rust-toolchain.toml
34
45# try to use flakes, if it fails use normal nix (ie. shell.nix)
56use flake || use nix
.github/ISSUE_TEMPLATE/bug_report.yaml+10
@@ -55,6 +55,16 @@ body:
5555 placeholder: wezterm 20220101-133340-7edc5b5a
5656 validations:
5757 required: true
58+ - type: input
59+ id: installation-method
60+ attributes:
61+ label: Installation Method
62+ description: >
63+ How you installed Helix - from a package manager like Homebrew or the
64+ AUR, built from source, downloaded a binary from the releases page, etc.
65+ placeholder: "source / brew / nixpkgs / flake / releases page"
66+ validations:
67+ required: true
5868 - type: input
5969 id: helix-version
6070 attributes:
.github/workflows/build.yml+15−7
@@ -14,7 +14,7 @@ jobs:
1414 runs-on: ubuntu-latest
1515 steps:
1616 - name: Checkout sources
17− uses: actions/checkout@v3
17+ uses: actions/checkout@v4
1818 - name: Install stable toolchain
1919 uses: helix-editor/rust-toolchain@v1
2020 with:
@@ -22,6 +22,8 @@ jobs:
2222 override: true
2323
2424 - uses: Swatinem/rust-cache@v2
25+ with:
26+ shared-key: "build"
2527
2628 - name: Run cargo check
2729 run: cargo check
@@ -34,12 +36,14 @@ jobs:
3436 HELIX_LOG_LEVEL: info
3537 steps:
3638 - name: Checkout sources
37− uses: actions/checkout@v3
39+ uses: actions/checkout@v4
3840
3941 - name: Install stable toolchain
40− uses: dtolnay/rust-toolchain@1.65
42+ uses: dtolnay/rust-toolchain@1.70
4143
4244 - uses: Swatinem/rust-cache@v2
45+ with:
46+ shared-key: "build"
4347
4448 - name: Cache test tree-sitter grammar
4549 uses: actions/cache@v3
@@ -63,14 +67,16 @@ jobs:
6367 runs-on: ubuntu-latest
6468 steps:
6569 - name: Checkout sources
66− uses: actions/checkout@v3
70+ uses: actions/checkout@v4
6771
6872 - name: Install stable toolchain
69− uses: dtolnay/rust-toolchain@1.65
73+ uses: dtolnay/rust-toolchain@1.70
7074 with:
7175 components: rustfmt, clippy
7276
7377 - uses: Swatinem/rust-cache@v2
78+ with:
79+ shared-key: "build"
7480
7581 - name: Run cargo fmt
7682 run: cargo fmt --all --check
@@ -88,12 +94,14 @@ jobs:
8894 runs-on: ubuntu-latest
8995 steps:
9096 - name: Checkout sources
91− uses: actions/checkout@v3
97+ uses: actions/checkout@v4
9298
9399 - name: Install stable toolchain
94− uses: dtolnay/rust-toolchain@1.65
100+ uses: dtolnay/rust-toolchain@1.70
95101
96102 - uses: Swatinem/rust-cache@v2
103+ with:
104+ shared-key: "build"
97105
98106 - name: Validate queries
99107 run: cargo xtask query-check
.github/workflows/cachix.yml+3−3
@@ -11,13 +11,13 @@ jobs:
1111 runs-on: ubuntu-latest
1212 steps:
1313 - name: Checkout sources
14− uses: actions/checkout@v3
14+ uses: actions/checkout@v4
1515
1616 - name: Install nix
17− uses: cachix/install-nix-action@v22
17+ uses: cachix/install-nix-action@v24
1818
1919 - name: Authenticate with Cachix
20− uses: cachix/cachix-action@v12
20+ uses: cachix/cachix-action@v13
2121 with:
2222 name: helix
2323 authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
.github/workflows/gh-pages.yml+5−5
@@ -11,7 +11,7 @@ jobs:
1111 deploy:
1212 runs-on: ubuntu-latest
1313 steps:
14− - uses: actions/checkout@v3
14+ - uses: actions/checkout@v4
1515
1616 - name: Setup mdBook
1717 uses: peaceiris/actions-mdbook@v1
@@ -26,16 +26,16 @@ jobs:
2626 OUTDIR=$(basename ${{ github.ref }})
2727 echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
2828
29− - name: Deploy
29+ - name: Deploy stable
3030 uses: peaceiris/actions-gh-pages@v3
31+ if: startswith(github.ref, 'refs/tags/')
3132 with:
3233 github_token: ${{ secrets.GITHUB_TOKEN }}
3334 publish_dir: ./book/book
34− destination_dir: ./${{ env.OUTDIR }}
3535
36− - name: Deploy stable
36+ - name: Deploy
3737 uses: peaceiris/actions-gh-pages@v3
38− if: startswith(github.ref, 'refs/tags/')
3938 with:
4039 github_token: ${{ secrets.GITHUB_TOKEN }}
4140 publish_dir: ./book/book
41+ destination_dir: ./${{ env.OUTDIR }}
.github/workflows/release.yml+15−15
@@ -23,7 +23,7 @@ jobs:
2323 runs-on: ubuntu-latest
2424 steps:
2525 - name: Checkout sources
26− uses: actions/checkout@v3
26+ uses: actions/checkout@v4
2727
2828 - name: Install stable toolchain
2929 uses: dtolnay/rust-toolchain@stable
@@ -36,7 +36,7 @@ jobs:
3636 - name: Bundle grammars
3737 run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
3838
39− - uses: actions/upload-artifact@v3
39+ - uses: actions/upload-artifact@v4
4040 with:
4141 name: grammars
4242 path: grammars.tar.xz
@@ -70,11 +70,11 @@ jobs:
7070 rust: stable
7171 target: aarch64-unknown-linux-gnu
7272 cross: true
73− - build: riscv64-linux
74− os: ubuntu-latest
75− rust: stable
76− target: riscv64gc-unknown-linux-gnu
77− cross: true
73+ # - build: riscv64-linux
74+ # os: ubuntu-latest
75+ # rust: stable
76+ # target: riscv64gc-unknown-linux-gnu
77+ # cross: true
7878 - build: x86_64-macos
7979 os: macos-latest
8080 rust: stable
@@ -103,10 +103,10 @@ jobs:
103103
104104 steps:
105105 - name: Checkout sources
106− uses: actions/checkout@v3
106+ uses: actions/checkout@v4
107107
108108 - name: Download grammars
109− uses: actions/download-artifact@v3
109+ uses: actions/download-artifact@v4
110110
111111 - name: Move grammars under runtime
112112 if: "!startsWith(matrix.os, 'windows')"
@@ -160,7 +160,7 @@ jobs:
160160
161161 - name: Build AppImage
162162 shell: bash
163− if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
163+ if: matrix.build == 'x86_64-linux'
164164 run: |
165165 # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
166166 sudo add-apt-repository universe
@@ -220,7 +220,7 @@ jobs:
220220 fi
221221 cp -r runtime dist
222222
223− - uses: actions/upload-artifact@v3
223+ - uses: actions/upload-artifact@v4
224224 with:
225225 name: bins-${{ matrix.build }}
226226 path: dist
@@ -231,9 +231,9 @@ jobs:
231231 runs-on: ubuntu-latest
232232 steps:
233233 - name: Checkout sources
234− uses: actions/checkout@v3
234+ uses: actions/checkout@v4
235235
236− - uses: actions/download-artifact@v3
236+ - uses: actions/download-artifact@v4
237237
238238 - name: Build archive
239239 shell: bash
@@ -263,7 +263,7 @@ jobs:
263263 mv bins-$platform/hx$exe $pkgname
264264 chmod +x $pkgname/hx$exe
265265
266− if [[ "$platform" = "aarch64-linux" || "$platform" = "x86_64-linux" ]]; then
266+ if [[ "$platform" = "x86_64-linux" ]]; then
267267 mv bins-$platform/helix-*.AppImage* dist/
268268 fi
269269
@@ -288,7 +288,7 @@ jobs:
288288 overwrite: true
289289
290290 - name: Upload binaries as artifact
291− uses: actions/upload-artifact@v3
291+ uses: actions/upload-artifact@v4
292292 if: env.preview == 'true'
293293 with:
294294 name: release
CHANGELOG.md+260
@@ -1,3 +1,263 @@
1+# 23.10 (2023-10-24)
2+
3+A big shout out to all the contributors! We had 118 contributors in this release.
4+
5+Breaking changes:
6+
7+- Support multiple language servers per language ([#2507](https://github.com/helix-editor/helix/pull/2507))
8+ - This is a breaking change to language configuration
9+
10+Features:
11+
12+- Support multiple language servers per language ([#2507](https://github.com/helix-editor/helix/pull/2507), [#7082](https://github.com/helix-editor/helix/pull/7082), [#7286](https://github.com/helix-editor/helix/pull/7286), [#8374](https://github.com/helix-editor/helix/pull/8374))
13+- Add a statusline element for the selected register ([#7222](https://github.com/helix-editor/helix/pull/7222))
14+- Add `%`, `#`, `.`, `*` and `+` special registers ([#6985](https://github.com/helix-editor/helix/pull/6985))
15+- Add initial support for LSP DidChangeWatchedFiles notifications ([#7665](https://github.com/helix-editor/helix/pull/7665))
16+- Search buffer contents in `global_search` ([#5652](https://github.com/helix-editor/helix/pull/5652))
17+- Add a "smart tab" command that intelligently jumps the cursor on tab ([#4443](https://github.com/helix-editor/helix/pull/4443))
18+- Add a statusline element for whether a file is read-only ([#7222](https://github.com/helix-editor/helix/pull/7222), [#7875](https://github.com/helix-editor/helix/pull/7875))
19+- Syntax highlight regex prompts ([#7738](https://github.com/helix-editor/helix/pull/7738))
20+- Allow defining alignment in indent queries ([#5355](https://github.com/helix-editor/helix/pull/5355))
21+- Show visual feedback in `surround_replace` ([#7588](https://github.com/helix-editor/helix/pull/7588))
22+- Switch to Nucleo for fuzzy matching ([#7814](https://github.com/helix-editor/helix/pull/7814), [#8148](https://github.com/helix-editor/helix/pull/8148), [#8192](https://github.com/helix-editor/helix/pull/8192), [#8194](https://github.com/helix-editor/helix/pull/8194))
23+- Insert a trailing newline on write ([#8157](https://github.com/helix-editor/helix/pull/8157))
24+- Add a `-w`/`--working-dir` CLI flag for specifying a working directory on startup ([#8223](https://github.com/helix-editor/helix/pull/8223), [#8498](https://github.com/helix-editor/helix/pull/8498), [#8520](https://github.com/helix-editor/helix/pull/8520))
25+- Accept a `+N` CLI argument to set the first file's line number ([#8521](https://github.com/helix-editor/helix/pull/8521))
26+- Accept Helix-specific ignore files in `.helix/ignore` and `~/.config/helix/ignore` ([#8099](https://github.com/helix-editor/helix/pull/8099))
27+
28+Commands:
29+
30+- `merge_selections` (`A-minus`) - merge all selections into one selection that covers all ranges ([#7053](https://github.com/helix-editor/helix/pull/7053))
31+- `move_prev_long_word_end` and `extend_prev_long_word_end` - move/extend to the end of the previous WORD ([#6905](https://github.com/helix-editor/helix/pull/6905))
32+- `reverse_selection_contents` - swaps the values of each selection so they are reversed ([#7329](https://github.com/helix-editor/helix/pull/7329))
33+- Add `:rl` and `:rla` aliases for `:reload` and `:reload-all` ([#7158](https://github.com/helix-editor/helix/pull/7158))
34+- `yank_joined` - join the selections and yank to the selected register ([#7195](https://github.com/helix-editor/helix/pull/7195))
35+- `:write-all!` (`:wa!`) - forcibly write all buffers to disk and create any necessary subdirectories ([#7577](https://github.com/helix-editor/helix/pull/7577))
36+- `:redraw` - clear re-render the UI ([#6949](https://github.com/helix-editor/helix/pull/6949))
37+- `:tree-sitter-highlight-name` - show the theme scope name of the highlight under the cursor ([#8170](https://github.com/helix-editor/helix/pull/8170))
38+
39+Usability improvements:
40+
41+- Allow cycling option values at runtime ([#4411](https://github.com/helix-editor/helix/pull/4411), [#7240](https://github.com/helix-editor/helix/pull/7240), [#7877](https://github.com/helix-editor/helix/pull/7877))
42+- Exit gracefully on termination signals ([#7236](https://github.com/helix-editor/helix/pull/7236))
43+- Add plaintext matching fallback to tree-sitter pair matching ([#4288](https://github.com/helix-editor/helix/pull/4288))
44+- Persist register selection in pending keymaps ([0e08349](https://github.com/helix-editor/helix/commit/0e08349))
45+- Propagate the count and register to command palette commands ([b394997](https://github.com/helix-editor/helix/commit/b394997))
46+- Auto indent on `insert_at_line_start` ([#5837](https://github.com/helix-editor/helix/pull/5837))
47+- Add a config option to control whether LSP completions are automatically inserted on preview ([#7189](https://github.com/helix-editor/helix/pull/7189))
48+- Add a config option for default line endings ([#5621](https://github.com/helix-editor/helix/pull/5621), [#7357](https://github.com/helix-editor/helix/pull/7357))
49+- Allow ANSI colors in themes ([#5119](https://github.com/helix-editor/helix/pull/5119))
50+- Match pairs that don't form a standalone tree-sitter node ([#7242](https://github.com/helix-editor/helix/pull/7242))
51+- Allow indent sizes of up to 16 columns ([#7429](https://github.com/helix-editor/helix/pull/7429))
52+- Improve performance of mapping positions through changes ([#7408](https://github.com/helix-editor/helix/pull/7408), [8d39a81](https://github.com/helix-editor/helix/commit/8d39a81), [#7471](https://github.com/helix-editor/helix/pull/7471))
53+- Mark buffers created from stdin as modified ([#7431](https://github.com/helix-editor/helix/pull/7431))
54+- Forcibly shut down uninitialized language servers ([#7449](https://github.com/helix-editor/helix/pull/7449))
55+- Add filename completer for shell prompts ([#7569](https://github.com/helix-editor/helix/pull/7569))
56+- Allow binding F13-F24 ([#7672](https://github.com/helix-editor/helix/pull/7672))
57+- Resolve LSP code actions ([#7677](https://github.com/helix-editor/helix/pull/7677), [#8421](https://github.com/helix-editor/helix/pull/8421))
58+- Save an undo checkpoint before accepting completions ([#7747](https://github.com/helix-editor/helix/pull/7747))
59+- Include gitignored files in debugger completions ([#7936](https://github.com/helix-editor/helix/pull/7936))
60+- Make editor remember the last search register ([#5244](https://github.com/helix-editor/helix/pull/5244))
61+- Open directories with `goto_file` ([#7909](https://github.com/helix-editor/helix/pull/7909))
62+- Use relative path to open buffer in `goto_file` (`gf`) ([#7965](https://github.com/helix-editor/helix/pull/7965))
63+- Support `default` color in themes ([#8083](https://github.com/helix-editor/helix/pull/8083), [#8114](https://github.com/helix-editor/helix/pull/8114))
64+- Toggle between relative and absolute line numbers when the terminal loses focus ([#7955](https://github.com/helix-editor/helix/pull/7955))
65+- Lower default idle-timeout to 250ms ([060e73a](https://github.com/helix-editor/helix/commit/060e73a))
66+- Allow theming diff gutters separately from other diff colors ([#8343](https://github.com/helix-editor/helix/pull/8343))
67+- Style bold/italic/strikethrough in markdown doc popups ([#8385](https://github.com/helix-editor/helix/pull/8385))
68+- Maintain the cursor position and view when splitting with `:hsplit`/`:vsplit` ([#8109](https://github.com/helix-editor/helix/pull/8109))
69+- Accept `-` in macros outside of `<`/`>` ([#8475](https://github.com/helix-editor/helix/pull/8475))
70+- Show all language servers for each language in `--health` ([#7315](https://github.com/helix-editor/helix/pull/7315))
71+- Don't break on hyphens in `:reflow` ([#8569](https://github.com/helix-editor/helix/pull/8569))
72+
73+Fixes:
74+
75+- Update diagnostics correctly on language server exit ([#7111](https://github.com/helix-editor/helix/pull/7111))
76+- Fix off-by-one in `select_references_to_symbol_under_cursor` ([#7132](https://github.com/helix-editor/helix/pull/7132))
77+- Extend selection with repeat-last-motion only if the original motion extended the selection ([#7159](https://github.com/helix-editor/helix/pull/7159))
78+- Fix undefined behavior in the diff gutter ([#7227](https://github.com/helix-editor/helix/pull/7227))
79+- Check that tab width is non-zero ([#7178](https://github.com/helix-editor/helix/pull/7178))
80+- Fix styles being overwritten in table rows with multiple cells ([#7281](https://github.com/helix-editor/helix/pull/7281))
81+- Add file for `--log` CLI arg in help text ([#7307](https://github.com/helix-editor/helix/pull/7307))
82+- Fix underflow when repeating a completion that has a negative shift position ([#7322](https://github.com/helix-editor/helix/pull/7322))
83+- Prefer longer matches in `select_next_sibling` and `select_prev_sibling` ([#7332](https://github.com/helix-editor/helix/pull/7332))
84+- Preview scratch buffers in the jumplist picker ([#7331](https://github.com/helix-editor/helix/pull/7331))
85+- Fix chunking by bytes in tree-sitter parsing ([#7417](https://github.com/helix-editor/helix/pull/7417))
86+- Discard LSP publishDiagnostic from uninitialized servers ([#7467](https://github.com/helix-editor/helix/pull/7467))
87+- Use negotiated position encoding for LSP workspace edits ([#7469](https://github.com/helix-editor/helix/pull/7469))
88+- Fix error message for unknown gutter types in config ([#7534](https://github.com/helix-editor/helix/pull/7534))
89+- Fix `:log-open` when `--log` CLI arg is specified ([#7573](https://github.com/helix-editor/helix/pull/7573), [#7585](https://github.com/helix-editor/helix/pull/7585))
90+- Fix debouncing of LSP messages to fix the last message sticking around ([#7538](https://github.com/helix-editor/helix/pull/7538), [#8023](https://github.com/helix-editor/helix/pull/8023))
91+- Fix crash when the current working directory is deleted ([#7185](https://github.com/helix-editor/helix/pull/7185))
92+- Fix piping to Helix on macOS ([#5468](https://github.com/helix-editor/helix/pull/5468))
93+- Fix crash when parsing overlapping injections ([#7621](https://github.com/helix-editor/helix/pull/7621))
94+- Clear the statusline when the prompt is visible ([#7646](https://github.com/helix-editor/helix/pull/7646))
95+- Fix range formatting error message typo ([#7823](https://github.com/helix-editor/helix/pull/7823))
96+- Skip rendering gutters when gutter width exceeds view width ([#7821](https://github.com/helix-editor/helix/pull/7821))
97+- Center the picker preview using visual lines ([#7837](https://github.com/helix-editor/helix/pull/7837))
98+- Align view correctly for background buffers opened with `A-ret` ([#7691](https://github.com/helix-editor/helix/pull/7691))
99+- Fix cursor resetting to block when quitting via a keybind ([#7931](https://github.com/helix-editor/helix/pull/7931))
100+- Remove path completions for the `:new` command ([#8010](https://github.com/helix-editor/helix/pull/8010))
101+- Use binary path resolved by `which` for formatter commands ([#8064](https://github.com/helix-editor/helix/pull/8064))
102+- Handle crossterm's `hidden` modifier ([#8120](https://github.com/helix-editor/helix/pull/8120))
103+- Clear completion when switching between windows with the mouse ([#8118](https://github.com/helix-editor/helix/pull/8118))
104+- Eagerly remove the last picker (`<space>'`) when the picker has many items ([#8127](https://github.com/helix-editor/helix/pull/8127))
105+- Fix find commands for buffers with non-LF line-endings ([#8111](https://github.com/helix-editor/helix/pull/8111))
106+- Detect the tmux clipboard provider on macOS ([#8182](https://github.com/helix-editor/helix/pull/8182))
107+- Fix syntax highlighting in dynamic picker preview pane ([#8206](https://github.com/helix-editor/helix/pull/8206))
108+- Recognize HTML code tags with attributes as code in markdown previews ([#8397](https://github.com/helix-editor/helix/pull/8397))
109+- Fix multicursor snippet placeholder directions ([#8423](https://github.com/helix-editor/helix/pull/8423))
110+- Only show diagnostic highlights when diagnostics are enabled for a language server ([#8551](https://github.com/helix-editor/helix/pull/8551))
111+
112+Themes:
113+
114+- Improve the selection color in `ferra` ([#7138](https://github.com/helix-editor/helix/pull/7138))
115+- Add `variable.other.member` theming to `spacebones_light` ([#7125](https://github.com/helix-editor/helix/pull/7125))
116+- Update `autumn` and theme the soft-wrap indicator ([#7229](https://github.com/helix-editor/helix/pull/7229))
117+- Add `gruvbox_dark_soft` ([#7139](https://github.com/helix-editor/helix/pull/7139))
118+- Add `merionette` ([#7186](https://github.com/helix-editor/helix/pull/7186))
119+- Add `zed_onedark` and `zed_onelight` ([#7250](https://github.com/helix-editor/helix/pull/7250))
120+- Use light-gray for `onedarker` inlay hint theming ([#7433](https://github.com/helix-editor/helix/pull/7433))
121+- Update the Nord theme to follow the style guidelines ([#7490](https://github.com/helix-editor/helix/pull/7490))
122+- Tune `dark_plus` inlay hint colors ([#7611](https://github.com/helix-editor/helix/pull/7611))
123+- Add `naysayer` ([#7570](https://github.com/helix-editor/helix/pull/7570))
124+- Add `kaolin-dark`, `kaolin-light` and `kaolin-valley-dark` ([#7151](https://github.com/helix-editor/helix/pull/7151))
125+- Fix selection highlighting in gruvbox variants ([#7717](https://github.com/helix-editor/helix/pull/7717))
126+- Add soft-wrap indicator to `gruvbox` ([#7736](https://github.com/helix-editor/helix/pull/7736))
127+- Add missing palette definitions in `everforest_dark` ([#7739](https://github.com/helix-editor/helix/pull/7739))
128+- Increase diagnostics clarity in `pop-dark` ([#7702](https://github.com/helix-editor/helix/pull/7702))
129+- Add `vim_dark_high_contrast` ([#7785](https://github.com/helix-editor/helix/pull/7785))
130+- Add `new_moon` ([#7834](https://github.com/helix-editor/helix/pull/7834))
131+- Add `yellowed` ([#7849](https://github.com/helix-editor/helix/pull/7849))
132+- Improve comment readability for `autumn` ([#7939](https://github.com/helix-editor/helix/pull/7939))
133+- Distinguish active bufferline buffer in `monokai` ([#7983](https://github.com/helix-editor/helix/pull/7983))
134+- Update ruler colors in `nord` ([#7995](https://github.com/helix-editor/helix/pull/7995))
135+- Update Catppuccin themes ([#8102](https://github.com/helix-editor/helix/pull/8102))
136+- Add text focus scope and diagnostics undercurls for `nord` ([#8165](https://github.com/helix-editor/helix/pull/8165))
137+- Add material theme collection ([#8211](https://github.com/helix-editor/helix/pull/8211))
138+- Improve indent line color in `dracula` ([#8266](https://github.com/helix-editor/helix/pull/8266))
139+- Clean up and refactor `papercolor` to use inheritance ([#8276](https://github.com/helix-editor/helix/pull/8276))
140+- Fix `zenburn` inlay hint color ([#8278](https://github.com/helix-editor/helix/pull/8278)a)
141+- Fix picker crash when previewing an invalid range ([e9d0bd7](https://github.com/helix-editor/helix/commit/e9d0bd7))
142+- Correctly center items in the picker preview ([13d4463](https://github.com/helix-editor/helix/commit/13d4463))
143+- Add `cyan_light` ([#8293](https://github.com/helix-editor/helix/pull/8293), [#8587](https://github.com/helix-editor/helix/pull/8587))
144+- Theme HTML tags in `onedark` ([#8409](https://github.com/helix-editor/helix/pull/8409))
145+- Refine `darcula` and `darcula-solid` themes ([#8412](https://github.com/helix-editor/helix/pull/8412))
146+- Improve `nord` highlights ([#8414](https://github.com/helix-editor/helix/pull/8414))
147+- Add `nord-night` ([#8549](https://github.com/helix-editor/helix/pull/8549))
148+
149+New languages:
150+
151+- Blueprint ([#7213](https://github.com/helix-editor/helix/pull/7213), [#8161](https://github.com/helix-editor/helix/pull/8161))
152+- Forth ([#7256](https://github.com/helix-editor/helix/pull/7256), [#7334](https://github.com/helix-editor/helix/pull/7334))
153+- t32 ([#7140](https://github.com/helix-editor/helix/pull/7140), [#7811](https://github.com/helix-editor/helix/pull/7811))
154+- WebC ([#7290](https://github.com/helix-editor/helix/pull/7290))
155+- Persistent DSL for Haskell ([#7261](https://github.com/helix-editor/helix/pull/7261))
156+- F# ([#7619](https://github.com/helix-editor/helix/pull/7619), [#8024](https://github.com/helix-editor/helix/pull/8024))
157+- Wren ([#7765](https://github.com/helix-editor/helix/pull/7765), [#7819](https://github.com/helix-editor/helix/pull/7819))
158+- Unison ([#7724](https://github.com/helix-editor/helix/pull/7724))
159+- Todo.txt ([#7835](https://github.com/helix-editor/helix/pull/7835))
160+- Jinja and Handlebars ([#7233](https://github.com/helix-editor/helix/pull/7233))
161+- Pod ([#7907](https://github.com/helix-editor/helix/pull/7907))
162+- Strace ([#7928](https://github.com/helix-editor/helix/pull/7928))
163+- Gemini ([#8070](https://github.com/helix-editor/helix/pull/8070))
164+- GNU Assembler (GAS) ([#8291](https://github.com/helix-editor/helix/pull/8291))
165+- JSON5 ([#8473](https://github.com/helix-editor/helix/pull/8473))
166+- TEMPL ([#8540](https://github.com/helix-editor/helix/pull/8540))
167+
168+Updated languages and queries:
169+
170+- Add one to the ruler numbers for git-commit ([#7072](https://github.com/helix-editor/helix/pull/7072))
171+- Recognize XAML files as XML ([#7083](https://github.com/helix-editor/helix/pull/7083))
172+- Recognize `Cargo.lock` as TOML ([#7095](https://github.com/helix-editor/helix/pull/7095))
173+- Use Rust grammar for Cairo ([c6d1430](https://github.com/helix-editor/helix/commit/c6d1430))
174+- Update tree-sitter-nickel ([#7059](https://github.com/helix-editor/helix/pull/7059), [#7551](https://github.com/helix-editor/helix/pull/7551))
175+- Tune auto-pair characters for Nickel ([#7059](https://github.com/helix-editor/helix/pull/7059))
176+- Recognize `Vagrantfile` as Ruby ([#7112](https://github.com/helix-editor/helix/pull/7112))
177+- Recognize hidden justfiles as Just ([#7088](https://github.com/helix-editor/helix/pull/7088))
178+- Update Java and TypeScript highlight queries ([#7145](https://github.com/helix-editor/helix/pull/7145))
179+- Recognize `.zimrc` as Bash ([#7146](https://github.com/helix-editor/helix/pull/7146))
180+- Recognize `.gir` as XML ([#7152](https://github.com/helix-editor/helix/pull/7152))
181+- Update tree-sitter-scala ([#7147](https://github.com/helix-editor/helix/pull/7147))
182+- Recognize make file-type as Makefile ([#7212](https://github.com/helix-editor/helix/pull/7212))
183+- Update tree-sitter-verilog ([#7262](https://github.com/helix-editor/helix/pull/7262))
184+- Update tree-sitter-cpp ([#7285](https://github.com/helix-editor/helix/pull/7285))
185+- Support core mode for delve debugger ([#7300](https://github.com/helix-editor/helix/pull/7300))
186+- Add Fortran comment injections ([#7305](https://github.com/helix-editor/helix/pull/7305))
187+- Switch Vue language server to `vue-language-server` ([#7312](https://github.com/helix-editor/helix/pull/7312))
188+- Update tree-sitter-sql ([#7387](https://github.com/helix-editor/helix/pull/7387), [#8464](https://github.com/helix-editor/helix/pull/8464))
189+- 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))
190+- Highlight TOML table headers ([#7441](https://github.com/helix-editor/helix/pull/7441))
191+- Recognize `cppm` file-type as C++ ([#7492](https://github.com/helix-editor/helix/pull/7492))
192+- Refactor ecma language queries into private and public queries ([#7207](https://github.com/helix-editor/helix/pull/7207))
193+- Update tree-sitter-dart ([#7576](https://github.com/helix-editor/helix/pull/7576))
194+- Add shebang for nushell files ([#7606](https://github.com/helix-editor/helix/pull/7606))
195+- Recognize systemd files as INI ([#7592](https://github.com/helix-editor/helix/pull/7592))
196+- Update TypeScript, TSX and Svelte grammars ([#6874](https://github.com/helix-editor/helix/pull/6874))
197+- Enable inlay hints in the Svelte language server ([#7622](https://github.com/helix-editor/helix/pull/7622))
198+- Recognize `Brewfile`s as Ruby ([#7629](https://github.com/helix-editor/helix/pull/7629))
199+- Add more file-types for R ([#7633](https://github.com/helix-editor/helix/pull/7633))
200+- Switch tree-sitter-perl to official upstream parser ([#7644](https://github.com/helix-editor/helix/pull/7644), [#7947](https://github.com/helix-editor/helix/pull/7947))
201+- Fix predicate typo in comment highlights ([#7732](https://github.com/helix-editor/helix/pull/7732))
202+- Update tree-sitter-prql ([#7771](https://github.com/helix-editor/helix/pull/7771))
203+- Recognize `.gitf` as JSON ([#7781](https://github.com/helix-editor/helix/pull/7781))
204+- Switch V language server to `v-analyzer` ([#7760](https://github.com/helix-editor/helix/pull/7760))
205+- Add protobuf language servers ([#7796](https://github.com/helix-editor/helix/pull/7796))
206+- Update tree-sitter-zig ([#7803](https://github.com/helix-editor/helix/pull/7803))
207+- Update tree-sitter-hare ([#7784](https://github.com/helix-editor/helix/pull/7784))
208+- Add Java indent queries ([#7844](https://github.com/helix-editor/helix/pull/7844))
209+- Update tree-sitter-scheme ([979933b](https://github.com/helix-editor/helix/commit/979933b))
210+- Recognize `scm` as Scheme instead of TSQ ([5707151](https://github.com/helix-editor/helix/commit/5707151))
211+- Update tree-sitter-git-commit ([#7831](https://github.com/helix-editor/helix/pull/7831))
212+- Update JavaScript, TypeScript and TSX grammars ([#7852](https://github.com/helix-editor/helix/pull/7852))
213+- Update tree-sitter-nu ([#7873](https://github.com/helix-editor/helix/pull/7873))
214+- Fix YAML indentation ([#6768](https://github.com/helix-editor/helix/pull/6768))
215+- Add `csharp-ls`, Pyright, Pylyzer and add roots for Python ([#7897](https://github.com/helix-editor/helix/pull/7897), [#8032](https://github.com/helix-editor/helix/pull/8032))
216+- Update tree-sitter-slint ([#7893](https://github.com/helix-editor/helix/pull/7893))
217+- Recognize more ZSH file-types as Bash ([#7930](https://github.com/helix-editor/helix/pull/7930))
218+- Recognize `star` extension as Starlark ([#7922](https://github.com/helix-editor/helix/pull/7922))
219+- Fix inline HTML tag highlighting in markdown ([#7960](https://github.com/helix-editor/helix/pull/7960))
220+- Update tree-sitter-robot ([#7970](https://github.com/helix-editor/helix/pull/7970))
221+- Highlight Dart 3 `sealed` and `base` keywords ([#7974](https://github.com/helix-editor/helix/pull/7974))
222+- Add configuration for `ltex-ls` to the default `languages.toml` ([#7838](https://github.com/helix-editor/helix/pull/7838))
223+- Update tree-sitter-strace ([#8087](https://github.com/helix-editor/helix/pull/8087))
224+- Update tree-sitter-gleam, enable auto-format ([#8085](https://github.com/helix-editor/helix/pull/8085))
225+- Update tree-sitter-esdl ([#8222](https://github.com/helix-editor/helix/pull/8222))
226+- Expand ignore file-types ([#8220](https://github.com/helix-editor/helix/pull/8220))
227+- Recognize feed related formats as XML ([#8232](https://github.com/helix-editor/helix/pull/8232))
228+- Improve YAML injections ([#8217](https://github.com/helix-editor/helix/pull/8217))
229+- Add shebangs for TypeScript, Julia, Java and OCaml ([95e994a](https://github.com/helix-editor/helix/commit/95e994a))
230+- Highlight abbreviations in Scheme ([ef23847](https://github.com/helix-editor/helix/commit/ef23847))
231+- Remove backtic auto-pair in OCaml ([#8260](https://github.com/helix-editor/helix/pull/8260))
232+- Recognize `flake.lock` as JSON ([#8304](https://github.com/helix-editor/helix/pull/8304))
233+- Add Python test script injection for Nix ([b4494e1](https://github.com/helix-editor/helix/commit/b4494e1))
234+- Fix Nix comment injection precedence ([37e48f4](https://github.com/helix-editor/helix/commit/37e48f4))
235+- Recognize editorconfig files as INI ([#8308](https://github.com/helix-editor/helix/pull/8308))
236+- Recognize `.babelrc` as JSON ([#8309](https://github.com/helix-editor/helix/pull/8309))
237+- Switch Purescript to its own tree-sitter parser ([#8306](https://github.com/helix-editor/helix/pull/8306), [#8338](https://github.com/helix-editor/helix/pull/8338), [#8527](https://github.com/helix-editor/helix/pull/8527))
238+- Update Unison highlights ([#8315](https://github.com/helix-editor/helix/pull/8315))
239+- Recognize `.webmanifest` as JSON ([#8342](https://github.com/helix-editor/helix/pull/8342))
240+- Recognize polkit policy files as XML ([#8369](https://github.com/helix-editor/helix/pull/8369))
241+- Recognize polkit rules files as JavaScript ([#8370](https://github.com/helix-editor/helix/pull/8370))
242+- Update Go highlight queries ([#8399](https://github.com/helix-editor/helix/pull/8399))
243+- Add shebangs for Makefiles ([#8410](https://github.com/helix-editor/helix/pull/8410))
244+- Add file-type associations from VSCode ([#8388](https://github.com/helix-editor/helix/pull/8388))
245+- Add validation to JSON/CSS language server configs ([#8433](https://github.com/helix-editor/helix/pull/8433))
246+- Add a configuration for the tailwind language server ([#8442](https://github.com/helix-editor/helix/pull/8442))
247+- Add a configuration for the ansible language server ([#7973](https://github.com/helix-editor/helix/pull/7973))
248+- Add a configuration for the GraphQL language server ([#8492](https://github.com/helix-editor/helix/pull/8492))
249+- Indent while statements in Bash ([#8528](https://github.com/helix-editor/helix/pull/8528))
250+- Update tree-sitter-haskell and queries ([#8558](https://github.com/helix-editor/helix/pull/8558))
251+
252+Packaging:
253+
254+- Add an overlay to the Nix flake ([#7078](https://github.com/helix-editor/helix/pull/7078))
255+- Check for `git` before fetching or building grammars ([#7320](https://github.com/helix-editor/helix/pull/7320))
256+- Refactor Nix flake to use Crane ([#7763](https://github.com/helix-editor/helix/pull/7763))
257+- Remove the aarch64 appimage from the release CI ([#7832](https://github.com/helix-editor/helix/pull/7832))
258+- Add desktop and icon files to Nix flake output ([#7979](https://github.com/helix-editor/helix/pull/7979))
259+- Build flake packages with the latest stable Rust ([#8133](https://github.com/helix-editor/helix/pull/8133))
260+
1261# 23.05 (2023-05-18)
2262
326323.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
Cargo.lock+538−615
@@ -19,41 +19,22 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
1919
2020[[package]]
2121name = "ahash"
22−version = "0.7.6"
23−source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
25−dependencies = [
26− "getrandom",
27− "once_cell",
28− "version_check",
29−]
30−
31−[[package]]
32−name = "ahash"
33−version = "0.8.3"
22+version = "0.8.6"
3423source = "registry+https://github.com/rust-lang/crates.io-index"
35−checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
24+checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
3625dependencies = [
3726 "cfg-if",
3827 "getrandom",
3928 "once_cell",
4029 "version_check",
30+ "zerocopy",
4131]
4232
4333[[package]]
4434name = "aho-corasick"
45−version = "0.7.20"
35+version = "1.1.2"
4636source = "registry+https://github.com/rust-lang/crates.io-index"
47−checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
48−dependencies = [
49− "memchr",
50−]
51−
52−[[package]]
53−name = "aho-corasick"
54−version = "1.0.1"
55−source = "registry+https://github.com/rust-lang/crates.io-index"
56−checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
37+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
5738dependencies = [
5839 "memchr",
5940]
@@ -81,9 +62,9 @@ dependencies = [
8162
8263[[package]]
8364name = "anyhow"
84−version = "1.0.71"
65+version = "1.0.76"
8566source = "registry+https://github.com/rust-lang/crates.io-index"
86−checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
67+checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
8768
8869[[package]]
8970name = "arc-swap"
@@ -107,7 +88,7 @@ dependencies = [
10788 "cc",
10889 "cfg-if",
10990 "libc",
110− "miniz_oxide 0.7.1",
91+ "miniz_oxide",
11192 "object",
11293 "rustc-demangle",
11394]
@@ -120,19 +101,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
120101
121102[[package]]
122103name = "bitflags"
123−version = "2.3.3"
104+version = "2.4.1"
124105source = "registry+https://github.com/rust-lang/crates.io-index"
125−checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
106+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
126107
127108[[package]]
128109name = "bstr"
129−version = "1.4.0"
110+version = "1.8.0"
130111source = "registry+https://github.com/rust-lang/crates.io-index"
131−checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
112+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
132113dependencies = [
133114 "memchr",
134− "once_cell",
135− "regex-automata 0.1.10",
115+ "regex-automata",
136116 "serde",
137117]
138118
@@ -151,12 +131,6 @@ version = "3.12.0"
151131source = "registry+https://github.com/rust-lang/crates.io-index"
152132checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
153133
154−[[package]]
155−name = "bytecount"
156−version = "0.6.3"
157−source = "registry+https://github.com/rust-lang/crates.io-index"
158−checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
159−
160134[[package]]
161135name = "bytes"
162136version = "1.4.0"
@@ -171,9 +145,12 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
171145
172146[[package]]
173147name = "cc"
174−version = "1.0.79"
148+version = "1.0.84"
175149source = "registry+https://github.com/rust-lang/crates.io-index"
176−checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
150+checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
151+dependencies = [
152+ "libc",
153+]
177154
178155[[package]]
179156name = "cfg-if"
@@ -194,25 +171,23 @@ dependencies = [
194171
195172[[package]]
196173name = "chrono"
197−version = "0.4.26"
174+version = "0.4.31"
198175source = "registry+https://github.com/rust-lang/crates.io-index"
199−checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
176+checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
200177dependencies = [
201178 "android-tzdata",
202179 "iana-time-zone",
203180 "num-traits",
204− "winapi",
181+ "windows-targets 0.48.0",
205182]
206183
207184[[package]]
208185name = "clipboard-win"
209−version = "4.5.0"
186+version = "5.0.0"
210187source = "registry+https://github.com/rust-lang/crates.io-index"
211−checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
188+checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc"
212189dependencies = [
213190 "error-code",
214− "str-buf",
215− "winapi",
216191]
217192
218193[[package]]
@@ -246,6 +221,12 @@ version = "0.8.4"
246221source = "registry+https://github.com/rust-lang/crates.io-index"
247222checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
248223
224+[[package]]
225+name = "cov-mark"
226+version = "1.1.0"
227+source = "registry+https://github.com/rust-lang/crates.io-index"
228+checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
229+
249230[[package]]
250231name = "crc32fast"
251232version = "1.3.2"
@@ -255,13 +236,56 @@ dependencies = [
255236 "cfg-if",
256237]
257238
239+[[package]]
240+name = "crossbeam-channel"
241+version = "0.5.8"
242+source = "registry+https://github.com/rust-lang/crates.io-index"
243+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
244+dependencies = [
245+ "cfg-if",
246+ "crossbeam-utils",
247+]
248+
249+[[package]]
250+name = "crossbeam-deque"
251+version = "0.8.3"
252+source = "registry+https://github.com/rust-lang/crates.io-index"
253+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
254+dependencies = [
255+ "cfg-if",
256+ "crossbeam-epoch",
257+ "crossbeam-utils",
258+]
259+
260+[[package]]
261+name = "crossbeam-epoch"
262+version = "0.9.15"
263+source = "registry+https://github.com/rust-lang/crates.io-index"
264+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
265+dependencies = [
266+ "autocfg",
267+ "cfg-if",
268+ "crossbeam-utils",
269+ "memoffset",
270+ "scopeguard",
271+]
272+
273+[[package]]
274+name = "crossbeam-utils"
275+version = "0.8.16"
276+source = "registry+https://github.com/rust-lang/crates.io-index"
277+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
278+dependencies = [
279+ "cfg-if",
280+]
281+
258282[[package]]
259283name = "crossterm"
260−version = "0.26.1"
284+version = "0.27.0"
261285source = "registry+https://github.com/rust-lang/crates.io-index"
262−checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
286+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
263287dependencies = [
264− "bitflags 1.3.2",
288+ "bitflags 2.4.1",
265289 "crossterm_winapi",
266290 "filedescriptor",
267291 "futures-core",
@@ -275,9 +299,9 @@ dependencies = [
275299
276300[[package]]
277301name = "crossterm_winapi"
278−version = "0.9.0"
302+version = "0.9.1"
279303source = "registry+https://github.com/rust-lang/crates.io-index"
280−checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
304+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
281305dependencies = [
282306 "winapi",
283307]
@@ -306,7 +330,7 @@ dependencies = [
306330 "proc-macro2",
307331 "quote",
308332 "scratch",
309− "syn 2.0.25",
333+ "syn 2.0.38",
310334]
311335
312336[[package]]
@@ -323,7 +347,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
323347dependencies = [
324348 "proc-macro2",
325349 "quote",
326− "syn 2.0.25",
350+ "syn 2.0.38",
327351]
328352
329353[[package]]
@@ -340,9 +364,9 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
340364
341365[[package]]
342366name = "encoding_rs"
343−version = "0.8.32"
367+version = "0.8.33"
344368source = "registry+https://github.com/rust-lang/crates.io-index"
345−checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
369+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
346370dependencies = [
347371 "cfg-if",
348372]
@@ -364,34 +388,19 @@ checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
364388
365389[[package]]
366390name = "errno"
367−version = "0.3.1"
368−source = "registry+https://github.com/rust-lang/crates.io-index"
369−checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
370−dependencies = [
371− "errno-dragonfly",
372− "libc",
373− "windows-sys 0.48.0",
374−]
375−
376−[[package]]
377−name = "errno-dragonfly"
378−version = "0.1.2"
391+version = "0.3.8"
379392source = "registry+https://github.com/rust-lang/crates.io-index"
380−checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
393+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
381394dependencies = [
382− "cc",
383395 "libc",
396+ "windows-sys 0.52.0",
384397]
385398
386399[[package]]
387400name = "error-code"
388−version = "2.3.1"
401+version = "3.0.0"
389402source = "registry+https://github.com/rust-lang/crates.io-index"
390−checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
391−dependencies = [
392− "libc",
393− "str-buf",
394−]
403+checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc"
395404
396405[[package]]
397406name = "etcetera"
@@ -405,12 +414,12 @@ dependencies = [
405414]
406415
407416[[package]]
408−name = "fastrand"
409−version = "1.9.0"
417+name = "faster-hex"
418+version = "0.9.0"
410419source = "registry+https://github.com/rust-lang/crates.io-index"
411−checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
420+checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
412421dependencies = [
413− "instant",
422+ "serde",
414423]
415424
416425[[package]]
@@ -439,54 +448,36 @@ dependencies = [
439448 "winapi",
440449]
441450
442−[[package]]
443−name = "filetime"
444−version = "0.2.21"
445−source = "registry+https://github.com/rust-lang/crates.io-index"
446−checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
447−dependencies = [
448− "cfg-if",
449− "libc",
450− "redox_syscall 0.2.16",
451− "windows-sys 0.48.0",
452−]
453−
454451[[package]]
455452name = "flate2"
456−version = "1.0.25"
453+version = "1.0.27"
457454source = "registry+https://github.com/rust-lang/crates.io-index"
458−checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
455+checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
459456dependencies = [
460457 "crc32fast",
461− "miniz_oxide 0.6.2",
458+ "miniz_oxide",
462459]
463460
464−[[package]]
465−name = "fnv"
466−version = "1.0.7"
467−source = "registry+https://github.com/rust-lang/crates.io-index"
468−checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
469−
470461[[package]]
471462name = "form_urlencoded"
472−version = "1.2.0"
463+version = "1.2.1"
473464source = "registry+https://github.com/rust-lang/crates.io-index"
474−checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
465+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
475466dependencies = [
476467 "percent-encoding",
477468]
478469
479470[[package]]
480471name = "futures-core"
481−version = "0.3.28"
472+version = "0.3.30"
482473source = "registry+https://github.com/rust-lang/crates.io-index"
483−checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
474+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
484475
485476[[package]]
486477name = "futures-executor"
487−version = "0.3.28"
478+version = "0.3.29"
488479source = "registry+https://github.com/rust-lang/crates.io-index"
489−checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
480+checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
490481dependencies = [
491482 "futures-core",
492483 "futures-task",
@@ -495,15 +486,15 @@ dependencies = [
495486
496487[[package]]
497488name = "futures-task"
498−version = "0.3.28"
489+version = "0.3.30"
499490source = "registry+https://github.com/rust-lang/crates.io-index"
500−checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
491+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
501492
502493[[package]]
503494name = "futures-util"
504−version = "0.3.28"
495+version = "0.3.30"
505496source = "registry+https://github.com/rust-lang/crates.io-index"
506−checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
497+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
507498dependencies = [
508499 "futures-core",
509500 "futures-task",
@@ -512,15 +503,6 @@ dependencies = [
512503 "slab",
513504]
514505
515−[[package]]
516−name = "fuzzy-matcher"
517−version = "0.3.7"
518−source = "registry+https://github.com/rust-lang/crates.io-index"
519−checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
520−dependencies = [
521− "thread_local",
522−]
523−
524506[[package]]
525507name = "getrandom"
526508version = "0.2.9"
@@ -540,15 +522,13 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
540522
541523[[package]]
542524name = "gix"
543−version = "0.48.0"
525+version = "0.57.0"
544526source = "registry+https://github.com/rust-lang/crates.io-index"
545−checksum = "c1e74cea676de7f53a79f3c0365812b11f6814b81e671b8ee4abae6ca09c7881"
527+checksum = "721c7497ab24b665ed5a1eb2b3526936aa60068e61ba260651f7e77c52feec69"
546528dependencies = [
547529 "gix-actor",
548− "gix-attributes",
549530 "gix-commitgraph",
550531 "gix-config",
551− "gix-credentials",
552532 "gix-date",
553533 "gix-diff",
554534 "gix-discover",
@@ -557,19 +537,16 @@ dependencies = [
557537 "gix-glob",
558538 "gix-hash",
559539 "gix-hashtable",
560− "gix-ignore",
561− "gix-index",
562540 "gix-lock",
563− "gix-mailmap",
564− "gix-negotiate",
541+ "gix-macros",
565542 "gix-object",
566543 "gix-odb",
567544 "gix-pack",
568545 "gix-path",
569− "gix-prompt",
570546 "gix-ref",
571547 "gix-refspec",
572548 "gix-revision",
549+ "gix-revwalk",
573550 "gix-sec",
574551 "gix-tempfile",
575552 "gix-trace",
@@ -577,10 +554,8 @@ dependencies = [
577554 "gix-url",
578555 "gix-utils",
579556 "gix-validate",
580− "gix-worktree",
581− "log",
582557 "once_cell",
583− "signal-hook",
558+ "parking_lot",
584559 "smallvec",
585560 "thiserror",
586561 "unicode-normalization",
@@ -588,81 +563,46 @@ dependencies = [
588563
589564[[package]]
590565name = "gix-actor"
591−version = "0.23.0"
566+version = "0.29.0"
592567source = "registry+https://github.com/rust-lang/crates.io-index"
593−checksum = "1969b77b9ee4cc1755c841987ec6f7622aaca95e952bcafb76973ae59d1b8716"
568+checksum = "886014c4865b93ce268f1d3eddd4fd3261242c3f3ee61eb36009f913016a9059"
594569dependencies = [
595570 "bstr",
596571 "btoi",
597572 "gix-date",
598573 "itoa",
599− "nom",
600− "thiserror",
601−]
602−
603−[[package]]
604−name = "gix-attributes"
605−version = "0.14.1"
606−source = "registry+https://github.com/rust-lang/crates.io-index"
607−checksum = "e3772b0129dcd1fc73e985bbd08a1482d082097d2915cb1ee31ce8092b8e4434"
608−dependencies = [
609− "bstr",
610− "gix-glob",
611− "gix-path",
612− "gix-quote",
613− "kstring",
614− "log",
615− "smallvec",
616− "thiserror",
617− "unicode-bom",
618−]
619−
620−[[package]]
621−name = "gix-bitmap"
622−version = "0.2.5"
623−source = "registry+https://github.com/rust-lang/crates.io-index"
624−checksum = "311e2fa997be6560c564b070c5da2d56d038b645a94e1e5796d5d85a350da33c"
625−dependencies = [
626574 "thiserror",
575+ "winnow 0.5.28",
627576]
628577
629578[[package]]
630579name = "gix-chunk"
631−version = "0.4.3"
580+version = "0.4.6"
632581source = "registry+https://github.com/rust-lang/crates.io-index"
633−checksum = "39db5ed0fc0a2e9b1b8265993f7efdbc30379dec268f3b91b7af0c2de4672fdd"
582+checksum = "2ade37ef69870de0ed966b97c57a3a947a22ff3482a52c3b99b205f77bcb08fb"
634583dependencies = [
635584 "thiserror",
636585]
637586
638−[[package]]
639−name = "gix-command"
640−version = "0.2.6"
641−source = "registry+https://github.com/rust-lang/crates.io-index"
642−checksum = "bb49ab557a37b0abb2415bca2b10e541277dff0565deb5bd5e99fd95f93f51eb"
643−dependencies = [
644− "bstr",
645−]
646−
647587[[package]]
648588name = "gix-commitgraph"
649−version = "0.17.1"
589+version = "0.23.0"
650590source = "registry+https://github.com/rust-lang/crates.io-index"
651−checksum = "ed42baa50075d41c1a0931074ce1a97c5797c7c6fe7591d9f1f2dcd448532c26"
591+checksum = "e7559ea9cefee188cd88b05afcc8e3ef7a3cb4a5c647bccf06b981e591b02b77"
652592dependencies = [
653593 "bstr",
654594 "gix-chunk",
655595 "gix-features",
656596 "gix-hash",
657− "memmap2 0.7.1",
597+ "memmap2",
658598 "thiserror",
659599]
660600
661601[[package]]
662602name = "gix-config"
663−version = "0.25.1"
603+version = "0.33.0"
664604source = "registry+https://github.com/rust-lang/crates.io-index"
665−checksum = "817688c7005a716d9363e267913526adea402dabd947f4ba63842d10cc5132af"
605+checksum = "9c35dc7f9c00a42bbc9cfa1ca2ec0a78ad1b76ff0736d3d35dfd612962244467"
666606dependencies = [
667607 "bstr",
668608 "gix-config-value",
@@ -671,49 +611,32 @@ dependencies = [
671611 "gix-path",
672612 "gix-ref",
673613 "gix-sec",
674− "log",
675614 "memchr",
676− "nom",
677615 "once_cell",
678616 "smallvec",
679617 "thiserror",
680618 "unicode-bom",
619+ "winnow 0.5.28",
681620]
682621
683622[[package]]
684623name = "gix-config-value"
685−version = "0.12.3"
624+version = "0.14.2"
686625source = "registry+https://github.com/rust-lang/crates.io-index"
687−checksum = "83960be5e99266bcf55dae5a24731bbd39f643bfb68f27e939d6b06836b5b87d"
626+checksum = "39f388cb2396aee82d6f460a2e7770659bdf8854e9e5478f7d2b1324a9698284"
688627dependencies = [
689− "bitflags 2.3.3",
628+ "bitflags 2.4.1",
690629 "bstr",
691630 "gix-path",
692631 "libc",
693632 "thiserror",
694633]
695634
696−[[package]]
697−name = "gix-credentials"
698−version = "0.16.1"
699−source = "registry+https://github.com/rust-lang/crates.io-index"
700−checksum = "75a75565e0e6e7f80cfa4eb1b05cc448c6846ddd48dcf413a28875fbc11ee9af"
701−dependencies = [
702− "bstr",
703− "gix-command",
704− "gix-config-value",
705− "gix-path",
706− "gix-prompt",
707− "gix-sec",
708− "gix-url",
709− "thiserror",
710−]
711−
712635[[package]]
713636name = "gix-date"
714−version = "0.7.0"
637+version = "0.8.2"
715638source = "registry+https://github.com/rust-lang/crates.io-index"
716−checksum = "8e9a04a1d2387c955ec91059d56b673000dd24f3c07cad08ed253e36381782bf"
639+checksum = "9d85f4a01e0d05c3de585e28bae514d4baf01655e3fc3f14ce6f30bf62405345"
717640dependencies = [
718641 "bstr",
719642 "itoa",
@@ -723,21 +646,21 @@ dependencies = [
723646
724647[[package]]
725648name = "gix-diff"
726−version = "0.32.0"
649+version = "0.39.0"
727650source = "registry+https://github.com/rust-lang/crates.io-index"
728−checksum = "aaf5d9b9b521b284ebe53ee69eee33341835ec70edc314f36b2100ea81396121"
651+checksum = "ac0e75f5afd2f6c47c800b6b0a000a08045739d0450d20482e8faa42543f62d1"
729652dependencies = [
653+ "bstr",
730654 "gix-hash",
731655 "gix-object",
732− "imara-diff",
733656 "thiserror",
734657]
735658
736659[[package]]
737660name = "gix-discover"
738−version = "0.21.1"
661+version = "0.28.0"
739662source = "registry+https://github.com/rust-lang/crates.io-index"
740−checksum = "272aad20dc63dedba76615373dd8885fb5aebe4795e5b5b0aa2a24e63c82085c"
663+checksum = "f0b63ce2ad81632ac1a84ac370f85a5e580c3261580bd85ea17ff35d3a0bba18"
741664dependencies = [
742665 "bstr",
743666 "dunce",
@@ -750,9 +673,9 @@ dependencies = [
750673
751674[[package]]
752675name = "gix-features"
753−version = "0.31.1"
676+version = "0.37.0"
754677source = "registry+https://github.com/rust-lang/crates.io-index"
755−checksum = "06142d8cff5d17509399b04052b64d2f9b3a311d5cff0b1a32b220f62cd0d595"
678+checksum = "befe7edea299a824504b5acc96d7a3a538125b38c42f3a8379f6912a29c90c81"
756679dependencies = [
757680 "crc32fast",
758681 "flate2",
@@ -768,20 +691,20 @@ dependencies = [
768691
769692[[package]]
770693name = "gix-fs"
771−version = "0.3.0"
694+version = "0.9.0"
772695source = "registry+https://github.com/rust-lang/crates.io-index"
773−checksum = "bb15956bc0256594c62a2399fcf6958a02a11724217eddfdc2b49b21b6292496"
696+checksum = "007017ce93b819ea52c0ec68306f7d72212a2d307c3d70f1548c7141c015d0a1"
774697dependencies = [
775698 "gix-features",
776699]
777700
778701[[package]]
779702name = "gix-glob"
780−version = "0.9.1"
703+version = "0.15.0"
781704source = "registry+https://github.com/rust-lang/crates.io-index"
782−checksum = "c18bdff83143d61e7d60da6183b87542a870d026b2a2d0b30170b8e9c0cd321a"
705+checksum = "61fb116c2516d3a1170e010118f639944a6389872c875a008960cdab2a44ac72"
783706dependencies = [
784− "bitflags 2.3.3",
707+ "bitflags 2.4.1",
785708 "bstr",
786709 "gix-features",
787710 "gix-path",
@@ -789,64 +712,30 @@ dependencies = [
789712
790713[[package]]
791714name = "gix-hash"
792−version = "0.11.3"
715+version = "0.14.0"
793716source = "registry+https://github.com/rust-lang/crates.io-index"
794−checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272"
717+checksum = "c52c3170a17b6031833cd2eb4ad7bc23f2755d06e6e70f78dec21d42e8fe1b30"
795718dependencies = [
796− "hex",
719+ "faster-hex",
797720 "thiserror",
798721]
799722
800723[[package]]
801724name = "gix-hashtable"
802−version = "0.2.3"
725+version = "0.5.0"
803726source = "registry+https://github.com/rust-lang/crates.io-index"
804−checksum = "9e133bc56d938eaec1c675af7c681a51de9662b0ada779f45607b967a10da77a"
727+checksum = "40a838e6366a5e5b84668b6997ce0981b833136468e8ba949f424c0ef2927eba"
805728dependencies = [
806729 "gix-hash",
807− "hashbrown 0.14.0",
730+ "hashbrown 0.14.3",
808731 "parking_lot",
809732]
810733
811−[[package]]
812−name = "gix-ignore"
813−version = "0.4.1"
814−source = "registry+https://github.com/rust-lang/crates.io-index"
815−checksum = "ca801f2d0535210f77b33e2c067d565aedecacc82f1b3dbce26da1388ebc4634"
816−dependencies = [
817− "bstr",
818− "gix-glob",
819− "gix-path",
820− "unicode-bom",
821−]
822−
823−[[package]]
824−name = "gix-index"
825−version = "0.20.0"
826−source = "registry+https://github.com/rust-lang/crates.io-index"
827−checksum = "68099abdf6ee50ae3c897e8b05de96871cbe54d52a37cdf559101f911b883562"
828−dependencies = [
829− "bitflags 2.3.3",
830− "bstr",
831− "btoi",
832− "filetime",
833− "gix-bitmap",
834− "gix-features",
835− "gix-hash",
836− "gix-lock",
837− "gix-object",
838− "gix-traverse",
839− "itoa",
840− "memmap2 0.7.1",
841− "smallvec",
842− "thiserror",
843−]
844−
845734[[package]]
846735name = "gix-lock"
847−version = "7.0.0"
736+version = "12.0.0"
848737source = "registry+https://github.com/rust-lang/crates.io-index"
849−checksum = "328f50aad713ab606caeaf834459ef915ccdfbb9133ac6cd54616d601aa9249f"
738+checksum = "6cf112ddee94223c119a8534dad027740dc3aba3365ac5edeef8a7f6660c74db"
850739dependencies = [
851740 "gix-tempfile",
852741 "gix-utils",
@@ -854,38 +743,21 @@ dependencies = [
854743]
855744
856745[[package]]
857−name = "gix-mailmap"
858−version = "0.15.0"
859−source = "registry+https://github.com/rust-lang/crates.io-index"
860−checksum = "1787e3c37fc43b1f7c0e3be6196c6837b3ba5f869190dfeaa444b816f0a7f34b"
861−dependencies = [
862− "bstr",
863− "gix-actor",
864− "gix-date",
865− "thiserror",
866−]
867−
868−[[package]]
869−name = "gix-negotiate"
870−version = "0.4.0"
746+name = "gix-macros"
747+version = "0.1.2"
871748source = "registry+https://github.com/rust-lang/crates.io-index"
872−checksum = "4e7bce64d4452dd609f44d04b14b29da2e0ad2c45fcdf4ce1472a5f5f8ec21c2"
749+checksum = "fc207b64189cf71bcb17fc841ab99a5d63d0845546b611e2703ce467f659323a"
873750dependencies = [
874− "bitflags 2.3.3",
875− "gix-commitgraph",
876− "gix-date",
877− "gix-hash",
878− "gix-object",
879− "gix-revwalk",
880− "smallvec",
881− "thiserror",
751+ "proc-macro2",
752+ "quote",
753+ "syn 2.0.38",
882754]
883755
884756[[package]]
885757name = "gix-object"
886−version = "0.32.0"
758+version = "0.40.0"
887759source = "registry+https://github.com/rust-lang/crates.io-index"
888−checksum = "a953f3d7ffad16734aa3ab1d05807972c80e339d1bd9dde03e0198716b99e2a6"
760+checksum = "e4c77e47ffba92127faf632a841fce23d19547e269bd8b88e68961a70eab4e93"
889761dependencies = [
890762 "bstr",
891763 "btoi",
@@ -894,18 +766,17 @@ dependencies = [
894766 "gix-features",
895767 "gix-hash",
896768 "gix-validate",
897− "hex",
898769 "itoa",
899− "nom",
900770 "smallvec",
901771 "thiserror",
772+ "winnow 0.5.28",
902773]
903774
904775[[package]]
905776name = "gix-odb"
906−version = "0.49.1"
777+version = "0.56.0"
907778source = "registry+https://github.com/rust-lang/crates.io-index"
908−checksum = "f6418cff00ecc2713b58c8e04bff30dda808fbba1a080e7248b299d069894a01"
779+checksum = "a3254f2005cc7553ea78e85e816a09150c6f7a64e6b7627b8d1fdc56721bea73"
909780dependencies = [
910781 "arc-swap",
911782 "gix-date",
@@ -922,21 +793,19 @@ dependencies = [
922793
923794[[package]]
924795name = "gix-pack"
925−version = "0.39.1"
796+version = "0.46.0"
926797source = "registry+https://github.com/rust-lang/crates.io-index"
927−checksum = "414935138d90043ea5898de7a93f02c2558e52652492719470e203ef26a8fd0a"
798+checksum = "02ebc8cd657eec207d82d8f876ca402361308ecd3c87a47935b0299506257b4f"
928799dependencies = [
929800 "clru",
930801 "gix-chunk",
931− "gix-diff",
932802 "gix-features",
933803 "gix-hash",
934804 "gix-hashtable",
935805 "gix-object",
936806 "gix-path",
937807 "gix-tempfile",
938− "gix-traverse",
939− "memmap2 0.7.1",
808+ "memmap2",
940809 "parking_lot",
941810 "smallvec",
942811 "thiserror",
@@ -944,9 +813,9 @@ dependencies = [
944813
945814[[package]]
946815name = "gix-path"
947−version = "0.8.3"
816+version = "0.10.2"
948817source = "registry+https://github.com/rust-lang/crates.io-index"
949−checksum = "dfca182d2575ded2ed38280f1ebf75cd5d3790b77e0872de07854cf085821fbe"
818+checksum = "10931652a3da126990ac93bce1b1c600cc99d7c268d712b6360ed52174ce1b68"
950819dependencies = [
951820 "bstr",
952821 "gix-trace",
@@ -955,24 +824,11 @@ dependencies = [
955824 "thiserror",
956825]
957826
958−[[package]]
959−name = "gix-prompt"
960−version = "0.5.2"
961−source = "registry+https://github.com/rust-lang/crates.io-index"
962−checksum = "8dfd363fd89a40c1e7bff9c9c1b136cd2002480f724b0c627c1bc771cd5480ec"
963−dependencies = [
964− "gix-command",
965− "gix-config-value",
966− "parking_lot",
967− "rustix",
968− "thiserror",
969−]
970−
971827[[package]]
972828name = "gix-quote"
973−version = "0.4.5"
829+version = "0.4.9"
974830source = "registry+https://github.com/rust-lang/crates.io-index"
975−checksum = "3874de636c2526de26a3405b8024b23ef1a327bebf4845d770d00d48700b6a40"
831+checksum = "6c43530cb94a7759807e6f8d180e17ac9c65673b891645c6c433831dc0cf4342"
976832dependencies = [
977833 "bstr",
978834 "btoi",
@@ -981,9 +837,9 @@ dependencies = [
981837
982838[[package]]
983839name = "gix-ref"
984−version = "0.32.1"
840+version = "0.40.0"
985841source = "registry+https://github.com/rust-lang/crates.io-index"
986−checksum = "39453f4e5f23cddc2e6e4cca2ba20adfdbec29379e3ca829714dfe98ae068ccd"
842+checksum = "baa951b3b850d6d1be4f900768e49af20b76bf9505beac22af723d57249a2f1d"
987843dependencies = [
988844 "gix-actor",
989845 "gix-date",
@@ -995,16 +851,16 @@ dependencies = [
995851 "gix-path",
996852 "gix-tempfile",
997853 "gix-validate",
998− "memmap2 0.7.1",
999− "nom",
854+ "memmap2",
1000855 "thiserror",
856+ "winnow 0.5.28",
1001857]
1002858
1003859[[package]]
1004860name = "gix-refspec"
1005−version = "0.13.0"
861+version = "0.21.0"
1006862source = "registry+https://github.com/rust-lang/crates.io-index"
1007−checksum = "b8e76ff1f82fba295a121e31ab02f69642994e532c45c0c899aa393f4b740302"
863+checksum = "88c61f849d58c06e3068a0b601cf10127d2a07cdad00a725ed66cf303f76f6b3"
1008864dependencies = [
1009865 "bstr",
1010866 "gix-hash",
@@ -1016,9 +872,9 @@ dependencies = [
1016872
1017873[[package]]
1018874name = "gix-revision"
1019−version = "0.17.0"
875+version = "0.25.0"
1020876source = "registry+https://github.com/rust-lang/crates.io-index"
1021−checksum = "237428a7d3978e8572964e1e45d984027c2acc94df47e594baa6c4b0da7c9922"
877+checksum = "bcbbf91f4200c5c76802ef5f057b96f5a336827508881fe55a780be71a794d22"
1022878dependencies = [
1023879 "bstr",
1024880 "gix-date",
@@ -1026,14 +882,15 @@ dependencies = [
1026882 "gix-hashtable",
1027883 "gix-object",
1028884 "gix-revwalk",
885+ "gix-trace",
1029886 "thiserror",
1030887]
1031888
1032889[[package]]
1033890name = "gix-revwalk"
1034−version = "0.3.0"
891+version = "0.11.0"
1035892source = "registry+https://github.com/rust-lang/crates.io-index"
1036−checksum = "028d50fcaf8326a8f79a359490d9ca9fb4e2b51ac9ac86503560d0bcc888d2eb"
893+checksum = "33ab66354d83f70f2730391747d0c75f94ef3c3dd40ebde2e206db9faaf7a0a7"
1037894dependencies = [
1038895 "gix-commitgraph",
1039896 "gix-date",
@@ -1046,42 +903,40 @@ dependencies = [
1046903
1047904[[package]]
1048905name = "gix-sec"
1049−version = "0.8.3"
906+version = "0.10.2"
1050907source = "registry+https://github.com/rust-lang/crates.io-index"
1051−checksum = "ede298863db2a0574a14070991710551e76d1f47c9783b62d4fcbca17f56371c"
908+checksum = "a9963f38a42144253ed4d571882d7db5ef644c12e6726e4b75135597cc9d0e1a"
1052909dependencies = [
1053− "bitflags 2.3.3",
910+ "bitflags 2.4.1",
1054911 "gix-path",
1055912 "libc",
1056− "windows",
913+ "windows 0.52.0",
1057914]
1058915
1059916[[package]]
1060917name = "gix-tempfile"
1061−version = "7.0.0"
918+version = "12.0.0"
1062919source = "registry+https://github.com/rust-lang/crates.io-index"
1063−checksum = "4fac8310c17406ea619af72f42ee46dac795110f68f41b4f4fa231b69889c6a2"
920+checksum = "e76a494bd530e1a1309188ff971825a24f159c76c2db0bf71fa5dfb469a2c915"
1064921dependencies = [
1065922 "gix-fs",
1066923 "libc",
1067924 "once_cell",
1068925 "parking_lot",
1069− "signal-hook",
1070− "signal-hook-registry",
1071926 "tempfile",
1072927]
1073928
1074929[[package]]
1075930name = "gix-trace"
1076−version = "0.1.2"
931+version = "0.1.5"
1077932source = "registry+https://github.com/rust-lang/crates.io-index"
1078−checksum = "103eac621617be3ebe0605c9065ca51a223279a23218aaf67d10daa6e452f663"
933+checksum = "bda62acb44dd86a40c7c3762a5403cfc1ac789ea559df54085cedf79864f809e"
1079934
1080935[[package]]
1081936name = "gix-traverse"
1082−version = "0.29.0"
937+version = "0.36.0"
1083938source = "registry+https://github.com/rust-lang/crates.io-index"
1084−checksum = "e3cdfd54598db4fae57d5ae6f52958422b2d13382d2745796bfe5c8015ffa86e"
939+checksum = "8661fab39985c9214e56d81a63ceb5886ac948cec2fba76c39494d1e0e307ea8"
1085940dependencies = [
1086941 "gix-commitgraph",
1087942 "gix-date",
@@ -1095,9 +950,9 @@ dependencies = [
1095950
1096951[[package]]
1097952name = "gix-url"
1098−version = "0.20.1"
953+version = "0.26.0"
1099954source = "registry+https://github.com/rust-lang/crates.io-index"
1100−checksum = "beaede6dbc83f408b19adfd95bb52f1dbf01fb8862c3faf6c6243e2e67fcdfa1"
955+checksum = "10a0129c1e8b52736d7c5128300a4485dbc85863001371e2771ac1754bd89fd7"
1101956dependencies = [
1102957 "bstr",
1103958 "gix-features",
@@ -1109,94 +964,71 @@ dependencies = [
1109964
1110965[[package]]
1111966name = "gix-utils"
1112−version = "0.1.4"
967+version = "0.1.7"
1113968source = "registry+https://github.com/rust-lang/crates.io-index"
1114−checksum = "7058c94f4164fcf5b8457d35f6d8f6e1007f9f7f938c9c7684a7e01d23c6ddde"
969+checksum = "ab9277a5e32e85d53f738096d872a4a9b76067ad471894ad31bd99c8fa2da1dc"
1115970dependencies = [
1116− "fastrand 2.0.0",
971+ "fastrand",
1117972]
1118973
1119974[[package]]
1120975name = "gix-validate"
1121−version = "0.7.6"
976+version = "0.8.2"
1122977source = "registry+https://github.com/rust-lang/crates.io-index"
1123−checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13"
978+checksum = "f805ebbdbaa4bfd98e2ee43e6d14099b8a4d9141f5d7b8202fea4d48e44263e7"
1124979dependencies = [
1125980 "bstr",
1126981 "thiserror",
1127982]
1128983
1129−[[package]]
1130−name = "gix-worktree"
1131−version = "0.21.1"
1132−source = "registry+https://github.com/rust-lang/crates.io-index"
1133−checksum = "c1363b9aa66b9e14412ac04e1f759827203f491729d92172535a8ce6cde02efa"
1134−dependencies = [
1135− "bstr",
1136− "filetime",
1137− "gix-attributes",
1138− "gix-features",
1139− "gix-fs",
1140− "gix-glob",
1141− "gix-hash",
1142− "gix-ignore",
1143− "gix-index",
1144− "gix-object",
1145− "gix-path",
1146− "io-close",
1147− "thiserror",
1148−]
1149−
1150984[[package]]
1151985name = "globset"
1152−version = "0.4.10"
986+version = "0.4.14"
1153987source = "registry+https://github.com/rust-lang/crates.io-index"
1154−checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
988+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
1155989dependencies = [
1156− "aho-corasick 0.7.20",
990+ "aho-corasick",
1157991 "bstr",
1158− "fnv",
1159992 "log",
1160− "regex",
993+ "regex-automata",
994+ "regex-syntax",
1161995]
1162996
1163997[[package]]
1164998name = "grep-matcher"
1165−version = "0.1.6"
999+version = "0.1.7"
11661000source = "registry+https://github.com/rust-lang/crates.io-index"
1167−checksum = "3902ca28f26945fe35cad349d776f163981d777fee382ccd6ef451126f51b319"
1001+checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02"
11681002dependencies = [
11691003 "memchr",
11701004]
11711005
11721006[[package]]
11731007name = "grep-regex"
1174−version = "0.1.11"
1008+version = "0.1.12"
11751009source = "registry+https://github.com/rust-lang/crates.io-index"
1176−checksum = "997598b41d53a37a2e3fc5300d5c11d825368c054420a9c65125b8fe1078463f"
1010+checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d"
11771011dependencies = [
1178− "aho-corasick 0.7.20",
11791012 "bstr",
11801013 "grep-matcher",
11811014 "log",
1182− "regex",
1183− "regex-syntax 0.6.29",
1184− "thread_local",
1015+ "regex-automata",
1016+ "regex-syntax",
11851017]
11861018
11871019[[package]]
11881020name = "grep-searcher"
1189−version = "0.1.11"
1021+version = "0.1.13"
11901022source = "registry+https://github.com/rust-lang/crates.io-index"
1191−checksum = "5601c4b9f480f0c9ebb40b1f6cbf447b8a50c5369223937a6c5214368c58779f"
1023+checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54"
11921024dependencies = [
11931025 "bstr",
1194− "bytecount",
11951026 "encoding_rs",
11961027 "encoding_rs_io",
11971028 "grep-matcher",
11981029 "log",
1199− "memmap2 0.5.10",
1030+ "memchr",
1031+ "memmap2",
12001032]
12011033
12021034[[package]]
@@ -1204,37 +1036,36 @@ name = "hashbrown"
12041036version = "0.12.3"
12051037source = "registry+https://github.com/rust-lang/crates.io-index"
12061038checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
1207−dependencies = [
1208− "ahash 0.7.6",
1209−]
12101039
12111040[[package]]
12121041name = "hashbrown"
1213−version = "0.14.0"
1042+version = "0.14.3"
12141043source = "registry+https://github.com/rust-lang/crates.io-index"
1215−checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
1044+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
12161045dependencies = [
1217− "ahash 0.8.3",
1046+ "ahash",
12181047 "allocator-api2",
12191048]
12201049
12211050[[package]]
12221051name = "helix-core"
1223−version = "0.6.0"
1052+version = "23.10.0"
12241053dependencies = [
1225− "ahash 0.8.3",
1054+ "ahash",
12261055 "arc-swap",
1227− "bitflags 2.3.3",
1056+ "bitflags 2.4.1",
12281057 "chrono",
12291058 "dunce",
12301059 "encoding_rs",
12311060 "etcetera",
1232− "hashbrown 0.14.0",
1061+ "hashbrown 0.14.3",
12331062 "helix-loader",
12341063 "imara-diff",
12351064 "indoc",
12361065 "log",
1066+ "nucleo",
12371067 "once_cell",
1068+ "parking_lot",
12381069 "quickcheck",
12391070 "regex",
12401071 "ropey",
@@ -1253,7 +1084,7 @@ dependencies = [
12531084
12541085[[package]]
12551086name = "helix-dap"
1256−version = "0.6.0"
1087+version = "23.10.0"
12571088dependencies = [
12581089 "anyhow",
12591090 "fern",
@@ -1266,9 +1097,17 @@ dependencies = [
12661097 "which",
12671098]
12681099
1100+[[package]]
1101+name = "helix-event"
1102+version = "23.10.0"
1103+dependencies = [
1104+ "parking_lot",
1105+ "tokio",
1106+]
1107+
12691108[[package]]
12701109name = "helix-loader"
1271−version = "0.6.0"
1110+version = "23.10.0"
12721111dependencies = [
12731112 "anyhow",
12741113 "cc",
@@ -1287,11 +1126,12 @@ dependencies = [
12871126
12881127[[package]]
12891128name = "helix-lsp"
1290−version = "0.6.0"
1129+version = "23.10.0"
12911130dependencies = [
12921131 "anyhow",
12931132 "futures-executor",
12941133 "futures-util",
1134+ "globset",
12951135 "helix-core",
12961136 "helix-loader",
12971137 "helix-parsec",
@@ -1308,11 +1148,11 @@ dependencies = [
13081148
13091149[[package]]
13101150name = "helix-parsec"
1311−version = "0.6.0"
1151+version = "23.10.0"
13121152
13131153[[package]]
13141154name = "helix-term"
1315−version = "0.6.0"
1155+version = "23.10.0"
13161156dependencies = [
13171157 "anyhow",
13181158 "arc-swap",
@@ -1321,11 +1161,11 @@ dependencies = [
13211161 "crossterm",
13221162 "fern",
13231163 "futures-util",
1324− "fuzzy-matcher",
13251164 "grep-regex",
13261165 "grep-searcher",
13271166 "helix-core",
13281167 "helix-dap",
1168+ "helix-event",
13291169 "helix-loader",
13301170 "helix-lsp",
13311171 "helix-tui",
@@ -1335,7 +1175,9 @@ dependencies = [
13351175 "indoc",
13361176 "libc",
13371177 "log",
1178+ "nucleo",
13381179 "once_cell",
1180+ "open",
13391181 "pulldown-cmark",
13401182 "serde",
13411183 "serde_json",
@@ -1346,14 +1188,15 @@ dependencies = [
13461188 "tokio",
13471189 "tokio-stream",
13481190 "toml",
1191+ "url",
13491192 "which",
13501193]
13511194
13521195[[package]]
13531196name = "helix-tui"
1354−version = "0.6.0"
1197+version = "23.10.0"
13551198dependencies = [
1356− "bitflags 2.3.3",
1199+ "bitflags 2.4.1",
13571200 "cassowary",
13581201 "crossterm",
13591202 "helix-core",
@@ -1367,12 +1210,13 @@ dependencies = [
13671210
13681211[[package]]
13691212name = "helix-vcs"
1370−version = "0.6.0"
1213+version = "23.10.0"
13711214dependencies = [
13721215 "anyhow",
13731216 "arc-swap",
13741217 "gix",
13751218 "helix-core",
1219+ "helix-event",
13761220 "imara-diff",
13771221 "log",
13781222 "parking_lot",
@@ -1382,17 +1226,18 @@ dependencies = [
13821226
13831227[[package]]
13841228name = "helix-view"
1385−version = "0.6.0"
1229+version = "23.10.0"
13861230dependencies = [
13871231 "anyhow",
13881232 "arc-swap",
1389− "bitflags 2.3.3",
1233+ "bitflags 2.4.1",
13901234 "chardetng",
13911235 "clipboard-win",
13921236 "crossterm",
13931237 "futures-util",
13941238 "helix-core",
13951239 "helix-dap",
1240+ "helix-event",
13961241 "helix-loader",
13971242 "helix-lsp",
13981243 "helix-tui",
@@ -1401,6 +1246,7 @@ dependencies = [
14011246 "log",
14021247 "once_cell",
14031248 "parking_lot",
1249+ "rustix",
14041250 "serde",
14051251 "serde_json",
14061252 "slotmap",
@@ -1420,25 +1266,13 @@ dependencies = [
14201266 "libc",
14211267]
14221268
1423−[[package]]
1424−name = "hermit-abi"
1425−version = "0.3.1"
1426−source = "registry+https://github.com/rust-lang/crates.io-index"
1427−checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
1428−
1429−[[package]]
1430−name = "hex"
1431−version = "0.4.3"
1432−source = "registry+https://github.com/rust-lang/crates.io-index"
1433−checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1434−
14351269[[package]]
14361270name = "home"
1437−version = "0.5.4"
1271+version = "0.5.5"
14381272source = "registry+https://github.com/rust-lang/crates.io-index"
1439−checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408"
1273+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
14401274dependencies = [
1441− "winapi",
1275+ "windows-sys 0.48.0",
14421276]
14431277
14441278[[package]]
@@ -1452,7 +1286,7 @@ dependencies = [
14521286 "iana-time-zone-haiku",
14531287 "js-sys",
14541288 "wasm-bindgen",
1455− "windows",
1289+ "windows 0.48.0",
14561290]
14571291
14581292[[package]]
@@ -1467,9 +1301,9 @@ dependencies = [
14671301
14681302[[package]]
14691303name = "idna"
1470−version = "0.4.0"
1304+version = "0.5.0"
14711305source = "registry+https://github.com/rust-lang/crates.io-index"
1472−checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
1306+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
14731307dependencies = [
14741308 "unicode-bidi",
14751309 "unicode-normalization",
@@ -1477,17 +1311,16 @@ dependencies = [
14771311
14781312[[package]]
14791313name = "ignore"
1480−version = "0.4.20"
1314+version = "0.4.21"
14811315source = "registry+https://github.com/rust-lang/crates.io-index"
1482−checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
1316+checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
14831317dependencies = [
1318+ "crossbeam-deque",
14841319 "globset",
1485− "lazy_static",
14861320 "log",
14871321 "memchr",
1488− "regex",
1322+ "regex-automata",
14891323 "same-file",
1490− "thread_local",
14911324 "walkdir",
14921325 "winapi-util",
14931326]
@@ -1498,7 +1331,7 @@ version = "0.1.5"
14981331source = "registry+https://github.com/rust-lang/crates.io-index"
14991332checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
15001333dependencies = [
1501− "ahash 0.8.3",
1334+ "ahash",
15021335 "hashbrown 0.12.3",
15031336]
15041337
@@ -1509,43 +1342,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
15091342checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
15101343dependencies = [
15111344 "equivalent",
1512− "hashbrown 0.14.0",
1345+ "hashbrown 0.14.3",
15131346]
15141347
15151348[[package]]
15161349name = "indoc"
1517−version = "2.0.2"
1350+version = "2.0.4"
15181351source = "registry+https://github.com/rust-lang/crates.io-index"
1519−checksum = "761cde40c27e2a9877f8c928fd248b7eec9dd48623dd514b256858ca593fbba7"
1352+checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
15201353
15211354[[package]]
1522−name = "instant"
1523−version = "0.1.12"
1355+name = "is-docker"
1356+version = "0.2.0"
15241357source = "registry+https://github.com/rust-lang/crates.io-index"
1525−checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
1358+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
15261359dependencies = [
1527− "cfg-if",
1528−]
1529−
1530−[[package]]
1531−name = "io-close"
1532−version = "0.3.7"
1533−source = "registry+https://github.com/rust-lang/crates.io-index"
1534−checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
1535−dependencies = [
1536− "libc",
1537− "winapi",
1360+ "once_cell",
15381361]
15391362
15401363[[package]]
1541−name = "io-lifetimes"
1542−version = "1.0.10"
1364+name = "is-wsl"
1365+version = "0.4.0"
15431366source = "registry+https://github.com/rust-lang/crates.io-index"
1544−checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
1367+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
15451368dependencies = [
1546− "hermit-abi 0.3.1",
1547− "libc",
1548− "windows-sys 0.48.0",
1369+ "is-docker",
1370+ "once_cell",
15491371]
15501372
15511373[[package]]
@@ -1563,32 +1385,17 @@ dependencies = [
15631385 "wasm-bindgen",
15641386]
15651387
1566−[[package]]
1567−name = "kstring"
1568−version = "2.0.0"
1569−source = "registry+https://github.com/rust-lang/crates.io-index"
1570−checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
1571−dependencies = [
1572− "static_assertions",
1573−]
1574−
1575−[[package]]
1576−name = "lazy_static"
1577−version = "1.4.0"
1578−source = "registry+https://github.com/rust-lang/crates.io-index"
1579−checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
1580−
15811388[[package]]
15821389name = "libc"
1583−version = "0.2.147"
1390+version = "0.2.151"
15841391source = "registry+https://github.com/rust-lang/crates.io-index"
1585−checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
1392+checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
15861393
15871394[[package]]
15881395name = "libloading"
1589−version = "0.8.0"
1396+version = "0.8.1"
15901397source = "registry+https://github.com/rust-lang/crates.io-index"
1591−checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
1398+checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
15921399dependencies = [
15931400 "cfg-if",
15941401 "windows-sys 0.48.0",
@@ -1605,9 +1412,9 @@ dependencies = [
16051412
16061413[[package]]
16071414name = "linux-raw-sys"
1608−version = "0.3.4"
1415+version = "0.4.11"
16091416source = "registry+https://github.com/rust-lang/crates.io-index"
1610−checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
1417+checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
16111418
16121419[[package]]
16131420name = "lock_api"
@@ -1621,15 +1428,15 @@ dependencies = [
16211428
16221429[[package]]
16231430name = "log"
1624−version = "0.4.19"
1431+version = "0.4.20"
16251432source = "registry+https://github.com/rust-lang/crates.io-index"
1626−checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
1433+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
16271434
16281435[[package]]
16291436name = "lsp-types"
1630−version = "0.94.0"
1437+version = "0.95.0"
16311438source = "registry+https://github.com/rust-lang/crates.io-index"
1632−checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
1439+checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
16331440dependencies = [
16341441 "bitflags 1.3.2",
16351442 "serde",
@@ -1640,41 +1447,26 @@ dependencies = [
16401447
16411448[[package]]
16421449name = "memchr"
1643−version = "2.5.0"
1450+version = "2.6.3"
16441451source = "registry+https://github.com/rust-lang/crates.io-index"
1645−checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
1452+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
16461453
16471454[[package]]
16481455name = "memmap2"
1649−version = "0.5.10"
1650−source = "registry+https://github.com/rust-lang/crates.io-index"
1651−checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
1652−dependencies = [
1653− "libc",
1654−]
1655−
1656−[[package]]
1657−name = "memmap2"
1658−version = "0.7.1"
1456+version = "0.9.0"
16591457source = "registry+https://github.com/rust-lang/crates.io-index"
1660−checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
1458+checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375"
16611459dependencies = [
16621460 "libc",
16631461]
16641462
16651463[[package]]
1666−name = "minimal-lexical"
1667−version = "0.2.1"
1668−source = "registry+https://github.com/rust-lang/crates.io-index"
1669−checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1670−
1671−[[package]]
1672−name = "miniz_oxide"
1673−version = "0.6.2"
1464+name = "memoffset"
1465+version = "0.9.0"
16741466source = "registry+https://github.com/rust-lang/crates.io-index"
1675−checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
1467+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
16761468dependencies = [
1677− "adler",
1469+ "autocfg",
16781470]
16791471
16801472[[package]]
@@ -1688,24 +1480,36 @@ dependencies = [
16881480
16891481[[package]]
16901482name = "mio"
1691−version = "0.8.6"
1483+version = "0.8.9"
16921484source = "registry+https://github.com/rust-lang/crates.io-index"
1693−checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
1485+checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
16941486dependencies = [
16951487 "libc",
16961488 "log",
16971489 "wasi",
1698− "windows-sys 0.45.0",
1490+ "windows-sys 0.48.0",
16991491]
17001492
17011493[[package]]
1702−name = "nom"
1703−version = "7.1.3"
1494+name = "nucleo"
1495+version = "0.2.1"
1496+source = "registry+https://github.com/rust-lang/crates.io-index"
1497+checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3"
1498+dependencies = [
1499+ "nucleo-matcher",
1500+ "parking_lot",
1501+ "rayon",
1502+]
1503+
1504+[[package]]
1505+name = "nucleo-matcher"
1506+version = "0.2.0"
17041507source = "registry+https://github.com/rust-lang/crates.io-index"
1705−checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1508+checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
17061509dependencies = [
1510+ "cov-mark",
17071511 "memchr",
1708− "minimal-lexical",
1512+ "unicode-segmentation",
17091513]
17101514
17111515[[package]]
@@ -1723,7 +1527,7 @@ version = "1.15.0"
17231527source = "registry+https://github.com/rust-lang/crates.io-index"
17241528checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
17251529dependencies = [
1726− "hermit-abi 0.2.6",
1530+ "hermit-abi",
17271531 "libc",
17281532]
17291533
@@ -1747,9 +1551,20 @@ dependencies = [
17471551
17481552[[package]]
17491553name = "once_cell"
1750−version = "1.18.0"
1554+version = "1.19.0"
17511555source = "registry+https://github.com/rust-lang/crates.io-index"
1752−checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
1556+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
1557+
1558+[[package]]
1559+name = "open"
1560+version = "5.0.1"
1561+source = "registry+https://github.com/rust-lang/crates.io-index"
1562+checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
1563+dependencies = [
1564+ "is-wsl",
1565+ "libc",
1566+ "pathdiff",
1567+]
17531568
17541569[[package]]
17551570name = "parking_lot"
@@ -1774,17 +1589,23 @@ dependencies = [
17741589 "windows-sys 0.45.0",
17751590]
17761591
1592+[[package]]
1593+name = "pathdiff"
1594+version = "0.2.1"
1595+source = "registry+https://github.com/rust-lang/crates.io-index"
1596+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
1597+
17771598[[package]]
17781599name = "percent-encoding"
1779−version = "2.3.0"
1600+version = "2.3.1"
17801601source = "registry+https://github.com/rust-lang/crates.io-index"
1781−checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
1602+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
17821603
17831604[[package]]
17841605name = "pin-project-lite"
1785−version = "0.2.9"
1606+version = "0.2.12"
17861607source = "registry+https://github.com/rust-lang/crates.io-index"
1787−checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
1608+checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
17881609
17891610[[package]]
17901611name = "pin-utils"
@@ -1794,18 +1615,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
17941615
17951616[[package]]
17961617name = "proc-macro2"
1797−version = "1.0.63"
1618+version = "1.0.69"
17981619source = "registry+https://github.com/rust-lang/crates.io-index"
1799−checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
1620+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
18001621dependencies = [
18011622 "unicode-ident",
18021623]
18031624
18041625[[package]]
18051626name = "prodash"
1806−version = "25.0.0"
1627+version = "28.0.0"
18071628source = "registry+https://github.com/rust-lang/crates.io-index"
1808−checksum = "3236ce1618b6da4c7b618e0143c4d5b5dc190f75f81c49f248221382f7e9e9ae"
1629+checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
18091630
18101631[[package]]
18111632name = "pulldown-cmark"
@@ -1854,6 +1675,28 @@ dependencies = [
18541675 "getrandom",
18551676]
18561677
1678+[[package]]
1679+name = "rayon"
1680+version = "1.7.0"
1681+source = "registry+https://github.com/rust-lang/crates.io-index"
1682+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
1683+dependencies = [
1684+ "either",
1685+ "rayon-core",
1686+]
1687+
1688+[[package]]
1689+name = "rayon-core"
1690+version = "1.11.0"
1691+source = "registry+https://github.com/rust-lang/crates.io-index"
1692+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
1693+dependencies = [
1694+ "crossbeam-channel",
1695+ "crossbeam-deque",
1696+ "crossbeam-utils",
1697+ "num_cpus",
1698+]
1699+
18571700[[package]]
18581701name = "redox_syscall"
18591702version = "0.2.16"
@@ -1865,59 +1708,47 @@ dependencies = [
18651708
18661709[[package]]
18671710name = "redox_syscall"
1868−version = "0.3.5"
1711+version = "0.4.1"
18691712source = "registry+https://github.com/rust-lang/crates.io-index"
1870−checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
1713+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
18711714dependencies = [
18721715 "bitflags 1.3.2",
18731716]
18741717
18751718[[package]]
18761719name = "regex"
1877−version = "1.9.1"
1720+version = "1.10.2"
18781721source = "registry+https://github.com/rust-lang/crates.io-index"
1879−checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
1722+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
18801723dependencies = [
1881− "aho-corasick 1.0.1",
1724+ "aho-corasick",
18821725 "memchr",
1883− "regex-automata 0.3.2",
1884− "regex-syntax 0.7.3",
1726+ "regex-automata",
1727+ "regex-syntax",
18851728]
18861729
18871730[[package]]
18881731name = "regex-automata"
1889−version = "0.1.10"
1890−source = "registry+https://github.com/rust-lang/crates.io-index"
1891−checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
1892−
1893−[[package]]
1894−name = "regex-automata"
1895−version = "0.3.2"
1732+version = "0.4.3"
18961733source = "registry+https://github.com/rust-lang/crates.io-index"
1897−checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
1734+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
18981735dependencies = [
1899− "aho-corasick 1.0.1",
1736+ "aho-corasick",
19001737 "memchr",
1901− "regex-syntax 0.7.3",
1738+ "regex-syntax",
19021739]
19031740
19041741[[package]]
19051742name = "regex-syntax"
1906−version = "0.6.29"
1907−source = "registry+https://github.com/rust-lang/crates.io-index"
1908−checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
1909−
1910−[[package]]
1911−name = "regex-syntax"
1912−version = "0.7.3"
1743+version = "0.8.2"
19131744source = "registry+https://github.com/rust-lang/crates.io-index"
1914−checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
1745+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
19151746
19161747[[package]]
19171748name = "ropey"
1918−version = "1.6.0"
1749+version = "1.6.1"
19191750source = "registry+https://github.com/rust-lang/crates.io-index"
1920−checksum = "53ce7a2c43a32e50d666e33c5a80251b31147bb4b49024bcab11fb6f20c671ed"
1751+checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
19211752dependencies = [
19221753 "smallvec",
19231754 "str_indices",
@@ -1931,16 +1762,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
19311762
19321763[[package]]
19331764name = "rustix"
1934−version = "0.37.15"
1765+version = "0.38.28"
19351766source = "registry+https://github.com/rust-lang/crates.io-index"
1936−checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
1767+checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
19371768dependencies = [
1938− "bitflags 1.3.2",
1769+ "bitflags 2.4.1",
19391770 "errno",
1940− "io-lifetimes",
19411771 "libc",
19421772 "linux-raw-sys",
1943− "windows-sys 0.48.0",
1773+ "windows-sys 0.52.0",
19441774]
19451775
19461776[[package]]
@@ -1972,29 +1802,29 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
19721802
19731803[[package]]
19741804name = "serde"
1975−version = "1.0.171"
1805+version = "1.0.193"
19761806source = "registry+https://github.com/rust-lang/crates.io-index"
1977−checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
1807+checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
19781808dependencies = [
19791809 "serde_derive",
19801810]
19811811
19821812[[package]]
19831813name = "serde_derive"
1984−version = "1.0.171"
1814+version = "1.0.193"
19851815source = "registry+https://github.com/rust-lang/crates.io-index"
1986−checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
1816+checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
19871817dependencies = [
19881818 "proc-macro2",
19891819 "quote",
1990− "syn 2.0.25",
1820+ "syn 2.0.38",
19911821]
19921822
19931823[[package]]
19941824name = "serde_json"
1995−version = "1.0.103"
1825+version = "1.0.108"
19961826source = "registry+https://github.com/rust-lang/crates.io-index"
1997−checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
1827+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
19981828dependencies = [
19991829 "itoa",
20001830 "ryu",
@@ -2009,7 +1839,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
20091839dependencies = [
20101840 "proc-macro2",
20111841 "quote",
2012− "syn 2.0.25",
1842+ "syn 2.0.38",
20131843]
20141844
20151845[[package]]
@@ -2029,9 +1859,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
20291859
20301860[[package]]
20311861name = "signal-hook"
2032−version = "0.3.15"
1862+version = "0.3.17"
20331863source = "registry+https://github.com/rust-lang/crates.io-index"
2034−checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
1864+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
20351865dependencies = [
20361866 "libc",
20371867 "signal-hook-registry",
@@ -2080,18 +1910,18 @@ dependencies = [
20801910
20811911[[package]]
20821912name = "slotmap"
2083−version = "1.0.6"
1913+version = "1.0.7"
20841914source = "registry+https://github.com/rust-lang/crates.io-index"
2085−checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
1915+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
20861916dependencies = [
20871917 "version_check",
20881918]
20891919
20901920[[package]]
20911921name = "smallvec"
2092−version = "1.11.0"
1922+version = "1.11.2"
20931923source = "registry+https://github.com/rust-lang/crates.io-index"
2094−checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
1924+checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
20951925
20961926[[package]]
20971927name = "smartstring"
@@ -2112,12 +1942,12 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
21121942
21131943[[package]]
21141944name = "socket2"
2115−version = "0.4.9"
1945+version = "0.5.5"
21161946source = "registry+https://github.com/rust-lang/crates.io-index"
2117−checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
1947+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
21181948dependencies = [
21191949 "libc",
2120− "winapi",
1950+ "windows-sys 0.48.0",
21211951]
21221952
21231953[[package]]
@@ -2126,12 +1956,6 @@ version = "1.1.0"
21261956source = "registry+https://github.com/rust-lang/crates.io-index"
21271957checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
21281958
2129−[[package]]
2130−name = "str-buf"
2131−version = "1.0.6"
2132−source = "registry+https://github.com/rust-lang/crates.io-index"
2133−checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
2134−
21351959[[package]]
21361960name = "str_indices"
21371961version = "0.4.1"
@@ -2151,9 +1975,9 @@ dependencies = [
21511975
21521976[[package]]
21531977name = "syn"
2154−version = "2.0.25"
1978+version = "2.0.38"
21551979source = "registry+https://github.com/rust-lang/crates.io-index"
2156−checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
1980+checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
21571981dependencies = [
21581982 "proc-macro2",
21591983 "quote",
@@ -2162,14 +1986,13 @@ dependencies = [
21621986
21631987[[package]]
21641988name = "tempfile"
2165−version = "3.6.0"
1989+version = "3.8.1"
21661990source = "registry+https://github.com/rust-lang/crates.io-index"
2167−checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
1991+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
21681992dependencies = [
2169− "autocfg",
21701993 "cfg-if",
2171− "fastrand 1.9.0",
2172− "redox_syscall 0.3.5",
1994+ "fastrand",
1995+ "redox_syscall 0.4.1",
21731996 "rustix",
21741997 "windows-sys 0.48.0",
21751998]
@@ -2205,32 +2028,22 @@ dependencies = [
22052028
22062029[[package]]
22072030name = "thiserror"
2208−version = "1.0.43"
2031+version = "1.0.52"
22092032source = "registry+https://github.com/rust-lang/crates.io-index"
2210−checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
2033+checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d"
22112034dependencies = [
22122035 "thiserror-impl",
22132036]
22142037
22152038[[package]]
22162039name = "thiserror-impl"
2217−version = "1.0.43"
2040+version = "1.0.52"
22182041source = "registry+https://github.com/rust-lang/crates.io-index"
2219−checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
2042+checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
22202043dependencies = [
22212044 "proc-macro2",
22222045 "quote",
2223− "syn 2.0.25",
2224−]
2225−
2226−[[package]]
2227−name = "thread_local"
2228−version = "1.1.7"
2229−source = "registry+https://github.com/rust-lang/crates.io-index"
2230−checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
2231−dependencies = [
2232− "cfg-if",
2233− "once_cell",
2046+ "syn 2.0.38",
22342047]
22352048
22362049[[package]]
@@ -2244,9 +2057,9 @@ dependencies = [
22442057
22452058[[package]]
22462059name = "time"
2247−version = "0.3.20"
2060+version = "0.3.23"
22482061source = "registry+https://github.com/rust-lang/crates.io-index"
2249−checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
2062+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
22502063dependencies = [
22512064 "itoa",
22522065 "libc",
@@ -2258,15 +2071,15 @@ dependencies = [
22582071
22592072[[package]]
22602073name = "time-core"
2261−version = "0.1.0"
2074+version = "0.1.1"
22622075source = "registry+https://github.com/rust-lang/crates.io-index"
2263−checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
2076+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
22642077
22652078[[package]]
22662079name = "time-macros"
2267−version = "0.2.8"
2080+version = "0.2.10"
22682081source = "registry+https://github.com/rust-lang/crates.io-index"
2269−checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
2082+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
22702083dependencies = [
22712084 "time-core",
22722085]
@@ -2288,11 +2101,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
22882101
22892102[[package]]
22902103name = "tokio"
2291−version = "1.29.1"
2104+version = "1.35.1"
22922105source = "registry+https://github.com/rust-lang/crates.io-index"
2293−checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
2106+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
22942107dependencies = [
2295− "autocfg",
22962108 "backtrace",
22972109 "bytes",
22982110 "libc",
@@ -2308,13 +2120,13 @@ dependencies = [
23082120
23092121[[package]]
23102122name = "tokio-macros"
2311−version = "2.1.0"
2123+version = "2.2.0"
23122124source = "registry+https://github.com/rust-lang/crates.io-index"
2313−checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
2125+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
23142126dependencies = [
23152127 "proc-macro2",
23162128 "quote",
2317− "syn 2.0.25",
2129+ "syn 2.0.38",
23182130]
23192131
23202132[[package]]
@@ -2359,14 +2171,13 @@ dependencies = [
23592171 "serde",
23602172 "serde_spanned",
23612173 "toml_datetime",
2362− "winnow",
2174+ "winnow 0.4.6",
23632175]
23642176
23652177[[package]]
23662178name = "tree-sitter"
23672179version = "0.20.10"
2368−source = "registry+https://github.com/rust-lang/crates.io-index"
2369−checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d"
2180+source = "git+https://github.com/tree-sitter/tree-sitter?rev=ab09ae20d640711174b8da8a654f6b3dec93da1a#ab09ae20d640711174b8da8a654f6b3dec93da1a"
23702181dependencies = [
23712182 "cc",
23722183 "regex",
@@ -2407,13 +2218,9 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
24072218
24082219[[package]]
24092220name = "unicode-linebreak"
2410−version = "0.1.4"
2221+version = "0.1.5"
24112222source = "registry+https://github.com/rust-lang/crates.io-index"
2412−checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
2413−dependencies = [
2414− "hashbrown 0.12.3",
2415− "regex",
2416−]
2223+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
24172224
24182225[[package]]
24192226name = "unicode-normalization"
@@ -2432,15 +2239,15 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
24322239
24332240[[package]]
24342241name = "unicode-width"
2435−version = "0.1.10"
2242+version = "0.1.11"
24362243source = "registry+https://github.com/rust-lang/crates.io-index"
2437−checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
2244+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
24382245
24392246[[package]]
24402247name = "url"
2441−version = "2.4.0"
2248+version = "2.5.0"
24422249source = "registry+https://github.com/rust-lang/crates.io-index"
2443−checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
2250+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
24442251dependencies = [
24452252 "form_urlencoded",
24462253 "idna",
@@ -2456,9 +2263,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
24562263
24572264[[package]]
24582265name = "walkdir"
2459−version = "2.3.3"
2266+version = "2.4.0"
24602267source = "registry+https://github.com/rust-lang/crates.io-index"
2461−checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
2268+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
24622269dependencies = [
24632270 "same-file",
24642271 "winapi-util",
@@ -2526,13 +2333,15 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
25262333
25272334[[package]]
25282335name = "which"
2529−version = "4.4.0"
2336+version = "5.0.0"
25302337source = "registry+https://github.com/rust-lang/crates.io-index"
2531−checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
2338+checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14"
25322339dependencies = [
25332340 "either",
2534− "libc",
2341+ "home",
25352342 "once_cell",
2343+ "rustix",
2344+ "windows-sys 0.48.0",
25362345]
25372346
25382347[[package]]
@@ -2575,6 +2384,25 @@ dependencies = [
25752384 "windows-targets 0.48.0",
25762385]
25772386
2387+[[package]]
2388+name = "windows"
2389+version = "0.52.0"
2390+source = "registry+https://github.com/rust-lang/crates.io-index"
2391+checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
2392+dependencies = [
2393+ "windows-core",
2394+ "windows-targets 0.52.0",
2395+]
2396+
2397+[[package]]
2398+name = "windows-core"
2399+version = "0.52.0"
2400+source = "registry+https://github.com/rust-lang/crates.io-index"
2401+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
2402+dependencies = [
2403+ "windows-targets 0.52.0",
2404+]
2405+
25782406[[package]]
25792407name = "windows-sys"
25802408version = "0.45.0"
@@ -2593,6 +2421,15 @@ dependencies = [
25932421 "windows-targets 0.48.0",
25942422]
25952423
2424+[[package]]
2425+name = "windows-sys"
2426+version = "0.52.0"
2427+source = "registry+https://github.com/rust-lang/crates.io-index"
2428+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2429+dependencies = [
2430+ "windows-targets 0.52.0",
2431+]
2432+
25962433[[package]]
25972434name = "windows-targets"
25982435version = "0.42.2"
@@ -2623,6 +2460,21 @@ dependencies = [
26232460 "windows_x86_64_msvc 0.48.0",
26242461]
26252462
2463+[[package]]
2464+name = "windows-targets"
2465+version = "0.52.0"
2466+source = "registry+https://github.com/rust-lang/crates.io-index"
2467+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
2468+dependencies = [
2469+ "windows_aarch64_gnullvm 0.52.0",
2470+ "windows_aarch64_msvc 0.52.0",
2471+ "windows_i686_gnu 0.52.0",
2472+ "windows_i686_msvc 0.52.0",
2473+ "windows_x86_64_gnu 0.52.0",
2474+ "windows_x86_64_gnullvm 0.52.0",
2475+ "windows_x86_64_msvc 0.52.0",
2476+]
2477+
26262478[[package]]
26272479name = "windows_aarch64_gnullvm"
26282480version = "0.42.2"
@@ -2635,6 +2487,12 @@ version = "0.48.0"
26352487source = "registry+https://github.com/rust-lang/crates.io-index"
26362488checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
26372489
2490+[[package]]
2491+name = "windows_aarch64_gnullvm"
2492+version = "0.52.0"
2493+source = "registry+https://github.com/rust-lang/crates.io-index"
2494+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
2495+
26382496[[package]]
26392497name = "windows_aarch64_msvc"
26402498version = "0.42.2"
@@ -2647,6 +2505,12 @@ version = "0.48.0"
26472505source = "registry+https://github.com/rust-lang/crates.io-index"
26482506checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
26492507
2508+[[package]]
2509+name = "windows_aarch64_msvc"
2510+version = "0.52.0"
2511+source = "registry+https://github.com/rust-lang/crates.io-index"
2512+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
2513+
26502514[[package]]
26512515name = "windows_i686_gnu"
26522516version = "0.42.2"
@@ -2659,6 +2523,12 @@ version = "0.48.0"
26592523source = "registry+https://github.com/rust-lang/crates.io-index"
26602524checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
26612525
2526+[[package]]
2527+name = "windows_i686_gnu"
2528+version = "0.52.0"
2529+source = "registry+https://github.com/rust-lang/crates.io-index"
2530+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
2531+
26622532[[package]]
26632533name = "windows_i686_msvc"
26642534version = "0.42.2"
@@ -2671,6 +2541,12 @@ version = "0.48.0"
26712541source = "registry+https://github.com/rust-lang/crates.io-index"
26722542checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
26732543
2544+[[package]]
2545+name = "windows_i686_msvc"
2546+version = "0.52.0"
2547+source = "registry+https://github.com/rust-lang/crates.io-index"
2548+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
2549+
26742550[[package]]
26752551name = "windows_x86_64_gnu"
26762552version = "0.42.2"
@@ -2683,6 +2559,12 @@ version = "0.48.0"
26832559source = "registry+https://github.com/rust-lang/crates.io-index"
26842560checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
26852561
2562+[[package]]
2563+name = "windows_x86_64_gnu"
2564+version = "0.52.0"
2565+source = "registry+https://github.com/rust-lang/crates.io-index"
2566+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
2567+
26862568[[package]]
26872569name = "windows_x86_64_gnullvm"
26882570version = "0.42.2"
@@ -2695,6 +2577,12 @@ version = "0.48.0"
26952577source = "registry+https://github.com/rust-lang/crates.io-index"
26962578checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
26972579
2580+[[package]]
2581+name = "windows_x86_64_gnullvm"
2582+version = "0.52.0"
2583+source = "registry+https://github.com/rust-lang/crates.io-index"
2584+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
2585+
26982586[[package]]
26992587name = "windows_x86_64_msvc"
27002588version = "0.42.2"
@@ -2707,6 +2595,12 @@ version = "0.48.0"
27072595source = "registry+https://github.com/rust-lang/crates.io-index"
27082596checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
27092597
2598+[[package]]
2599+name = "windows_x86_64_msvc"
2600+version = "0.52.0"
2601+source = "registry+https://github.com/rust-lang/crates.io-index"
2602+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
2603+
27102604[[package]]
27112605name = "winnow"
27122606version = "0.4.6"
@@ -2716,9 +2610,18 @@ dependencies = [
27162610 "memchr",
27172611]
27182612
2613+[[package]]
2614+name = "winnow"
2615+version = "0.5.28"
2616+source = "registry+https://github.com/rust-lang/crates.io-index"
2617+checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2"
2618+dependencies = [
2619+ "memchr",
2620+]
2621+
27192622[[package]]
27202623name = "xtask"
2721−version = "0.6.0"
2624+version = "23.10.0"
27222625dependencies = [
27232626 "helix-core",
27242627 "helix-loader",
@@ -2726,3 +2629,23 @@ dependencies = [
27262629 "helix-view",
27272630 "toml",
27282631]
2632+
2633+[[package]]
2634+name = "zerocopy"
2635+version = "0.7.31"
2636+source = "registry+https://github.com/rust-lang/crates.io-index"
2637+checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d"
2638+dependencies = [
2639+ "zerocopy-derive",
2640+]
2641+
2642+[[package]]
2643+name = "zerocopy-derive"
2644+version = "0.7.31"
2645+source = "registry+https://github.com/rust-lang/crates.io-index"
2646+checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
2647+dependencies = [
2648+ "proc-macro2",
2649+ "quote",
2650+ "syn 2.0.38",
2651+]
Cargo.toml+16
@@ -1,10 +1,12 @@
11[workspace]
2+resolver = "2"
23members = [
34 "helix-core",
45 "helix-view",
56 "helix-term",
67 "helix-tui",
78 "helix-lsp",
9+ "helix-event",
810 "helix-dap",
911 "helix-loader",
1012 "helix-vcs",
@@ -32,3 +34,17 @@ inherits = "test"
3234package.helix-core.opt-level = 2
3335package.helix-tui.opt-level = 2
3436package.helix-term.opt-level = 2
37+
38+[workspace.dependencies]
39+tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
40+nucleo = "0.2.0"
41+
42+[workspace.package]
43+version = "23.10.0"
44+edition = "2021"
45+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
46+categories = ["editor"]
47+repository = "https://github.com/helix-editor/helix"
48+homepage = "https://helix-editor.com"
49+license = "MPL-2.0"
50+rust-version = "1.70"
README.md+1−1
@@ -18,7 +18,7 @@
1818
1919![Screenshot](./screenshot.png)
2020
21−A Kakoune / Neovim inspired editor, written in Rust.
21+A [Kakoune](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.
2222
2323The editing model is very heavily based on Kakoune; during development I found
2424myself agreeing with most of Kakoune's design decisions.
VERSIONdeleted−1
@@ -1 +0,0 @@
1−23.05
book/book.toml+3
@@ -11,3 +11,6 @@ preferred-dark-theme = "colibri"
1111git-repository-url = "https://github.com/helix-editor/helix"
1212edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
1313additional-css = ["custom.css"]
14+
15+[output.html.search]
16+use-boolean-and = true
book/custom.css+1−1
@@ -164,7 +164,7 @@ code.hljs {
164164 --searchresults-header-fg: #5f5f71;
165165 --searchresults-border-color: #5c5c68;
166166 --searchresults-li-bg: #242430;
167− --search-mark-bg: #acff5;
167+ --search-mark-bg: #a2cff5;
168168}
169169
170170.colibri .content .header {
book/src/configuration.md+38−11
@@ -51,7 +51,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
5151| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
5252| `auto-format` | Enable automatic formatting on save | `true` |
5353| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
54−| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
54+| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
5555| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
5656| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
5757| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
@@ -64,6 +64,9 @@ Its settings will be merged with the configuration directory `config.toml` and t
6464| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
6565| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
6666| `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` |
67+| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
68+| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
69+| `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`
6770
6871### `[editor.statusline]` Section
6972
@@ -89,9 +92,9 @@ The `[editor.statusline]` key takes the following sub-keys:
8992
9093| Key | Description | Default |
9194| --- | --- | --- |
92−| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` |
95+| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
9396| `center` | A list of elements aligned to the middle of the statusline | `[]` |
94−| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "position", "file-encoding"]` |
97+| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
9598| `separator` | The character used to separate elements in the statusline | `"│"` |
9699| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
97100| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
@@ -108,6 +111,7 @@ The following statusline elements can be configured:
108111| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
109112| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
110113| `file-line-ending` | The file line endings (CRLF or LF) |
114+| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
111115| `total-line-numbers` | The total line numbers of the opened file |
112116| `file-type` | The type of the opened file |
113117| `diagnostics` | The number of warnings and/or errors |
@@ -164,16 +168,31 @@ All git related options are only enabled in a git repository.
164168
165169| Key | Description | Default |
166170|--|--|---------|
167−|`hidden` | Enables ignoring hidden files | true
168−|`follow-symlinks` | Follow symlinks instead of ignoring them | true
169−|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | true
170−|`parents` | Enables reading ignore files from parent directories | true
171−|`ignore` | Enables reading `.ignore` files | true
172−|`git-ignore` | Enables reading `.gitignore` files | true
173−|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludefile` option | true
174−|`git-exclude` | Enables reading `.git/info/exclude` files | true
171+|`hidden` | Enables ignoring hidden files | `true`
172+|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
173+|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true`
174+|`parents` | Enables reading ignore files from parent directories | `true`
175+|`ignore` | Enables reading `.ignore` files | `true`
176+|`git-ignore` | Enables reading `.gitignore` files | `true`
177+|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
178+|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
175179|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
176180
181+Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
182+
183+Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
184+- Linux and Mac: `~/.config/helix/ignore`
185+- Windows: `%AppData%\helix\ignore`
186+
187+Example:
188+
189+```ini
190+# unignore in file picker and global search
191+!.github/
192+!.gitignore
193+!.gitattributes
194+```
195+
177196### `[editor.auto-pairs]` Section
178197
179198Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
@@ -347,3 +366,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
347366max-indent-retain = 0
348367wrap-indicator = "" # set wrap-indicator to "" to hide it
349368```
369+
370+### `[editor.smart-tab]` Section
371+
372+
373+| Key | Description | Default |
374+|------------|-------------|---------|
375+| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
376+| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
book/src/generated/lang-support.md+34−15
@@ -1,8 +1,9 @@
11| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
22| --- | --- | --- | --- | --- |
3+| agda | ✓ | | | |
34| astro | ✓ | | | |
45| awk | ✓ | ✓ | | `awk-language-server` |
5−| bash | ✓ | | ✓ | `bash-language-server` |
6+| bash | ✓ | ✓ | ✓ | `bash-language-server` |
67| bass | ✓ | | | `bass` |
78| beancount | ✓ | | | |
89| bibtex | ✓ | | | `texlab` |
@@ -10,13 +11,13 @@
1011| blueprint | ✓ | | | `blueprint-compiler` |
1112| c | ✓ | ✓ | ✓ | `clangd` |
1213| c-sharp | ✓ | ✓ | | `OmniSharp` |
13−| cabal | | | | |
14+| cabal | | | | `haskell-language-server-wrapper` |
1415| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
1516| capnp | ✓ | | ✓ | |
1617| clojure | ✓ | | | `clojure-lsp` |
1718| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
1819| comment | ✓ | | | |
19−| common-lisp | ✓ | | | `cl-lsp` |
20+| common-lisp | ✓ | | ✓ | `cl-lsp` |
2021| cpon | ✓ | | ✓ | |
2122| cpp | ✓ | ✓ | ✓ | `clangd` |
2223| crystal | ✓ | ✓ | | `crystalline` |
@@ -24,6 +25,7 @@
2425| cue | ✓ | | | `cuelsp` |
2526| d | ✓ | ✓ | ✓ | `serve-d` |
2627| dart | ✓ | | ✓ | `dart` |
28+| dbml | ✓ | | | |
2729| devicetree | ✓ | | | |
2830| dhall | ✓ | ✓ | | `dhall-lsp-server` |
2931| diff | ✓ | | | |
@@ -44,7 +46,9 @@
4446| forth | ✓ | | | `forth-lsp` |
4547| fortran | ✓ | | ✓ | `fortls` |
4648| fsharp | ✓ | | | `fsautocomplete` |
49+| gas | ✓ | ✓ | | |
4750| gdscript | ✓ | ✓ | ✓ | |
51+| gemini | ✓ | | | |
4852| git-attributes | ✓ | | | |
4953| git-commit | ✓ | ✓ | | |
5054| git-config | ✓ | | | |
@@ -52,12 +56,13 @@
5256| git-rebase | ✓ | | | |
5357| gleam | ✓ | ✓ | | `gleam` |
5458| glsl | ✓ | ✓ | ✓ | |
55−| go | ✓ | ✓ | ✓ | `gopls` |
59+| gn | ✓ | | | |
60+| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
5661| godot-resource | ✓ | | | |
5762| gomod | ✓ | | | `gopls` |
5863| gotmpl | ✓ | | | `gopls` |
5964| gowork | ✓ | | | `gopls` |
60−| graphql | ✓ | | | |
65+| graphql | ✓ | | | `graphql-lsp` |
6166| hare | ✓ | | | |
6267| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
6368| haskell-persistent | ✓ | | | |
@@ -69,15 +74,18 @@
6974| idris | | | | `idris2-lsp` |
7075| iex | ✓ | | | |
7176| ini | ✓ | | | |
72−| java | ✓ | ✓ | | `jdtls` |
77+| janet | ✓ | | | |
78+| java | ✓ | ✓ | ✓ | `jdtls` |
7379| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
80+| jinja | ✓ | | | |
7481| jsdoc | ✓ | | | |
7582| json | ✓ | | ✓ | `vscode-json-language-server` |
83+| json5 | ✓ | | | |
7684| jsonnet | ✓ | | | `jsonnet-language-server` |
7785| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
7886| julia | ✓ | ✓ | ✓ | `julia` |
7987| just | ✓ | ✓ | ✓ | |
80−| kdl | ✓ | | | |
88+| kdl | ✓ | ✓ | ✓ | |
8189| kotlin | ✓ | | | `kotlin-language-server` |
8290| latex | ✓ | ✓ | | `texlab` |
8391| lean | ✓ | | | `lean` |
@@ -85,6 +93,8 @@
8593| llvm | ✓ | ✓ | ✓ | |
8694| llvm-mir | ✓ | ✓ | ✓ | |
8795| llvm-mir-yaml | ✓ | | ✓ | |
96+| log | ✓ | | | |
97+| lpf | ✓ | | | |
8898| lua | ✓ | ✓ | ✓ | `lua-language-server` |
8999| make | ✓ | | | |
90100| markdoc | ✓ | | | `markdoc-ls` |
@@ -99,7 +109,8 @@
99109| nickel | ✓ | | ✓ | `nls` |
100110| nim | ✓ | ✓ | ✓ | `nimlangserver` |
101111| nix | ✓ | | | `nil` |
102−| nu | ✓ | | | |
112+| nu | ✓ | | | `nu` |
113+| nunjucks | ✓ | | | |
103114| ocaml | ✓ | | ✓ | `ocamllsp` |
104115| ocaml-interface | ✓ | | | `ocamllsp` |
105116| odin | ✓ | | ✓ | `ols` |
@@ -109,19 +120,20 @@
109120| pascal | ✓ | ✓ | | `pasls` |
110121| passwd | ✓ | | | |
111122| pem | ✓ | | | |
112−| perl | ✓ | | | `perlnavigator` |
123+| perl | ✓ | ✓ | ✓ | `perlnavigator` |
113124| php | ✓ | ✓ | ✓ | `intelephense` |
114125| po | ✓ | ✓ | | |
126+| pod | ✓ | | | |
115127| ponylang | ✓ | ✓ | ✓ | |
116128| prisma | ✓ | | | `prisma-language-server` |
117129| prolog | | | | `swipl` |
118−| protobuf | ✓ | | ✓ | |
130+| protobuf | ✓ | | ✓ | `bufls`, `pb` |
119131| prql | ✓ | | | |
120−| purescript | ✓ | | | `purescript-language-server` |
132+| purescript | ✓ | ✓ | | `purescript-language-server` |
121133| python | ✓ | ✓ | ✓ | `pylsp` |
122134| qml | ✓ | | ✓ | `qmlls` |
123135| r | ✓ | | | `R` |
124−| racket | ✓ | | | `racket` |
136+| racket | ✓ | | ✓ | `racket` |
125137| regex | ✓ | | | |
126138| rego | ✓ | | | `regols` |
127139| rescript | ✓ | ✓ | | `rescript-language-server` |
@@ -133,30 +145,36 @@
133145| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
134146| sage | ✓ | ✓ | | |
135147| scala | ✓ | | ✓ | `metals` |
136−| scheme | ✓ | | | |
148+| scheme | ✓ | | ✓ | |
137149| scss | ✓ | | | `vscode-css-language-server` |
138150| slint | ✓ | | ✓ | `slint-lsp` |
151+| smali | ✓ | | ✓ | |
139152| smithy | ✓ | | | `cs` |
140153| sml | ✓ | | | |
141154| solidity | ✓ | | | `solc` |
142155| sql | ✓ | | | |
143156| sshclientconfig | ✓ | | | |
144157| starlark | ✓ | ✓ | | |
158+| strace | ✓ | | | |
145159| svelte | ✓ | | ✓ | `svelteserver` |
146160| sway | ✓ | ✓ | ✓ | `forc` |
147161| swift | ✓ | | | `sourcekit-lsp` |
148162| t32 | ✓ | | | |
149163| tablegen | ✓ | ✓ | ✓ | |
150164| task | ✓ | | | |
165+| templ | ✓ | | | `templ` |
151166| tfvars | ✓ | | ✓ | `terraform-ls` |
167+| todotxt | ✓ | | | |
152168| toml | ✓ | | | `taplo` |
153169| tsq | ✓ | | | |
154170| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
155171| twig | ✓ | | | |
156172| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
173+| typst | ✓ | | | `typst-lsp` |
157174| ungrammar | ✓ | | | |
175+| unison | ✓ | | | |
158176| uxntal | ✓ | | | |
159−| v | ✓ | ✓ | ✓ | `v` |
177+| v | ✓ | ✓ | ✓ | `v-analyzer` |
160178| vala | ✓ | | | `vala-language-server` |
161179| verilog | ✓ | ✓ | | `svlangserver` |
162180| vhdl | ✓ | | | `vhdl_ls` |
@@ -167,8 +185,9 @@
167185| webc | ✓ | | | |
168186| wgsl | ✓ | | | `wgsl_analyzer` |
169187| wit | ✓ | | ✓ | |
188+| wren | ✓ | ✓ | ✓ | |
170189| xit | ✓ | | | |
171190| xml | ✓ | | ✓ | |
172−| yaml | ✓ | | ✓ | `yaml-language-server` |
191+| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
173192| yuck | ✓ | | | |
174193| zig | ✓ | ✓ | ✓ | `zls` |
book/src/generated/typable-cmd.md+4−1
@@ -17,7 +17,7 @@
1717| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) |
1818| `:new`, `:n` | Create a new scratch buffer. |
1919| `:format`, `:fmt` | Format the file using the LSP formatter. |
20−| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
20+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.) |
2121| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
2222| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
2323| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
@@ -55,6 +55,7 @@
5555| `:lsp-restart` | Restarts the language servers used by the current doc |
5656| `:lsp-stop` | Stops the language servers that are used by the current doc |
5757| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
58+| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. |
5859| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
5960| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
6061| `:debug-eval` | Evaluate expression in current debug context. |
@@ -83,3 +84,5 @@
8384| `:run-shell-command`, `:sh` | Run a shell command |
8485| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
8586| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
87+| `:redraw` | Clear and re-render the whole UI |
88+| `:move` | Move the current buffer and its corresponding file to a different path |
book/src/guides/adding_languages.md+2
@@ -36,6 +36,7 @@ below.
36363. Refer to the
3737 [tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
3838 for more information on writing queries.
39+4. A list of highlight captures can be found [on the themes page](https://docs.helix-editor.com/themes.html#scopes).
3940
4041> 💡 In Helix, the first matching query takes precedence when evaluating
4142> queries, which is different from other editors such as Neovim where the last
@@ -51,3 +52,4 @@ below.
5152 grammars.
5253- If a parser is causing a segfault, or you want to remove it, make sure to
5354 remove the compiled parser located at `runtime/grammars/<name>.so`.
55+- If you are attempting to add queries and Helix is unable to locate them, ensure that the environment variable `HELIX_RUNTIME` is set to the location of the `runtime` folder you're developing in.
book/src/guides/indent.md+319−43
@@ -1,76 +1,308 @@
11# Adding indent queries
22
3−Helix uses tree-sitter to correctly indent new lines. This requires
4−a tree-sitter grammar and an `indent.scm` query file placed in
5−`runtime/queries/{language}/indents.scm`. The indentation for a line
6−is calculated by traversing the syntax tree from the lowest node at the
7−beginning of the new line. Each of these nodes contributes to the total
8−indent when it is captured by the query (in what way depends on the name
9−of the capture).
3+Helix uses tree-sitter to correctly indent new lines. This requires a tree-
4+sitter grammar and an `indent.scm` query file placed in `runtime/queries/
5+{language}/indents.scm`. The indentation for a line is calculated by traversing
6+the syntax tree from the lowest node at the beginning of the new line (see
7+[Indent queries](#indent-queries)). Each of these nodes contributes to the total
8+indent when it is captured by the query (in what way depends on the name of
9+the capture.
1010
1111Note that it matters where these added indents begin. For example,
1212multiple indent level increases that start on the same line only increase
13−the total indent level by 1.
13+the total indent level by 1. See [Capture types](#capture-types).
1414
15−## Scopes
15+By default, Helix uses the `hybrid` indentation heuristic. This means that
16+indent queries are not used to compute the expected absolute indentation of a
17+line but rather the expected difference in indentation between the new and an
18+already existing line. This difference is then added to the actual indentation
19+of the already existing line. Since this makes errors in the indent queries
20+harder to find, it is recommended to disable it when testing via
21+`:set indent-heuristic tree-sitter`. The rest of this guide assumes that
22+the `tree-sitter` heuristic is used.
1623
17−Added indents don't always apply to the whole node. For example, in most
18−cases when a node should be indented, we actually only want everything
19−except for its first line to be indented. For this, there are several
20−scopes (more scopes may be added in the future if required):
24+## Indent queries
2125
22−- `all`:
23−This scope applies to the whole captured node. This is only different from
24−`tail` when the captured node is the first node on its line.
26+When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
27+the indent level for the new line, the query in `indents.scm` is run on the
28+document. The starting position of the query is the end of the line above where
29+a new line will be inserted.
2530
26−- `tail`:
27−This scope applies to everything except for the first line of the
28−captured node.
31+For `o`, the inserted line is the line below the cursor, so that starting
32+position of the query is the end of the current line.
2933
30−Every capture type has a default scope which should do the right thing
31−in most situations. When a different scope is required, this can be
32−changed by using a `#set!` declaration anywhere in the pattern:
33−```scm
34−(assignment_expression
35− right: (_) @indent
36− (#set! "scope" "all"))
34+```rust
35+fn need_hero(some_hero: Hero, life: Life) -> {
36+ matches!(some_hero, Hero { // ←─────────────────╮
37+ strong: true,//←╮ ↑ ↑ │
38+ fast: true, // │ │ ╰── query start │
39+ sure: true, // │ ╰───── cursor ├─ traversal
40+ soon: true, // ╰──────── new line inserted │ start node
41+ }) && // │
42+// ↑ │
43+// ╰───────────────────────────────────────────────╯
44+ some_hero > life
45+}
3746```
3847
39−## Capture types
48+For `O`, the newly inserted line is the *current* line, so the starting position
49+of the query is the end of the line above the cursor.
4050
41−- `@indent` (default scope `tail`):
42−Increase the indent level by 1. Multiple occurrences in the same line
43−don't stack. If there is at least one `@indent` and one `@outdent`
44−capture on the same line, the indent level isn't changed at all.
51+```rust
52+fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
53+ matches!(some_hero, Hero { // ←╮ ↑ │
54+ strong: true,// ↑ ╭───╯ │ │
55+ fast: true, // │ │ query start ─╯ │
56+ sure: true, // ╰───┼ cursor ├─ traversal
57+ soon: true, // ╰ new line inserted │ start node
58+ }) && // │
59+ some_hero > life // │
60+} // ←──────────────────────────────────────────────╯
61+```
4562
46−- `@outdent` (default scope `all`):
47−Decrease the indent level by 1. The same rules as for `@indent` apply.
63+From this starting node, the syntax tree is traversed up until the root node.
64+Each indent capture is collected along the way, and then combined according to
65+their [capture types](#capture-types) and [scopes](#scopes) to a final indent
66+level for the line.
4867
49−- `@extend`:
50−Extend the range of this node to the end of the line and to lines that
51−are indented more than the line that this node starts on. This is useful
52−for languages like Python, where for the purpose of indentation some nodes
53−(like functions or classes) should also contain indented lines that follow them.
68+### Capture types
5469
70+- `@indent` (default scope `tail`):
71+ Increase the indent level by 1. Multiple occurrences in the same line *do not*
72+ stack. If there is at least one `@indent` and one `@outdent` capture on the
73+ same line, the indent level isn't changed at all.
74+- `@outdent` (default scope `all`):
75+ Decrease the indent level by 1. The same rules as for `@indent` apply.
76+- `@indent.always` (default scope `tail`):
77+ Increase the indent level by 1. Multiple occurrences on the same line *do*
78+ stack. The final indent level is `@indent.always` – `@outdent.always`. If
79+ an `@indent` and an `@indent.always` are on the same line, the `@indent` is
80+ ignored.
81+- `@outdent.always` (default scope `all`):
82+ Decrease the indent level by 1. The same rules as for `@indent.always` apply.
83+- `@align` (default scope `all`):
84+ Align everything inside this node to some anchor. The anchor is given
85+ by the start of the node captured by `@anchor` in the same pattern.
86+ Every pattern with an `@align` should contain exactly one `@anchor`.
87+ Indent (and outdent) for nodes below (in terms of their starting line)
88+ the `@align` node is added to the indentation required for alignment.
89+- `@extend`:
90+ Extend the range of this node to the end of the line and to lines that are
91+ indented more than the line that this node starts on. This is useful for
92+ languages like Python, where for the purpose of indentation some nodes (like
93+ functions or classes) should also contain indented lines that follow them.
5594- `@extend.prevent-once`:
56−Prevents the first extension of an ancestor of this node. For example, in Python
57−a return expression always ends the block that it is in. Note that this only stops the
58−extension of the next `@extend` capture. If multiple ancestors are captured,
59−only the extension of the innermost one is prevented. All other ancestors are unaffected
60−(regardless of whether the innermost ancestor would actually have been extended).
95+ Prevents the first extension of an ancestor of this node. For example, in Python
96+ a return expression always ends the block that it is in. Note that this only
97+ stops the extension of the next `@extend` capture. If multiple ancestors are
98+ captured, only the extension of the innermost one is prevented. All other
99+ ancestors are unaffected (regardless of whether the innermost ancestor would
100+ actually have been extended).
101+
102+#### `@indent` / `@outdent`
103+
104+Consider this example:
105+
106+```rust
107+fn shout(things: Vec<Thing>) {
108+ // ↑
109+ // ├───────────────────────╮ indent level
110+ // @indent ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
111+ // │
112+ let it_all = |out| { things.filter(|thing| { // │ 1
113+ // ↑ ↑ │
114+ // ├───────────────────────┼─────┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄
115+ // @indent @indent │
116+ // │ 2
117+ thing.can_do_with(out) // │
118+ })}; // ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
119+ //↑↑↑ │ 1
120+} //╰┼┴──────────────────────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄
121+// 3x @outdent
122+```
123+
124+```scm
125+((block) @indent)
126+["}" ")"] @outdent
127+```
128+
129+Note how on the second line, we have two blocks begin on the same line. In this
130+case, since both captures occur on the same line, they are combined and only
131+result in a net increase of 1. Also note that the closing `}`s are part of the
132+`@indent` captures, but the 3 `@outdent`s also combine into 1 and result in that
133+line losing one indent level.
134+
135+#### `@extend` / `@extend.prevent-once`
136+
137+For an example of where `@extend` can be useful, consider Python, which is
138+whitespace-sensitive.
139+
140+```scm
141+]
142+ (parenthesized_expression)
143+ (function_definition)
144+ (class_definition)
145+] @indent
146+
147+```
148+
149+```python
150+class Hero:
151+ def __init__(self, strong, fast, sure, soon):# ←─╮
152+ self.is_strong = strong # │
153+ self.is_fast = fast # ╭─── query start │
154+ self.is_sure = sure # │ ╭─ cursor │
155+ self.is_soon = soon # │ │ │
156+ # ↑ ↑ │ │ │
157+ # │ ╰──────╯ │ │
158+ # ╰─────────────────────╯ │
159+ # ├─ traversal
160+ def need_hero(self, life): # │ start node
161+ return ( # │
162+ self.is_strong # │
163+ and self.is_fast # │
164+ and self.is_sure # │
165+ and self.is_soon # │
166+ and self > life # │
167+ ) # ←─────────────────────────────────────────╯
168+```
169+
170+Without braces to catch the scope of the function, the smallest descendant of
171+the cursor on a line feed ends up being the entire inside of the class. Because
172+of this, it will miss the entire function node and its indent capture, leading
173+to an indent level one too small.
174+
175+To address this case, `@extend` tells helix to "extend" the captured node's span
176+to the line feed and every consecutive line that has a greater indent level than
177+the line of the node.
178+
179+```scm
180+(parenthesized_expression) @indent
181+
182+]
183+ (function_definition)
184+ (class_definition)
185+] @indent @extend
186+
187+```
188+
189+```python
190+class Hero:
191+ def __init__(self, strong, fast, sure, soon):# ←─╮
192+ self.is_strong = strong # │
193+ self.is_fast = fast # ╭─── query start ├─ traversal
194+ self.is_sure = sure # │ ╭─ cursor │ start node
195+ self.is_soon = soon # │ │ ←───────────────╯
196+ # ↑ ↑ │ │
197+ # │ ╰──────╯ │
198+ # ╰─────────────────────╯
199+ def need_hero(self, life):
200+ return (
201+ self.is_strong
202+ and self.is_fast
203+ and self.is_sure
204+ and self.is_soon
205+ and self > life
206+ )
207+```
61208
209+Furthermore, there are some cases where extending to everything with a greater
210+indent level may not be desirable. Consider the `need_hero` function above. If
211+our cursor is on the last line of the returned expression.
212+
213+```python
214+class Hero:
215+ def __init__(self, strong, fast, sure, soon):
216+ self.is_strong = strong
217+ self.is_fast = fast
218+ self.is_sure = sure
219+ self.is_soon = soon
220+
221+ def need_hero(self, life):
222+ return (
223+ self.is_strong
224+ and self.is_fast
225+ and self.is_sure
226+ and self.is_soon
227+ and self > life
228+ ) # ←─── cursor
229+ #←────────── where cursor should go on new line
230+```
231+
232+In Python, the are a few tokens that will always end a scope, such as a return
233+statement. Since the scope ends, so should the indent level. But because the
234+function span is extended to every line with a greater indent level, a new line
235+would just continue on the same level. And an `@outdent` would not help us here
236+either, since it would cause everything in the parentheses to become outdented
237+as well.
238+
239+To help, we need to signal an end to the extension. We can do this with
240+`@extend.prevent-once`.
241+
242+```scm
243+(parenthesized_expression) @indent
244+
245+]
246+ (function_definition)
247+ (class_definition)
248+] @indent @extend
249+
250+(return_statement) @extend.prevent-once
251+```
252+
253+#### `@indent.always` / `@outdent.always`
254+
255+As mentioned before, normally if there is more than one `@indent` or `@outdent`
256+capture on the same line, they are combined.
257+
258+Sometimes, there are cases when you may want to ensure that every indent capture
259+is additive, regardless of how many occur on the same line. Consider this
260+example in YAML.
261+
262+```yaml
263+ - foo: bar
264+# ↑ ↑
265+# │ ╰─────────────── start of map
266+# ╰───────────────── start of list element
267+ baz: quux # ←─── cursor
268+ # ←───────────── where the cursor should go on a new line
269+ garply: waldo
270+ - quux:
271+ bar: baz
272+ xyzzy: thud
273+ fred: plugh
274+```
275+
276+In YAML, you often have lists of maps. In these cases, the syntax is such that
277+the list element and the map both start on the same line. But we really do want
278+to start an indentation for each of these so that subsequent keys in the map
279+hang over the list and align properly. This is where `@indent.always` helps.
280+
281+```scm
282+((block_sequence_item) @item @indent.always @extend
283+ (#not-one-line? @item))
284+
285+((block_mapping_pair
286+ key: (_) @key
287+ value: (_) @val
288+ (#not-same-line? @key @val)
289+ ) @indent.always @extend
290+)
291+```
62292
63293## Predicates
64294
65295In some cases, an S-expression cannot express exactly what pattern should be matched.
66296For that, tree-sitter allows for predicates to appear anywhere within a pattern,
67297similar to how `#set!` declarations work:
298+
68299```scm
69300(some_kind
70301 (child_kind) @indent
71302 (#predicate? arg1 arg2 ...)
72303)
73304```
305+
74306The number of arguments depends on the predicate that's used.
75307Each argument is either a capture (`@name`) or a string (`"some string"`).
76308The following predicates are supported by tree-sitter:
@@ -91,3 +323,47 @@ argument (a string).
91323
92324- `#same-line?`/`#not-same-line?`:
93325The captures given by the 2 arguments must/must not start on the same line.
326+
327+- `#one-line?`/`#not-one-line?`:
328+The captures given by the fist argument must/must span a total of one line.
329+
330+### Scopes
331+
332+Added indents don't always apply to the whole node. For example, in most
333+cases when a node should be indented, we actually only want everything
334+except for its first line to be indented. For this, there are several
335+scopes (more scopes may be added in the future if required):
336+
337+- `tail`:
338+This scope applies to everything except for the first line of the
339+captured node.
340+- `all`:
341+This scope applies to the whole captured node. This is only different from
342+`tail` when the captured node is the first node on its line.
343+
344+For example, imagine we have the following function
345+
346+```rust
347+fn aha() { // ←─────────────────────────────────────╮
348+ let take = "on me"; // ←──────────────╮ scope: │
349+ let take = "me on"; // ├─ "tail" ├─ (block) @indent
350+ let ill = be_gone_days(1 || 2); // │ │
351+} // ←───────────────────────────────────┴──────────┴─ "}" @outdent
352+ // scope: "all"
353+```
354+
355+We can write the following query with the `#set!` declaration:
356+
357+ ```scm
358+ ((block) @indent
359+ (#set! "scope" "tail"))
360+ ("}" @outdent
361+ (#set! "scope" "all"))
362+ ```
363+
364+As we can see, the "tail" scope covers the node, except for the first line.
365+Everything up to and including the closing brace gets an indent level of 1.
366+Then, on the closing brace, we encounter an outdent with a scope of "all", which
367+means the first line is included, and the indent level is cancelled out on this
368+line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
369+written explicitly for demonstration.)
book/src/guides/injection.md+1−1
@@ -1,7 +1,7 @@
11# Adding Injection Queries
22
33Writing language injection queries allows one to highlight a specific node as a different language.
4−In addition to the [standard](upstream-docs) language injection options used by tree-sitter, there
4+In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there
55are a few Helix specific extensions that allow for more control.
66
77And example of a simple query that would highlight all strings as bash in Nix:
book/src/install.md+30−6
@@ -13,6 +13,7 @@
1313 - [AppImage](#appimage)
1414- [macOS](#macos)
1515 - [Homebrew Core](#homebrew-core)
16+ - [MacPorts](#macports)
1617- [Windows](#windows)
1718 - [Winget](#winget)
1819 - [Scoop](#scoop)
@@ -64,10 +65,7 @@ sudo apt install helix
6465
6566### Fedora/RHEL
6667
67−Enable the `COPR` repository for Helix:
68−
6968```sh
70−sudo dnf copr enable varlad/helix
7169sudo dnf install helix
7270```
7371
@@ -133,6 +131,12 @@ chmod +x helix-*.AppImage # change permission for executable mode
133131brew install helix
134132```
135133
134+### MacPorts
135+
136+```sh
137+port install helix
138+```
139+
136140## Windows
137141
138142Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
@@ -224,7 +228,7 @@ Or, create a symbolic link:
224228ln -Ts $PWD/runtime ~/.config/helix/runtime
225229```
226230
227−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.
231+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.
228232
229233#### Windows
230234
@@ -257,12 +261,32 @@ following order:
2572611. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
258262 developing and testing helix only).
2592632. `runtime/` subdirectory of OS-dependent helix user config directory.
260−3. `$HELIX_RUNTIME`.
261−4. `runtime/` subdirectory of path to Helix executable.
264+3. `$HELIX_RUNTIME`
265+4. Distribution-specific fallback directory (set at compile time—not run time—
266+ with the `HELIX_DEFAULT_RUNTIME` environment variable)
267+5. `runtime/` subdirectory of path to Helix executable.
262268
263269This order also sets the priority for selecting which file will be used if multiple runtime
264270directories have files with the same name.
265271
272+#### Note to packagers
273+
274+If you are making a package of Helix for end users, to provide a good out of
275+the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
276+variable at build time (before invoking `cargo build`) to a directory which
277+will store the final runtime files after installation. For example, say you want
278+to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
279+script could follow are:
280+
281+1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
282+1. `cargo build --profile opt --locked --path helix-term`
283+1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
284+1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
285+
286+This way the resulting `hx` binary will always look for its runtime directory in
287+`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
288+or `HELIX_RUNTIME`.
289+
266290### Validating the installation
267291
268292To make sure everything is set up as expected you should run the Helix health
book/src/keymap.md+16−15
@@ -25,7 +25,7 @@
2525
2626## Normal mode
2727
28−Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
28+Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
2929
3030### Movement
3131
@@ -291,7 +291,7 @@ This layer is a kludge of mappings, mostly pickers.
291291| `w` | Enter [window mode](#window-mode) | N/A |
292292| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
293293| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
294−| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
294+| `y` | Yank selections to clipboard | `yank_to_clipboard` |
295295| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
296296| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
297297| `/` | Global search in workspace folder | `global_search` |
@@ -411,19 +411,20 @@ you to selectively add search terms to your selections.
411411
412412Keys to use within picker. Remapping currently not supported.
413413
414−| Key | Description |
415−| ----- | ------------- |
416−| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
417−| `Tab`, `Down`, `Ctrl-n` | Next entry |
418−| `PageUp`, `Ctrl-u` | Page up |
419−| `PageDown`, `Ctrl-d` | Page down |
420−| `Home` | Go to first entry |
421−| `End` | Go to last entry |
422−| `Enter` | Open selected |
423−| `Ctrl-s` | Open horizontally |
424−| `Ctrl-v` | Open vertically |
425−| `Ctrl-t` | Toggle preview |
426−| `Escape`, `Ctrl-c` | Close picker |
414+| Key | Description |
415+| ----- | ------------- |
416+| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
417+| `Tab`, `Down`, `Ctrl-n` | Next entry |
418+| `PageUp`, `Ctrl-u` | Page up |
419+| `PageDown`, `Ctrl-d` | Page down |
420+| `Home` | Go to first entry |
421+| `End` | Go to last entry |
422+| `Enter` | Open selected |
423+| `Alt-Enter` | Open selected in the background without closing the picker |
424+| `Ctrl-s` | Open horizontally |
425+| `Ctrl-v` | Open vertically |
426+| `Ctrl-t` | Toggle preview |
427+| `Escape`, `Ctrl-c` | Close picker |
427428
428429## Prompt
429430
book/src/languages.md+3−1
@@ -66,8 +66,10 @@ These configuration keys are available:
6666| `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) |
6767| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
6868| `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 |
69+| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
6970| `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` |
7071| `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. |
72+| `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.
7173
7274### File-type detection and the `file-types` key
7375
@@ -127,7 +129,7 @@ These are the available options for a language server.
127129| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
128130
129131A `format` sub-table within `config` can be used to pass extra formatting options to
130−[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
132+[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
131133For example, with typescript:
132134
133135```toml
book/src/themes.md+9−1
@@ -70,6 +70,7 @@ over it and is merged into the default palette.
7070
7171| Color Name |
7272| --- |
73+| `default` |
7374| `black` |
7475| `red` |
7576| `green` |
@@ -154,6 +155,7 @@ We use a similar set of scopes as
154155
155156- `type` - Types
156157 - `builtin` - Primitive types provided by the language (`int`, `usize`)
158+ - `parameter` - Generic type parameters (`T`)
157159 - `enum`
158160 - `variant`
159161- `constructor`
@@ -244,9 +246,12 @@ We use a similar set of scopes as
244246
245247- `diff` - version control changes
246248 - `plus` - additions
249+ - `gutter` - gutter indicator
247250 - `minus` - deletions
251+ - `gutter` - gutter indicator
248252 - `delta` - modifications
249253 - `moved` - renamed or moved files/changes
254+ - `gutter` - gutter indicator
250255
251256#### Interface
252257
@@ -291,11 +296,14 @@ These scopes are used for theming the editor interface:
291296| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
292297| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
293298| `ui.statusline.separator` | Separator character in statusline |
299+| `ui.bufferline` | Style for the buffer line |
300+| `ui.bufferline.active` | Style for the active buffer in buffer line |
301+| `ui.bufferline.background` | Style for bufferline background |
294302| `ui.popup` | Documentation popups (e.g. Space + k) |
295303| `ui.popup.info` | Prompt for multiple key options |
296304| `ui.window` | Borderlines separating splits |
297305| `ui.help` | Description box for commands |
298−| `ui.text` | Command prompts, popup text, etc. |
306+| `ui.text` | Default text style, command prompts, popup text, etc. |
299307| `ui.text.focus` | The currently selected line in the picker |
300308| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
301309| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
book/src/usage.md+21−5
@@ -37,19 +37,35 @@ If a register is selected before invoking a change or delete command, the select
3737- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
3838- `"md` - Store the selection in register `m` and delete it.
3939
40−### Special registers
40+### Default registers
41+
42+Commands that use registers, like yank (`y`), use a default register if none is specified.
43+These registers are used as defaults:
4144
4245| Register character | Contains |
4346| --- | --- |
4447| `/` | Last search |
4548| `:` | Last executed command |
4649| `"` | Last yanked text |
47−| `_` | Black hole |
50+| `@` | Last recorded macro |
4851
49−The system clipboard is not directly supported by a special register. Instead, special commands and keybindings are provided. Refer to the
50−[key map](keymap.md#space-mode) for more details.
52+### Special registers
5153
52−The black hole register is a no-op register, meaning that no data will be read or written to it.
54+Some registers have special behavior when read from and written to.
55+
56+| Register character | When read | When written |
57+| --- | --- | --- |
58+| `_` | No values are returned | All values are discarded |
59+| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
60+| `.` | Contents of the current selections | This register is not writable |
61+| `%` | Name of the current file | This register is not writable |
62+| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
63+| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
64+
65+When yanking multiple selections to the clipboard registers, the selections
66+are joined with newlines. Pasting from these registers will paste multiple
67+selections if the clipboard was last yanked to by the Helix session. Otherwise
68+the clipboard contents are pasted as one selection.
5369
5470## Surround
5571
book/theme/index.hbs+18−15
@@ -1,11 +1,11 @@
11<!DOCTYPE HTML>
2−<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
2+<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
33 <head>
44 <!-- Book generated using mdBook -->
55 <meta charset="UTF-8">
66 <title>{{ title }}</title>
77 {{#if is_print }}
8− <meta name="robots" content="noindex" />
8+ <meta name="robots" content="noindex">
99 {{/if}}
1010 {{#if base_url}}
1111 <base href="{{ base_url }}">
@@ -17,7 +17,7 @@
1717
1818 <meta name="description" content="{{ description }}">
1919 <meta name="viewport" content="width=device-width, initial-scale=1">
20− <meta name="theme-color" content="#ffffff" />
20+ <meta name="theme-color" content="#ffffff">
2121
2222 {{#if favicon_svg}}
2323 <link rel="icon" href="{{ path_to_root }}favicon.svg">
@@ -34,8 +34,6 @@
3434
3535 <!-- Fonts -->
3636 <link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
37− <link rel="preconnect" href="https://fonts.gstatic.com">
38− <link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;400;500;700&display=swap" rel="stylesheet">
3937 {{#if copy_fonts}}
4038 <link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
4139 {{/if}}
@@ -55,7 +53,7 @@
5553 <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
5654 {{/if}}
5755 </head>
58− <body>
56+ <body class="sidebar-visible no-js">
5957 <div id="body-container">
6058 <!-- Provide site root to javascript -->
6159 <script>
@@ -85,24 +83,29 @@
8583 try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
8684 if (theme === null || theme === undefined) { theme = default_theme; }
8785 var html = document.querySelector('html');
88− html.classList.remove('no-js')
8986 html.classList.remove('{{ default_theme }}')
9087 html.classList.add(theme);
91− html.classList.add('js');
88+ var body = document.querySelector('body');
89+ body.classList.remove('no-js')
90+ body.classList.add('js');
9291 </script>
9392
93+ <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
94+
9495 <!-- Hide / unhide sidebar before it is displayed -->
9596 <script>
96− var html = document.querySelector('html');
97+ var body = document.querySelector('body');
9798 var sidebar = null;
99+ var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
98100 if (document.body.clientWidth >= 1080) {
99101 try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
100102 sidebar = sidebar || 'visible';
101103 } else {
102104 sidebar = 'hidden';
103105 }
104− html.classList.remove('sidebar-visible');
105− html.classList.add("sidebar-" + sidebar);
106+ sidebar_toggle.checked = sidebar === 'visible';
107+ body.classList.remove('sidebar-visible');
108+ body.classList.add("sidebar-" + sidebar);
106109 </script>
107110
108111 <nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -141,9 +144,9 @@
141144 <div id="menu-bar-hover-placeholder"></div>
142145 <div id="menu-bar" class="menu-bar sticky">
143146 <div class="left-buttons">
144− <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
147+ <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
145148 <i class="fa fa-bars"></i>
146− </button>
149+ </label>
147150 <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
148151 <i class="fa fa-paint-brush"></i>
149152 </button>
@@ -220,7 +223,7 @@
220223 {{/previous}}
221224
222225 {{#next}}
223− <a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
226+ <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
224227 <i class="fa fa-angle-right"></i>
225228 </a>
226229 {{/next}}
@@ -238,7 +241,7 @@
238241 {{/previous}}
239242
240243 {{#next}}
241− <a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
244+ <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
242245 <i class="fa fa-angle-right"></i>
243246 </a>
244247 {{/next}}
contrib/Helix.appdata.xml+15
@@ -5,6 +5,7 @@
55 <project_license>MPL-2.0</project_license>
66 <name>Helix</name>
77 <summary>A post-modern text editor</summary>
8+ <summary xml:lang="ar">مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه</summary>
89
910 <description>
1011 <p>
@@ -17,6 +18,17 @@
1718 <li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
1819 </ul>
1920 </description>
21+ <description xml:lang="ar">
22+ <p>
23+ مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة.
24+ </p>
25+ <ul>
26+ <li>تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim</li>
27+ <li>تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة</li>
28+ <li>دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات</li>
29+ <li>تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter</li>
30+ </ul>
31+ </description>
2032
2133 <launchable type="desktop-id">Helix.desktop</launchable>
2234
@@ -36,6 +48,9 @@
3648 <content_rating type="oars-1.1" />
3749
3850 <releases>
51+ <release version="23.10" date="2023-10-24">
52+ <url>https://helix-editor.com/news/release-23-10-highlights/</url>
53+ </release>
3954 <release version="23.05" date="2023-05-18">
4055 <url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
4156 </release>
contrib/Helix.desktop+3−1
@@ -1,6 +1,7 @@
11[Desktop Entry]
22Name=Helix
33GenericName=Text Editor
4+GenericName[ar]=مُحَرِّرُ نُصُوص
45GenericName[de]=Texteditor
56GenericName[fr]=Éditeur de texte
67GenericName[ru]=Текстовый редактор
@@ -9,7 +10,7 @@ GenericName[tr]=Metin Düzenleyici
910Comment=Edit text files
1011Comment[af]=Redigeer tekslêers
1112Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
12−Comment[ar]=حرّر ملفات نصية
13+Comment[ar]=مُحَرِّرُ مِلَفَّاتٍ نَصِّيَّة
1314Comment[az]=Mətn fayllarını redaktə edin
1415Comment[be]=Рэдагаваньне тэкставых файлаў
1516Comment[bg]=Редактиране на текстови файлове
@@ -79,6 +80,7 @@ Exec=hx %F
7980Terminal=true
8081Type=Application
8182Keywords=Text;editor;
83+Keywords[ar]=نص;نصوص;محرر;
8284Keywords[fr]=Texte;éditeur;
8385Keywords[ru]=текст;текстовый редактор;
8486Keywords[sr]=Текст;едитор;
docs/CONTRIBUTING.md+10−3
@@ -29,9 +29,15 @@ files, run
2929cargo xtask docgen
3030```
3131
32−inside the project. We use [xtask][xtask] as an ad-hoc task runner and
33−thus do not require any dependencies other than `cargo` (You don't have
34−to `cargo install` anything either).
32+inside the project. We use [xtask][xtask] as an ad-hoc task runner.
33+
34+To preview the book itself, install [mdbook][mdbook]. Then, run
35+
36+```shell
37+mdbook serve book
38+```
39+
40+and visit [http://localhost:3000](http://localhost:3000).
3541
3642# Testing
3743
@@ -58,4 +64,5 @@ The current MSRV and future changes to the MSRV are listed in the [Firefox docum
5864[architecture.md]: ./architecture.md
5965[docs]: https://docs.helix-editor.com/
6066[xtask]: https://github.com/matklad/cargo-xtask
67+[mdbook]: https://rust-lang.github.io/mdBook/guide/installation.html
6168[helpers.rs]: ../helix-term/tests/test/helpers.rs
docs/releases.md+12−5
@@ -1,16 +1,23 @@
11## Checklist
22
33Helix releases are versioned in the Calendar Versioning scheme:
4−`YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions
5−we'll use `<tag>` as a placeholder for the tag being published.
4+`YY.0M(.MICRO)`, for example, `22.05` for May of 2022, or in a patch release,
5+`22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag
6+being published.
67
78* Merge the changelog PR
89* Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html)
910* Tag and push
1011 * `git tag -s -m "<tag>" -a <tag> && git push`
1112 * Make sure to switch to master and pull first
12−* Edit the `VERSION` file and change the date to the next planned release
13− * Releases are planned to happen every two months, so `22.05` would change to `22.07`
13+* Edit the `Cargo.toml` file and change the date in the `version` field to the next planned release
14+ * Due to Cargo having a strict requirement on SemVer with 3 or more version
15+ numbers, a `0` is required in the micro version; however, unless we are
16+ publishing a patch release after a major release, the `.0` is dropped in
17+ the user facing version.
18+ * Releases are planned to happen every two months, so `22.05.0` would change to `22.07.0`
19+ * If we are pushing a patch/bugfix release in the same month as the previous
20+ release, bump the micro version, e.g. `22.07.0` to `22.07.1`
1421* Wait for the Release CI to finish
1522 * It will automatically turn the git tag into a GitHub release when it uploads artifacts
1623* Edit the new release
@@ -57,4 +64,4 @@ versions for convenience:
5764> release. For the full log, check out the git log.
5865
5966Typically, small changes like dependencies or documentation updates, refactors,
60−or meta changes like GitHub Actions work are left out.
67+or meta changes like GitHub Actions work are left out.
flake.lock+21−237
@@ -1,126 +1,22 @@
11{
22 "nodes": {
33 "crane": {
4− "flake": false,
5− "locked": {
6− "lastModified": 1681175776,
7− "narHash": "sha256-7SsUy9114fryHAZ8p1L6G6YSu7jjz55FddEwa2U8XZc=",
8− "owner": "ipetkov",
9− "repo": "crane",
10− "rev": "445a3d222947632b5593112bb817850e8a9cf737",
11− "type": "github"
12− },
13− "original": {
14− "owner": "ipetkov",
15− "ref": "v0.12.1",
16− "repo": "crane",
17− "type": "github"
18− }
19− },
20− "dream2nix": {
214 "inputs": {
22− "all-cabal-json": [
23− "nci"
24− ],
25− "crane": "crane",
26− "devshell": [
27− "nci"
28− ],
29− "drv-parts": "drv-parts",
30− "flake-compat": "flake-compat",
31− "flake-parts": [
32− "nci",
33− "parts"
34− ],
35− "flake-utils-pre-commit": [
36− "nci"
37− ],
38− "ghc-utils": [
39− "nci"
40− ],
41− "gomod2nix": [
42− "nci"
43− ],
44− "mach-nix": [
45− "nci"
46− ],
47− "nix-pypi-fetcher": [
48− "nci"
49− ],
505 "nixpkgs": [
51− "nci",
526 "nixpkgs"
53− ],
54− "nixpkgsV1": "nixpkgsV1",
55− "poetry2nix": [
56− "nci"
57− ],
58− "pre-commit-hooks": [
59− "nci"
60− ],
61− "pruned-racket-catalog": [
62− "nci"
637 ]
648 },
659 "locked": {
66− "lastModified": 1683212002,
67− "narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=",
68− "owner": "nix-community",
69− "repo": "dream2nix",
70− "rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096",
71− "type": "github"
72− },
73− "original": {
74− "owner": "nix-community",
75− "repo": "dream2nix",
76− "type": "github"
77− }
78− },
79− "drv-parts": {
80− "inputs": {
81− "flake-compat": [
82− "nci",
83− "dream2nix",
84− "flake-compat"
85− ],
86− "flake-parts": [
87− "nci",
88− "dream2nix",
89− "flake-parts"
90− ],
91− "nixpkgs": [
92− "nci",
93− "dream2nix",
94− "nixpkgs"
95− ]
96− },
97− "locked": {
98− "lastModified": 1680698112,
99− "narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=",
100− "owner": "davhau",
101− "repo": "drv-parts",
102− "rev": "e8c2ec1157dc1edb002989669a0dbd935f430201",
103− "type": "github"
104− },
105− "original": {
106− "owner": "davhau",
107− "repo": "drv-parts",
108− "type": "github"
109− }
110− },
111− "flake-compat": {
112− "flake": false,
113− "locked": {
114− "lastModified": 1673956053,
115− "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
116− "owner": "edolstra",
117− "repo": "flake-compat",
118− "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
10+ "lastModified": 1701025348,
11+ "narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
12+ "owner": "ipetkov",
13+ "repo": "crane",
14+ "rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
11915 "type": "github"
12016 },
12117 "original": {
122− "owner": "edolstra",
123− "repo": "flake-compat",
18+ "owner": "ipetkov",
19+ "repo": "crane",
12420 "type": "github"
12521 }
12622 },
@@ -129,11 +25,11 @@
12925 "systems": "systems"
13026 },
13127 "locked": {
132− "lastModified": 1681202837,
133− "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
28+ "lastModified": 1694529238,
29+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
13430 "owner": "numtide",
13531 "repo": "flake-utils",
136− "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
32+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
13733 "type": "github"
13834 },
13935 "original": {
@@ -142,55 +38,13 @@
14238 "type": "github"
14339 }
14440 },
145− "mk-naked-shell": {
146− "flake": false,
147− "locked": {
148− "lastModified": 1681286841,
149− "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
150− "owner": "yusdacra",
151− "repo": "mk-naked-shell",
152− "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
153− "type": "github"
154− },
155− "original": {
156− "owner": "yusdacra",
157− "repo": "mk-naked-shell",
158− "type": "github"
159− }
160− },
161− "nci": {
162− "inputs": {
163− "dream2nix": "dream2nix",
164− "mk-naked-shell": "mk-naked-shell",
165− "nixpkgs": [
166− "nixpkgs"
167− ],
168− "parts": "parts",
169− "rust-overlay": [
170− "rust-overlay"
171− ]
172− },
173− "locked": {
174− "lastModified": 1683699050,
175− "narHash": "sha256-UWKQpzVcSshB+sU2O8CCHjOSTQrNS7Kk9V3+UeBsJpg=",
176− "owner": "yusdacra",
177− "repo": "nix-cargo-integration",
178− "rev": "ed27173cd1b223f598343ea3c15aacb1d140feac",
179− "type": "github"
180− },
181− "original": {
182− "owner": "yusdacra",
183− "repo": "nix-cargo-integration",
184− "type": "github"
185− }
186− },
18741 "nixpkgs": {
18842 "locked": {
189− "lastModified": 1683408522,
190− "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
43+ "lastModified": 1700794826,
44+ "narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
19145 "owner": "nixos",
19246 "repo": "nixpkgs",
193− "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
47+ "rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
19448 "type": "github"
19549 },
19650 "original": {
@@ -200,99 +54,29 @@
20054 "type": "github"
20155 }
20256 },
203− "nixpkgs-lib": {
204− "locked": {
205− "dir": "lib",
206− "lastModified": 1682879489,
207− "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
208− "owner": "NixOS",
209− "repo": "nixpkgs",
210− "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
211− "type": "github"
212− },
213− "original": {
214− "dir": "lib",
215− "owner": "NixOS",
216− "ref": "nixos-unstable",
217− "repo": "nixpkgs",
218− "type": "github"
219− }
220− },
221− "nixpkgsV1": {
222− "locked": {
223− "lastModified": 1678500271,
224− "narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=",
225− "owner": "NixOS",
226− "repo": "nixpkgs",
227− "rev": "5eb98948b66de29f899c7fe27ae112a47964baf8",
228− "type": "github"
229− },
230− "original": {
231− "id": "nixpkgs",
232− "ref": "nixos-22.11",
233− "type": "indirect"
234− }
235− },
236− "parts": {
237− "inputs": {
238− "nixpkgs-lib": [
239− "nci",
240− "nixpkgs"
241− ]
242− },
243− "locked": {
244− "lastModified": 1683560683,
245− "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
246− "owner": "hercules-ci",
247− "repo": "flake-parts",
248− "rev": "006c75898cf814ef9497252b022e91c946ba8e17",
249− "type": "github"
250− },
251− "original": {
252− "owner": "hercules-ci",
253− "repo": "flake-parts",
254− "type": "github"
255− }
256− },
257− "parts_2": {
258− "inputs": {
259− "nixpkgs-lib": "nixpkgs-lib"
260− },
261− "locked": {
262− "lastModified": 1683560683,
263− "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
264− "owner": "hercules-ci",
265− "repo": "flake-parts",
266− "rev": "006c75898cf814ef9497252b022e91c946ba8e17",
267− "type": "github"
268− },
269− "original": {
270− "owner": "hercules-ci",
271− "repo": "flake-parts",
272− "type": "github"
273− }
274− },
27557 "root": {
27658 "inputs": {
277− "nci": "nci",
59+ "crane": "crane",
60+ "flake-utils": "flake-utils",
27861 "nixpkgs": "nixpkgs",
279− "parts": "parts_2",
28062 "rust-overlay": "rust-overlay"
28163 }
28264 },
28365 "rust-overlay": {
28466 "inputs": {
285− "flake-utils": "flake-utils",
67+ "flake-utils": [
68+ "flake-utils"
69+ ],
28670 "nixpkgs": [
28771 "nixpkgs"
28872 ]
28973 },
29074 "locked": {
291− "lastModified": 1683771545,
292− "narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=",
75+ "lastModified": 1701137803,
76+ "narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
29377 "owner": "oxalica",
29478 "repo": "rust-overlay",
295− "rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed",
79+ "rev": "9dd940c967502f844eacea52a61e9596268d4f70",
29680 "type": "github"
29781 },
29882 "original": {
flake.nix+170−143
@@ -3,166 +3,193 @@
33
44 inputs = {
55 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6+ flake-utils.url = "github:numtide/flake-utils";
67 rust-overlay = {
78 url = "github:oxalica/rust-overlay";
8− inputs.nixpkgs.follows = "nixpkgs";
9+ inputs = {
10+ nixpkgs.follows = "nixpkgs";
11+ flake-utils.follows = "flake-utils";
12+ };
913 };
10− nci = {
11− url = "github:yusdacra/nix-cargo-integration";
14+ crane = {
15+ url = "github:ipetkov/crane";
1216 inputs.nixpkgs.follows = "nixpkgs";
13− inputs.rust-overlay.follows = "rust-overlay";
1417 };
15− parts.url = "github:hercules-ci/flake-parts";
1618 };
1719
18− outputs = inp: let
19− mkRootPath = rel:
20− builtins.path {
21− path = "${toString ./.}/${rel}";
22− name = rel;
23− };
24− filteredSource = let
25− pathsToIgnore = [
26− ".envrc"
27− ".ignore"
28− ".github"
29− ".gitignore"
30− "logo.svg"
31− "logo_dark.svg"
32− "logo_light.svg"
33− "rust-toolchain.toml"
34− "rustfmt.toml"
35− "runtime"
36− "screenshot.png"
37− "book"
38− "contrib"
39− "docs"
40− "README.md"
41− "CHANGELOG.md"
42− "shell.nix"
43− "default.nix"
44− "grammars.nix"
45− "flake.nix"
46− "flake.lock"
47− ];
48− ignorePaths = path: type: let
49− inherit (inp.nixpkgs) lib;
50− # split the nix store path into its components
51− components = lib.splitString "/" path;
52− # drop off the `/nix/hash-source` section from the path
53− relPathComponents = lib.drop 4 components;
54− # reassemble the path components
55− relPath = lib.concatStringsSep "/" relPathComponents;
56− in
57− lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
58− in
59− builtins.path {
60− name = "helix-source";
61− path = toString ./.;
62− # filter out unnecessary paths
63− filter = ignorePaths;
20+ outputs = {
21+ self,
22+ nixpkgs,
23+ crane,
24+ flake-utils,
25+ rust-overlay,
26+ ...
27+ }:
28+ flake-utils.lib.eachDefaultSystem (system: let
29+ pkgs = import nixpkgs {
30+ inherit system;
31+ overlays = [(import rust-overlay)];
6432 };
65− in
66− inp.parts.lib.mkFlake {inputs = inp;} {
67− imports = [inp.nci.flakeModule inp.parts.flakeModules.easyOverlay];
68− systems = [
69− "x86_64-linux"
70− "x86_64-darwin"
71− "aarch64-linux"
72− "aarch64-darwin"
73− "i686-linux"
74− ];
75− perSystem = {
76− config,
77− pkgs,
78− lib,
79− ...
80− }: let
81− makeOverridableHelix = old: config: let
82− grammars = pkgs.callPackage ./grammars.nix config;
83− runtimeDir = pkgs.runCommand "helix-runtime" {} ''
84− mkdir -p $out
85− ln -s ${mkRootPath "runtime"}/* $out
86− rm -r $out/grammars
87− ln -s ${grammars} $out/grammars
88− '';
89− helix-wrapped =
90− pkgs.runCommand
91− old.name
92− {
93− inherit (old) pname version;
94− meta = old.meta or {};
95− passthru =
96− (old.passthru or {})
97− // {
98− unwrapped = old;
99− };
100− nativeBuildInputs = [pkgs.makeWrapper];
101− makeWrapperArgs = config.makeWrapperArgs or [];
102− }
103− ''
104− cp -rs --no-preserve=mode,ownership ${old} $out
105− wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
106− '';
33+ mkRootPath = rel:
34+ builtins.path {
35+ path = "${toString ./.}/${rel}";
36+ name = rel;
37+ };
38+ filteredSource = let
39+ pathsToIgnore = [
40+ ".envrc"
41+ ".ignore"
42+ ".github"
43+ ".gitignore"
44+ "logo_dark.svg"
45+ "logo_light.svg"
46+ "rust-toolchain.toml"
47+ "rustfmt.toml"
48+ "runtime"
49+ "screenshot.png"
50+ "book"
51+ "docs"
52+ "README.md"
53+ "CHANGELOG.md"
54+ "shell.nix"
55+ "default.nix"
56+ "grammars.nix"
57+ "flake.nix"
58+ "flake.lock"
59+ ];
60+ ignorePaths = path: type: let
61+ inherit (nixpkgs) lib;
62+ # split the nix store path into its components
63+ components = lib.splitString "/" path;
64+ # drop off the `/nix/hash-source` section from the path
65+ relPathComponents = lib.drop 4 components;
66+ # reassemble the path components
67+ relPath = lib.concatStringsSep "/" relPathComponents;
10768 in
108− helix-wrapped
109− // {
110− override = makeOverridableHelix old;
69+ lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
70+ in
71+ builtins.path {
72+ name = "helix-source";
73+ path = toString ./.;
74+ # filter out unnecessary paths
75+ filter = ignorePaths;
76+ };
77+ makeOverridableHelix = old: config: let
78+ grammars = pkgs.callPackage ./grammars.nix config;
79+ runtimeDir = pkgs.runCommand "helix-runtime" {} ''
80+ mkdir -p $out
81+ ln -s ${mkRootPath "runtime"}/* $out
82+ rm -r $out/grammars
83+ ln -s ${grammars} $out/grammars
84+ '';
85+ helix-wrapped =
86+ pkgs.runCommand
87+ old.name
88+ {
89+ inherit (old) pname version;
90+ meta = old.meta or {};
11191 passthru =
112− helix-wrapped.passthru
92+ (old.passthru or {})
11393 // {
114− wrapper = old: makeOverridableHelix old config;
94+ unwrapped = old;
11595 };
116− };
117− stdenv =
118− if pkgs.stdenv.isLinux
119− then pkgs.stdenv
120− else pkgs.clangStdenv;
121− rustFlagsEnv =
122− if stdenv.isLinux
123− then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
124− else "$RUSTFLAGS";
125− in {
126− nci.projects."helix-project".relPath = "";
127− nci.crates."helix-term" = {
128− overrides = {
129− add-meta.override = _: {meta.mainProgram = "hx";};
130− add-inputs.overrideAttrs = prev: {
131− buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib];
132− };
133− disable-grammar-builds = {
134− # disable fetching and building of tree-sitter grammars in the helix-term build.rs
135− HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
96+ nativeBuildInputs = [pkgs.makeWrapper];
97+ makeWrapperArgs = config.makeWrapperArgs or [];
98+ }
99+ ''
100+ cp -rs --no-preserve=mode,ownership ${old} $out
101+ wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
102+ '';
103+ in
104+ helix-wrapped
105+ // {
106+ override = makeOverridableHelix old;
107+ passthru =
108+ helix-wrapped.passthru
109+ // {
110+ wrapper = old: makeOverridableHelix old config;
136111 };
137− disable-tests = {checkPhase = ":";};
138− set-stdenv.override = _: {inherit stdenv;};
139− set-filtered-src.override = _: {src = filteredSource;};
140− };
141112 };
113+ stdenv =
114+ if pkgs.stdenv.isLinux
115+ then pkgs.stdenv
116+ else pkgs.clangStdenv;
117+ rustFlagsEnv =
118+ if stdenv.isLinux
119+ then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
120+ else "$RUSTFLAGS";
121+ rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
122+ craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
123+ craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
124+ commonArgs = {
125+ inherit stdenv;
126+ inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname;
127+ inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version;
128+ src = filteredSource;
129+ # disable fetching and building of tree-sitter grammars in the helix-term build.rs
130+ HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
131+ buildInputs = [stdenv.cc.cc.lib];
132+ # disable tests
133+ doCheck = false;
134+ meta.mainProgram = "hx";
135+ };
136+ cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs;
137+ in {
138+ packages = {
139+ helix-unwrapped = craneLibStable.buildPackage (commonArgs
140+ // {
141+ cargoArtifacts = craneLibStable.buildDepsOnly commonArgs;
142+ postInstall = ''
143+ mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps
144+ cp contrib/Helix.desktop $out/share/applications
145+ cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
146+ cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
147+ '';
148+ });
149+ helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
150+ default = self.packages.${system}.helix;
151+ };
142152
143− packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release;
144− packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev;
145− packages.helix = makeOverridableHelix config.packages.helix-unwrapped {};
146− packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
147− packages.default = config.packages.helix;
153+ checks = {
154+ # Build the crate itself
155+ inherit (self.packages.${system}) helix;
148156
149− overlayAttrs = {
150− inherit (config.packages) helix;
151− };
157+ clippy = craneLibMSRV.cargoClippy (commonArgs
158+ // {
159+ inherit cargoArtifacts;
160+ cargoClippyExtraArgs = "--all-targets -- --deny warnings";
161+ });
152162
153− devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: {
154− nativeBuildInputs =
155− (old.nativeBuildInputs or [])
156− ++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer])
157− ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
158− ++ (lib.optional stdenv.isLinux pkgs.lldb)
159− ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
160− shellHook = ''
161− export HELIX_RUNTIME="$PWD/runtime"
162− export RUST_BACKTRACE="1"
163− export RUSTFLAGS="${rustFlagsEnv}"
164− '';
165− });
163+ fmt = craneLibMSRV.cargoFmt commonArgs;
164+
165+ doc = craneLibMSRV.cargoDoc (commonArgs
166+ // {
167+ inherit cargoArtifacts;
168+ });
169+
170+ test = craneLibMSRV.cargoTest (commonArgs
171+ // {
172+ inherit cargoArtifacts;
173+ });
174+ };
175+
176+ devShells.default = pkgs.mkShell {
177+ inputsFrom = builtins.attrValues self.checks.${system};
178+ nativeBuildInputs = with pkgs;
179+ [lld_13 cargo-flamegraph rust-analyzer]
180+ ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
181+ ++ (lib.optional stdenv.isLinux pkgs.lldb)
182+ ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
183+ shellHook = ''
184+ export HELIX_RUNTIME="$PWD/runtime"
185+ export RUST_BACKTRACE="1"
186+ export RUSTFLAGS="${rustFlagsEnv}"
187+ '';
188+ };
189+ })
190+ // {
191+ overlays.default = final: prev: {
192+ inherit (self.packages.${final.system}) helix;
166193 };
167194 };
168195
grammars.nix+24−21
@@ -5,6 +5,7 @@
55 runCommand,
66 yj,
77 includeGrammarIf ? _: true,
8+ grammarOverlays ? [],
89 ...
910}: let
1011 # HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
@@ -48,22 +49,22 @@
4849 then sourceGitHub
4950 else sourceGit;
5051 in
51− stdenv.mkDerivation rec {
52+ stdenv.mkDerivation {
5253 # see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
5354
5455 pname = "helix-tree-sitter-${grammar.name}";
5556 version = grammar.source.rev;
5657
57− src =
58− if builtins.hasAttr "subpath" grammar.source
59− then "${source}/${grammar.source.subpath}"
60− else source;
58+ src = source;
59+ sourceRoot = if builtins.hasAttr "subpath" grammar.source then
60+ "source/${grammar.source.subpath}"
61+ else
62+ "source";
6163
62− dontUnpack = true;
6364 dontConfigure = true;
6465
6566 FLAGS = [
66− "-I${src}/src"
67+ "-Isrc"
6768 "-g"
6869 "-O3"
6970 "-fPIC"
@@ -76,13 +77,13 @@
7677 buildPhase = ''
7778 runHook preBuild
7879
79− if [[ -e "$src/src/scanner.cc" ]]; then
80− $CXX -c "$src/src/scanner.cc" -o scanner.o $FLAGS
81− elif [[ -e "$src/src/scanner.c" ]]; then
82− $CC -c "$src/src/scanner.c" -o scanner.o $FLAGS
80+ if [[ -e src/scanner.cc ]]; then
81+ $CXX -c src/scanner.cc -o scanner.o $FLAGS
82+ elif [[ -e src/scanner.c ]]; then
83+ $CC -c src/scanner.c -o scanner.o $FLAGS
8384 fi
8485
85− $CC -c "$src/src/parser.c" -o parser.o $FLAGS
86+ $CC -c src/parser.c -o parser.o $FLAGS
8687 $CXX -shared -o $NAME.so *.o
8788
8889 ls -al
@@ -105,15 +106,17 @@
105106 '';
106107 };
107108 grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
108− builtGrammars =
109− builtins.map (grammar: {
110− inherit (grammar) name;
111− artifact = buildGrammar grammar;
112− })
113− grammarsToBuild;
114− grammarLinks =
115− builtins.map (grammar: "ln -s ${grammar.artifact}/${grammar.name}.so $out/${grammar.name}.so")
116− builtGrammars;
109+ builtGrammars = builtins.map (grammar: {
110+ inherit (grammar) name;
111+ value = buildGrammar grammar;
112+ }) grammarsToBuild;
113+ extensibleGrammars =
114+ lib.makeExtensible (self: builtins.listToAttrs builtGrammars);
115+ overlayedGrammars = lib.pipe extensibleGrammars
116+ (builtins.map (overlay: grammar: grammar.extend overlay) grammarOverlays);
117+ grammarLinks = lib.mapAttrsToList
118+ (name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
119+ (lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
117120in
118121 runCommand "consolidated-helix-grammars" {} ''
119122 mkdir -p $out
helix-core/Cargo.toml+19−15
@@ -1,23 +1,24 @@
11[package]
22name = "helix-core"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
73description = "Helix editor core editing primitives"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
114include = ["src/**/*", "README.md"]
5+version.workspace = true
6+authors.workspace = true
7+edition.workspace = true
8+license.workspace = true
9+rust-version.workspace = true
10+categories.workspace = true
11+repository.workspace = true
12+homepage.workspace = true
1213
1314[features]
1415unicode-lines = ["ropey/unicode_lines"]
1516integration = []
1617
1718[dependencies]
18−helix-loader = { version = "0.6", path = "../helix-loader" }
19+helix-loader = { path = "../helix-loader" }
1920
20−ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
21+ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
2122smallvec = "1.11"
2223smartstring = "1.0.1"
2324unicode-segmentation = "1.10"
@@ -25,13 +26,13 @@ unicode-width = "0.1"
2526unicode-general-category = "0.6"
2627# slab = "0.4.2"
2728slotmap = "1.0"
28−tree-sitter = "0.20"
29−once_cell = "1.18"
29+tree-sitter.workspace = true
30+once_cell = "1.19"
3031arc-swap = "1"
3132regex = "1"
32−bitflags = "2.3"
33−ahash = "0.8.3"
34−hashbrown = { version = "0.14.0", features = ["raw"] }
33+bitflags = "2.4"
34+ahash = "0.8.6"
35+hashbrown = { version = "0.14.3", features = ["raw"] }
3536dunce = "1.0"
3637
3738log = "0.4"
@@ -48,6 +49,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
4849etcetera = "0.8"
4950textwrap = "0.16.0"
5051
52+nucleo.workspace = true
53+parking_lot = "0.12"
54+
5155[dev-dependencies]
5256quickcheck = { version = "1", default-features = false }
53−indoc = "2.0.2"
57+indoc = "2.0.4"
helix-core/src/diagnostic.rs+4
@@ -39,6 +39,10 @@ pub enum DiagnosticTag {
3939#[derive(Debug, Clone)]
4040pub struct Diagnostic {
4141 pub range: Range,
42+ // whether this diagnostic ends at the end of(or inside) a word
43+ pub ends_at_word: bool,
44+ pub starts_at_word: bool,
45+ pub zero_width: bool,
4246 pub line: usize,
4347 pub message: String,
4448 pub severity: Option<Severity>,
helix-core/src/fuzzy.rsnew file+43
@@ -0,0 +1,43 @@
1+use std::ops::DerefMut;
2+
3+use nucleo::pattern::{Atom, AtomKind, CaseMatching};
4+use nucleo::Config;
5+use parking_lot::Mutex;
6+
7+pub struct LazyMutex<T> {
8+ inner: Mutex<Option<T>>,
9+ init: fn() -> T,
10+}
11+
12+impl<T> LazyMutex<T> {
13+ pub const fn new(init: fn() -> T) -> Self {
14+ Self {
15+ inner: Mutex::new(None),
16+ init,
17+ }
18+ }
19+
20+ pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
21+ parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
22+ }
23+}
24+
25+pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
26+
27+/// convenience function to easily fuzzy match
28+/// on a (relatively small list of inputs). This is not recommended for building a full tui
29+/// application that can match large numbers of matches as all matching is done on the current
30+/// thread, effectively blocking the UI
31+pub fn fuzzy_match<T: AsRef<str>>(
32+ pattern: &str,
33+ items: impl IntoIterator<Item = T>,
34+ path: bool,
35+) -> Vec<(T, u16)> {
36+ let mut matcher = MATCHER.lock();
37+ matcher.config = Config::DEFAULT;
38+ if path {
39+ matcher.config.set_match_paths();
40+ }
41+ let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
42+ pattern.match_list(items, &mut matcher)
43+}
helix-core/src/graphemes.rs+1−1
@@ -481,7 +481,7 @@ impl<'a> From<String> for GraphemeStr<'a> {
481481 let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
482482 GraphemeStr {
483483 ptr: unsafe { NonNull::new_unchecked(ptr) },
484− len: i32::try_from(len).unwrap() as u32,
484+ len: (i32::try_from(len).unwrap() as u32) | Self::MASK_OWNED,
485485 phantom: PhantomData,
486486 }
487487 }
helix-core/src/history.rs+2−2
@@ -72,8 +72,8 @@ impl Default for History {
7272 revisions: vec![Revision {
7373 parent: 0,
7474 last_child: None,
75− transaction: Transaction::from(ChangeSet::new(&Rope::new())),
76− inversion: Transaction::from(ChangeSet::new(&Rope::new())),
75+ transaction: Transaction::from(ChangeSet::new("".into())),
76+ inversion: Transaction::from(ChangeSet::new("".into())),
7777 timestamp: Instant::now(),
7878 }],
7979 current: 0,
helix-core/src/indent.rs+586−127
@@ -1,13 +1,14 @@
1−use std::collections::HashMap;
1+use std::{borrow::Cow, collections::HashMap};
22
33use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
44
55use crate::{
66 chars::{char_is_line_ending, char_is_whitespace},
7− graphemes::tab_width_at,
8− syntax::{LanguageConfiguration, RopeProvider, Syntax},
7+ find_first_non_whitespace_char,
8+ graphemes::{grapheme_width, tab_width_at},
9+ syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
910 tree_sitter::Node,
10− Rope, RopeSlice,
11+ Position, Rope, RopeGraphemes, RopeSlice,
1112};
1213
1314/// Enum representing indentation style.
@@ -21,7 +22,7 @@ pub enum IndentStyle {
2122
2223// 16 spaces
2324const INDENTS: &str = " ";
24−const MAX_INDENT: u8 = 16;
25+pub const MAX_INDENT: u8 = 16;
2526
2627impl IndentStyle {
2728 /// Creates an `IndentStyle` from an indentation string.
@@ -196,6 +197,56 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
196197 len / indent_width
197198}
198199
200+/// Create a string of tabs & spaces that has the same visual width as the given RopeSlice (independent of the tab width).
201+fn whitespace_with_same_width(text: RopeSlice) -> String {
202+ let mut s = String::new();
203+ for grapheme in RopeGraphemes::new(text) {
204+ if grapheme == "\t" {
205+ s.push('\t');
206+ } else {
207+ s.extend(std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))));
208+ }
209+ }
210+ s
211+}
212+
213+fn add_indent_level(
214+ mut base_indent: String,
215+ added_indent_level: isize,
216+ indent_style: &IndentStyle,
217+ tab_width: usize,
218+) -> String {
219+ if added_indent_level >= 0 {
220+ // Adding a non-negative indent is easy, we can simply append the indent string
221+ base_indent.push_str(&indent_style.as_str().repeat(added_indent_level as usize));
222+ base_indent
223+ } else {
224+ // In this case, we want to return a prefix of `base_indent`.
225+ // Since the width of a tab depends on its offset, we cannot simply iterate over
226+ // the chars of `base_indent` in reverse until we have the desired indent reduction,
227+ // instead we iterate over them twice in forward direction.
228+ let base_indent_rope = RopeSlice::from(base_indent.as_str());
229+ #[allow(deprecated)]
230+ let base_indent_width =
231+ crate::visual_coords_at_pos(base_indent_rope, base_indent_rope.len_chars(), tab_width)
232+ .col;
233+ let target_indent_width = base_indent_width
234+ .saturating_sub((-added_indent_level) as usize * indent_style.indent_width(tab_width));
235+ #[allow(deprecated)]
236+ let char_end_idx = crate::pos_at_visual_coords(
237+ base_indent_rope,
238+ Position {
239+ row: 0,
240+ col: target_indent_width,
241+ },
242+ tab_width,
243+ );
244+ let byte_end_idx = base_indent_rope.char_to_byte(char_end_idx);
245+ base_indent.truncate(byte_end_idx);
246+ base_indent
247+ }
248+}
249+
199250/// Computes for node and all ancestors whether they are the first node on their line.
200251/// The first entry in the return value represents the root node, the last one the node itself
201252fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
@@ -237,68 +288,140 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
237288/// This is usually constructed in one of 2 ways:
238289/// - Successively add indent captures to get the (added) indent from a single line
239290/// - Successively add the indent results for each line
240−#[derive(Default)]
241−pub struct Indentation {
242− /// The total indent (the number of indent levels) is defined as max(0, indent-outdent).
243− /// The string that this results in depends on the indent style (spaces or tabs, etc.)
291+/// The string that this indentation defines starts with the string contained in the align field (unless it is None), followed by:
292+/// - max(0, indent - outdent) tabs, if tabs are used for indentation
293+/// - max(0, indent - outdent)*indent_width spaces, if spaces are used for indentation
294+#[derive(Default, Debug, PartialEq, Eq, Clone)]
295+pub struct Indentation<'a> {
244296 indent: usize,
297+ indent_always: usize,
245298 outdent: usize,
299+ outdent_always: usize,
300+ /// The alignment, as a string containing only tabs & spaces. Storing this as a string instead of e.g.
301+ /// the (visual) width ensures that the alignment is preserved even if the tab width changes.
302+ align: Option<RopeSlice<'a>>,
246303}
247−impl Indentation {
304+
305+impl<'a> Indentation<'a> {
248306 /// Add some other [Indentation] to this.
249− /// The added indent should be the total added indent from one line
250− fn add_line(&mut self, added: &Indentation) {
251− if added.indent > 0 && added.outdent == 0 {
252− self.indent += 1;
253− } else if added.outdent > 0 && added.indent == 0 {
254− self.outdent += 1;
307+ /// The added indent should be the total added indent from one line.
308+ /// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node).
309+ fn add_line(&mut self, added: Indentation<'a>) {
310+ // Align overrides the indent from outer scopes.
311+ if self.align.is_some() {
312+ return;
313+ }
314+ if added.align.is_some() {
315+ self.align = added.align;
316+ return;
255317 }
318+ self.indent += added.indent;
319+ self.indent_always += added.indent_always;
320+ self.outdent += added.outdent;
321+ self.outdent_always += added.outdent_always;
256322 }
323+
257324 /// Add an indent capture to this indent.
258− /// All the captures that are added in this way should be on the same line.
259− fn add_capture(&mut self, added: IndentCaptureType) {
325+ /// Only captures that apply to the same line should be added together in this way (otherwise use `add_line`)
326+ /// and the captures should be added starting from the innermost tree-sitter node (currently this only matters
327+ /// if multiple `@align` patterns occur on the same line).
328+ fn add_capture(&mut self, added: IndentCaptureType<'a>) {
260329 match added {
261330 IndentCaptureType::Indent => {
262− self.indent = 1;
331+ if self.indent_always == 0 {
332+ self.indent = 1;
333+ }
334+ }
335+ IndentCaptureType::IndentAlways => {
336+ // any time we encounter an `indent.always` on the same line, we
337+ // want to cancel out all regular indents
338+ self.indent_always += 1;
339+ self.indent = 0;
263340 }
264341 IndentCaptureType::Outdent => {
265− self.outdent = 1;
342+ if self.outdent_always == 0 {
343+ self.outdent = 1;
344+ }
345+ }
346+ IndentCaptureType::OutdentAlways => {
347+ self.outdent_always += 1;
348+ self.outdent = 0;
349+ }
350+ IndentCaptureType::Align(align) => {
351+ if self.align.is_none() {
352+ self.align = Some(align);
353+ }
266354 }
267355 }
268356 }
269− fn as_string(&self, indent_style: &IndentStyle) -> String {
270− let indent_level = if self.indent >= self.outdent {
271− self.indent - self.outdent
357+ fn net_indent(&self) -> isize {
358+ (self.indent + self.indent_always) as isize
359+ - ((self.outdent + self.outdent_always) as isize)
360+ }
361+ /// Convert `self` into a string, taking into account the computed and actual indentation of some other line.
362+ fn relative_indent(
363+ &self,
364+ other_computed_indent: &Self,
365+ other_leading_whitespace: RopeSlice,
366+ indent_style: &IndentStyle,
367+ tab_width: usize,
368+ ) -> Option<String> {
369+ if self.align == other_computed_indent.align {
370+ // If self and baseline are either not aligned to anything or both aligned the same way,
371+ // we can simply take `other_leading_whitespace` and add some indent / outdent to it (in the second
372+ // case, the alignment should already be accounted for in `other_leading_whitespace`).
373+ let indent_diff = self.net_indent() - other_computed_indent.net_indent();
374+ Some(add_indent_level(
375+ String::from(other_leading_whitespace),
376+ indent_diff,
377+ indent_style,
378+ tab_width,
379+ ))
272380 } else {
273− log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
274− 0
275− };
276− indent_style.as_str().repeat(indent_level)
381+ // If the alignment of both lines is different, we cannot compare their indentation in any meaningful way
382+ None
383+ }
384+ }
385+ pub fn to_string(&self, indent_style: &IndentStyle, tab_width: usize) -> String {
386+ add_indent_level(
387+ self.align
388+ .map_or_else(String::new, whitespace_with_same_width),
389+ self.net_indent(),
390+ indent_style,
391+ tab_width,
392+ )
277393 }
278394}
279395
280396/// An indent definition which corresponds to a capture from the indent query
281−struct IndentCapture {
282− capture_type: IndentCaptureType,
397+#[derive(Debug)]
398+struct IndentCapture<'a> {
399+ capture_type: IndentCaptureType<'a>,
283400 scope: IndentScope,
284401}
285−#[derive(Clone, Copy)]
286−enum IndentCaptureType {
402+#[derive(Debug, Clone, PartialEq)]
403+enum IndentCaptureType<'a> {
287404 Indent,
405+ IndentAlways,
288406 Outdent,
407+ OutdentAlways,
408+ /// Alignment given as a string of whitespace
409+ Align(RopeSlice<'a>),
289410}
290−impl IndentCaptureType {
411+
412+impl<'a> IndentCaptureType<'a> {
291413 fn default_scope(&self) -> IndentScope {
292414 match self {
293− IndentCaptureType::Indent => IndentScope::Tail,
294− IndentCaptureType::Outdent => IndentScope::All,
415+ IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
416+ IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
417+ IndentCaptureType::Align(_) => IndentScope::All,
295418 }
296419 }
297420}
298421/// This defines which part of a node an [IndentCapture] applies to.
299422/// Each [IndentCaptureType] has a default scope, but the scope can be changed
300423/// with `#set!` property declarations.
301−#[derive(Clone, Copy)]
424+#[derive(Debug, Clone, Copy)]
302425enum IndentScope {
303426 /// The indent applies to the whole node
304427 All,
@@ -308,6 +431,7 @@ enum IndentScope {
308431
309432/// A capture from the indent query which does not define an indent but extends
310433/// the range of a node. This is used before the indent is calculated.
434+#[derive(Debug)]
311435enum ExtendCapture {
312436 Extend,
313437 PreventOnce,
@@ -316,24 +440,41 @@ enum ExtendCapture {
316440/// The result of running a tree-sitter indent query. This stores for
317441/// each node (identified by its ID) the relevant captures (already filtered
318442/// by predicates).
319−struct IndentQueryResult {
320− indent_captures: HashMap<usize, Vec<IndentCapture>>,
443+#[derive(Debug)]
444+struct IndentQueryResult<'a> {
445+ indent_captures: HashMap<usize, Vec<IndentCapture<'a>>>,
321446 extend_captures: HashMap<usize, Vec<ExtendCapture>>,
322447}
323448
324−fn query_indents(
449+fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
450+ let mut node_line = node.start_position().row;
451+ // Adjust for the new line that will be inserted
452+ if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
453+ node_line += 1;
454+ }
455+ node_line
456+}
457+fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
458+ let mut node_line = node.end_position().row;
459+ // Adjust for the new line that will be inserted (with a strict inequality since end_byte is exclusive)
460+ if new_line_byte_pos.map_or(false, |pos| node.end_byte() > pos) {
461+ node_line += 1;
462+ }
463+ node_line
464+}
465+
466+fn query_indents<'a>(
325467 query: &Query,
326468 syntax: &Syntax,
327469 cursor: &mut QueryCursor,
328− text: RopeSlice,
470+ text: RopeSlice<'a>,
329471 range: std::ops::Range<usize>,
330− // Position of the (optional) newly inserted line break.
331− // Given as (line, byte_pos)
332− new_line_break: Option<(usize, usize)>,
333−) -> IndentQueryResult {
472+ new_line_byte_pos: Option<usize>,
473+) -> IndentQueryResult<'a> {
334474 let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
335475 let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
336476 cursor.set_byte_range(range);
477+
337478 // Iterate over all captures from the query
338479 for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
339480 // Skip matches where not all custom predicates are fulfilled
@@ -360,21 +501,13 @@ fn query_indents(
360501 Some(QueryPredicateArg::Capture(capt1)),
361502 Some(QueryPredicateArg::Capture(capt2))
362503 ) => {
363− let get_line_num = |node: Node| {
364− let mut node_line = node.start_position().row;
365− // Adjust for the new line that will be inserted
366− if let Some((line, byte)) = new_line_break {
367− if node_line==line && node.start_byte()>=byte {
368− node_line += 1;
369− }
370− }
371− node_line
372− };
373504 let n1 = m.nodes_for_capture_index(*capt1).next();
374505 let n2 = m.nodes_for_capture_index(*capt2).next();
375506 match (n1, n2) {
376507 (Some(n1), Some(n2)) => {
377− let same_line = get_line_num(n1)==get_line_num(n2);
508+ let n1_line = get_node_start_line(n1, new_line_byte_pos);
509+ let n2_line = get_node_start_line(n2, new_line_byte_pos);
510+ let same_line = n1_line == n2_line;
378511 same_line==(pred.operator.as_ref()=="same-line?")
379512 }
380513 _ => true,
@@ -385,6 +518,23 @@ fn query_indents(
385518 }
386519 }
387520 }
521+ "one-line?" | "not-one-line?" => match pred.args.get(0) {
522+ Some(QueryPredicateArg::Capture(capture_idx)) => {
523+ let node = m.nodes_for_capture_index(*capture_idx).next();
524+
525+ match node {
526+ Some(node) => {
527+ let (start_line, end_line) = (get_node_start_line(node,new_line_byte_pos), get_node_end_line(node, new_line_byte_pos));
528+ let one_line = end_line == start_line;
529+ one_line != (pred.operator.as_ref() == "not-one-line?")
530+ },
531+ _ => true,
532+ }
533+ }
534+ _ => {
535+ panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
536+ }
537+ },
388538 _ => {
389539 panic!(
390540 "Invalid indent query: Unknown predicate (\"{}\")",
@@ -395,11 +545,28 @@ fn query_indents(
395545 }) {
396546 continue;
397547 }
548+ // A list of pairs (node_id, indent_capture) that are added by this match.
549+ // They cannot be added to indent_captures immediately since they may depend on other captures (such as an @anchor).
550+ let mut added_indent_captures: Vec<(usize, IndentCapture)> = Vec::new();
551+ // The row/column position of the optional anchor in this query
552+ let mut anchor: Option<tree_sitter::Node> = None;
398553 for capture in m.captures {
399554 let capture_name = query.capture_names()[capture.index as usize].as_str();
400555 let capture_type = match capture_name {
401556 "indent" => IndentCaptureType::Indent,
557+ "indent.always" => IndentCaptureType::IndentAlways,
402558 "outdent" => IndentCaptureType::Outdent,
559+ "outdent.always" => IndentCaptureType::OutdentAlways,
560+ // The alignment will be updated to the correct value at the end, when the anchor is known.
561+ "align" => IndentCaptureType::Align(RopeSlice::from("")),
562+ "anchor" => {
563+ if anchor.is_some() {
564+ log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
565+ } else {
566+ anchor = Some(capture.node);
567+ }
568+ continue;
569+ }
403570 "extend" => {
404571 extend_captures
405572 .entry(capture.node.id())
@@ -449,17 +616,40 @@ fn query_indents(
449616 }
450617 }
451618 }
619+ added_indent_captures.push((capture.node.id(), indent_capture))
620+ }
621+ for (node_id, mut capture) in added_indent_captures {
622+ // Set the anchor for all align queries.
623+ if let IndentCaptureType::Align(_) = capture.capture_type {
624+ let anchor = match anchor {
625+ None => {
626+ log::error!(
627+ "Invalid indent query: @align requires an accompanying @anchor."
628+ );
629+ continue;
630+ }
631+ Some(anchor) => anchor,
632+ };
633+ capture.capture_type = IndentCaptureType::Align(
634+ text.line(anchor.start_position().row)
635+ .byte_slice(0..anchor.start_position().column),
636+ );
637+ }
452638 indent_captures
453− .entry(capture.node.id())
454− // Most entries only need to contain a single IndentCapture
639+ .entry(node_id)
455640 .or_insert_with(|| Vec::with_capacity(1))
456− .push(indent_capture);
641+ .push(capture);
457642 }
458643 }
459− IndentQueryResult {
644+
645+ let result = IndentQueryResult {
460646 indent_captures,
461647 extend_captures,
462− }
648+ };
649+
650+ log::trace!("indent result = {:?}", result);
651+
652+ result
463653}
464654
465655/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
@@ -529,61 +719,27 @@ fn extend_nodes<'a>(
529719 }
530720}
531721
532−/// Use the syntax tree to determine the indentation for a given position.
533−/// This can be used in 2 ways:
534−///
535−/// - To get the correct indentation for an existing line (new_line=false), not necessarily equal to the current indentation.
536−/// - In this case, pos should be inside the first tree-sitter node on that line.
537−/// In most cases, this can just be the first non-whitespace on that line.
538−/// - To get the indentation for a new line (new_line=true). This behaves like the first usecase if the part of the current line
539−/// after pos were moved to a new line.
540−///
541−/// The indentation is determined by traversing all the tree-sitter nodes containing the position.
542−/// Each of these nodes produces some [Indentation] for:
543−///
544−/// - The line of the (beginning of the) node. This is defined by the scope `all` if this is the first node on its line.
545−/// - The line after the node. This is defined by:
546−/// - The scope `tail`.
547−/// - The scope `all` if this node is not the first node on its line.
548−/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
549−/// The indents from different nodes for the same line are then combined.
550−/// The result [Indentation] is simply the sum of the [Indentation] for all lines.
551−///
552−/// Specifying which line exactly an [Indentation] applies to is important because indents on the same line combine differently than indents on different lines:
553−/// ```ignore
554−/// some_function(|| {
555−/// // Both the function parameters as well as the contained block should be indented.
556−/// // Because they are on the same line, this only yields one indent level
557−/// });
558−/// ```
559−///
560−/// ```ignore
561−/// some_function(
562−/// param1,
563−/// || {
564−/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines
565−/// },
566−/// );
567−/// ```
722+/// Prepare an indent query by computing:
723+/// - The node from which to start the query (this is non-trivial due to `@extend` captures)
724+/// - The indent captures for all relevant nodes.
568725#[allow(clippy::too_many_arguments)]
569−pub fn treesitter_indent_for_pos(
726+fn init_indent_query<'a, 'b>(
570727 query: &Query,
571− syntax: &Syntax,
572− indent_style: &IndentStyle,
728+ syntax: &'a Syntax,
729+ text: RopeSlice<'b>,
573730 tab_width: usize,
574731 indent_width: usize,
575− text: RopeSlice,
576732 line: usize,
577− pos: usize,
578− new_line: bool,
579−) -> Option<String> {
580− let byte_pos = text.char_to_byte(pos);
733+ byte_pos: usize,
734+ new_line_byte_pos: Option<usize>,
735+) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
581736 // The innermost tree-sitter node which is considered for the indent
582737 // computation. It may change if some predeceding node is extended
583738 let mut node = syntax
584739 .tree()
585740 .root_node()
586741 .descendant_for_byte_range(byte_pos, byte_pos)?;
742+
587743 let (query_result, deepest_preceding) = {
588744 // The query range should intersect with all nodes directly preceding
589745 // the position of the indent query in case one of them is extended.
@@ -614,13 +770,12 @@ pub fn treesitter_indent_for_pos(
614770 &mut cursor,
615771 text,
616772 query_range,
617− new_line.then_some((line, byte_pos)),
773+ new_line_byte_pos,
618774 );
619775 ts_parser.cursors.push(cursor);
620776 (query_result, deepest_preceding)
621777 })
622778 };
623− let indent_captures = query_result.indent_captures;
624779 let extend_captures = query_result.extend_captures;
625780
626781 // Check for extend captures, potentially changing the node that the indent calculation starts with
@@ -635,6 +790,68 @@ pub fn treesitter_indent_for_pos(
635790 indent_width,
636791 );
637792 }
793+ Some((node, query_result.indent_captures))
794+}
795+
796+/// Use the syntax tree to determine the indentation for a given position.
797+/// This can be used in 2 ways:
798+///
799+/// - To get the correct indentation for an existing line (new_line=false), not necessarily equal to the current indentation.
800+/// - In this case, pos should be inside the first tree-sitter node on that line.
801+/// In most cases, this can just be the first non-whitespace on that line.
802+/// - To get the indentation for a new line (new_line=true). This behaves like the first usecase if the part of the current line
803+/// after pos were moved to a new line.
804+///
805+/// The indentation is determined by traversing all the tree-sitter nodes containing the position.
806+/// Each of these nodes produces some [Indentation] for:
807+///
808+/// - The line of the (beginning of the) node. This is defined by the scope `all` if this is the first node on its line.
809+/// - The line after the node. This is defined by:
810+/// - The scope `tail`.
811+/// - The scope `all` if this node is not the first node on its line.
812+/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
813+/// The indents from different nodes for the same line are then combined.
814+/// The result [Indentation] is simply the sum of the [Indentation] for all lines.
815+///
816+/// Specifying which line exactly an [Indentation] applies to is important because indents on the same line combine differently than indents on different lines:
817+/// ```ignore
818+/// some_function(|| {
819+/// // Both the function parameters as well as the contained block should be indented.
820+/// // Because they are on the same line, this only yields one indent level
821+/// });
822+/// ```
823+///
824+/// ```ignore
825+/// some_function(
826+/// param1,
827+/// || {
828+/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines
829+/// },
830+/// );
831+/// ```
832+#[allow(clippy::too_many_arguments)]
833+pub fn treesitter_indent_for_pos<'a>(
834+ query: &Query,
835+ syntax: &Syntax,
836+ tab_width: usize,
837+ indent_width: usize,
838+ text: RopeSlice<'a>,
839+ line: usize,
840+ pos: usize,
841+ new_line: bool,
842+) -> Option<Indentation<'a>> {
843+ let byte_pos = text.char_to_byte(pos);
844+ let new_line_byte_pos = new_line.then_some(byte_pos);
845+ let (mut node, mut indent_captures) = init_indent_query(
846+ query,
847+ syntax,
848+ text,
849+ tab_width,
850+ indent_width,
851+ line,
852+ byte_pos,
853+ new_line_byte_pos,
854+ )?;
638855 let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
639856
640857 let mut result = Indentation::default();
@@ -642,12 +859,16 @@ pub fn treesitter_indent_for_pos(
642859 // even if there are multiple "indent" nodes on the same line
643860 let mut indent_for_line = Indentation::default();
644861 let mut indent_for_line_below = Indentation::default();
862+
645863 loop {
646864 // This can safely be unwrapped because `first_in_line` contains
647865 // one entry for each ancestor of the node (which is what we iterate over)
648866 let is_first = *first_in_line.last().unwrap();
649− // Apply all indent definitions for this node
650− if let Some(definitions) = indent_captures.get(&node.id()) {
867+
868+ // Apply all indent definitions for this node.
869+ // Since we only iterate over each node once, we can remove the
870+ // corresponding captures from the HashMap to avoid cloning them.
871+ if let Some(definitions) = indent_captures.remove(&node.id()) {
651872 for definition in definitions {
652873 match definition.scope {
653874 IndentScope::All => {
@@ -665,28 +886,22 @@ pub fn treesitter_indent_for_pos(
665886 }
666887
667888 if let Some(parent) = node.parent() {
668− let mut node_line = node.start_position().row;
669− let mut parent_line = parent.start_position().row;
670− if node_line == line && new_line {
671− // Also consider the line that will be inserted
672− if node.start_byte() >= byte_pos {
673− node_line += 1;
674− }
675− if parent.start_byte() >= byte_pos {
676− parent_line += 1;
677− }
678− };
889+ let node_line = get_node_start_line(node, new_line_byte_pos);
890+ let parent_line = get_node_start_line(parent, new_line_byte_pos);
891+
679892 if node_line != parent_line {
893+ // Don't add indent for the line below the line of the query
680894 if node_line < line + (new_line as usize) {
681− // Don't add indent for the line below the line of the query
682− result.add_line(&indent_for_line_below);
895+ result.add_line(indent_for_line_below);
683896 }
897+
684898 if node_line == parent_line + 1 {
685899 indent_for_line_below = indent_for_line;
686900 } else {
687− result.add_line(&indent_for_line);
901+ result.add_line(indent_for_line);
688902 indent_for_line_below = Indentation::default();
689903 }
904+
690905 indent_for_line = Indentation::default();
691906 }
692907
@@ -698,13 +913,13 @@ pub fn treesitter_indent_for_pos(
698913 if (node.start_position().row < line)
699914 || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
700915 {
701− result.add_line(&indent_for_line_below);
916+ result.add_line(indent_for_line_below);
702917 }
703− result.add_line(&indent_for_line);
918+ result.add_line(indent_for_line);
704919 break;
705920 }
706921 }
707− Some(result.as_string(indent_style))
922+ Some(result)
708923}
709924
710925/// Returns the indentation for a new line.
@@ -713,6 +928,7 @@ pub fn treesitter_indent_for_pos(
713928pub fn indent_for_newline(
714929 language_config: Option<&LanguageConfiguration>,
715930 syntax: Option<&Syntax>,
931+ indent_heuristic: &IndentationHeuristic,
716932 indent_style: &IndentStyle,
717933 tab_width: usize,
718934 text: RopeSlice,
@@ -721,14 +937,18 @@ pub fn indent_for_newline(
721937 current_line: usize,
722938) -> String {
723939 let indent_width = indent_style.indent_width(tab_width);
724− if let (Some(query), Some(syntax)) = (
940+ if let (
941+ IndentationHeuristic::TreeSitter | IndentationHeuristic::Hybrid,
942+ Some(query),
943+ Some(syntax),
944+ ) = (
945+ indent_heuristic,
725946 language_config.and_then(|config| config.indent_query()),
726947 syntax,
727948 ) {
728949 if let Some(indent) = treesitter_indent_for_pos(
729950 query,
730951 syntax,
731− indent_style,
732952 tab_width,
733953 indent_width,
734954 text,
@@ -736,9 +956,57 @@ pub fn indent_for_newline(
736956 line_before_end_pos,
737957 true,
738958 ) {
739− return indent;
959+ if *indent_heuristic == IndentationHeuristic::Hybrid {
960+ // We want to compute the indentation not only based on the
961+ // syntax tree but also on the actual indentation of a previous
962+ // line. This makes indentation computation more resilient to
963+ // incomplete queries, incomplete source code & differing indentation
964+ // styles for the same language.
965+ // However, using the indent of a previous line as a baseline may not
966+ // make sense, e.g. if it has a different alignment than the new line.
967+ // In order to prevent edge cases with long running times, we only try
968+ // a constant number of (non-empty) lines.
969+ const MAX_ATTEMPTS: usize = 4;
970+ let mut num_attempts = 0;
971+ for line_idx in (0..=line_before).rev() {
972+ let line = text.line(line_idx);
973+ let first_non_whitespace_char = match find_first_non_whitespace_char(line) {
974+ Some(i) => i,
975+ None => {
976+ continue;
977+ }
978+ };
979+ if let Some(indent) = (|| {
980+ let computed_indent = treesitter_indent_for_pos(
981+ query,
982+ syntax,
983+ tab_width,
984+ indent_width,
985+ text,
986+ line_idx,
987+ text.line_to_char(line_idx) + first_non_whitespace_char,
988+ false,
989+ )?;
990+ let leading_whitespace = line.slice(0..first_non_whitespace_char);
991+ indent.relative_indent(
992+ &computed_indent,
993+ leading_whitespace,
994+ indent_style,
995+ tab_width,
996+ )
997+ })() {
998+ return indent;
999+ }
1000+ num_attempts += 1;
1001+ if num_attempts == MAX_ATTEMPTS {
1002+ break;
1003+ }
1004+ }
1005+ }
1006+ return indent.to_string(indent_style, tab_width);
7401007 };
7411008 }
1009+ // Fallback in case we either don't have indent queries or they failed for some reason
7421010 let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
7431011 indent_style.as_str().repeat(indent_level)
7441012}
@@ -810,4 +1078,195 @@ mod test {
8101078 2
8111079 );
8121080 }
1081+
1082+ #[test]
1083+ fn add_capture() {
1084+ let indent = || Indentation {
1085+ indent: 1,
1086+ ..Default::default()
1087+ };
1088+ let indent_always = || Indentation {
1089+ indent_always: 1,
1090+ ..Default::default()
1091+ };
1092+ let outdent = || Indentation {
1093+ outdent: 1,
1094+ ..Default::default()
1095+ };
1096+ let outdent_always = || Indentation {
1097+ outdent_always: 1,
1098+ ..Default::default()
1099+ };
1100+
1101+ fn add_capture<'a>(
1102+ mut indent: Indentation<'a>,
1103+ capture: IndentCaptureType<'a>,
1104+ ) -> Indentation<'a> {
1105+ indent.add_capture(capture);
1106+ indent
1107+ }
1108+
1109+ // adding an indent to no indent makes an indent
1110+ assert_eq!(
1111+ indent(),
1112+ add_capture(Indentation::default(), IndentCaptureType::Indent)
1113+ );
1114+ assert_eq!(
1115+ indent_always(),
1116+ add_capture(Indentation::default(), IndentCaptureType::IndentAlways)
1117+ );
1118+ assert_eq!(
1119+ outdent(),
1120+ add_capture(Indentation::default(), IndentCaptureType::Outdent)
1121+ );
1122+ assert_eq!(
1123+ outdent_always(),
1124+ add_capture(Indentation::default(), IndentCaptureType::OutdentAlways)
1125+ );
1126+
1127+ // adding an indent to an already indented has no effect
1128+ assert_eq!(indent(), add_capture(indent(), IndentCaptureType::Indent));
1129+ assert_eq!(
1130+ outdent(),
1131+ add_capture(outdent(), IndentCaptureType::Outdent)
1132+ );
1133+
1134+ // adding an always to a regular makes it always
1135+ assert_eq!(
1136+ indent_always(),
1137+ add_capture(indent(), IndentCaptureType::IndentAlways)
1138+ );
1139+ assert_eq!(
1140+ outdent_always(),
1141+ add_capture(outdent(), IndentCaptureType::OutdentAlways)
1142+ );
1143+
1144+ // adding an always to an always is additive
1145+ assert_eq!(
1146+ Indentation {
1147+ indent_always: 2,
1148+ ..Default::default()
1149+ },
1150+ add_capture(indent_always(), IndentCaptureType::IndentAlways)
1151+ );
1152+ assert_eq!(
1153+ Indentation {
1154+ outdent_always: 2,
1155+ ..Default::default()
1156+ },
1157+ add_capture(outdent_always(), IndentCaptureType::OutdentAlways)
1158+ );
1159+
1160+ // adding regular to always should be associative
1161+ assert_eq!(
1162+ Indentation {
1163+ indent_always: 1,
1164+ ..Default::default()
1165+ },
1166+ add_capture(
1167+ add_capture(indent(), IndentCaptureType::Indent),
1168+ IndentCaptureType::IndentAlways
1169+ )
1170+ );
1171+ assert_eq!(
1172+ Indentation {
1173+ indent_always: 1,
1174+ ..Default::default()
1175+ },
1176+ add_capture(
1177+ add_capture(indent(), IndentCaptureType::IndentAlways),
1178+ IndentCaptureType::Indent
1179+ )
1180+ );
1181+ assert_eq!(
1182+ Indentation {
1183+ outdent_always: 1,
1184+ ..Default::default()
1185+ },
1186+ add_capture(
1187+ add_capture(outdent(), IndentCaptureType::Outdent),
1188+ IndentCaptureType::OutdentAlways
1189+ )
1190+ );
1191+ assert_eq!(
1192+ Indentation {
1193+ outdent_always: 1,
1194+ ..Default::default()
1195+ },
1196+ add_capture(
1197+ add_capture(outdent(), IndentCaptureType::OutdentAlways),
1198+ IndentCaptureType::Outdent
1199+ )
1200+ );
1201+ }
1202+
1203+ #[test]
1204+ fn test_relative_indent() {
1205+ let indent_style = IndentStyle::Spaces(4);
1206+ let tab_width: usize = 4;
1207+ let no_align = [
1208+ Indentation::default(),
1209+ Indentation {
1210+ indent: 1,
1211+ ..Default::default()
1212+ },
1213+ Indentation {
1214+ indent: 5,
1215+ outdent: 1,
1216+ ..Default::default()
1217+ },
1218+ ];
1219+ let align = no_align.clone().map(|indent| Indentation {
1220+ align: Some(RopeSlice::from("12345")),
1221+ ..indent
1222+ });
1223+ let different_align = Indentation {
1224+ align: Some(RopeSlice::from("123456")),
1225+ ..Default::default()
1226+ };
1227+
1228+ // Check that relative and absolute indentation computation are the same when the line we compare to is
1229+ // indented as we expect.
1230+ let check_consistency = |indent: &Indentation, other: &Indentation| {
1231+ assert_eq!(
1232+ indent.relative_indent(
1233+ other,
1234+ RopeSlice::from(other.to_string(&indent_style, tab_width).as_str()),
1235+ &indent_style,
1236+ tab_width
1237+ ),
1238+ Some(indent.to_string(&indent_style, tab_width))
1239+ );
1240+ };
1241+ for a in &no_align {
1242+ for b in &no_align {
1243+ check_consistency(a, b);
1244+ }
1245+ }
1246+ for a in &align {
1247+ for b in &align {
1248+ check_consistency(a, b);
1249+ }
1250+ }
1251+
1252+ // Relative indent computation makes no sense if the alignment differs
1253+ assert_eq!(
1254+ align[0].relative_indent(
1255+ &no_align[0],
1256+ RopeSlice::from(" "),
1257+ &indent_style,
1258+ tab_width
1259+ ),
1260+ None
1261+ );
1262+ assert_eq!(
1263+ align[0].relative_indent(
1264+ &different_align,
1265+ RopeSlice::from(" "),
1266+ &indent_style,
1267+ tab_width
1268+ ),
1269+ None
1270+ );
1271+ }
8131272}
helix-core/src/lib.rs+1−1
@@ -7,6 +7,7 @@ pub mod config;
77pub mod diagnostic;
88pub mod diff;
99pub mod doc_formatter;
10+pub mod fuzzy;
1011pub mod graphemes;
1112pub mod history;
1213pub mod increment;
@@ -18,7 +19,6 @@ pub mod movement;
1819pub mod object;
1920pub mod path;
2021mod position;
21−pub mod register;
2222pub mod search;
2323pub mod selection;
2424pub mod shellwords;
helix-core/src/match_brackets.rs+6−2
@@ -106,12 +106,16 @@ fn find_pair(
106106 for close in
107107 iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
108108 {
109− let Some(open) = as_close_pair(doc, &close) else { continue; };
109+ let Some(open) = as_close_pair(doc, &close) else {
110+ continue;
111+ };
110112 if find_pair_end(doc, Some(node), open, Backward).is_some() {
111113 return doc.try_byte_to_char(close.start_byte()).ok();
112114 }
113115 }
114− let Some(parent) = node.parent() else { break; };
116+ let Some(parent) = node.parent() else {
117+ break;
118+ };
115119 node = parent;
116120 }
117121 let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
helix-core/src/movement.rs+80−1
@@ -16,7 +16,7 @@ use crate::{
1616 syntax::LanguageConfiguration,
1717 text_annotations::TextAnnotations,
1818 textobject::TextObject,
19− visual_offset_from_block, Range, RopeSlice,
19+ visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
2020};
2121
2222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
556556 last_range
557557}
558558
559+fn find_parent_start(mut node: Node) -> Option<Node> {
560+ let start = node.start_byte();
561+
562+ while node.start_byte() >= start || !node.is_named() {
563+ node = node.parent()?;
564+ }
565+
566+ Some(node)
567+}
568+
569+pub fn move_parent_node_end(
570+ syntax: &Syntax,
571+ text: RopeSlice,
572+ selection: Selection,
573+ dir: Direction,
574+ movement: Movement,
575+) -> Selection {
576+ let tree = syntax.tree();
577+
578+ selection.transform(|range| {
579+ let start_from = text.char_to_byte(range.from());
580+ let start_to = text.char_to_byte(range.to());
581+
582+ let mut node = match tree
583+ .root_node()
584+ .named_descendant_for_byte_range(start_from, start_to)
585+ {
586+ Some(node) => node,
587+ None => {
588+ log::debug!(
589+ "no descendant found for byte range: {} - {}",
590+ start_from,
591+ start_to
592+ );
593+ return range;
594+ }
595+ };
596+
597+ let mut end_head = match dir {
598+ // moving forward, we always want to move one past the end of the
599+ // current node, so use the end byte of the current node, which is an exclusive
600+ // end of the range
601+ Direction::Forward => text.byte_to_char(node.end_byte()),
602+
603+ // moving backward, we want the cursor to land on the start char of
604+ // the current node, or if it is already at the start of a node, to traverse up to
605+ // the parent
606+ Direction::Backward => {
607+ let end_head = text.byte_to_char(node.start_byte());
608+
609+ // if we're already on the beginning, look up to the parent
610+ if end_head == range.cursor(text) {
611+ node = find_parent_start(node).unwrap_or(node);
612+ text.byte_to_char(node.start_byte())
613+ } else {
614+ end_head
615+ }
616+ }
617+ };
618+
619+ if movement == Movement::Move {
620+ // preserve direction of original range
621+ if range.direction() == Direction::Forward {
622+ Range::new(end_head, end_head + 1)
623+ } else {
624+ Range::new(end_head + 1, end_head)
625+ }
626+ } else {
627+ // if we end up with a forward range, then adjust it to be one past
628+ // where we want
629+ if end_head >= range.anchor {
630+ end_head += 1;
631+ }
632+
633+ Range::new(range.anchor, end_head)
634+ }
635+ })
636+}
637+
559638#[cfg(test)]
560639mod test {
561640 use ropey::Rope;
helix-core/src/path.rs+2−2
@@ -85,7 +85,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
8585///
8686/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
8787/// here if the path exists, just normalize it's components.
88−pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
88+pub fn get_canonicalized_path(path: &Path) -> PathBuf {
8989 let path = expand_tilde(path);
9090 let path = if path.is_relative() {
9191 helix_loader::current_working_dir().join(path)
@@ -93,7 +93,7 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
9393 path
9494 };
9595
96− Ok(get_normalized_path(path.as_path()))
96+ get_normalized_path(path.as_path())
9797}
9898
9999pub fn get_relative_path(path: &Path) -> PathBuf {
helix-core/src/register.rsdeleted−89
@@ -1,89 +0,0 @@
1−use std::collections::HashMap;
2−
3−#[derive(Debug)]
4−pub struct Register {
5− name: char,
6− values: Vec<String>,
7−}
8−
9−impl Register {
10− pub const fn new(name: char) -> Self {
11− Self {
12− name,
13− values: Vec::new(),
14− }
15− }
16−
17− pub fn new_with_values(name: char, values: Vec<String>) -> Self {
18− Self { name, values }
19− }
20−
21− pub const fn name(&self) -> char {
22− self.name
23− }
24−
25− pub fn read(&self) -> &[String] {
26− &self.values
27− }
28−
29− pub fn write(&mut self, values: Vec<String>) {
30− self.values = values;
31− }
32−
33− pub fn push(&mut self, value: String) {
34− self.values.push(value);
35− }
36−}
37−
38−/// Currently just wraps a `HashMap` of `Register`s
39−#[derive(Debug, Default)]
40−pub struct Registers {
41− inner: HashMap<char, Register>,
42−}
43−
44−impl Registers {
45− pub fn get(&self, name: char) -> Option<&Register> {
46− self.inner.get(&name)
47− }
48−
49− pub fn read(&self, name: char) -> Option<&[String]> {
50− self.get(name).map(|reg| reg.read())
51− }
52−
53− pub fn write(&mut self, name: char, values: Vec<String>) {
54− if name != '_' {
55− self.inner
56− .insert(name, Register::new_with_values(name, values));
57− }
58− }
59−
60− pub fn push(&mut self, name: char, value: String) {
61− if name != '_' {
62− if let Some(r) = self.inner.get_mut(&name) {
63− r.push(value);
64− } else {
65− self.write(name, vec![value]);
66− }
67− }
68− }
69−
70− pub fn first(&self, name: char) -> Option<&String> {
71− self.read(name).and_then(|entries| entries.first())
72− }
73−
74− pub fn last(&self, name: char) -> Option<&String> {
75− self.read(name).and_then(|entries| entries.last())
76− }
77−
78− pub fn inner(&self) -> &HashMap<char, Register> {
79− &self.inner
80− }
81−
82− pub fn clear(&mut self) {
83− self.inner.clear();
84− }
85−
86− pub fn remove(&mut self, name: char) -> Option<Register> {
87− self.inner.remove(&name)
88− }
89−}
helix-core/src/selection.rs+10−2
@@ -630,11 +630,19 @@ impl Selection {
630630 self.transform(|range| Range::point(range.cursor(text)))
631631 }
632632
633− pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
633+ pub fn fragments<'a>(
634+ &'a self,
635+ text: RopeSlice<'a>,
636+ ) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<str>> + 'a
637+ {
634638 self.ranges.iter().map(move |range| range.fragment(text))
635639 }
636640
637− pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a {
641+ pub fn slices<'a>(
642+ &'a self,
643+ text: RopeSlice<'a>,
644+ ) -> impl DoubleEndedIterator<Item = RopeSlice<'a>> + ExactSizeIterator<Item = RopeSlice<'a>> + 'a
645+ {
638646 self.ranges.iter().map(move |range| range.slice(text))
639647 }
640648
helix-core/src/syntax.rs+40−21
@@ -4,7 +4,7 @@ use crate::{
44 diagnostic::Severity,
55 regex::Regex,
66 transaction::{ChangeSet, Operation},
7− Rope, RopeSlice, Tendril,
7+ RopeSlice, Tendril,
88};
99
1010use ahash::RandomState;
@@ -101,7 +101,8 @@ pub struct LanguageConfiguration {
101101 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
102102 #[serde(default)]
103103 pub shebangs: Vec<String>, // interpreter(s) associated with language
104− pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
104+ #[serde(default)]
105+ pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
105106 pub comment_token: Option<String>,
106107 pub text_width: Option<usize>,
107108 pub soft_wrap: Option<SoftWrap>,
@@ -154,6 +155,8 @@ pub struct LanguageConfiguration {
154155 /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
155156 /// Falling back to the current working directory if none are configured.
156157 pub workspace_lsp_roots: Option<Vec<PathBuf>>,
158+ #[serde(default)]
159+ pub persistent_diagnostic_sources: Vec<String>,
157160}
158161
159162#[derive(Debug, PartialEq, Eq, Hash)]
@@ -211,10 +214,7 @@ impl<'de> Deserialize<'de> for FileType {
211214 {
212215 match map.next_entry::<String, String>()? {
213216 Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
214− // FIXME: use `suffix.replace('/', std::path::MAIN_SEPARATOR_STR)`
215− // if MSRV is updated to 1.68
216− let mut separator = [0; 1];
217− suffix.replace('/', std::path::MAIN_SEPARATOR.encode_utf8(&mut separator))
217+ suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
218218 })),
219219 Some((key, _value)) => Err(serde::de::Error::custom(format!(
220220 "unknown key in `file-types` list: {}",
@@ -444,6 +444,22 @@ pub struct IndentationConfiguration {
444444 pub unit: String,
445445}
446446
447+/// How the indentation for a newly inserted line should be determined.
448+/// If the selected heuristic is not available (e.g. because the current
449+/// language has no tree-sitter indent queries), a simpler one will be used.
450+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
451+#[serde(rename_all = "kebab-case")]
452+pub enum IndentationHeuristic {
453+ /// Just copy the indentation of the line that the cursor is currently on.
454+ Simple,
455+ /// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
456+ TreeSitter,
457+ /// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
458+ /// and the line before. Add this to the actual indentation level of the line before.
459+ #[default]
460+ Hybrid,
461+}
462+
447463/// Configuration for auto pairs
448464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449465#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
@@ -818,7 +834,10 @@ impl Loader {
818834 // TODO: content_regex handling conflict resolution
819835 }
820836
821− pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
837+ pub fn language_config_for_shebang(
838+ &self,
839+ source: RopeSlice,
840+ ) -> Option<Arc<LanguageConfiguration>> {
822841 let line = Cow::from(source.line(0));
823842 static SHEBANG_REGEX: Lazy<Regex> =
824843 Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap());
@@ -928,7 +947,7 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
928947
929948impl Syntax {
930949 pub fn new(
931− source: &Rope,
950+ source: RopeSlice,
932951 config: Arc<HighlightConfiguration>,
933952 loader: Arc<Loader>,
934953 ) -> Option<Self> {
@@ -959,7 +978,7 @@ impl Syntax {
959978 let res = syntax.update(source, source, &ChangeSet::new(source));
960979
961980 if res.is_err() {
962− log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
981+ log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
963982 return None;
964983 }
965984 Some(syntax)
@@ -967,8 +986,8 @@ impl Syntax {
967986
968987 pub fn update(
969988 &mut self,
970− old_source: &Rope,
971− source: &Rope,
989+ old_source: RopeSlice,
990+ source: RopeSlice,
972991 changeset: &ChangeSet,
973992 ) -> Result<(), Error> {
974993 let mut queue = VecDeque::new();
@@ -1387,7 +1406,7 @@ impl LanguageLayer {
13871406 self.tree.as_ref().unwrap()
13881407 }
13891408
1390− fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
1409+ fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error> {
13911410 parser
13921411 .set_included_ranges(&self.ranges)
13931412 .map_err(|_| Error::InvalidRanges)?;
@@ -1418,7 +1437,7 @@ impl LanguageLayer {
14181437}
14191438
14201439pub(crate) fn generate_edits(
1421− old_text: &Rope,
1440+ old_text: RopeSlice,
14221441 changeset: &ChangeSet,
14231442) -> Vec<tree_sitter::InputEdit> {
14241443 use Operation::*;
@@ -1434,7 +1453,7 @@ pub(crate) fn generate_edits(
14341453
14351454 // TODO; this is a lot easier with Change instead of Operation.
14361455
1437− fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
1456+ fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
14381457 let byte = text.char_to_byte(pos); // <- attempted to index past end
14391458 let line = text.char_to_line(pos);
14401459 let line_start_byte = text.line_to_byte(line);
@@ -1611,7 +1630,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
16111630}
16121631
16131632pub struct RopeProvider<'a>(pub RopeSlice<'a>);
1614−impl<'a> TextProvider<'a> for RopeProvider<'a> {
1633+impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
16151634 type I = ChunksBytes<'a>;
16161635
16171636 fn text(&mut self, node: Node) -> Self::I {
@@ -1625,7 +1644,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
16251644struct HighlightIterLayer<'a> {
16261645 _tree: Option<Tree>,
16271646 cursor: QueryCursor,
1628− captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
1647+ captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
16291648 config: &'a HighlightConfiguration,
16301649 highlight_end_stack: Vec<usize>,
16311650 scope_stack: Vec<LocalScope<'a>>,
@@ -2529,7 +2548,7 @@ mod test {
25292548 let mut cursor = QueryCursor::new();
25302549
25312550 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2532− let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
2551+ let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
25332552
25342553 let root = syntax.tree().root_node();
25352554 let mut test = |capture, range| {
@@ -2603,7 +2622,7 @@ mod test {
26032622 fn main() {}
26042623 ",
26052624 );
2606− let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
2625+ let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
26072626 let tree = syntax.tree();
26082627 let root = tree.root_node();
26092628 assert_eq!(root.kind(), "source_file");
@@ -2630,7 +2649,7 @@ mod test {
26302649 &doc,
26312650 vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
26322651 );
2633− let edits = generate_edits(&doc, transaction.changes());
2652+ let edits = generate_edits(doc.slice(..), transaction.changes());
26342653 // transaction.apply(&mut state);
26352654
26362655 assert_eq!(
@@ -2659,7 +2678,7 @@ mod test {
26592678 let mut doc = Rope::from("fn test() {}");
26602679 let transaction =
26612680 Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
2662− let edits = generate_edits(&doc, transaction.changes());
2681+ let edits = generate_edits(doc.slice(..), transaction.changes());
26632682 transaction.apply(&mut doc);
26642683
26652684 assert_eq!(doc, "fn test(a: u32) {}");
@@ -2693,7 +2712,7 @@ mod test {
26932712 let language = get_language(language_name).unwrap();
26942713
26952714 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2696− let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
2715+ let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
26972716
26982717 let root = syntax
26992718 .tree()
helix-core/src/transaction.rs+84−21
@@ -1,6 +1,7 @@
1+use ropey::RopeSlice;
12use smallvec::SmallVec;
23
3−use crate::{Range, Rope, Selection, Tendril};
4+use crate::{chars::char_is_word, Range, Rope, Selection, Tendril};
45use std::{borrow::Cow, iter::once};
56
67/// (from, to, replacement)
@@ -22,6 +23,30 @@ pub enum Operation {
2223pub enum Assoc {
2324 Before,
2425 After,
26+ /// Acts like `After` if a word character is inserted
27+ /// after the position, otherwise acts like `Before`
28+ AfterWord,
29+ /// Acts like `Before` if a word character is inserted
30+ /// before the position, otherwise acts like `After`
31+ BeforeWord,
32+}
33+
34+impl Assoc {
35+ /// Whether to stick to gaps
36+ fn stay_at_gaps(self) -> bool {
37+ !matches!(self, Self::BeforeWord | Self::AfterWord)
38+ }
39+
40+ fn insert_offset(self, s: &str) -> usize {
41+ let chars = s.chars().count();
42+ match self {
43+ Assoc::After => chars,
44+ Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(),
45+ // return position before inserted text
46+ Assoc::Before => 0,
47+ Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(),
48+ }
49+ }
2550}
2651
2752#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -42,7 +67,7 @@ impl ChangeSet {
4267 }
4368
4469 #[must_use]
45− pub fn new(doc: &Rope) -> Self {
70+ pub fn new(doc: RopeSlice) -> Self {
4671 let len = doc.len_chars();
4772 Self {
4873 changes: Vec::new(),
@@ -388,7 +413,10 @@ impl ChangeSet {
388413 }
389414
390415 let Some((i, change)) = iter.next() else {
391− map!(|pos, _| (old_pos == pos).then_some(new_pos), self.changes.len());
416+ map!(
417+ |pos, _| (old_pos == pos).then_some(new_pos),
418+ self.changes.len()
419+ );
392420 break;
393421 };
394422
@@ -411,8 +439,6 @@ impl ChangeSet {
411439 map!(|pos, _| (old_end > pos).then_some(new_pos), i);
412440 }
413441 Insert(s) => {
414− let ins = s.chars().count();
415−
416442 // a subsequent delete means a replace, consume it
417443 if let Some((_, Delete(len))) = iter.peek() {
418444 iter.next();
@@ -420,13 +446,13 @@ impl ChangeSet {
420446 old_end = old_pos + len;
421447 // in range of replaced text
422448 map!(
423− |pos, assoc| (old_end > pos).then(|| {
449+ |pos, assoc: Assoc| (old_end > pos).then(|| {
424450 // at point or tracking before
425− if pos == old_pos || assoc == Assoc::Before {
451+ if pos == old_pos && assoc.stay_at_gaps() {
426452 new_pos
427453 } else {
428454 // place to end of insert
429− new_pos + ins
455+ new_pos + assoc.insert_offset(s)
430456 }
431457 }),
432458 i
@@ -434,20 +460,15 @@ impl ChangeSet {
434460 } else {
435461 // at insert point
436462 map!(
437− |pos, assoc| (old_pos == pos).then(|| {
463+ |pos, assoc: Assoc| (old_pos == pos).then(|| {
438464 // return position before inserted text
439− if assoc == Assoc::Before {
440− new_pos
441− } else {
442− // after text
443− new_pos + ins
444− }
465+ new_pos + assoc.insert_offset(s)
445466 }),
446467 i
447468 );
448469 }
449470
450− new_pos += ins;
471+ new_pos += s.chars().count();
451472 }
452473 }
453474 old_pos = old_end;
@@ -485,7 +506,7 @@ impl Transaction {
485506 /// Create a new, empty transaction.
486507 pub fn new(doc: &Rope) -> Self {
487508 Self {
488− changes: ChangeSet::new(doc),
509+ changes: ChangeSet::new(doc.slice(..)),
489510 selection: None,
490511 }
491512 }
@@ -880,6 +901,48 @@ mod test {
880901 let mut positions = [4, 2];
881902 cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
882903 assert_eq!(positions, [4, 2]);
904+ // stays at word boundary
905+ let cs = ChangeSet {
906+ changes: vec![
907+ Retain(2), // <space><space>
908+ Insert(" ab".into()),
909+ Retain(2), // cd
910+ Insert("de ".into()),
911+ ],
912+ len: 4,
913+ len_after: 10,
914+ };
915+ assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 3);
916+ assert_eq!(cs.map_pos(4, Assoc::AfterWord), 9);
917+ let cs = ChangeSet {
918+ changes: vec![
919+ Retain(1), // <space>
920+ Insert(" b".into()),
921+ Delete(1), // c
922+ Retain(1), // d
923+ Insert("e ".into()),
924+ Delete(1), // <space>
925+ ],
926+ len: 5,
927+ len_after: 7,
928+ };
929+ assert_eq!(cs.map_pos(1, Assoc::BeforeWord), 2);
930+ assert_eq!(cs.map_pos(3, Assoc::AfterWord), 5);
931+ let cs = ChangeSet {
932+ changes: vec![
933+ Retain(1), // <space>
934+ Insert("a".into()),
935+ Delete(2), // <space>b
936+ Retain(1), // d
937+ Insert("e".into()),
938+ Delete(1), // f
939+ Retain(1), // <space>
940+ ],
941+ len: 5,
942+ len_after: 7,
943+ };
944+ assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 1);
945+ assert_eq!(cs.map_pos(4, Assoc::AfterWord), 4);
883946 }
884947
885948 #[test]
@@ -946,9 +1009,9 @@ mod test {
9461009 #[test]
9471010 fn combine_with_empty() {
9481011 let empty = Rope::from("");
949− let a = ChangeSet::new(&empty);
1012+ let a = ChangeSet::new(empty.slice(..));
9501013
951− let mut b = ChangeSet::new(&empty);
1014+ let mut b = ChangeSet::new(empty.slice(..));
9521015 b.insert("a".into());
9531016
9541017 let changes = a.compose(b);
@@ -962,9 +1025,9 @@ mod test {
9621025 const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
9631026
9641027 let empty = Rope::from("");
965− let a = ChangeSet::new(&empty);
1028+ let a = ChangeSet::new(empty.slice(..));
9661029
967− let mut b = ChangeSet::new(&empty);
1030+ let mut b = ChangeSet::new(empty.slice(..));
9681031 b.insert(TEST_CASE.into());
9691032
9701033 let changes = a.compose(b);
helix-core/src/wrap.rs+3−1
@@ -1,7 +1,9 @@
11use smartstring::{LazyCompact, SmartString};
2+use textwrap::{Options, WordSplitter::NoHyphenation};
23
34/// Given a slice of text, return the text re-wrapped to fit it
45/// within the given width.
56pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
6− textwrap::refill(text, text_width).into()
7+ let options = Options::new(text_width).word_splitter(NoHyphenation);
8+ textwrap::refill(text, options).into()
79}
helix-core/tests/data/indent/cpp.cppnew file+48
@@ -0,0 +1,48 @@
1+std::vector<std::string>
2+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
3+ char* parm5, bool parm6);
4+
5+std::vector<std::string>
6+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
7+ char* parm5, bool parm6) {
8+ auto lambda = []() {
9+ return 0;
10+ };
11+ auto lambda_with_a_really_long_name_that_uses_a_whole_line
12+ = [](int some_more_aligned_parameters,
13+ std::string parm2) {
14+ do_smth();
15+ };
16+ if (brace_on_same_line) {
17+ do_smth();
18+ } else if (brace_on_next_line)
19+ {
20+ do_smth();
21+ } else if (another_condition) {
22+ do_smth();
23+ }
24+ else {
25+ do_smth();
26+ }
27+ if (inline_if_statement)
28+ do_smth();
29+ if (another_inline_if_statement)
30+ return [](int parm1, char* parm2) {
31+ this_is_a_really_pointless_lambda();
32+ };
33+
34+ switch (var) {
35+ case true:
36+ return -1;
37+ case false:
38+ return 42;
39+ }
40+}
41+
42+class MyClass : public MyBaseClass {
43+public:
44+ MyClass();
45+ void public_fn();
46+private:
47+ super_secret_private_fn();
48+}
helix-core/tests/data/indent/indent.rsdeleted−1
@@ -1 +0,0 @@
1−../../../src/indent.rs
helix-core/tests/data/indent/languages.toml+13
@@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " }
1111[[grammar]]
1212name = "rust"
1313source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
14+
15+[[language]]
16+name = "cpp"
17+scope = "source.cpp"
18+injection-regex = "cpp"
19+file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"]
20+roots = []
21+comment-token = "//"
22+indent = { tab-width = 2, unit = " " }
23+
24+[[grammar]]
25+name = "cpp"
26+source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "2d2c4aee8672af4c7c8edff68e7dd4c07e88d2b1" }
helix-core/tests/indent.rs+153−25
@@ -1,20 +1,122 @@
11use helix_core::{
22 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
3− syntax::Loader,
3+ syntax::{Configuration, Loader},
44 Syntax,
55};
6−use std::path::PathBuf;
6+use ropey::Rope;
7+use std::{ops::Range, path::PathBuf, process::Command};
78
89#[test]
910fn test_treesitter_indent_rust() {
10− test_treesitter_indent("rust.rs", "source.rust");
11+ standard_treesitter_test("rust.rs", "source.rust");
1112}
13+
1214#[test]
13−fn test_treesitter_indent_rust_2() {
14− test_treesitter_indent("indent.rs", "source.rust");
15− // TODO Use commands.rs as indentation test.
16− // Currently this fails because we can't align the parameters of a closure yet
17− // test_treesitter_indent("commands.rs", "source.rust");
15+fn test_treesitter_indent_cpp() {
16+ standard_treesitter_test("cpp.cpp", "source.cpp");
17+}
18+
19+#[test]
20+fn test_treesitter_indent_rust_helix() {
21+ // We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
22+ // Ideally, someone updates this once in a while and fixes any errors that occur.
23+ let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
24+ let files = Command::new("git")
25+ .args([
26+ "ls-tree",
27+ "-r",
28+ "--name-only",
29+ "--full-tree",
30+ rev,
31+ "helix-term/src",
32+ ])
33+ .output()
34+ .unwrap();
35+ let files = String::from_utf8(files.stdout).unwrap();
36+
37+ let ignored_files = vec![
38+ // Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
39+ "helix-term/src/health.rs",
40+ ];
41+
42+ for file in files.split_whitespace() {
43+ if ignored_files.contains(&file) {
44+ continue;
45+ }
46+ let ignored_lines: Vec<Range<usize>> = match file {
47+ "helix-term/src/application.rs" => vec![
48+ // We can't handle complicated indent rules inside macros (`json!` in this case) since
49+ // the tree-sitter grammar only parses them as `token_tree` and `identifier` nodes.
50+ 1045..1051,
51+ ],
52+ "helix-term/src/commands.rs" => vec![
53+ // This is broken because of the current handling of `call_expression`
54+ // (i.e. having an indent query for it but outdenting again in specific cases).
55+ // The indent query is needed to correctly handle multi-line arguments in function calls
56+ // inside indented `field_expression` nodes (which occurs fairly often).
57+ //
58+ // Once we have the `@indent.always` capture type, it might be possible to just have an indent
59+ // capture for the `arguments` field of a call expression. That could enable us to correctly
60+ // handle this.
61+ 2226..2230,
62+ ],
63+ "helix-term/src/commands/dap.rs" => vec![
64+ // Complex `format!` macro
65+ 46..52,
66+ ],
67+ "helix-term/src/commands/lsp.rs" => vec![
68+ // Macro
69+ 624..627,
70+ // Return type declaration of a closure. `cargo fmt` adds an additional space here,
71+ // which we cannot (yet) model with our indent queries.
72+ 878..879,
73+ // Same as in `helix-term/src/commands.rs`
74+ 1335..1343,
75+ ],
76+ "helix-term/src/config.rs" => vec![
77+ // Multiline string
78+ 146..152,
79+ ],
80+ "helix-term/src/keymap.rs" => vec![
81+ // Complex macro (see above)
82+ 456..470,
83+ // Multiline string without indent
84+ 563..567,
85+ ],
86+ "helix-term/src/main.rs" => vec![
87+ // Multiline string
88+ 44..70,
89+ ],
90+ "helix-term/src/ui/completion.rs" => vec![
91+ // Macro
92+ 218..232,
93+ ],
94+ "helix-term/src/ui/editor.rs" => vec![
95+ // The chained function calls here are not indented, probably because of the comment
96+ // in between. Since `cargo fmt` doesn't even attempt to format it, there's probably
97+ // no point in trying to indent this correctly.
98+ 342..350,
99+ ],
100+ "helix-term/src/ui/lsp.rs" => vec![
101+ // Macro
102+ 56..61,
103+ ],
104+ "helix-term/src/ui/statusline.rs" => vec![
105+ // Same as in `helix-term/src/commands.rs`
106+ 436..442,
107+ 450..456,
108+ ],
109+ _ => Vec::new(),
110+ };
111+
112+ let git_object = rev.to_string() + ":" + file;
113+ let content = Command::new("git")
114+ .args(["cat-file", "blob", &git_object])
115+ .output()
116+ .unwrap();
117+ let doc = Rope::from_reader(&mut content.stdout.as_slice()).unwrap();
118+ test_treesitter_indent(file, doc, "source.rust", ignored_lines);
119+ }
18120}
19121
20122#[test]
@@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
50152 assert_eq!(indent_level, 2)
51153}
52154
53−fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
155+fn indent_tests_dir() -> PathBuf {
54156 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
55157 test_dir.push("tests/data/indent");
158+ test_dir
159+}
56160
57− let mut test_file = test_dir.clone();
58− test_file.push(file_name);
59− let test_file = std::fs::File::open(test_file).unwrap();
161+fn indent_test_path(name: &str) -> PathBuf {
162+ let mut path = indent_tests_dir();
163+ path.push(name);
164+ path
165+}
166+
167+fn indent_tests_config() -> Configuration {
168+ let mut config_path = indent_tests_dir();
169+ config_path.push("languages.toml");
170+ let config = std::fs::read_to_string(config_path).unwrap();
171+ toml::from_str(&config).unwrap()
172+}
173+
174+fn standard_treesitter_test(file_name: &str, lang_scope: &str) {
175+ let test_path = indent_test_path(file_name);
176+ let test_file = std::fs::File::open(test_path).unwrap();
60177 let doc = ropey::Rope::from_reader(test_file).unwrap();
178+ test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
179+}
61180
62− let mut config_file = test_dir;
63− config_file.push("languages.toml");
64− let config = std::fs::read_to_string(config_file).unwrap();
65− let config = toml::from_str(&config).unwrap();
66− let loader = Loader::new(config);
181+/// Test that all the lines in the given file are indented as expected.
182+/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
183+fn test_treesitter_indent(
184+ test_name: &str,
185+ doc: Rope,
186+ lang_scope: &str,
187+ ignored_lines: Vec<std::ops::Range<usize>>,
188+) {
189+ let loader = Loader::new(indent_tests_config());
67190
68191 // set runtime path so we can find the queries
69192 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -71,30 +194,35 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
71194 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
72195
73196 let language_config = loader.language_config_for_scope(lang_scope).unwrap();
197+ let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
74198 let highlight_config = language_config.highlight_config(&[]).unwrap();
75− let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)).unwrap();
76− let indent_query = language_config.indent_query().unwrap();
77199 let text = doc.slice(..);
200+ let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
201+ let indent_query = language_config.indent_query().unwrap();
78202
79203 for i in 0..doc.len_lines() {
80204 let line = text.line(i);
205+ if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
206+ continue;
207+ }
81208 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
82− let tab_and_indent_width: usize = 4;
209+ let tab_width: usize = 4;
83210 let suggested_indent = treesitter_indent_for_pos(
84211 indent_query,
85212 &syntax,
86− &IndentStyle::Spaces(tab_and_indent_width as u8),
87− tab_and_indent_width,
88− tab_and_indent_width,
213+ tab_width,
214+ indent_style.indent_width(tab_width),
89215 text,
90216 i,
91217 text.line_to_char(i) + pos,
92218 false,
93219 )
94− .unwrap();
220+ .unwrap()
221+ .to_string(&indent_style, tab_width);
95222 assert!(
96223 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
97− "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
224+ "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
225+ test_name,
98226 i+1,
99227 line.slice(..line.len_chars()-1),
100228 suggested_indent,
helix-dap/Cargo.toml+11−9
@@ -1,25 +1,27 @@
11[package]
22name = "helix-dap"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
6−license = "MPL-2.0"
73description = "DAP client implementation for Helix project"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
4+version.workspace = true
5+authors.workspace = true
6+edition.workspace = true
7+license.workspace = true
8+rust-version.workspace = true
9+categories.workspace = true
10+repository.workspace = true
11+homepage.workspace = true
1112
1213# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1314
1415[dependencies]
15−helix-core = { version = "0.6", path = "../helix-core" }
16+helix-core = { path = "../helix-core" }
17+
1618anyhow = "1.0"
1719log = "0.4"
1820serde = { version = "1.0", features = ["derive"] }
1921serde_json = "1.0"
2022thiserror = "1.0"
2123tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
22−which = "4.4"
24+which = "5.0.0"
2325
2426[dev-dependencies]
2527fern = "0.6"
helix-event/Cargo.tomlnew file+16
@@ -0,0 +1,16 @@
1+[package]
2+name = "helix-event"
3+version.workspace = true
4+authors.workspace = true
5+edition.workspace = true
6+license.workspace = true
7+rust-version.workspace = true
8+categories.workspace = true
9+repository.workspace = true
10+homepage.workspace = true
11+
12+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13+
14+[dependencies]
15+tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
16+parking_lot = { version = "0.12", features = ["send_guard"] }
helix-event/src/lib.rsnew file+8
@@ -0,0 +1,8 @@
1+//! `helix-event` contains systems that allow (often async) communication between
2+//! different editor components without strongly coupling them. Currently this
3+//! crate only contains some smaller facilities but the intend is to add more
4+//! functionality in the future ( like a generic hook system)
5+
6+pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
7+
8+mod redraw;
helix-event/src/redraw.rsnew file+49
@@ -0,0 +1,49 @@
1+//! Signals that control when/if the editor redraws
2+
3+use std::future::Future;
4+
5+use parking_lot::{RwLock, RwLockReadGuard};
6+use tokio::sync::Notify;
7+
8+/// A `Notify` instance that can be used to (asynchronously) request
9+/// the editor the render a new frame.
10+static REDRAW_NOTIFY: Notify = Notify::const_new();
11+
12+/// A `RwLock` that prevents the next frame from being
13+/// drawn until an exclusive (write) lock can be acquired.
14+/// This allows asynchsonous tasks to acquire `non-exclusive`
15+/// locks (read) to prevent the next frame from being drawn
16+/// until a certain computation has finished.
17+static RENDER_LOCK: RwLock<()> = RwLock::new(());
18+
19+pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
20+
21+/// Requests that the editor is redrawn. The redraws are debounced (currently to
22+/// 30FPS) so this can be called many times without causing a ton of frames to
23+/// be rendered.
24+pub fn request_redraw() {
25+ REDRAW_NOTIFY.notify_one();
26+}
27+
28+/// Returns a future that will yield once a redraw has been asynchronously
29+/// requested using [`request_redraw`].
30+pub fn redraw_requested() -> impl Future<Output = ()> {
31+ REDRAW_NOTIFY.notified()
32+}
33+
34+/// Wait until all locks acquired with [`lock_frame`] have been released.
35+/// This function is called before rendering and is intended to allow the frame
36+/// to wait for async computations that should be included in the current frame.
37+pub fn start_frame() {
38+ drop(RENDER_LOCK.write());
39+ // exhaust any leftover redraw notifications
40+ let notify = REDRAW_NOTIFY.notified();
41+ tokio::pin!(notify);
42+ notify.enable();
43+}
44+
45+/// Acquires the render lock which will prevent the next frame from being drawn
46+/// until the returned guard is dropped.
47+pub fn lock_frame() -> RenderLockGuard {
48+ RENDER_LOCK.read()
49+}
helix-loader/Cargo.toml+13−12
@@ -1,13 +1,14 @@
11[package]
22name = "helix-loader"
3−version = "0.6.0"
4−description = "A post-modern text editor."
5−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
6−edition = "2021"
7−license = "MPL-2.0"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
3+description = "Build bootstrapping for Helix crates"
4+version.workspace = true
5+authors.workspace = true
6+edition.workspace = true
7+license.workspace = true
8+rust-version.workspace = true
9+categories.workspace = true
10+repository.workspace = true
11+homepage.workspace = true
1112
1213[[bin]]
1314name = "hx-loader"
@@ -18,17 +19,17 @@ anyhow = "1"
1819serde = { version = "1.0", features = ["derive"] }
1920toml = "0.7"
2021etcetera = "0.8"
21−tree-sitter = "0.20"
22−once_cell = "1.18"
22+tree-sitter.workspace = true
23+once_cell = "1.19"
2324log = "0.4"
24−which = "4.4"
25+which = "5.0.0"
2526
2627# TODO: these two should be on !wasm32 only
2728
2829# cloning/compiling tree-sitter grammars
2930cc = { version = "1" }
3031threadpool = { version = "1.0" }
31−tempfile = "3.6.0"
32+tempfile = "3.8.1"
3233dunce = "1.0.4"
3334
3435[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
helix-loader/build.rs+20−6
@@ -2,7 +2,17 @@ use std::borrow::Cow;
22use std::path::Path;
33use std::process::Command;
44
5−const VERSION: &str = include_str!("../VERSION");
5+const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
6+const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
7+const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
8+
9+fn get_calver() -> String {
10+ if PATCH == "0" {
11+ format!("{MAJOR}.{MINOR}")
12+ } else {
13+ format!("{MAJOR}.{MINOR}.{PATCH}")
14+ }
15+}
616
717fn main() {
818 let git_hash = Command::new("git")
@@ -12,9 +22,10 @@ fn main() {
1222 .filter(|output| output.status.success())
1323 .and_then(|x| String::from_utf8(x.stdout).ok());
1424
25+ let calver = get_calver();
1526 let version: Cow<_> = match &git_hash {
16− Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
17− None => VERSION.into(),
27+ Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(),
28+ None => calver.into(),
1829 };
1930
2031 println!(
@@ -22,7 +33,6 @@ fn main() {
2233 std::env::var("TARGET").unwrap()
2334 );
2435
25− println!("cargo:rerun-if-changed=../VERSION");
2636 println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
2737
2838 if git_hash.is_none() {
@@ -40,7 +50,9 @@ fn main() {
4050 .ok()
4151 .filter(|output| output.status.success())
4252 .and_then(|x| String::from_utf8(x.stdout).ok())
43− else{ return; };
53+ else {
54+ return;
55+ };
4456 // If heads starts pointing at something else (different branch)
4557 // we need to return
4658 let head = Path::new(&git_dir).join("HEAD");
@@ -55,7 +67,9 @@ fn main() {
5567 .ok()
5668 .filter(|output| output.status.success())
5769 .and_then(|x| String::from_utf8(x.stdout).ok())
58− else{ return; };
70+ else {
71+ return;
72+ };
5973 let head_ref = Path::new(&git_dir).join(head_ref);
6074 if head_ref.exists() {
6175 println!("cargo:rerun-if-changed={}", head_ref.display());
helix-loader/src/lib.rs+13−4
@@ -33,9 +33,9 @@ pub fn current_working_dir() -> PathBuf {
3333 path
3434}
3535
36−pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> {
36+pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
3737 let path = dunce::canonicalize(path)?;
38− std::env::set_current_dir(path.clone())?;
38+ std::env::set_current_dir(&path)?;
3939 let mut cwd = CWD.write().unwrap();
4040 *cwd = Some(path);
4141 Ok(())
@@ -60,7 +60,8 @@ pub fn initialize_log_file(specified_file: Option<PathBuf>) {
6060/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
6161/// 2. subdirectory of user config directory (always included)
6262/// 3. `HELIX_RUNTIME` (if environment variable is set)
63−/// 4. subdirectory of path to helix executable (always included)
63+/// 4. `HELIX_DEFAULT_RUNTIME` (if environment variable is set *at build time*)
64+/// 5. subdirectory of path to helix executable (always included)
6465///
6566/// Postcondition: returns at least two paths (they might not exist).
6667fn prioritize_runtime_dirs() -> Vec<PathBuf> {
@@ -81,6 +82,14 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
8182 rt_dirs.push(dir.into());
8283 }
8384
85+ // If this variable is set during build time, it will always be included
86+ // in the lookup list. This allows downstream packagers to set a fallback
87+ // directory to a location that is conventional on their distro so that they
88+ // need not resort to a wrapper script or a global environment variable.
89+ if let Some(dir) = std::option_env!("HELIX_DEFAULT_RUNTIME") {
90+ rt_dirs.push(dir.into());
91+ }
92+
8493 // fallback to location of the executable being run
8594 // canonicalize the path in case the executable is symlinked
8695 let exe_rt_dir = std::env::current_exe()
@@ -280,7 +289,7 @@ mod merge_toml_tests {
280289 let cwd = current_working_dir();
281290 assert_ne!(cwd, new_path);
282291
283− set_current_working_dir(new_path.clone()).expect("Couldn't set new path");
292+ set_current_working_dir(&new_path).expect("Couldn't set new path");
284293
285294 let cwd = current_working_dir();
286295 assert_eq!(cwd, new_path);
helix-lsp/Cargo.toml+15−13
@@ -1,30 +1,32 @@
11[package]
22name = "helix-lsp"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
73description = "LSP client implementation for Helix project"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
4+version.workspace = true
5+authors.workspace = true
6+edition.workspace = true
7+license.workspace = true
8+rust-version.workspace = true
9+categories.workspace = true
10+repository.workspace = true
11+homepage.workspace = true
1112
1213# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1314
1415[dependencies]
15−helix-core = { version = "0.6", path = "../helix-core" }
16−helix-loader = { version = "0.6", path = "../helix-loader" }
17−helix-parsec = { version = "0.6", path = "../helix-parsec" }
16+helix-core = { path = "../helix-core" }
17+helix-loader = { path = "../helix-loader" }
18+helix-parsec = { path = "../helix-parsec" }
1819
1920anyhow = "1.0"
2021futures-executor = "0.3"
2122futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
23+globset = "0.4.14"
2224log = "0.4"
23−lsp-types = { version = "0.94" }
25+lsp-types = { version = "0.95" }
2426serde = { version = "1.0", features = ["derive"] }
2527serde_json = "1.0"
2628thiserror = "1.0"
27−tokio = { version = "1.29", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
29+tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
2830tokio-stream = "0.1.14"
29−which = "4.4"
31+which = "5.0.0"
3032parking_lot = "0.12.1"
helix-lsp/src/client.rs+123−11
@@ -7,8 +7,9 @@ use crate::{
77use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
88use helix_loader::{self, VERSION_AND_GIT_HASH};
99use lsp::{
10− notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
11− PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
10+ notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
11+ DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
12+ WorkspaceFoldersChangeEvent,
1213};
1314use lsp_types as lsp;
1415use parking_lot::Mutex;
@@ -400,12 +401,22 @@ impl Client {
400401 &self,
401402 params: R::Params,
402403 ) -> impl Future<Output = Result<Value>>
404+ where
405+ R::Params: serde::Serialize,
406+ {
407+ self.call_with_timeout::<R>(params, self.req_timeout)
408+ }
409+
410+ fn call_with_timeout<R: lsp::request::Request>(
411+ &self,
412+ params: R::Params,
413+ timeout_secs: u64,
414+ ) -> impl Future<Output = Result<Value>>
403415 where
404416 R::Params: serde::Serialize,
405417 {
406418 let server_tx = self.server_tx.clone();
407419 let id = self.next_request_id();
408− let timeout_secs = self.req_timeout;
409420
410421 async move {
411422 use std::time::Duration;
@@ -543,6 +554,15 @@ impl Client {
543554 normalizes_line_endings: Some(false),
544555 change_annotation_support: None,
545556 }),
557+ did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
558+ dynamic_registration: Some(true),
559+ relative_pattern_support: Some(false),
560+ }),
561+ file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities {
562+ will_rename: Some(true),
563+ did_rename: Some(true),
564+ ..Default::default()
565+ }),
546566 ..Default::default()
547567 }),
548568 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -609,6 +629,12 @@ impl Client {
609629 .collect(),
610630 },
611631 }),
632+ is_preferred_support: Some(true),
633+ disabled_support: Some(true),
634+ data_support: Some(true),
635+ resolve_support: Some(CodeActionCapabilityResolveSupport {
636+ properties: vec!["edit".to_owned(), "command".to_owned()],
637+ }),
612638 ..Default::default()
613639 }),
614640 publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
@@ -641,6 +667,7 @@ impl Client {
641667 version: Some(String::from(VERSION_AND_GIT_HASH)),
642668 }),
643669 locale: None, // TODO
670+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
644671 };
645672
646673 self.request::<lsp::request::Initialize>(params).await
@@ -689,6 +716,65 @@ impl Client {
689716 })
690717 }
691718
719+ pub fn prepare_file_rename(
720+ &self,
721+ old_uri: &lsp::Url,
722+ new_uri: &lsp::Url,
723+ ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
724+ let capabilities = self.capabilities.get().unwrap();
725+
726+ // Return early if the server does not support willRename feature
727+ match &capabilities.workspace {
728+ Some(workspace) => match &workspace.file_operations {
729+ Some(op) => {
730+ op.will_rename.as_ref()?;
731+ }
732+ _ => return None,
733+ },
734+ _ => return None,
735+ }
736+
737+ let files = vec![lsp::FileRename {
738+ old_uri: old_uri.to_string(),
739+ new_uri: new_uri.to_string(),
740+ }];
741+ let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
742+ lsp::RenameFilesParams { files },
743+ 5,
744+ );
745+
746+ Some(async move {
747+ let json = request.await?;
748+ let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
749+ Ok(response.unwrap_or_default())
750+ })
751+ }
752+
753+ pub fn did_file_rename(
754+ &self,
755+ old_uri: &lsp::Url,
756+ new_uri: &lsp::Url,
757+ ) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
758+ let capabilities = self.capabilities.get().unwrap();
759+
760+ // Return early if the server does not support DidRename feature
761+ match &capabilities.workspace {
762+ Some(workspace) => match &workspace.file_operations {
763+ Some(op) => {
764+ op.did_rename.as_ref()?;
765+ }
766+ _ => return None,
767+ },
768+ _ => return None,
769+ }
770+
771+ let files = vec![lsp::FileRename {
772+ old_uri: old_uri.to_string(),
773+ new_uri: new_uri.to_string(),
774+ }];
775+ Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
776+ }
777+
692778 // -------------------------------------------------------------------------------------------
693779 // Text document
694780 // -------------------------------------------------------------------------------------------
@@ -884,20 +970,19 @@ impl Client {
884970 ) -> Option<impl Future<Output = Result<()>>> {
885971 let capabilities = self.capabilities.get().unwrap();
886972
887− let include_text = match &capabilities.text_document_sync {
888− Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
889− save: Some(options),
973+ let include_text = match &capabilities.text_document_sync.as_ref()? {
974+ lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
975+ save: options,
890976 ..
891− })) => match options {
977+ }) => match options.as_ref()? {
892978 lsp::TextDocumentSyncSaveOptions::Supported(true) => false,
893979 lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions {
894980 include_text,
895981 }) => include_text.unwrap_or(false),
896− // Supported(false)
897− _ => return None,
982+ lsp::TextDocumentSyncSaveOptions::Supported(false) => return None,
898983 },
899− // unsupported
900− _ => return None,
984+ // see: https://github.com/microsoft/language-server-protocol/issues/288
985+ lsp::TextDocumentSyncCapability::Kind(..) => false,
901986 };
902987
903988 Some(self.notify::<lsp::notification::DidSaveTextDocument>(
@@ -954,6 +1039,24 @@ impl Client {
9541039 Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
9551040 }
9561041
1042+ pub fn resolve_code_action(
1043+ &self,
1044+ code_action: lsp::CodeAction,
1045+ ) -> Option<impl Future<Output = Result<Value>>> {
1046+ let capabilities = self.capabilities.get().unwrap();
1047+
1048+ // Return early if the server does not support resolving code actions.
1049+ match capabilities.code_action_provider {
1050+ Some(lsp::CodeActionProviderCapability::Options(lsp::CodeActionOptions {
1051+ resolve_provider: Some(true),
1052+ ..
1053+ })) => (),
1054+ _ => return None,
1055+ }
1056+
1057+ Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
1058+ }
1059+
9571060 pub fn text_document_signature_help(
9581061 &self,
9591062 text_document: lsp::TextDocumentIdentifier,
@@ -1428,4 +1531,13 @@ impl Client {
14281531
14291532 Some(self.call::<lsp::request::ExecuteCommand>(params))
14301533 }
1534+
1535+ pub fn did_change_watched_files(
1536+ &self,
1537+ changes: Vec<lsp::FileEvent>,
1538+ ) -> impl Future<Output = std::result::Result<(), Error>> {
1539+ self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
1540+ changes,
1541+ })
1542+ }
14311543}
helix-lsp/src/file_event.rsnew file+193
@@ -0,0 +1,193 @@
1+use std::{collections::HashMap, path::PathBuf, sync::Weak};
2+
3+use globset::{GlobBuilder, GlobSetBuilder};
4+use tokio::sync::mpsc;
5+
6+use crate::{lsp, Client};
7+
8+enum Event {
9+ FileChanged {
10+ path: PathBuf,
11+ },
12+ Register {
13+ client_id: usize,
14+ client: Weak<Client>,
15+ registration_id: String,
16+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
17+ },
18+ Unregister {
19+ client_id: usize,
20+ registration_id: String,
21+ },
22+ RemoveClient {
23+ client_id: usize,
24+ },
25+}
26+
27+#[derive(Default)]
28+struct ClientState {
29+ client: Weak<Client>,
30+ registered: HashMap<String, globset::GlobSet>,
31+}
32+
33+/// The Handler uses a dedicated tokio task to respond to file change events by
34+/// forwarding changes to LSPs that have registered for notifications with a
35+/// matching glob.
36+///
37+/// When an LSP registers for the DidChangeWatchedFiles notification, the
38+/// Handler is notified by sending the registration details in addition to a
39+/// weak reference to the LSP client. This is done so that the Handler can have
40+/// access to the client without preventing the client from being dropped if it
41+/// is closed and the Handler isn't properly notified.
42+#[derive(Clone, Debug)]
43+pub struct Handler {
44+ tx: mpsc::UnboundedSender<Event>,
45+}
46+
47+impl Default for Handler {
48+ fn default() -> Self {
49+ Self::new()
50+ }
51+}
52+
53+impl Handler {
54+ pub fn new() -> Self {
55+ let (tx, rx) = mpsc::unbounded_channel();
56+ tokio::spawn(Self::run(rx));
57+ Self { tx }
58+ }
59+
60+ pub fn register(
61+ &self,
62+ client_id: usize,
63+ client: Weak<Client>,
64+ registration_id: String,
65+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
66+ ) {
67+ let _ = self.tx.send(Event::Register {
68+ client_id,
69+ client,
70+ registration_id,
71+ options,
72+ });
73+ }
74+
75+ pub fn unregister(&self, client_id: usize, registration_id: String) {
76+ let _ = self.tx.send(Event::Unregister {
77+ client_id,
78+ registration_id,
79+ });
80+ }
81+
82+ pub fn file_changed(&self, path: PathBuf) {
83+ let _ = self.tx.send(Event::FileChanged { path });
84+ }
85+
86+ pub fn remove_client(&self, client_id: usize) {
87+ let _ = self.tx.send(Event::RemoveClient { client_id });
88+ }
89+
90+ async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
91+ let mut state: HashMap<usize, ClientState> = HashMap::new();
92+ while let Some(event) = rx.recv().await {
93+ match event {
94+ Event::FileChanged { path } => {
95+ log::debug!("Received file event for {:?}", &path);
96+
97+ state.retain(|id, client_state| {
98+ if !client_state
99+ .registered
100+ .values()
101+ .any(|glob| glob.is_match(&path))
102+ {
103+ return true;
104+ }
105+ let Some(client) = client_state.client.upgrade() else {
106+ log::warn!("LSP client was dropped: {id}");
107+ return false;
108+ };
109+ let Ok(uri) = lsp::Url::from_file_path(&path) else {
110+ return true;
111+ };
112+ log::debug!(
113+ "Sending didChangeWatchedFiles notification to client '{}'",
114+ client.name()
115+ );
116+ if let Err(err) = crate::block_on(client
117+ .did_change_watched_files(vec![lsp::FileEvent {
118+ uri,
119+ // We currently always send the CHANGED state
120+ // since we don't actually have more context at
121+ // the moment.
122+ typ: lsp::FileChangeType::CHANGED,
123+ }]))
124+ {
125+ log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
126+ }
127+ true
128+ });
129+ }
130+ Event::Register {
131+ client_id,
132+ client,
133+ registration_id,
134+ options: ops,
135+ } => {
136+ log::debug!(
137+ "Registering didChangeWatchedFiles for client '{}' with id '{}'",
138+ client_id,
139+ registration_id
140+ );
141+
142+ let entry = state.entry(client_id).or_insert_with(ClientState::default);
143+ entry.client = client;
144+
145+ let mut builder = GlobSetBuilder::new();
146+ for watcher in ops.watchers {
147+ if let lsp::GlobPattern::String(pattern) = watcher.glob_pattern {
148+ if let Ok(glob) = GlobBuilder::new(&pattern).build() {
149+ builder.add(glob);
150+ }
151+ }
152+ }
153+ match builder.build() {
154+ Ok(globset) => {
155+ entry.registered.insert(registration_id, globset);
156+ }
157+ Err(err) => {
158+ // Remove any old state for that registration id and
159+ // remove the entire client if it's now empty.
160+ entry.registered.remove(&registration_id);
161+ if entry.registered.is_empty() {
162+ state.remove(&client_id);
163+ }
164+ log::warn!(
165+ "Unable to build globset for LSP didChangeWatchedFiles {err}"
166+ )
167+ }
168+ }
169+ }
170+ Event::Unregister {
171+ client_id,
172+ registration_id,
173+ } => {
174+ log::debug!(
175+ "Unregistering didChangeWatchedFiles with id '{}' for client '{}'",
176+ registration_id,
177+ client_id
178+ );
179+ if let Some(client_state) = state.get_mut(&client_id) {
180+ client_state.registered.remove(&registration_id);
181+ if client_state.registered.is_empty() {
182+ state.remove(&client_id);
183+ }
184+ }
185+ }
186+ Event::RemoveClient { client_id } => {
187+ log::debug!("Removing LSP client: {client_id}");
188+ state.remove(&client_id);
189+ }
190+ }
191+ }
192+ }
193+}
helix-lsp/src/lib.rs+44−29
@@ -1,4 +1,5 @@
11mod client;
2+pub mod file_event;
23pub mod jsonrpc;
34pub mod snippet;
45mod transport;
@@ -434,7 +435,7 @@ pub mod util {
434435 }
435436
436437 let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
437− let Some(tabstops) = tabstops else{
438+ let Some(tabstops) = tabstops else {
438439 // no tabstop normal mapping
439440 mapped_selection.push(range);
440441 continue;
@@ -444,36 +445,36 @@ pub mod util {
444445 // the tabstop closest to the range simply replaces `head` while anchor remains in place
445446 // the remaining tabstops receive their own single-width cursor
446447 if range.head < range.anchor {
447− let first_tabstop = tabstop_anchor + tabstops[0].1;
448+ let last_idx = tabstops.len() - 1;
449+ let last_tabstop = tabstop_anchor + tabstops[last_idx].0;
448450
449451 // if selection is forward but was moved to the right it is
450452 // contained entirely in the replacement text, just do a point
451453 // selection (fallback below)
452− if range.anchor >= first_tabstop {
453− let range = Range::new(range.anchor, first_tabstop);
454+ if range.anchor > last_tabstop {
455+ let range = Range::new(range.anchor, last_tabstop);
454456 mapped_selection.push(range);
455− let rem_tabstops = tabstops[1..]
457+ let rem_tabstops = tabstops[..last_idx]
456458 .iter()
457− .map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
459+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
458460 mapped_selection.extend(rem_tabstops);
459461 continue;
460462 }
461463 } else {
462− let last_idx = tabstops.len() - 1;
463− let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
464+ let first_tabstop = tabstop_anchor + tabstops[0].0;
464465
465466 // if selection is forward but was moved to the right it is
466467 // contained entirely in the replacement text, just do a point
467468 // selection (fallback below)
468− if range.anchor <= last_tabstop {
469+ if range.anchor < first_tabstop {
469470 // we can't properly compute the the next grapheme
470471 // here because the transaction hasn't been applied yet
471472 // that is not a problem because the range gets grapheme aligned anyway
472473 // tough so just adding one will always cause head to be grapheme
473474 // aligned correctly when applied to the document
474− let range = Range::new(range.anchor, last_tabstop + 1);
475+ let range = Range::new(range.anchor, first_tabstop + 1);
475476 mapped_selection.push(range);
476− let rem_tabstops = tabstops[..last_idx]
477+ let rem_tabstops = tabstops[1..]
477478 .iter()
478479 .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
479480 mapped_selection.extend(rem_tabstops);
@@ -547,6 +548,7 @@ pub enum MethodCall {
547548 WorkspaceFolders,
548549 WorkspaceConfiguration(lsp::ConfigurationParams),
549550 RegisterCapability(lsp::RegistrationParams),
551+ UnregisterCapability(lsp::UnregistrationParams),
550552}
551553
552554impl MethodCall {
@@ -570,6 +572,10 @@ impl MethodCall {
570572 let params: lsp::RegistrationParams = params.parse()?;
571573 Self::RegisterCapability(params)
572574 }
575+ lsp::request::UnregisterCapability::METHOD => {
576+ let params: lsp::UnregistrationParams = params.parse()?;
577+ Self::UnregisterCapability(params)
578+ }
573579 _ => {
574580 return Err(Error::Unhandled);
575581 }
@@ -629,6 +635,7 @@ pub struct Registry {
629635 syn_loader: Arc<helix_core::syntax::Loader>,
630636 counter: usize,
631637 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
638+ pub file_event_handler: file_event::Handler,
632639}
633640
634641impl Registry {
@@ -638,6 +645,7 @@ impl Registry {
638645 syn_loader,
639646 counter: 0,
640647 incoming: SelectAll::new(),
648+ file_event_handler: file_event::Handler::new(),
641649 }
642650 }
643651
@@ -650,6 +658,7 @@ impl Registry {
650658 }
651659
652660 pub fn remove_by_id(&mut self, id: usize) {
661+ self.file_event_handler.remove_client(id);
653662 self.inner.retain(|_, language_servers| {
654663 language_servers.retain(|ls| id != ls.id());
655664 !language_servers.is_empty()
@@ -715,6 +724,7 @@ impl Registry {
715724 .unwrap();
716725
717726 for old_client in old_clients {
727+ self.file_event_handler.remove_client(old_client.id());
718728 tokio::spawn(async move {
719729 let _ = old_client.force_shutdown().await;
720730 });
@@ -731,6 +741,7 @@ impl Registry {
731741 pub fn stop(&mut self, name: &str) {
732742 if let Some(clients) = self.inner.remove(name) {
733743 for client in clients {
744+ self.file_event_handler.remove_client(client.id());
734745 tokio::spawn(async move {
735746 let _ = client.force_shutdown().await;
736747 });
@@ -738,36 +749,40 @@ impl Registry {
738749 }
739750 }
740751
741− pub fn get(
742− &mut self,
743− language_config: &LanguageConfiguration,
744− doc_path: Option<&std::path::PathBuf>,
745− root_dirs: &[PathBuf],
752+ pub fn get<'a>(
753+ &'a mut self,
754+ language_config: &'a LanguageConfiguration,
755+ doc_path: Option<&'a std::path::PathBuf>,
756+ root_dirs: &'a [PathBuf],
746757 enable_snippets: bool,
747− ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
748− language_config
749− .language_servers
750− .iter()
751− .map(|LanguageServerFeatures { name, .. }| {
758+ ) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
759+ language_config.language_servers.iter().map(
760+ move |LanguageServerFeatures { name, .. }| {
752761 if let Some(clients) = self.inner.get(name) {
753762 if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
754763 client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
755764 }) {
756− return Ok((name.to_owned(), client.clone()));
765+ return (name.to_owned(), Ok(client.clone()));
757766 }
758767 }
759− let client = self.start_client(
768+ match self.start_client(
760769 name.clone(),
761770 language_config,
762771 doc_path,
763772 root_dirs,
764773 enable_snippets,
765− )?;
766− let clients = self.inner.entry(name.clone()).or_default();
767− clients.push(client.clone());
768− Ok((name.clone(), client))
769− })
770− .collect()
774+ ) {
775+ Ok(client) => {
776+ self.inner
777+ .entry(name.to_owned())
778+ .or_default()
779+ .push(client.clone());
780+ (name.clone(), Ok(client))
781+ }
782+ Err(err) => (name.to_owned(), Err(err)),
783+ }
784+ },
785+ )
771786 }
772787
773788 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
helix-parsec/Cargo.toml+8−7
@@ -1,13 +1,14 @@
11[package]
22name = "helix-parsec"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
73description = "Parser combinators for Helix"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
114include = ["src/**/*", "README.md"]
5+version.workspace = true
6+authors.workspace = true
7+edition.workspace = true
8+license.workspace = true
9+rust-version.workspace = true
10+categories.workspace = true
11+repository.workspace = true
12+homepage.workspace = true
1213
1314[dependencies]
helix-term/Cargo.toml+30−25
@@ -1,16 +1,16 @@
11[package]
22name = "helix-term"
3−version = "0.6.0"
43description = "A post-modern text editor."
5−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
6−edition = "2021"
7−license = "MPL-2.0"
8−categories = ["editor", "command-line-utilities"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
114include = ["src/**/*", "README.md"]
125default-run = "hx"
13−rust-version = "1.65"
6+version.workspace = true
7+authors.workspace = true
8+edition.workspace = true
9+license.workspace = true
10+rust-version.workspace = true
11+categories.workspace = true
12+repository.workspace = true
13+homepage.workspace = true
1414
1515[features]
1616default = ["git"]
@@ -23,21 +23,22 @@ name = "hx"
2323path = "src/main.rs"
2424
2525[dependencies]
26−helix-core = { version = "0.6", path = "../helix-core" }
27−helix-view = { version = "0.6", path = "../helix-view" }
28−helix-lsp = { version = "0.6", path = "../helix-lsp" }
29−helix-dap = { version = "0.6", path = "../helix-dap" }
30−helix-vcs = { version = "0.6", path = "../helix-vcs" }
31−helix-loader = { version = "0.6", path = "../helix-loader" }
26+helix-core = { path = "../helix-core" }
27+helix-event = { path = "../helix-event" }
28+helix-view = { path = "../helix-view" }
29+helix-lsp = { path = "../helix-lsp" }
30+helix-dap = { path = "../helix-dap" }
31+helix-vcs = { path = "../helix-vcs" }
32+helix-loader = { path = "../helix-loader" }
3233
3334anyhow = "1"
34−once_cell = "1.18"
35+once_cell = "1.19"
3536
36−which = "4.4"
37+which = "5.0.0"
3738
3839tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
3940tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
40−crossterm = { version = "0.26", features = ["event-stream"] }
41+crossterm = { version = "0.27", features = ["event-stream"] }
4142signal-hook = "0.3"
4243tokio-stream = "0.1"
4344futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -49,13 +50,17 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
4950log = "0.4"
5051
5152# File picker
52−fuzzy-matcher = "0.3"
53+nucleo.workspace = true
5354ignore = "0.4"
5455# markdown doc rendering
5556pulldown-cmark = { version = "0.9", default-features = false }
5657# file type detection
5758content_inspector = "0.2.4"
5859
60+# opening URLs
61+open = "5.0.1"
62+url = "2.5.0"
63+
5964# config
6065toml = "0.7"
6166
@@ -63,20 +68,20 @@ serde_json = "1.0"
6368serde = { version = "1.0", features = ["derive"] }
6469
6570# ripgrep for global search
66−grep-regex = "0.1.11"
67−grep-searcher = "0.1.11"
71+grep-regex = "0.1.12"
72+grep-searcher = "0.1.13"
6873
6974[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
7075signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
71−libc = "0.2.147"
76+libc = "0.2.151"
7277
7378[target.'cfg(target_os = "macos")'.dependencies]
74−crossterm = { version = "0.26", features = ["event-stream", "use-dev-tty"] }
79+crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
7580
7681[build-dependencies]
77−helix-loader = { version = "0.6", path = "../helix-loader" }
82+helix-loader = { path = "../helix-loader" }
7883
7984[dev-dependencies]
8085smallvec = "1.11"
81−indoc = "2.0.2"
82−tempfile = "3.6.0"
86+indoc = "2.0.4"
87+tempfile = "3.8.1"
helix-term/src/application.rs+175−90
@@ -1,11 +1,16 @@
11use arc_swap::{access::Map, ArcSwap};
22use futures_util::Stream;
33use helix_core::{
4+ chars::char_is_word,
45 diagnostic::{DiagnosticTag, NumberOrString},
56 path::get_relative_path,
67 pos_at_coords, syntax, Selection,
78};
8−use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
9+use helix_lsp::{
10+ lsp::{self, notification::Notification},
11+ util::lsp_pos_to_pos,
12+ LspProgressMap,
13+};
914use helix_view::{
1015 align_view,
1116 document::DocumentSavedEventResult,
@@ -29,12 +34,9 @@ use crate::{
2934};
3035
3136use log::{debug, error, warn};
32−use std::{
33− collections::btree_map::Entry,
34− io::{stdin, stdout},
35− path::Path,
36− sync::Arc,
37−};
37+#[cfg(not(feature = "integration"))]
38+use std::io::stdout;
39+use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
3840
3941use anyhow::{Context, Error};
4042
@@ -159,17 +161,21 @@ impl Application {
159161 let path = helix_loader::runtime_file(Path::new("tutor"));
160162 editor.open(&path, Action::VerticalSplit)?;
161163 // Unset path to prevent accidentally saving to the original tutor file.
162− doc_mut!(editor).set_path(None)?;
164+ doc_mut!(editor).set_path(None);
163165 } else if !args.files.is_empty() {
164− let first = &args.files[0].0; // we know it's not empty
165− if first.is_dir() {
166− helix_loader::set_current_working_dir(first.clone())?;
167− editor.new_file(Action::VerticalSplit);
168− let picker = ui::file_picker(".".into(), &config.load().editor);
166+ let mut files_it = args.files.into_iter().peekable();
167+
168+ // If the first file is a directory, skip it and open a picker
169+ if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
170+ let picker = ui::file_picker(first, &config.load().editor);
169171 compositor.push(Box::new(overlaid(picker)));
170− } else {
171− let nr_of_files = args.files.len();
172− for (i, (file, pos)) in args.files.into_iter().enumerate() {
172+ }
173+
174+ // If there are any more files specified, open them
175+ if files_it.peek().is_some() {
176+ let mut nr_of_files = 0;
177+ for (file, pos) in files_it {
178+ nr_of_files += 1;
173179 if file.is_dir() {
174180 return Err(anyhow::anyhow!(
175181 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
@@ -181,7 +187,7 @@ impl Application {
181187 // option. If neither of those two arguments are passed
182188 // in, just load the files normally.
183189 let action = match args.split {
184− _ if i == 0 => Action::VerticalSplit,
190+ _ if nr_of_files == 1 => Action::VerticalSplit,
185191 Some(Layout::Vertical) => Action::VerticalSplit,
186192 Some(Layout::Horizontal) => Action::HorizontalSplit,
187193 None => Action::Load,
@@ -208,6 +214,8 @@ impl Application {
208214 // does not affect views without pos since it is at the top
209215 let (view, doc) = current!(editor);
210216 align_view(doc, view, Align::Center);
217+ } else {
218+ editor.new_file(Action::VerticalSplit);
211219 }
212220 } else if stdin().is_tty() || cfg!(feature = "integration") {
213221 editor.new_file(Action::VerticalSplit);
@@ -250,22 +258,19 @@ impl Application {
250258 }
251259
252260 async fn render(&mut self) {
261+ if self.compositor.full_redraw {
262+ self.terminal.clear().expect("Cannot clear the terminal");
263+ self.compositor.full_redraw = false;
264+ }
265+
253266 let mut cx = crate::compositor::Context {
254267 editor: &mut self.editor,
255268 jobs: &mut self.jobs,
256269 scroll: None,
257270 };
258271
259− // Acquire mutable access to the redraw_handle lock
260− // to ensure that there are no tasks running that want to block rendering
261− drop(cx.editor.redraw_handle.1.write().await);
272+ helix_event::start_frame();
262273 cx.editor.needs_redraw = false;
263− {
264− // exhaust any leftover redraw notifications
265− let notify = cx.editor.redraw_handle.0.notified();
266− tokio::pin!(notify);
267− notify.enable();
268− }
269274
270275 let area = self
271276 .terminal
@@ -287,7 +292,7 @@ impl Application {
287292
288293 pub async fn event_loop<S>(&mut self, input_stream: &mut S)
289294 where
290− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
295+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
291296 {
292297 self.render().await;
293298
@@ -300,7 +305,7 @@ impl Application {
300305
301306 pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
302307 where
303− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
308+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
304309 {
305310 loop {
306311 if self.editor.should_close() {
@@ -553,16 +558,7 @@ impl Application {
553558 let bytes = doc_save_event.text.len_bytes();
554559
555560 if doc.path() != Some(&doc_save_event.path) {
556− if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
557− log::error!(
558− "error setting path for doc '{:?}': {}",
559− doc.path(),
560− err.to_string(),
561− );
562−
563− self.editor.set_error(err.to_string());
564− return;
565− }
561+ doc.set_path(Some(&doc_save_event.path));
566562
567563 let loader = self.editor.syn_loader.clone();
568564
@@ -598,7 +594,7 @@ impl Application {
598594 EditorEvent::LanguageServerMessage((id, call)) => {
599595 self.handle_language_server_message(call, id).await;
600596 // limit render calls for fast language server messages
601− self.editor.redraw_handle.0.notify_one();
597+ helix_event::request_redraw();
602598 }
603599 EditorEvent::DebuggerEvent(payload) => {
604600 let needs_render = self.editor.handle_debugger_message(payload).await;
@@ -606,6 +602,9 @@ impl Application {
606602 self.render().await;
607603 }
608604 }
605+ EditorEvent::Redraw => {
606+ self.render().await;
607+ }
609608 EditorEvent::IdleTimer => {
610609 self.editor.clear_idle_timer();
611610 self.handle_idle_timeout().await;
@@ -620,10 +619,7 @@ impl Application {
620619 false
621620 }
622621
623− pub async fn handle_terminal_events(
624− &mut self,
625− event: Result<CrosstermEvent, crossterm::ErrorKind>,
626− ) {
622+ pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
627623 let mut cx = crate::compositor::Context {
628624 editor: &mut self.editor,
629625 jobs: &mut self.jobs,
@@ -722,7 +718,7 @@ impl Application {
722718 ));
723719 }
724720 }
725− Notification::PublishDiagnostics(params) => {
721+ Notification::PublishDiagnostics(mut params) => {
726722 let path = match params.uri.to_file_path() {
727723 Ok(path) => path,
728724 Err(_) => {
@@ -736,31 +732,69 @@ impl Application {
736732 return;
737733 }
738734 let offset_encoding = language_server.offset_encoding();
739− let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
740− if let Some(version) = params.version {
741− if version != doc.version() {
742− log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
743− return false;
735+ // have to inline the function because of borrow checking...
736+ let doc = self.editor.documents.values_mut()
737+ .find(|doc| doc.path().map(|p| p == &path).unwrap_or(false))
738+ .filter(|doc| {
739+ if let Some(version) = params.version {
740+ if version != doc.version() {
741+ log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
742+ return false;
743+ }
744744 }
745− }
746−
747− true
748− });
745+ true
746+ });
749747
750748 if let Some(doc) = doc {
751− let lang_conf = doc.language_config();
752− let text = doc.text();
749+ let lang_conf = doc.language.clone();
750+ let text = doc.text().clone();
751+
752+ let mut unchaged_diag_sources_ = Vec::new();
753+ if let Some(lang_conf) = &lang_conf {
754+ if let Some(old_diagnostics) =
755+ self.editor.diagnostics.get(&params.uri)
756+ {
757+ if !lang_conf.persistent_diagnostic_sources.is_empty() {
758+ // Sort diagnostics first by severity and then by line numbers.
759+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
760+ params
761+ .diagnostics
762+ .sort_unstable_by_key(|d| (d.severity, d.range.start));
763+ }
764+ for source in &lang_conf.persistent_diagnostic_sources {
765+ let new_diagnostics = params
766+ .diagnostics
767+ .iter()
768+ .filter(|d| d.source.as_ref() == Some(source));
769+ let old_diagnostics = old_diagnostics
770+ .iter()
771+ .filter(|(d, d_server)| {
772+ *d_server == server_id
773+ && d.source.as_ref() == Some(source)
774+ })
775+ .map(|(d, _)| d);
776+ if new_diagnostics.eq(old_diagnostics) {
777+ unchaged_diag_sources_.push(source.clone())
778+ }
779+ }
780+ }
781+ }
753782
754− let diagnostics = params
755− .diagnostics
756− .iter()
757− .filter_map(|diagnostic| {
783+ let unchaged_diag_sources = &unchaged_diag_sources_;
784+ let diagnostics =
785+ params.diagnostics.iter().filter_map(move |diagnostic| {
758786 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
759787 use lsp::DiagnosticSeverity;
760788
789+ if diagnostic.source.as_ref().map_or(false, |source| {
790+ unchaged_diag_sources.contains(source)
791+ }) {
792+ return None;
793+ }
794+
761795 // TODO: convert inside server
762796 let start = if let Some(start) = lsp_pos_to_pos(
763− text,
797+ &text,
764798 diagnostic.range.start,
765799 offset_encoding,
766800 ) {
@@ -771,14 +805,13 @@ impl Application {
771805 };
772806
773807 let end = if let Some(end) =
774− lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
808+ lsp_pos_to_pos(&text, diagnostic.range.end, offset_encoding)
775809 {
776810 end
777811 } else {
778812 log::warn!("lsp position out of bounds - {:?}", diagnostic);
779813 return None;
780814 };
781−
782815 let severity =
783816 diagnostic.severity.map(|severity| match severity {
784817 DiagnosticSeverity::ERROR => Error,
@@ -791,7 +824,7 @@ impl Application {
791824 ),
792825 });
793826
794− if let Some(lang_conf) = lang_conf {
827+ if let Some(lang_conf) = &lang_conf {
795828 if let Some(severity) = severity {
796829 if severity < lang_conf.diagnostic_severity {
797830 return None;
@@ -830,8 +863,17 @@ impl Application {
830863 Vec::new()
831864 };
832865
866+ let ends_at_word = start != end
867+ && end != 0
868+ && text.get_char(end - 1).map_or(false, char_is_word);
869+ let starts_at_word = start != end
870+ && text.get_char(start).map_or(false, char_is_word);
871+
833872 Some(Diagnostic {
834873 range: Range { start, end },
874+ ends_at_word,
875+ starts_at_word,
876+ zero_width: start == end,
835877 line: diagnostic.range.start.line as usize,
836878 message: diagnostic.message.clone(),
837879 severity,
@@ -841,38 +883,33 @@ impl Application {
841883 data: diagnostic.data.clone(),
842884 language_server_id: server_id,
843885 })
844− })
845− .collect();
886+ });
846887
847− doc.replace_diagnostics(diagnostics, server_id);
888+ doc.replace_diagnostics(diagnostics, unchaged_diag_sources, server_id);
848889 }
849890
850− let mut diagnostics = params
851− .diagnostics
852− .into_iter()
853− .map(|d| (d, server_id))
854− .collect();
891+ let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
855892
856893 // Insert the original lsp::Diagnostics here because we may have no open document
857894 // for diagnosic message and so we can't calculate the exact position.
858895 // When using them later in the diagnostics picker, we calculate them on-demand.
859− match self.editor.diagnostics.entry(params.uri) {
896+ let diagnostics = match self.editor.diagnostics.entry(params.uri) {
860897 Entry::Occupied(o) => {
861898 let current_diagnostics = o.into_mut();
862899 // there may entries of other language servers, which is why we can't overwrite the whole entry
863900 current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
864− current_diagnostics.append(&mut diagnostics);
865− // Sort diagnostics first by severity and then by line numbers.
866− // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
901+ current_diagnostics.extend(diagnostics);
867902 current_diagnostics
868− .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
869− }
870− Entry::Vacant(v) => {
871− diagnostics
872− .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
873− v.insert(diagnostics);
903+ // Sort diagnostics first by severity and then by line numbers.
874904 }
905+ Entry::Vacant(v) => v.insert(diagnostics.collect()),
875906 };
907+
908+ // Sort diagnostics first by severity and then by line numbers.
909+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
910+ diagnostics.sort_unstable_by_key(|(d, server_id)| {
911+ (d.severity, d.range.start, *server_id)
912+ });
876913 }
877914 Notification::ShowMessage(params) => {
878915 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -1080,17 +1117,65 @@ impl Application {
10801117 .collect();
10811118 Ok(json!(result))
10821119 }
1083− Ok(MethodCall::RegisterCapability(_params)) => {
1084− log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
1085− // Language Servers based on the `vscode-languageserver-node` library often send
1086− // client/registerCapability even though we do not enable dynamic registration
1087− // for any capabilities. We should send a MethodNotFound JSONRPC error in this
1088− // case but that rejects the registration promise in the server which causes an
1089− // exit. So we work around this by ignoring the request and sending back an OK
1090− // response.
1120+ Ok(MethodCall::RegisterCapability(params)) => {
1121+ if let Some(client) = self
1122+ .editor
1123+ .language_servers
1124+ .iter_clients()
1125+ .find(|client| client.id() == server_id)
1126+ {
1127+ for reg in params.registrations {
1128+ match reg.method.as_str() {
1129+ lsp::notification::DidChangeWatchedFiles::METHOD => {
1130+ let Some(options) = reg.register_options else {
1131+ continue;
1132+ };
1133+ let ops: lsp::DidChangeWatchedFilesRegistrationOptions =
1134+ match serde_json::from_value(options) {
1135+ Ok(ops) => ops,
1136+ Err(err) => {
1137+ log::warn!("Failed to deserialize DidChangeWatchedFilesRegistrationOptions: {err}");
1138+ continue;
1139+ }
1140+ };
1141+ self.editor.language_servers.file_event_handler.register(
1142+ client.id(),
1143+ Arc::downgrade(client),
1144+ reg.id,
1145+ ops,
1146+ )
1147+ }
1148+ _ => {
1149+ // Language Servers based on the `vscode-languageserver-node` library often send
1150+ // client/registerCapability even though we do not enable dynamic registration
1151+ // for most capabilities. We should send a MethodNotFound JSONRPC error in this
1152+ // case but that rejects the registration promise in the server which causes an
1153+ // exit. So we work around this by ignoring the request and sending back an OK
1154+ // response.
1155+ log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
1156+ }
1157+ }
1158+ }
1159+ }
10911160
10921161 Ok(serde_json::Value::Null)
10931162 }
1163+ Ok(MethodCall::UnregisterCapability(params)) => {
1164+ for unreg in params.unregisterations {
1165+ match unreg.method.as_str() {
1166+ lsp::notification::DidChangeWatchedFiles::METHOD => {
1167+ self.editor
1168+ .language_servers
1169+ .file_event_handler
1170+ .unregister(server_id, unreg.id);
1171+ }
1172+ _ => {
1173+ log::warn!("Received unregistration request for unsupported method: {}", unreg.method);
1174+ }
1175+ }
1176+ }
1177+ Ok(serde_json::Value::Null)
1178+ }
10941179 };
10951180
10961181 tokio::spawn(language_server!().reply(id, reply));
@@ -1116,7 +1201,7 @@ impl Application {
11161201
11171202 pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
11181203 where
1119− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
1204+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
11201205 {
11211206 self.claim_term().await?;
11221207
helix-term/src/args.rs+29
@@ -17,12 +17,14 @@ pub struct Args {
1717 pub log_file: Option<PathBuf>,
1818 pub config_file: Option<PathBuf>,
1919 pub files: Vec<(PathBuf, Position)>,
20+ pub working_directory: Option<PathBuf>,
2021}
2122
2223impl Args {
2324 pub fn parse_args() -> Result<Args> {
2425 let mut args = Args::default();
2526 let mut argv = std::env::args().peekable();
27+ let mut line_number = 0;
2628
2729 argv.next(); // skip the program, we don't care about that
2830
@@ -59,6 +61,20 @@ impl Args {
5961 Some(path) => args.log_file = Some(path.into()),
6062 None => anyhow::bail!("--log must specify a path to write"),
6163 },
64+ "-w" | "--working-dir" => match argv.next().as_deref() {
65+ Some(path) => {
66+ args.working_directory = if Path::new(path).is_dir() {
67+ Some(PathBuf::from(path))
68+ } else {
69+ anyhow::bail!(
70+ "--working-dir specified does not exist or is not a directory"
71+ )
72+ }
73+ }
74+ None => {
75+ anyhow::bail!("--working-dir must specify an initial working directory")
76+ }
77+ },
6278 arg if arg.starts_with("--") => {
6379 anyhow::bail!("unexpected double dash argument: {}", arg)
6480 }
@@ -73,6 +89,13 @@ impl Args {
7389 }
7490 }
7591 }
92+ arg if arg.starts_with('+') => {
93+ let arg = &arg[1..];
94+ line_number = match arg.parse::<usize>() {
95+ Ok(n) => n.saturating_sub(1),
96+ _ => anyhow::bail!("bad line number after +"),
97+ };
98+ }
7699 arg => args.files.push(parse_file(arg)),
77100 }
78101 }
@@ -82,6 +105,12 @@ impl Args {
82105 args.files.push(parse_file(&arg));
83106 }
84107
108+ if let Some(file) = args.files.first_mut() {
109+ if line_number != 0 {
110+ file.1.row = line_number;
111+ }
112+ }
113+
85114 Ok(args)
86115 }
87116}
helix-term/src/commands.rs+545−351
@@ -32,7 +32,6 @@ use helix_core::{
3232 RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
3333};
3434use helix_view::{
35− clipboard::ClipboardType,
3635 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
3736 editor::{Action, CompleteAction},
3837 info::Info,
@@ -44,7 +43,6 @@ use helix_view::{
4443};
4544
4645use anyhow::{anyhow, bail, ensure, Context as _};
47−use fuzzy_matcher::FuzzyMatcher;
4846use insert::*;
4947use movement::Movement;
5048
@@ -61,9 +59,14 @@ use crate::{
6159};
6260
6361use crate::job::{self, Jobs};
64−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
65−use std::{collections::HashMap, fmt, future::Future};
66−use std::{collections::HashSet, num::NonZeroUsize};
62+use futures_util::{stream::FuturesUnordered, TryStreamExt};
63+use std::{
64+ collections::{HashMap, HashSet},
65+ fmt,
66+ future::Future,
67+ io::Read,
68+ num::NonZeroUsize,
69+};
6770
6871use std::{
6972 borrow::Cow,
@@ -72,11 +75,11 @@ use std::{
7275
7376use once_cell::sync::Lazy;
7477use serde::de::{self, Deserialize, Deserializer};
78+use url::Url;
7579
7680use grep_regex::RegexMatcherBuilder;
7781use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
7882use ignore::{DirEntry, WalkBuilder, WalkState};
79−use tokio_stream::wrappers::UnboundedReceiverStream;
8083
8184pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
8285
@@ -248,6 +251,8 @@ impl MappableCommand {
248251 move_prev_long_word_start, "Move to start of previous long word",
249252 move_next_long_word_end, "Move to end of next long word",
250253 move_prev_long_word_end, "Move to end of previous long word",
254+ move_parent_node_end, "Move to end of the parent node",
255+ move_parent_node_start, "Move to beginning of the parent node",
251256 extend_next_word_start, "Extend to start of next word",
252257 extend_prev_word_start, "Extend to start of previous word",
253258 extend_next_word_end, "Extend to end of next word",
@@ -256,6 +261,8 @@ impl MappableCommand {
256261 extend_prev_long_word_start, "Extend to start of previous long word",
257262 extend_next_long_word_end, "Extend to end of next long word",
258263 extend_prev_long_word_end, "Extend to end of prev long word",
264+ extend_parent_node_end, "Extend to end of the parent node",
265+ extend_parent_node_start, "Extend to beginning of the parent node",
259266 find_till_char, "Move till next occurrence of char",
260267 find_next_char, "Move to next occurrence of char",
261268 extend_till_char, "Extend till next occurrence of char",
@@ -330,7 +337,7 @@ impl MappableCommand {
330337 goto_implementation, "Goto implementation",
331338 goto_file_start, "Goto line number <n> else file start",
332339 goto_file_end, "Goto file end",
333− goto_file, "Goto files in selection",
340+ goto_file, "Goto files/URLs in selection",
334341 goto_file_hsplit, "Goto files in selection (hsplit)",
335342 goto_file_vsplit, "Goto files in selection (vsplit)",
336343 goto_reference, "Goto references",
@@ -362,6 +369,7 @@ impl MappableCommand {
362369 extend_to_line_end, "Extend to line end",
363370 extend_to_line_end_newline, "Extend to line end",
364371 signature_help, "Show signature help",
372+ smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
365373 insert_tab, "Insert tab char",
366374 insert_newline, "Insert newline char",
367375 delete_char_backward, "Delete previous char",
@@ -376,6 +384,8 @@ impl MappableCommand {
376384 later, "Move forward in history",
377385 commit_undo_checkpoint, "Commit changes to new checkpoint",
378386 yank, "Yank selection",
387+ yank_to_clipboard, "Yank selections to clipboard",
388+ yank_to_primary_clipboard, "Yank selections to primary clipboard",
379389 yank_joined, "Join and yank selections",
380390 yank_joined_to_clipboard, "Join and yank selections to clipboard",
381391 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -980,6 +990,7 @@ fn align_selections(cx: &mut Context) {
980990
981991 let transaction = Transaction::change(doc.text(), changes.into_iter());
982992 doc.apply(&transaction, view.id);
993+ exit_select_mode(cx);
983994}
984995
985996fn goto_window(cx: &mut Context, align: Align) {
@@ -1154,6 +1165,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11541165 let (view, doc) = current_ref!(cx.editor);
11551166 let text = doc.text();
11561167 let selections = doc.selection(view.id);
1168+ let rel_path = doc
1169+ .relative_path()
1170+ .map(|path| path.parent().unwrap().to_path_buf())
1171+ .unwrap_or_default();
11571172 let mut paths: Vec<_> = selections
11581173 .iter()
11591174 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1181,16 +1196,80 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11811196 .to_string(),
11821197 );
11831198 }
1199+
11841200 for sel in paths {
11851201 let p = sel.trim();
1186− if !p.is_empty() {
1187− if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
1202+ if p.is_empty() {
1203+ continue;
1204+ }
1205+
1206+ if let Ok(url) = Url::parse(p) {
1207+ return open_url(cx, url, action);
1208+ }
1209+
1210+ let path = &rel_path.join(p);
1211+ if path.is_dir() {
1212+ let picker = ui::file_picker(path.into(), &cx.editor.config());
1213+ cx.push_layer(Box::new(overlaid(picker)));
1214+ } else if let Err(e) = cx.editor.open(path, action) {
1215+ cx.editor.set_error(format!("Open file failed: {:?}", e));
1216+ }
1217+ }
1218+}
1219+
1220+/// Opens the given url. If the URL points to a valid textual file it is open in helix.
1221+// Otherwise, the file is open using external program.
1222+fn open_url(cx: &mut Context, url: Url, action: Action) {
1223+ let doc = doc!(cx.editor);
1224+ let rel_path = doc
1225+ .relative_path()
1226+ .map(|path| path.parent().unwrap().to_path_buf())
1227+ .unwrap_or_default();
1228+
1229+ if url.scheme() != "file" {
1230+ return open_external_url(cx, url);
1231+ }
1232+
1233+ let content_type = std::fs::File::open(url.path()).and_then(|file| {
1234+ // Read up to 1kb to detect the content type
1235+ let mut read_buffer = Vec::new();
1236+ let n = file.take(1024).read_to_end(&mut read_buffer)?;
1237+ Ok(content_inspector::inspect(&read_buffer[..n]))
1238+ });
1239+
1240+ // we attempt to open binary files - files that can't be open in helix - using external
1241+ // program as well, e.g. pdf files or images
1242+ match content_type {
1243+ Ok(content_inspector::ContentType::BINARY) => open_external_url(cx, url),
1244+ Ok(_) | Err(_) => {
1245+ let path = &rel_path.join(url.path());
1246+ if path.is_dir() {
1247+ let picker = ui::file_picker(path.into(), &cx.editor.config());
1248+ cx.push_layer(Box::new(overlaid(picker)));
1249+ } else if let Err(e) = cx.editor.open(path, action) {
11881250 cx.editor.set_error(format!("Open file failed: {:?}", e));
11891251 }
11901252 }
11911253 }
11921254}
11931255
1256+/// Opens URL in external program.
1257+fn open_external_url(cx: &mut Context, url: Url) {
1258+ let commands = open::commands(url.as_str());
1259+ cx.jobs.callback(async {
1260+ for cmd in commands {
1261+ let mut command = tokio::process::Command::new(cmd.get_program());
1262+ command.args(cmd.get_args());
1263+ if command.output().await.is_ok() {
1264+ return Ok(job::Callback::Editor(Box::new(|_| {})));
1265+ }
1266+ }
1267+ Ok(job::Callback::Editor(Box::new(move |editor| {
1268+ editor.set_error("Opening URL in external program failed")
1269+ })))
1270+ });
1271+}
1272+
11941273fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
11951274where
11961275 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1239,6 +1318,65 @@ fn extend_next_long_word_end(cx: &mut Context) {
12391318 extend_word_impl(cx, movement::move_next_long_word_end)
12401319}
12411320
1321+/// Separate branch to find_char designed only for `<ret>` char.
1322+//
1323+// This is necessary because the one document can have different line endings inside. And we
1324+// cannot predict what character to find when <ret> is pressed. On the current line it can be `lf`
1325+// but on the next line it can be `crlf`. That's why [`find_char_impl`] cannot be applied here.
1326+fn find_char_line_ending(
1327+ cx: &mut Context,
1328+ count: usize,
1329+ direction: Direction,
1330+ inclusive: bool,
1331+ extend: bool,
1332+) {
1333+ let (view, doc) = current!(cx.editor);
1334+ let text = doc.text().slice(..);
1335+
1336+ let selection = doc.selection(view.id).clone().transform(|range| {
1337+ let cursor = range.cursor(text);
1338+ let cursor_line = range.cursor_line(text);
1339+
1340+ // Finding the line where we're going to find <ret>. Depends mostly on
1341+ // `count`, but also takes into account edge cases where we're already at the end
1342+ // of a line or the beginning of a line
1343+ let find_on_line = match direction {
1344+ Direction::Forward => {
1345+ let on_edge = line_end_char_index(&text, cursor_line) == cursor;
1346+ let line = cursor_line + count - 1 + (on_edge as usize);
1347+ if line >= text.len_lines() - 1 {
1348+ return range;
1349+ } else {
1350+ line
1351+ }
1352+ }
1353+ Direction::Backward => {
1354+ let on_edge = text.line_to_char(cursor_line) == cursor && !inclusive;
1355+ let line = cursor_line as isize - (count as isize - 1 + on_edge as isize);
1356+ if line <= 0 {
1357+ return range;
1358+ } else {
1359+ line as usize
1360+ }
1361+ }
1362+ };
1363+
1364+ let pos = match (direction, inclusive) {
1365+ (Direction::Forward, true) => line_end_char_index(&text, find_on_line),
1366+ (Direction::Forward, false) => line_end_char_index(&text, find_on_line) - 1,
1367+ (Direction::Backward, true) => line_end_char_index(&text, find_on_line - 1),
1368+ (Direction::Backward, false) => text.line_to_char(find_on_line),
1369+ };
1370+
1371+ if extend {
1372+ range.put_cursor(text, pos, true)
1373+ } else {
1374+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
1375+ }
1376+ });
1377+ doc.set_selection(view.id, selection);
1378+}
1379+
12421380fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
12431381 // TODO: count is reset to 1 before next key so we move it into the closure here.
12441382 // Would be nice to carry over.
@@ -1252,13 +1390,9 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo
12521390 KeyEvent {
12531391 code: KeyCode::Enter,
12541392 ..
1255− } =>
1256− // TODO: this isn't quite correct when CRLF is involved.
1257− // This hack will work in most cases, since documents don't
1258− // usually mix line endings. But we should fix it eventually
1259− // anyway.
1260− {
1261− doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
1393+ } => {
1394+ find_char_line_ending(cx, count, direction, inclusive, extend);
1395+ return;
12621396 }
12631397
12641398 KeyEvent {
@@ -1460,6 +1594,7 @@ where
14601594 });
14611595
14621596 doc.apply(&transaction, view.id);
1597+ exit_select_mode(cx);
14631598}
14641599
14651600fn switch_case(cx: &mut Context) {
@@ -1701,8 +1836,8 @@ fn select_regex(cx: &mut Context) {
17011836 "select:".into(),
17021837 Some(reg),
17031838 ui::completers::none,
1704− move |editor, regex, event| {
1705− let (view, doc) = current!(editor);
1839+ move |cx, regex, event| {
1840+ let (view, doc) = current!(cx.editor);
17061841 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
17071842 return;
17081843 }
@@ -1723,8 +1858,8 @@ fn split_selection(cx: &mut Context) {
17231858 "split:".into(),
17241859 Some(reg),
17251860 ui::completers::none,
1726− move |editor, regex, event| {
1727− let (view, doc) = current!(editor);
1861+ move |cx, regex, event| {
1862+ let (view, doc) = current!(cx.editor);
17281863 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
17291864 return;
17301865 }
@@ -1847,11 +1982,11 @@ fn search_impl(
18471982
18481983fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
18491984 let mut items = reg
1850− .and_then(|reg| cx.editor.registers.get(reg))
1851− .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1985+ .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1986+ .map_or(Vec::new(), |reg| reg.take(200).collect());
18521987 items.sort_unstable();
18531988 items.dedup();
1854− items.into_iter().cloned().collect()
1989+ items.into_iter().map(|value| value.to_string()).collect()
18551990}
18561991
18571992fn search(cx: &mut Context) {
@@ -1888,12 +2023,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
18882023 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
18892024 .collect()
18902025 },
1891− move |editor, regex, event| {
1892− if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
2026+ move |cx, regex, event| {
2027+ if event == PromptEvent::Validate {
2028+ cx.editor.registers.last_search_register = reg;
2029+ } else if event != PromptEvent::Update {
18932030 return;
18942031 }
18952032 search_impl(
1896− editor,
2033+ cx.editor,
18972034 &contents,
18982035 &regex,
18992036 Movement::Move,
@@ -1908,11 +2045,13 @@ fn searcher(cx: &mut Context, direction: Direction) {
19082045
19092046fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
19102047 let count = cx.count();
2048+ let register = cx
2049+ .register
2050+ .unwrap_or(cx.editor.registers.last_search_register);
19112051 let config = cx.editor.config();
19122052 let scrolloff = config.scrolloff;
1913− let (_, doc) = current!(cx.editor);
1914− let registers = &cx.editor.registers;
1915− if let Some(query) = registers.read('/').and_then(|query| query.last()) {
2053+ if let Some(query) = cx.editor.registers.first(register, cx.editor) {
2054+ let doc = doc!(cx.editor);
19162055 let contents = doc.text().slice(..).to_string();
19172056 let search_config = &config.search;
19182057 let case_insensitive = if search_config.smart_case {
@@ -1921,7 +2060,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
19212060 false
19222061 };
19232062 let wrap_around = search_config.wrap_around;
1924− if let Ok(regex) = RegexBuilder::new(query)
2063+ if let Ok(regex) = RegexBuilder::new(&query)
19252064 .case_insensitive(case_insensitive)
19262065 .multi_line(true)
19272066 .build()
@@ -1961,6 +2100,7 @@ fn extend_search_prev(cx: &mut Context) {
19612100}
19622101
19632102fn search_selection(cx: &mut Context) {
2103+ let register = cx.register.unwrap_or('/');
19642104 let (view, doc) = current!(cx.editor);
19652105 let contents = doc.text().slice(..);
19662106
@@ -1973,13 +2113,24 @@ fn search_selection(cx: &mut Context) {
19732113 .collect::<Vec<_>>()
19742114 .join("|");
19752115
1976− let msg = format!("register '{}' set to '{}'", '/', &regex);
1977− cx.editor.registers.push('/', regex);
1978− cx.editor.set_status(msg);
2116+ let msg = format!("register '{}' set to '{}'", register, &regex);
2117+ match cx.editor.registers.push(register, regex) {
2118+ Ok(_) => {
2119+ cx.editor.registers.last_search_register = register;
2120+ cx.editor.set_status(msg)
2121+ }
2122+ Err(err) => cx.editor.set_error(err.to_string()),
2123+ }
19792124}
19802125
19812126fn make_search_word_bounded(cx: &mut Context) {
1982− let regex = match cx.editor.registers.last('/') {
2127+ // Defaults to the active search register instead `/` to be more ergonomic assuming most people
2128+ // would use this command following `search_selection`. This avoids selecting the register
2129+ // twice.
2130+ let register = cx
2131+ .register
2132+ .unwrap_or(cx.editor.registers.last_search_register);
2133+ let regex = match cx.editor.registers.first(register, cx.editor) {
19832134 Some(regex) => regex,
19842135 None => return,
19852136 };
@@ -1997,14 +2148,19 @@ fn make_search_word_bounded(cx: &mut Context) {
19972148 if !start_anchored {
19982149 new_regex.push_str("\\b");
19992150 }
2000− new_regex.push_str(regex);
2151+ new_regex.push_str(&regex);
20012152 if !end_anchored {
20022153 new_regex.push_str("\\b");
20032154 }
20042155
2005− let msg = format!("register '{}' set to '{}'", '/', &new_regex);
2006− cx.editor.registers.push('/', new_regex);
2007− cx.editor.set_status(msg);
2156+ let msg = format!("register '{}' set to '{}'", register, &new_regex);
2157+ match cx.editor.registers.push(register, new_regex) {
2158+ Ok(_) => {
2159+ cx.editor.registers.last_search_register = register;
2160+ cx.editor.set_status(msg)
2161+ }
2162+ Err(err) => cx.editor.set_error(err.to_string()),
2163+ }
20082164}
20092165
20102166fn global_search(cx: &mut Context) {
@@ -2043,13 +2199,11 @@ fn global_search(cx: &mut Context) {
20432199 }
20442200 }
20452201
2046− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
20472202 let config = cx.editor.config();
20482203 let smart_case = config.search.smart_case;
20492204 let file_picker_config = config.file_picker.clone();
20502205
20512206 let reg = cx.register.unwrap_or('/');
2052−
20532207 let completions = search_completions(cx, Some(reg));
20542208 ui::regex_prompt(
20552209 cx,
@@ -2062,52 +2216,66 @@ fn global_search(cx: &mut Context) {
20622216 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
20632217 .collect()
20642218 },
2065− move |editor, regex, event| {
2219+ move |cx, regex, event| {
20662220 if event != PromptEvent::Validate {
20672221 return;
20682222 }
2223+ cx.editor.registers.last_search_register = reg;
20692224
2070− let documents: Vec<_> = editor
2225+ let current_path = doc_mut!(cx.editor).path().cloned();
2226+ let documents: Vec<_> = cx
2227+ .editor
20712228 .documents()
2072− .map(|doc| (doc.path(), doc.text()))
2229+ .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
20732230 .collect();
20742231
20752232 if let Ok(matcher) = RegexMatcherBuilder::new()
20762233 .case_smart(smart_case)
20772234 .build(regex.as_str())
20782235 {
2079− let searcher = SearcherBuilder::new()
2080− .binary_detection(BinaryDetection::quit(b'\x00'))
2081− .build();
2082−
20832236 let search_root = helix_loader::current_working_dir();
20842237 if !search_root.exists() {
2085− editor.set_error("Current working directory does not exist");
2238+ cx.editor
2239+ .set_error("Current working directory does not exist");
20862240 return;
20872241 }
20882242
2243+ let (picker, injector) = Picker::stream(current_path);
2244+
20892245 let dedup_symlinks = file_picker_config.deduplicate_links;
20902246 let absolute_root = search_root
20912247 .canonicalize()
20922248 .unwrap_or_else(|_| search_root.clone());
2093−
2094− WalkBuilder::new(search_root)
2095− .hidden(file_picker_config.hidden)
2096− .parents(file_picker_config.parents)
2097− .ignore(file_picker_config.ignore)
2098− .follow_links(file_picker_config.follow_symlinks)
2099− .git_ignore(file_picker_config.git_ignore)
2100− .git_global(file_picker_config.git_global)
2101− .git_exclude(file_picker_config.git_exclude)
2102− .max_depth(file_picker_config.max_depth)
2103− .filter_entry(move |entry| {
2104− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2105− })
2106− .build_parallel()
2107− .run(|| {
2249+ let injector_ = injector.clone();
2250+
2251+ std::thread::spawn(move || {
2252+ let searcher = SearcherBuilder::new()
2253+ .binary_detection(BinaryDetection::quit(b'\x00'))
2254+ .build();
2255+
2256+ let mut walk_builder = WalkBuilder::new(search_root);
2257+
2258+ walk_builder
2259+ .hidden(file_picker_config.hidden)
2260+ .parents(file_picker_config.parents)
2261+ .ignore(file_picker_config.ignore)
2262+ .follow_links(file_picker_config.follow_symlinks)
2263+ .git_ignore(file_picker_config.git_ignore)
2264+ .git_global(file_picker_config.git_global)
2265+ .git_exclude(file_picker_config.git_exclude)
2266+ .max_depth(file_picker_config.max_depth)
2267+ .filter_entry(move |entry| {
2268+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2269+ });
2270+
2271+ walk_builder
2272+ .add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
2273+ walk_builder.add_custom_ignore_filename(".helix/ignore");
2274+
2275+ walk_builder.build_parallel().run(|| {
21082276 let mut searcher = searcher.clone();
21092277 let matcher = matcher.clone();
2110− let all_matches_sx = all_matches_sx.clone();
2278+ let injector = injector_.clone();
21112279 let documents = &documents;
21122280 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
21132281 let entry = match entry {
@@ -2121,21 +2289,24 @@ fn global_search(cx: &mut Context) {
21212289 _ => return WalkState::Continue,
21222290 };
21232291
2292+ let mut stop = false;
21242293 let sink = sinks::UTF8(|line_num, _| {
2125− all_matches_sx
2126− .send(FileResult::new(entry.path(), line_num as usize - 1))
2127− .unwrap();
2294+ stop = injector
2295+ .push(FileResult::new(entry.path(), line_num as usize - 1))
2296+ .is_err();
21282297
2129− Ok(true)
2298+ Ok(!stop)
21302299 });
21312300 let doc = documents.iter().find(|&(doc_path, _)| {
2132− doc_path.map_or(false, |doc_path| doc_path == entry.path())
2301+ doc_path
2302+ .as_ref()
2303+ .map_or(false, |doc_path| doc_path == entry.path())
21332304 });
21342305
21352306 let result = if let Some((_, doc)) = doc {
21362307 // there is already a buffer for this file
21372308 // search the buffer instead of the file because it's faster
2138− // and captures new edits without requireing a save
2309+ // and captures new edits without requiring a save
21392310 if searcher.multi_line_with_matcher(&matcher) {
21402311 // in this case a continous buffer is required
21412312 // convert the rope to a string
@@ -2159,67 +2330,66 @@ fn global_search(cx: &mut Context) {
21592330 err
21602331 );
21612332 }
2162− WalkState::Continue
2333+ if stop {
2334+ WalkState::Quit
2335+ } else {
2336+ WalkState::Continue
2337+ }
21632338 })
21642339 });
2340+ });
2341+
2342+ cx.jobs.callback(async move {
2343+ let call = move |_: &mut Editor, compositor: &mut Compositor| {
2344+ let picker = Picker::with_stream(
2345+ picker,
2346+ injector,
2347+ move |cx, FileResult { path, line_num }, action| {
2348+ let doc = match cx.editor.open(path, action) {
2349+ Ok(id) => doc_mut!(cx.editor, &id),
2350+ Err(e) => {
2351+ cx.editor.set_error(format!(
2352+ "Failed to open file '{}': {}",
2353+ path.display(),
2354+ e
2355+ ));
2356+ return;
2357+ }
2358+ };
2359+
2360+ let line_num = *line_num;
2361+ let view = view_mut!(cx.editor);
2362+ let text = doc.text();
2363+ if line_num >= text.len_lines() {
2364+ cx.editor.set_error(
2365+ "The line you jumped to does not exist anymore because the file has changed.",
2366+ );
2367+ return;
2368+ }
2369+ let start = text.line_to_char(line_num);
2370+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2371+
2372+ doc.set_selection(view.id, Selection::single(start, end));
2373+ if action.align_view(view, doc.id()) {
2374+ align_view(doc, view, Align::Center);
2375+ }
2376+ },
2377+ )
2378+ .with_preview(
2379+ |_editor, FileResult { path, line_num }| {
2380+ Some((path.clone().into(), Some((*line_num, *line_num))))
2381+ },
2382+ );
2383+ compositor.push(Box::new(overlaid(picker)))
2384+ };
2385+ Ok(Callback::EditorCompositor(Box::new(call)))
2386+ })
21652387 } else {
21662388 // Otherwise do nothing
21672389 // log::warn!("Global Search Invalid Pattern")
21682390 }
21692391 },
21702392 );
2171−
2172− let current_path = doc_mut!(cx.editor).path().cloned();
2173−
2174− let show_picker = async move {
2175− let all_matches: Vec<FileResult> =
2176− UnboundedReceiverStream::new(all_matches_rx).collect().await;
2177− let call: job::Callback = Callback::EditorCompositor(Box::new(
2178− move |editor: &mut Editor, compositor: &mut Compositor| {
2179− if all_matches.is_empty() {
2180− if !editor.is_err() {
2181− editor.set_status("No matches found");
2182− }
2183− return;
2184− }
2185−
2186− let picker = Picker::new(
2187− all_matches,
2188− current_path,
2189− move |cx, FileResult { path, line_num }, action| {
2190− match cx.editor.open(path, action) {
2191− Ok(_) => {}
2192− Err(e) => {
2193− cx.editor.set_error(format!(
2194− "Failed to open file '{}': {}",
2195− path.display(),
2196− e
2197− ));
2198− return;
2199− }
2200− }
2201−
2202− let line_num = *line_num;
2203− let (view, doc) = current!(cx.editor);
2204− let text = doc.text();
2205− if line_num >= text.len_lines() {
2206− cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2207− return;
2208− }
2209− let start = text.line_to_char(line_num);
2210− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2211−
2212− doc.set_selection(view.id, Selection::single(start, end));
2213− align_view(doc, view, Align::Center);
2214− }).with_preview(|_editor, FileResult { path, line_num }| {
2215− Some((path.clone().into(), Some((*line_num, *line_num))))
2216− });
2217− compositor.push(Box::new(overlaid(picker)));
2218− },
2219− ));
2220− Ok(call)
2221− };
2222− cx.jobs.callback(show_picker);
22232393}
22242394
22252395enum Extend {
@@ -2367,7 +2537,10 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
23672537 let text = doc.text().slice(..);
23682538 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
23692539 let reg_name = cx.register.unwrap_or('"');
2370− cx.editor.registers.write(reg_name, values);
2540+ if let Err(err) = cx.editor.registers.write(reg_name, values) {
2541+ cx.editor.set_error(err.to_string());
2542+ return;
2543+ }
23712544 };
23722545
23732546 // then delete
@@ -2507,6 +2680,10 @@ fn insert_mode(cx: &mut Context) {
25072680 .transform(|range| Range::new(range.to(), range.from()));
25082681
25092682 doc.set_selection(view.id, selection);
2683+
2684+ // [TODO] temporary workaround until we're not using the idle timer to
2685+ // trigger auto completions any more
2686+ cx.editor.clear_idle_timer();
25102687}
25112688
25122689// inserts at the end of each selection
@@ -2567,6 +2744,7 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
25672744 let picker = ui::file_picker(path, &cx.editor.config());
25682745 cx.push_layer(Box::new(overlaid(picker)));
25692746}
2747+
25702748fn file_picker_in_current_directory(cx: &mut Context) {
25712749 let cwd = helix_loader::current_working_dir();
25722750 if !cwd.exists() {
@@ -2723,9 +2901,11 @@ fn jumplist_picker(cx: &mut Context) {
27232901 |cx, meta, action| {
27242902 cx.editor.switch(meta.id, action);
27252903 let config = cx.editor.config();
2726− let (view, doc) = current!(cx.editor);
2904+ let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
27272905 doc.set_selection(view.id, meta.selection.clone());
2728− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2906+ if action.align_view(view, doc.id()) {
2907+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
2908+ }
27292909 },
27302910 )
27312911 .with_preview(|editor, meta| {
@@ -2873,6 +3053,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
28733053 let indent = indent::indent_for_newline(
28743054 language_config,
28753055 syntax,
3056+ &doc.config.load().indent_heuristic,
28763057 &doc.indent_style,
28773058 tab_width,
28783059 text,
@@ -2977,6 +3158,7 @@ fn open(cx: &mut Context, open: Open) {
29773158 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
29783159 Open::Above => range.from(),
29793160 });
3161+
29803162 let new_line = match open {
29813163 // adjust position to the end of the line (next line - 1)
29823164 Open::Below => cursor_line + 1,
@@ -2984,13 +3166,15 @@ fn open(cx: &mut Context, open: Open) {
29843166 Open::Above => cursor_line,
29853167 };
29863168
3169+ let line_num = new_line.saturating_sub(1);
3170+
29873171 // Index to insert newlines after, as well as the char width
29883172 // to use to compensate for those inserted newlines.
29893173 let (line_end_index, line_end_offset_width) = if new_line == 0 {
29903174 (0, 0)
29913175 } else {
29923176 (
2993− line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
3177+ line_end_char_index(&text, line_num),
29943178 doc.line_ending.len_chars(),
29953179 )
29963180 };
@@ -2998,13 +3182,15 @@ fn open(cx: &mut Context, open: Open) {
29983182 let indent = indent::indent_for_newline(
29993183 doc.language_config(),
30003184 doc.syntax(),
3185+ &doc.config.load().indent_heuristic,
30013186 &doc.indent_style,
30023187 doc.tab_width(),
30033188 text,
3004− new_line.saturating_sub(1),
3189+ line_num,
30053190 line_end_index,
30063191 cursor_line,
30073192 );
3193+
30083194 let indent_len = indent.len();
30093195 let mut text = String::with_capacity(1 + indent_len);
30103196 text.push_str(doc.line_ending.as_str());
@@ -3430,6 +3616,7 @@ pub mod insert {
34303616 }
34313617
34323618 use helix_core::auto_pairs;
3619+ use helix_view::editor::SmartTabConfig;
34333620
34343621 pub fn insert_char(cx: &mut Context, c: char) {
34353622 let (view, doc) = current_ref!(cx.editor);
@@ -3455,6 +3642,31 @@ pub mod insert {
34553642 }
34563643 }
34573644
3645+ pub fn smart_tab(cx: &mut Context) {
3646+ let (view, doc) = current_ref!(cx.editor);
3647+ let view_id = view.id;
3648+
3649+ if matches!(
3650+ cx.editor.config().smart_tab,
3651+ Some(SmartTabConfig { enable: true, .. })
3652+ ) {
3653+ let cursors_after_whitespace = doc.selection(view_id).ranges().iter().all(|range| {
3654+ let cursor = range.cursor(doc.text().slice(..));
3655+ let current_line_num = doc.text().char_to_line(cursor);
3656+ let current_line_start = doc.text().line_to_char(current_line_num);
3657+ let left = doc.text().slice(current_line_start..cursor);
3658+ left.chars().all(|c| c.is_whitespace())
3659+ });
3660+
3661+ if !cursors_after_whitespace {
3662+ move_parent_node_end(cx);
3663+ return;
3664+ }
3665+ }
3666+
3667+ insert_tab(cx);
3668+ }
3669+
34583670 pub fn insert_tab(cx: &mut Context) {
34593671 let (view, doc) = current!(cx.editor);
34603672 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3510,6 +3722,7 @@ pub mod insert {
35103722 let indent = indent::indent_for_newline(
35113723 doc.language_config(),
35123724 doc.syntax(),
3725+ &doc.config.load().indent_heuristic,
35133726 &doc.indent_style,
35143727 doc.tab_width(),
35153728 text,
@@ -3750,7 +3963,22 @@ fn commit_undo_checkpoint(cx: &mut Context) {
37503963// Yank / Paste
37513964
37523965fn yank(cx: &mut Context) {
3753− let (view, doc) = current!(cx.editor);
3966+ yank_impl(cx.editor, cx.register.unwrap_or('"'));
3967+ exit_select_mode(cx);
3968+}
3969+
3970+fn yank_to_clipboard(cx: &mut Context) {
3971+ yank_impl(cx.editor, '+');
3972+ exit_select_mode(cx);
3973+}
3974+
3975+fn yank_to_primary_clipboard(cx: &mut Context) {
3976+ yank_impl(cx.editor, '*');
3977+ exit_select_mode(cx);
3978+}
3979+
3980+fn yank_impl(editor: &mut Editor, register: char) {
3981+ let (view, doc) = current!(editor);
37543982 let text = doc.text().slice(..);
37553983
37563984 let values: Vec<String> = doc
@@ -3758,19 +3986,15 @@ fn yank(cx: &mut Context) {
37583986 .fragments(text)
37593987 .map(Cow::into_owned)
37603988 .collect();
3989+ let selections = values.len();
37613990
3762− let msg = format!(
3763− "yanked {} selection(s) to register {}",
3764− values.len(),
3765− cx.register.unwrap_or('"')
3766− );
3767−
3768− cx.editor
3769− .registers
3770− .write(cx.register.unwrap_or('"'), values);
3771−
3772− cx.editor.set_status(msg);
3773− exit_select_mode(cx);
3991+ match editor.registers.write(register, values) {
3992+ Ok(_) => editor.set_status(format!(
3993+ "yanked {selections} selection{} to register {register}",
3994+ if selections == 1 { "" } else { "s" }
3995+ )),
3996+ Err(err) => editor.set_error(err.to_string()),
3997+ }
37743998}
37753999
37764000fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
@@ -3778,6 +4002,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
37784002 let text = doc.text().slice(..);
37794003
37804004 let selection = doc.selection(view.id);
4005+ let selections = selection.len();
37814006 let joined = selection
37824007 .fragments(text)
37834008 .fold(String::new(), |mut acc, fragment| {
@@ -3788,104 +4013,52 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
37884013 acc
37894014 });
37904015
3791− let msg = format!(
3792− "joined and yanked {} selection(s) to register {}",
3793− selection.len(),
3794− register,
3795− );
3796−
3797− editor.registers.write(register, vec![joined]);
3798− editor.set_status(msg);
4016+ match editor.registers.write(register, vec![joined]) {
4017+ Ok(_) => editor.set_status(format!(
4018+ "joined and yanked {selections} selection{} to register {register}",
4019+ if selections == 1 { "" } else { "s" }
4020+ )),
4021+ Err(err) => editor.set_error(err.to_string()),
4022+ }
37994023}
38004024
38014025fn yank_joined(cx: &mut Context) {
3802− let line_ending = doc!(cx.editor).line_ending;
3803− let register = cx.register.unwrap_or('"');
3804− yank_joined_impl(cx.editor, line_ending.as_str(), register);
4026+ let separator = doc!(cx.editor).line_ending.as_str();
4027+ yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
38054028 exit_select_mode(cx);
38064029}
38074030
3808−fn yank_joined_to_clipboard_impl(
3809− editor: &mut Editor,
3810− separator: &str,
3811− clipboard_type: ClipboardType,
3812−) -> anyhow::Result<()> {
3813− let (view, doc) = current!(editor);
3814− let text = doc.text().slice(..);
3815−
3816− let values: Vec<String> = doc
3817− .selection(view.id)
3818− .fragments(text)
3819− .map(Cow::into_owned)
3820− .collect();
3821−
3822− let clipboard_text = match clipboard_type {
3823− ClipboardType::Clipboard => "system clipboard",
3824− ClipboardType::Selection => "primary clipboard",
3825− };
3826−
3827− let msg = format!(
3828− "joined and yanked {} selection(s) to {}",
3829− values.len(),
3830− clipboard_text,
3831− );
3832−
3833− let joined = values.join(separator);
3834−
3835− editor
3836− .clipboard_provider
3837− .set_contents(joined, clipboard_type)
3838− .context("Couldn't set system clipboard content")?;
3839−
3840− editor.set_status(msg);
3841−
3842− Ok(())
4031+fn yank_joined_to_clipboard(cx: &mut Context) {
4032+ let line_ending = doc!(cx.editor).line_ending;
4033+ yank_joined_impl(cx.editor, line_ending.as_str(), '+');
4034+ exit_select_mode(cx);
38434035}
38444036
3845−fn yank_joined_to_clipboard(cx: &mut Context) {
4037+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
38464038 let line_ending = doc!(cx.editor).line_ending;
3847− let _ =
3848− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
4039+ yank_joined_impl(cx.editor, line_ending.as_str(), '*');
38494040 exit_select_mode(cx);
38504041}
38514042
3852−fn yank_main_selection_to_clipboard_impl(
3853− editor: &mut Editor,
3854− clipboard_type: ClipboardType,
3855−) -> anyhow::Result<()> {
4043+fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
38564044 let (view, doc) = current!(editor);
38574045 let text = doc.text().slice(..);
38584046
3859− let message_text = match clipboard_type {
3860− ClipboardType::Clipboard => "yanked main selection to system clipboard",
3861− ClipboardType::Selection => "yanked main selection to primary clipboard",
3862− };
3863−
3864− let value = doc.selection(view.id).primary().fragment(text);
4047+ let selection = doc.selection(view.id).primary().fragment(text).to_string();
38654048
3866− if let Err(e) = editor
3867− .clipboard_provider
3868− .set_contents(value.into_owned(), clipboard_type)
3869− {
3870− bail!("Couldn't set system clipboard content: {}", e);
4049+ match editor.registers.write(register, vec![selection]) {
4050+ Ok(_) => editor.set_status(format!("yanked primary selection to register {register}",)),
4051+ Err(err) => editor.set_error(err.to_string()),
38714052 }
3872−
3873− editor.set_status(message_text);
3874− Ok(())
38754053}
38764054
38774055fn yank_main_selection_to_clipboard(cx: &mut Context) {
3878− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
3879−}
3880−
3881−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3882− let line_ending = doc!(cx.editor).line_ending;
3883− let _ =
3884− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
4056+ yank_primary_selection_impl(cx.editor, '+');
4057+ exit_select_mode(cx);
38854058}
38864059
38874060fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3888− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
4061+ yank_primary_selection_impl(cx.editor, '*');
38894062 exit_select_mode(cx);
38904063}
38914064
@@ -3983,146 +4156,99 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
39834156 };
39844157 let (view, doc) = current!(cx.editor);
39854158 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
3986−}
3987−
3988−fn paste_clipboard_impl(
3989− editor: &mut Editor,
3990− action: Paste,
3991− clipboard_type: ClipboardType,
3992− count: usize,
3993−) -> anyhow::Result<()> {
3994− let (view, doc) = current!(editor);
3995− match editor.clipboard_provider.get_contents(clipboard_type) {
3996− Ok(contents) => {
3997− paste_impl(&[contents], doc, view, action, count, editor.mode);
3998− Ok(())
3999− }
4000− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
4001− }
4159+ exit_select_mode(cx);
40024160}
40034161
40044162fn paste_clipboard_after(cx: &mut Context) {
4005− let _ = paste_clipboard_impl(
4006− cx.editor,
4007− Paste::After,
4008− ClipboardType::Clipboard,
4009− cx.count(),
4010− );
4163+ paste(cx.editor, '+', Paste::After, cx.count());
4164+ exit_select_mode(cx);
40114165}
40124166
40134167fn paste_clipboard_before(cx: &mut Context) {
4014− let _ = paste_clipboard_impl(
4015− cx.editor,
4016− Paste::Before,
4017− ClipboardType::Clipboard,
4018− cx.count(),
4019− );
4168+ paste(cx.editor, '+', Paste::Before, cx.count());
4169+ exit_select_mode(cx);
40204170}
40214171
40224172fn paste_primary_clipboard_after(cx: &mut Context) {
4023− let _ = paste_clipboard_impl(
4024− cx.editor,
4025− Paste::After,
4026− ClipboardType::Selection,
4027− cx.count(),
4028− );
4173+ paste(cx.editor, '*', Paste::After, cx.count());
4174+ exit_select_mode(cx);
40294175}
40304176
40314177fn paste_primary_clipboard_before(cx: &mut Context) {
4032− let _ = paste_clipboard_impl(
4033− cx.editor,
4034− Paste::Before,
4035− ClipboardType::Selection,
4036− cx.count(),
4037− );
4178+ paste(cx.editor, '*', Paste::Before, cx.count());
4179+ exit_select_mode(cx);
40384180}
40394181
40404182fn replace_with_yanked(cx: &mut Context) {
4041− let count = cx.count();
4042− let reg_name = cx.register.unwrap_or('"');
4043− let (view, doc) = current!(cx.editor);
4044− let registers = &mut cx.editor.registers;
4045−
4046− if let Some(values) = registers.read(reg_name) {
4047− if !values.is_empty() {
4048− let repeat = std::iter::repeat(
4049− values
4050− .last()
4051− .map(|value| Tendril::from(&value.repeat(count)))
4052− .unwrap(),
4053− );
4054− let mut values = values
4055− .iter()
4056− .map(|value| Tendril::from(&value.repeat(count)))
4057− .chain(repeat);
4058− let selection = doc.selection(view.id);
4059− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4060− if !range.is_empty() {
4061− (range.from(), range.to(), Some(values.next().unwrap()))
4062− } else {
4063− (range.from(), range.to(), None)
4064− }
4065− });
4066−
4067− doc.apply(&transaction, view.id);
4068− exit_select_mode(cx);
4069− }
4070− }
4183+ replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
4184+ exit_select_mode(cx);
40714185}
40724186
4073−fn replace_selections_with_clipboard_impl(
4074− cx: &mut Context,
4075− clipboard_type: ClipboardType,
4076−) -> anyhow::Result<()> {
4077− let count = cx.count();
4078− let (view, doc) = current!(cx.editor);
4187+fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) {
4188+ let Some(values) = editor.registers
4189+ .read(register, editor)
4190+ .filter(|values| values.len() > 0) else { return };
4191+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
40794192
4080− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
4081− Ok(contents) => {
4082− let selection = doc.selection(view.id);
4083− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4084− (
4085− range.from(),
4086− range.to(),
4087− Some(contents.repeat(count).as_str().into()),
4088− )
4089− });
4090−
4091− doc.apply(&transaction, view.id);
4092− doc.append_changes_to_history(view);
4193+ let (view, doc) = current!(editor);
4194+ let repeat = std::iter::repeat(
4195+ values
4196+ .last()
4197+ .map(|value| Tendril::from(&value.repeat(count)))
4198+ .unwrap(),
4199+ );
4200+ let mut values = values
4201+ .iter()
4202+ .map(|value| Tendril::from(&value.repeat(count)))
4203+ .chain(repeat);
4204+ let selection = doc.selection(view.id);
4205+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4206+ if !range.is_empty() {
4207+ (range.from(), range.to(), Some(values.next().unwrap()))
4208+ } else {
4209+ (range.from(), range.to(), None)
40934210 }
4094− Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
4095− }
4211+ });
40964212
4097− exit_select_mode(cx);
4098− Ok(())
4213+ doc.apply(&transaction, view.id);
40994214}
41004215
41014216fn replace_selections_with_clipboard(cx: &mut Context) {
4102− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
4217+ replace_with_yanked_impl(cx.editor, '+', cx.count());
4218+ exit_select_mode(cx);
41034219}
41044220
41054221fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4106− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
4222+ replace_with_yanked_impl(cx.editor, '*', cx.count());
4223+ exit_select_mode(cx);
41074224}
41084225
4109−fn paste(cx: &mut Context, pos: Paste) {
4110− let count = cx.count();
4111− let reg_name = cx.register.unwrap_or('"');
4112− let (view, doc) = current!(cx.editor);
4113− let registers = &mut cx.editor.registers;
4226+fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
4227+ let Some(values) = editor.registers.read(register, editor) else { return };
4228+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
41144229
4115− if let Some(values) = registers.read(reg_name) {
4116− paste_impl(values, doc, view, pos, count, cx.editor.mode);
4117− }
4230+ let (view, doc) = current!(editor);
4231+ paste_impl(&values, doc, view, pos, count, editor.mode);
41184232}
41194233
41204234fn paste_after(cx: &mut Context) {
4121− paste(cx, Paste::After)
4235+ paste(
4236+ cx.editor,
4237+ cx.register.unwrap_or('"'),
4238+ Paste::After,
4239+ cx.count(),
4240+ );
4241+ exit_select_mode(cx);
41224242}
41234243
41244244fn paste_before(cx: &mut Context) {
4125− paste(cx, Paste::Before)
4245+ paste(
4246+ cx.editor,
4247+ cx.register.unwrap_or('"'),
4248+ Paste::Before,
4249+ cx.count(),
4250+ );
4251+ exit_select_mode(cx);
41264252}
41274253
41284254fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4161,6 +4287,7 @@ fn indent(cx: &mut Context) {
41614287 }),
41624288 );
41634289 doc.apply(&transaction, view.id);
4290+ exit_select_mode(cx);
41644291}
41654292
41664293fn unindent(cx: &mut Context) {
@@ -4200,6 +4327,7 @@ fn unindent(cx: &mut Context) {
42004327 let transaction = Transaction::change(doc.text(), changes.into_iter());
42014328
42024329 doc.apply(&transaction, view.id);
4330+ exit_select_mode(cx);
42034331}
42044332
42054333fn format_selections(cx: &mut Context) {
@@ -4229,7 +4357,7 @@ fn format_selections(cx: &mut Context) {
42294357 })
42304358 else {
42314359 cx.editor
4232− .set_error("No configured language server does not support range formatting");
4360+ .set_error("No configured language server supports range formatting");
42334361 return;
42344362 };
42354363
@@ -4266,10 +4394,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
42664394 use movement::skip_while;
42674395 let (view, doc) = current!(cx.editor);
42684396 let text = doc.text();
4269− let slice = doc.text().slice(..);
4397+ let slice = text.slice(..);
42704398
42714399 let mut changes = Vec::new();
4272− let fragment = Tendril::from(" ");
42734400
42744401 for selection in doc.selection(view.id) {
42754402 let (start, mut end) = selection.line_range(slice);
@@ -4285,9 +4412,13 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
42854412 let mut end = text.line_to_char(line + 1);
42864413 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
42874414
4288− // need to skip from start, not end
4289− let change = (start, end, Some(fragment.clone()));
4290− changes.push(change);
4415+ let separator = if end == line_end_char_index(&slice, line + 1) {
4416+ // the joining line contains only space-characters => don't include a whitespace when joining
4417+ None
4418+ } else {
4419+ Some(Tendril::from(" "))
4420+ };
4421+ changes.push((start, end, separator));
42914422 }
42924423 }
42934424
@@ -4299,9 +4430,6 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
42994430 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
43004431 changes.dedup();
43014432
4302− // TODO: joining multiple empty lines should be replaced by a single space.
4303− // need to merge change ranges that touch
4304−
43054433 // select inserted spaces
43064434 let transaction = if select_space {
43074435 let ranges: SmallVec<_> = changes
@@ -4313,9 +4441,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
43134441 })
43144442 .collect();
43154443 let selection = Selection::new(ranges, 0);
4316− Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
4444+ Transaction::change(text, changes.into_iter()).with_selection(selection)
43174445 } else {
4318− Transaction::change(doc.text(), changes.into_iter())
4446+ Transaction::change(text, changes.into_iter())
43194447 };
43204448
43214449 doc.apply(&transaction, view.id);
@@ -4329,8 +4457,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
43294457 if remove { "remove:" } else { "keep:" }.into(),
43304458 Some(reg),
43314459 ui::completers::none,
4332− move |editor, regex, event| {
4333− let (view, doc) = current!(editor);
4460+ move |cx, regex, event| {
4461+ let (view, doc) = current!(cx.editor);
43344462 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
43354463 return;
43364464 }
@@ -4691,6 +4819,49 @@ fn select_prev_sibling(cx: &mut Context) {
46914819 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
46924820}
46934821
4822+fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
4823+ let motion = move |editor: &mut Editor| {
4824+ let (view, doc) = current!(editor);
4825+
4826+ if let Some(syntax) = doc.syntax() {
4827+ let text = doc.text().slice(..);
4828+ let current_selection = doc.selection(view.id);
4829+
4830+ let selection = movement::move_parent_node_end(
4831+ syntax,
4832+ text,
4833+ current_selection.clone(),
4834+ dir,
4835+ movement,
4836+ );
4837+
4838+ doc.set_selection(view.id, selection);
4839+
4840+ // [TODO] temporary workaround until we're not using the idle timer to
4841+ // trigger auto completions any more
4842+ editor.clear_idle_timer();
4843+ }
4844+ };
4845+
4846+ cx.editor.apply_motion(motion);
4847+}
4848+
4849+pub fn move_parent_node_end(cx: &mut Context) {
4850+ move_node_bound_impl(cx, Direction::Forward, Movement::Move)
4851+}
4852+
4853+pub fn move_parent_node_start(cx: &mut Context) {
4854+ move_node_bound_impl(cx, Direction::Backward, Movement::Move)
4855+}
4856+
4857+pub fn extend_parent_node_end(cx: &mut Context) {
4858+ move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
4859+}
4860+
4861+pub fn extend_parent_node_start(cx: &mut Context) {
4862+ move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
4863+}
4864+
46944865fn match_brackets(cx: &mut Context) {
46954866 let (view, doc) = current!(cx.editor);
46964867 let is_select = cx.editor.mode == Mode::Select;
@@ -4804,17 +4975,19 @@ fn transpose_view(cx: &mut Context) {
48044975 cx.editor.transpose_view()
48054976}
48064977
4807−// split helper, clear it later
4808−fn split(cx: &mut Context, action: Action) {
4809− let (view, doc) = current!(cx.editor);
4978+/// Open a new split in the given direction specified by the action.
4979+///
4980+/// Maintain the current view (both the cursor's position and view in document).
4981+fn split(editor: &mut Editor, action: Action) {
4982+ let (view, doc) = current!(editor);
48104983 let id = doc.id();
48114984 let selection = doc.selection(view.id).clone();
48124985 let offset = view.offset;
48134986
4814− cx.editor.switch(id, action);
4987+ editor.switch(id, action);
48154988
48164989 // match the selection in the previous view
4817− let (view, doc) = current!(cx.editor);
4990+ let (view, doc) = current!(editor);
48184991 doc.set_selection(view.id, selection);
48194992 // match the view scroll offset (switch doesn't handle this fully
48204993 // since the selection is only matched after the split)
@@ -4822,7 +4995,7 @@ fn split(cx: &mut Context, action: Action) {
48224995}
48234996
48244997fn hsplit(cx: &mut Context) {
4825− split(cx, Action::HorizontalSplit);
4998+ split(cx.editor, Action::HorizontalSplit);
48264999}
48275000
48285001fn hsplit_new(cx: &mut Context) {
@@ -4830,7 +5003,7 @@ fn hsplit_new(cx: &mut Context) {
48305003}
48315004
48325005fn vsplit(cx: &mut Context) {
4833− split(cx, Action::VerticalSplit);
5006+ split(cx.editor, Action::VerticalSplit);
48345007}
48355008
48365009fn vsplit_new(cx: &mut Context) {
@@ -4879,7 +5052,12 @@ fn insert_register(cx: &mut Context) {
48795052 if let Some(ch) = event.char() {
48805053 cx.editor.autoinfo = None;
48815054 cx.register = Some(ch);
4882− paste(cx, Paste::Cursor);
5055+ paste(
5056+ cx.editor,
5057+ cx.register.unwrap_or('"'),
5058+ Paste::Cursor,
5059+ cx.count(),
5060+ );
48835061 }
48845062 })
48855063}
@@ -5431,12 +5609,18 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
54315609 };
54325610
54335611 // These `usize`s cannot underflow because selection ranges cannot overlap.
5434− // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5435− // arithmetic to assert this.
5436− let anchor = (to as isize + offset - deleted_len as isize) as usize;
5612+ let anchor = to
5613+ .checked_add_signed(offset)
5614+ .expect("Selection ranges cannot overlap")
5615+ .checked_sub(deleted_len)
5616+ .expect("Selection ranges cannot overlap");
54375617 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
54385618 ranges.push(new_range);
5439− offset = offset + output_len as isize - deleted_len as isize;
5619+ offset = offset
5620+ .checked_add_unsigned(output_len)
5621+ .expect("Selection ranges cannot overlap")
5622+ .checked_sub_unsigned(deleted_len)
5623+ .expect("Selection ranges cannot overlap");
54405624
54415625 changes.push((from, to, Some(output)));
54425626 }
@@ -5575,6 +5759,7 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
55755759 let transaction = Transaction::change(doc.text(), changes.into_iter());
55765760 let transaction = transaction.with_selection(new_selection);
55775761 doc.apply(&transaction, view.id);
5762+ exit_select_mode(cx);
55785763 }
55795764}
55805765
@@ -5593,9 +5778,12 @@ fn record_macro(cx: &mut Context) {
55935778 }
55945779 })
55955780 .collect::<String>();
5596− cx.editor.registers.write(reg, vec![s]);
5597− cx.editor
5598− .set_status(format!("Recorded to register [{}]", reg));
5781+ match cx.editor.registers.write(reg, vec![s]) {
5782+ Ok(_) => cx
5783+ .editor
5784+ .set_status(format!("Recorded to register [{}]", reg)),
5785+ Err(err) => cx.editor.set_error(err.to_string()),
5786+ }
55995787 } else {
56005788 let reg = cx.register.take().unwrap_or('@');
56015789 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5615,8 +5803,14 @@ fn replay_macro(cx: &mut Context) {
56155803 return;
56165804 }
56175805
5618− let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
5619− match helix_view::input::parse_macro(keys_str) {
5806+ let keys: Vec<KeyEvent> = if let Some(keys) = cx
5807+ .editor
5808+ .registers
5809+ .read(reg, cx.editor)
5810+ .filter(|values| values.len() == 1)
5811+ .map(|mut values| values.next().unwrap())
5812+ {
5813+ match helix_view::input::parse_macro(&keys) {
56205814 Ok(keys) => keys,
56215815 Err(err) => {
56225816 cx.editor.set_error(format!("Invalid macro: {}", err));
helix-term/src/commands/dap.rs+13−4
@@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
88use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
99use helix_dap::{self as dap, Client};
1010use helix_lsp::block_on;
11−use helix_view::editor::Breakpoint;
11+use helix_view::{editor::Breakpoint, graphics::Margin};
1212
1313use serde_json::{to_value, Value};
1414use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -339,8 +339,12 @@ fn debug_parameter_prompt(
339339 .to_owned();
340340
341341 let completer = match field_type {
342− "filename" => ui::completers::filename,
343− "directory" => ui::completers::directory,
342+ "filename" => |editor: &Editor, input: &str| {
343+ ui::completers::filename_with_git_ignore(editor, input, false)
344+ },
345+ "directory" => |editor: &Editor, input: &str| {
346+ ui::completers::directory_with_git_ignore(editor, input, false)
347+ },
344348 _ => ui::completers::none,
345349 };
346350
@@ -577,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
577581 }
578582
579583 let contents = Text::from(tui::text::Text::from(variables));
580− let popup = Popup::new("dap-variables", contents);
584+ let margin = if cx.editor.popup_border() {
585+ Margin::all(1)
586+ } else {
587+ Margin::none()
588+ };
589+ let popup = Popup::new("dap-variables", contents).margin(margin);
581590 cx.replace_or_push_layer("dap-variables", popup);
582591}
583592
helix-term/src/commands/lsp.rs+59−61
@@ -23,6 +23,7 @@ use helix_core::{
2323use helix_view::{
2424 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
2525 editor::Action,
26+ graphics::Margin,
2627 theme::Style,
2728 Document, View,
2829};
@@ -49,7 +50,7 @@ use std::{
4950/// If there is no configured language server that supports the feature, this displays a status message.
5051/// Using this macro in a context where the editor automatically queries the LSP
5152/// (instead of when the user explicitly does so via a keybind like `gd`)
52−/// will spam the "No configured language server supports <feature>" status message confusingly.
53+/// will spam the "No configured language server supports \<feature>" status message confusingly.
5354#[macro_export]
5455macro_rules! language_server_with_feature {
5556 ($editor:expr, $doc:expr, $feature:expr) => {{
@@ -195,7 +196,6 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
195196 (path.into(), line)
196197}
197198
198−// TODO: share with symbol picker(symbol.location)
199199fn jump_to_location(
200200 editor: &mut Editor,
201201 location: &lsp::Location,
@@ -213,15 +213,16 @@ fn jump_to_location(
213213 return;
214214 }
215215 };
216− match editor.open(&path, action) {
217− Ok(_) => (),
216+
217+ let doc = match editor.open(&path, action) {
218+ Ok(id) => doc_mut!(editor, &id),
218219 Err(err) => {
219220 let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
220221 editor.set_error(err);
221222 return;
222223 }
223− }
224− let (view, doc) = current!(editor);
224+ };
225+ let view = view_mut!(editor);
225226 // TODO: convert inside server
226227 let new_range =
227228 if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
@@ -233,45 +234,22 @@ fn jump_to_location(
233234 // we flip the range so that the cursor sits on the start of the symbol
234235 // (for example start of the function).
235236 doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
236− align_view(doc, view, Align::Center);
237+ if action.align_view(view, doc.id()) {
238+ align_view(doc, view, Align::Center);
239+ }
237240}
238241
239242type SymbolPicker = Picker<SymbolInformationItem>;
240243
241244fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
242245 // TODO: drop current_path comparison and instead use workspace: bool flag?
243− Picker::new(symbols, current_path.clone(), move |cx, item, action| {
244− let (view, doc) = current!(cx.editor);
245− push_jump(view, doc);
246−
247− if current_path.as_ref() != Some(&item.symbol.location.uri) {
248− let uri = &item.symbol.location.uri;
249− let path = match uri.to_file_path() {
250− Ok(path) => path,
251− Err(_) => {
252− let err = format!("unable to convert URI to filepath: {}", uri);
253− cx.editor.set_error(err);
254− return;
255− }
256− };
257− if let Err(err) = cx.editor.open(&path, action) {
258− let err = format!("failed to open document: {}: {}", uri, err);
259− log::error!("{}", err);
260− cx.editor.set_error(err);
261− return;
262− }
263− }
264−
265− let (view, doc) = current!(cx.editor);
266−
267− if let Some(range) =
268− lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
269− {
270− // we flip the range so that the cursor sits on the start of the symbol
271− // (for example start of the function).
272− doc.set_selection(view.id, Selection::single(range.head, range.anchor));
273− align_view(doc, view, Align::Center);
274− }
246+ Picker::new(symbols, current_path, move |cx, item, action| {
247+ jump_to_location(
248+ cx.editor,
249+ &item.symbol.location,
250+ item.offset_encoding,
251+ action,
252+ );
275253 })
276254 .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
277255 .truncate_start(false)
@@ -286,7 +264,7 @@ enum DiagnosticsFormat {
286264fn diag_picker(
287265 cx: &Context,
288266 diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
289− current_path: Option<lsp::Url>,
267+ _current_path: Option<lsp::Url>,
290268 format: DiagnosticsFormat,
291269) -> Picker<PickerDiagnostic> {
292270 // TODO: drop current_path comparison and instead use workspace: bool flag?
@@ -324,22 +302,12 @@ fn diag_picker(
324302 offset_encoding,
325303 },
326304 action| {
327− if current_path.as_ref() == Some(url) {
328− let (view, doc) = current!(cx.editor);
329− push_jump(view, doc);
330− } else {
331− let path = url.to_file_path().unwrap();
332− cx.editor.open(&path, action).expect("editor.open failed");
333− }
334−
335− let (view, doc) = current!(cx.editor);
336−
337− if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
338− // we flip the range so that the cursor sits on the start of the symbol
339− // (for example start of the function).
340− doc.set_selection(view.id, Selection::single(range.head, range.anchor));
341− align_view(doc, view, Align::Center);
342− }
305+ jump_to_location(
306+ cx.editor,
307+ &lsp::Location::new(url.clone(), diag.range),
308+ *offset_encoding,
309+ action,
310+ )
343311 },
344312 )
345313 .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
@@ -730,7 +698,8 @@ pub fn code_action(cx: &mut Context) {
730698
731699 // always present here
732700 let action = action.unwrap();
733− let Some(language_server) = editor.language_server_by_id(action.language_server_id) else {
701+ let Some(language_server) = editor.language_server_by_id(action.language_server_id)
702+ else {
734703 editor.set_error("Language Server disappeared");
735704 return;
736705 };
@@ -743,7 +712,25 @@ pub fn code_action(cx: &mut Context) {
743712 }
744713 lsp::CodeActionOrCommand::CodeAction(code_action) => {
745714 log::debug!("code action: {:?}", code_action);
746− if let Some(ref workspace_edit) = code_action.edit {
715+ // we support lsp "codeAction/resolve" for `edit` and `command` fields
716+ let mut resolved_code_action = None;
717+ if code_action.edit.is_none() || code_action.command.is_none() {
718+ if let Some(future) =
719+ language_server.resolve_code_action(code_action.clone())
720+ {
721+ if let Ok(response) = helix_lsp::block_on(future) {
722+ if let Ok(code_action) =
723+ serde_json::from_value::<CodeAction>(response)
724+ {
725+ resolved_code_action = Some(code_action);
726+ }
727+ }
728+ }
729+ }
730+ let resolved_code_action =
731+ resolved_code_action.as_ref().unwrap_or(code_action);
732+
733+ if let Some(ref workspace_edit) = resolved_code_action.edit {
747734 log::debug!("edit: {:?}", workspace_edit);
748735 let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
749736 }
@@ -758,7 +745,16 @@ pub fn code_action(cx: &mut Context) {
758745 });
759746 picker.move_down(); // pre-select the first item
760747
761− let popup = Popup::new("code-action", picker).with_scrollbar(false);
748+ let margin = if editor.menu_border() {
749+ Margin::vertical(1)
750+ } else {
751+ Margin::none()
752+ };
753+
754+ let popup = Popup::new("code-action", picker)
755+ .with_scrollbar(false)
756+ .margin(margin);
757+
762758 compositor.replace_or_push("code-action", popup);
763759 };
764760
@@ -1173,7 +1169,8 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11731169 // Do not show the message if signature help was invoked
11741170 // automatically on backspace, trigger characters, etc.
11751171 if invoked == SignatureHelpInvoked::Manual {
1176− cx.editor.set_error("No configured language server supports signature-help");
1172+ cx.editor
1173+ .set_error("No configured language server supports signature-help");
11771174 }
11781175 return;
11791176 };
@@ -1398,7 +1395,8 @@ pub fn rename_symbol(cx: &mut Context) {
13981395 .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
13991396 .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
14001397 else {
1401− cx.editor.set_error("No configured language server supports symbol renaming");
1398+ cx.editor
1399+ .set_error("No configured language server supports symbol renaming");
14021400 return;
14031401 };
14041402
helix-term/src/commands/typed.rs+905−698
@@ -5,7 +5,10 @@ use crate::job::Job;
55
66use super::*;
77
8−use helix_core::{encoding, shellwords::Shellwords};
8+use helix_core::fuzzy::fuzzy_match;
9+use helix_core::indent::MAX_INDENT;
10+use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
11+use helix_lsp::{OffsetEncoding, Url};
912use helix_view::document::DEFAULT_LANGUAGE_NAME;
1013use helix_view::editor::{Action, CloseError, ConfigEvent};
1114use serde_json::Value;
@@ -329,12 +332,16 @@ fn write_impl(
329332 path: Option<&Cow<str>>,
330333 force: bool,
331334) -> anyhow::Result<()> {
332− let editor_auto_fmt = cx.editor.config().auto_format;
335+ let config = cx.editor.config();
333336 let jobs = &mut cx.jobs;
334337 let (view, doc) = current!(cx.editor);
335338 let path = path.map(AsRef::as_ref);
336339
337− let fmt = if editor_auto_fmt {
340+ if config.insert_final_newline {
341+ insert_final_newline(doc, view);
342+ }
343+
344+ let fmt = if config.auto_format {
338345 doc.auto_format().map(|fmt| {
339346 let callback = make_format_callback(
340347 doc.id(),
@@ -358,6 +365,16 @@ fn write_impl(
358365 Ok(())
359366}
360367
368+fn insert_final_newline(doc: &mut Document, view: &mut View) {
369+ let text = doc.text();
370+ if line_ending::get_line_ending(&text.slice(..)).is_none() {
371+ let eof = Selection::point(text.len_chars());
372+ let insert = Transaction::insert(text, &eof, doc.line_ending.as_str().into());
373+ doc.apply(&insert, view.id);
374+ doc.append_changes_to_history(view);
375+ }
376+}
377+
361378fn write(
362379 cx: &mut compositor::Context,
363380 args: &[Cow<str>],
@@ -460,8 +477,7 @@ fn set_indent_style(
460477 cx.editor.set_status(match style {
461478 Tabs => "tabs".to_owned(),
462479 Spaces(1) => "1 space".to_owned(),
463− Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
464− _ => unreachable!(), // Shouldn't happen.
480+ Spaces(n) => format!("{} spaces", n),
465481 });
466482 return Ok(());
467483 }
@@ -473,7 +489,7 @@ fn set_indent_style(
473489 Some(arg) => arg
474490 .parse::<u8>()
475491 .ok()
476− .filter(|n| (1..=8).contains(n))
492+ .filter(|n| (1..=MAX_INDENT).contains(n))
477493 .map(Spaces),
478494 _ => None,
479495 };
@@ -558,7 +574,6 @@ fn set_line_ending(
558574
559575 Ok(())
560576}
561−
562577fn earlier(
563578 cx: &mut compositor::Context,
564579 args: &[Cow<str>],
@@ -657,11 +672,10 @@ pub fn write_all_impl(
657672 write_scratch: bool,
658673) -> anyhow::Result<()> {
659674 let mut errors: Vec<&'static str> = Vec::new();
660− let auto_format = cx.editor.config().auto_format;
675+ let config = cx.editor.config();
661676 let jobs = &mut cx.jobs;
662677 let current_view = view!(cx.editor);
663678
664− // save all documents
665679 let saves: Vec<_> = cx
666680 .editor
667681 .documents
@@ -672,7 +686,7 @@ pub fn write_all_impl(
672686 }
673687 if doc.path().is_none() {
674688 if write_scratch {
675− errors.push("cannot write a buffer without a filename\n");
689+ errors.push("cannot write a buffer without a filename");
676690 }
677691 return None;
678692 }
@@ -692,32 +706,35 @@ pub fn write_all_impl(
692706 current_view.id
693707 };
694708
695− let fmt = if auto_format {
696− doc.auto_format().map(|fmt| {
697− let callback = make_format_callback(
698− doc.id(),
699− doc.version(),
700− target_view,
701− fmt,
702− Some((None, force)),
703− );
704− jobs.add(Job::with_callback(callback).wait_before_exiting());
705− })
706− } else {
707− None
708− };
709+ Some((doc.id(), target_view))
710+ })
711+ .collect();
709712
710− if fmt.is_none() {
711− return Some(doc.id());
712− }
713+ for (doc_id, target_view) in saves {
714+ let doc = doc_mut!(cx.editor, &doc_id);
715+
716+ if config.insert_final_newline {
717+ insert_final_newline(doc, view_mut!(cx.editor, target_view));
718+ }
713719
720+ let fmt = if config.auto_format {
721+ doc.auto_format().map(|fmt| {
722+ let callback = make_format_callback(
723+ doc_id,
724+ doc.version(),
725+ target_view,
726+ fmt,
727+ Some((None, force)),
728+ );
729+ jobs.add(Job::with_callback(callback).wait_before_exiting());
730+ })
731+ } else {
714732 None
715− })
716− .collect();
733+ };
717734
718− // manually call save for the rest of docs that don't have a formatter
719− for id in saves {
720− cx.editor.save::<PathBuf>(id, None, force)?;
735+ if fmt.is_none() {
736+ cx.editor.save::<PathBuf>(doc_id, None, force)?;
737+ }
721738 }
722739
723740 if !errors.is_empty() && !force {
@@ -904,7 +921,8 @@ fn yank_main_selection_to_clipboard(
904921 return Ok(());
905922 }
906923
907− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
924+ yank_primary_selection_impl(cx.editor, '+');
925+ Ok(())
908926}
909927
910928fn yank_joined(
@@ -938,7 +956,8 @@ fn yank_joined_to_clipboard(
938956 let doc = doc!(cx.editor);
939957 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
940958 let separator = args.first().unwrap_or(&default_sep);
941− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
959+ yank_joined_impl(cx.editor, separator, '+');
960+ Ok(())
942961}
943962
944963fn yank_main_selection_to_primary_clipboard(
@@ -950,7 +969,8 @@ fn yank_main_selection_to_primary_clipboard(
950969 return Ok(());
951970 }
952971
953− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
972+ yank_primary_selection_impl(cx.editor, '*');
973+ Ok(())
954974}
955975
956976fn yank_joined_to_primary_clipboard(
@@ -965,7 +985,8 @@ fn yank_joined_to_primary_clipboard(
965985 let doc = doc!(cx.editor);
966986 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
967987 let separator = args.first().unwrap_or(&default_sep);
968− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
988+ yank_joined_impl(cx.editor, separator, '*');
989+ Ok(())
969990}
970991
971992fn paste_clipboard_after(
@@ -977,7 +998,8 @@ fn paste_clipboard_after(
977998 return Ok(());
978999 }
9791000
980− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
1001+ paste(cx.editor, '+', Paste::After, 1);
1002+ Ok(())
9811003}
9821004
9831005fn paste_clipboard_before(
@@ -989,7 +1011,8 @@ fn paste_clipboard_before(
9891011 return Ok(());
9901012 }
9911013
992− paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard, 1)
1014+ paste(cx.editor, '+', Paste::Before, 1);
1015+ Ok(())
9931016}
9941017
9951018fn paste_primary_clipboard_after(
@@ -1001,7 +1024,8 @@ fn paste_primary_clipboard_after(
10011024 return Ok(());
10021025 }
10031026
1004− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
1027+ paste(cx.editor, '*', Paste::After, 1);
1028+ Ok(())
10051029}
10061030
10071031fn paste_primary_clipboard_before(
@@ -1013,30 +1037,8 @@ fn paste_primary_clipboard_before(
10131037 return Ok(());
10141038 }
10151039
1016− paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection, 1)
1017−}
1018−
1019−fn replace_selections_with_clipboard_impl(
1020− cx: &mut compositor::Context,
1021− clipboard_type: ClipboardType,
1022−) -> anyhow::Result<()> {
1023− let scrolloff = cx.editor.config().scrolloff;
1024− let (view, doc) = current!(cx.editor);
1025−
1026− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
1027− Ok(contents) => {
1028− let selection = doc.selection(view.id);
1029− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1030− (range.from(), range.to(), Some(contents.as_str().into()))
1031− });
1032−
1033− doc.apply(&transaction, view.id);
1034− doc.append_changes_to_history(view);
1035− view.ensure_cursor_in_view(doc, scrolloff);
1036− Ok(())
1037− }
1038− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
1039− }
1040+ paste(cx.editor, '*', Paste::Before, 1);
1041+ Ok(())
10401042}
10411043
10421044fn replace_selections_with_clipboard(
@@ -1048,7 +1050,8 @@ fn replace_selections_with_clipboard(
10481050 return Ok(());
10491051 }
10501052
1051− replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
1053+ replace_with_yanked_impl(cx.editor, '+', 1);
1054+ Ok(())
10521055}
10531056
10541057fn replace_selections_with_primary_clipboard(
@@ -1060,7 +1063,8 @@ fn replace_selections_with_primary_clipboard(
10601063 return Ok(());
10611064 }
10621065
1063− replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
1066+ replace_with_yanked_impl(cx.editor, '*', 1);
1067+ Ok(())
10641068}
10651069
10661070fn show_clipboard_provider(
@@ -1073,7 +1077,7 @@ fn show_clipboard_provider(
10731077 }
10741078
10751079 cx.editor
1076− .set_status(cx.editor.clipboard_provider.name().to_string());
1080+ .set_status(cx.editor.registers.clipboard_provider_name().to_string());
10771081 Ok(())
10781082}
10791083
@@ -1278,12 +1282,17 @@ fn reload(
12781282 }
12791283
12801284 let scrolloff = cx.editor.config().scrolloff;
1281− let redraw_handle = cx.editor.redraw_handle.clone();
12821285 let (view, doc) = current!(cx.editor);
1283− doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1284− .map(|_| {
1285− view.ensure_cursor_in_view(doc, scrolloff);
1286− })
1286+ doc.reload(view, &cx.editor.diff_providers).map(|_| {
1287+ view.ensure_cursor_in_view(doc, scrolloff);
1288+ })?;
1289+ if let Some(path) = doc.path() {
1290+ cx.editor
1291+ .language_servers
1292+ .file_event_handler
1293+ .file_changed(path.clone());
1294+ }
1295+ Ok(())
12871296}
12881297
12891298fn reload_all(
@@ -1322,8 +1331,13 @@ fn reload_all(
13221331 // Ensure that the view is synced with the document's history.
13231332 view.sync_changes(doc);
13241333
1325− let redraw_handle = cx.editor.redraw_handle.clone();
1326− doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1334+ doc.reload(view, &cx.editor.diff_providers)?;
1335+ if let Some(path) = doc.path() {
1336+ cx.editor
1337+ .language_servers
1338+ .file_event_handler
1339+ .file_changed(path.clone());
1340+ }
13271341
13281342 for view_id in view_ids {
13291343 let view = view_mut!(cx.editor, view_id);
@@ -1372,9 +1386,8 @@ fn lsp_workspace_command(
13721386 .map(|options| (ls.id(), options))
13731387 })
13741388 else {
1375− cx.editor.set_status(
1376− "No active language servers for this document support workspace commands",
1377− );
1389+ cx.editor
1390+ .set_status("No active language servers for this document support workspace commands");
13781391 return Ok(());
13791392 };
13801393
@@ -1530,6 +1543,84 @@ fn tree_sitter_scopes(
15301543 Ok(())
15311544}
15321545
1546+fn tree_sitter_highlight_name(
1547+ cx: &mut compositor::Context,
1548+ _args: &[Cow<str>],
1549+ event: PromptEvent,
1550+) -> anyhow::Result<()> {
1551+ fn find_highlight_at_cursor(
1552+ cx: &mut compositor::Context<'_>,
1553+ ) -> Option<helix_core::syntax::Highlight> {
1554+ use helix_core::syntax::HighlightEvent;
1555+
1556+ let (view, doc) = current!(cx.editor);
1557+ let syntax = doc.syntax()?;
1558+ let text = doc.text().slice(..);
1559+ let cursor = doc.selection(view.id).primary().cursor(text);
1560+ let byte = text.char_to_byte(cursor);
1561+ let node = syntax
1562+ .tree()
1563+ .root_node()
1564+ .descendant_for_byte_range(byte, byte)?;
1565+ // Query the same range as the one used in syntax highlighting.
1566+ let range = {
1567+ // Calculate viewport byte ranges:
1568+ let row = text.char_to_line(view.offset.anchor.min(text.len_chars()));
1569+ // Saturating subs to make it inclusive zero indexing.
1570+ let last_line = text.len_lines().saturating_sub(1);
1571+ let height = view.inner_area(doc).height;
1572+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
1573+ let start = text.line_to_byte(row.min(last_line));
1574+ let end = text.line_to_byte(last_visible_line + 1);
1575+
1576+ start..end
1577+ };
1578+
1579+ let mut highlight = None;
1580+
1581+ for event in syntax.highlight_iter(text, Some(range), None) {
1582+ match event.unwrap() {
1583+ HighlightEvent::Source { start, end }
1584+ if start == node.start_byte() && end == node.end_byte() =>
1585+ {
1586+ return highlight;
1587+ }
1588+ HighlightEvent::HighlightStart(hl) => {
1589+ highlight = Some(hl);
1590+ }
1591+ _ => (),
1592+ }
1593+ }
1594+
1595+ None
1596+ }
1597+
1598+ if event != PromptEvent::Validate {
1599+ return Ok(());
1600+ }
1601+
1602+ let Some(highlight) = find_highlight_at_cursor(cx) else {
1603+ return Ok(());
1604+ };
1605+
1606+ let content = cx.editor.theme.scope(highlight.0).to_string();
1607+
1608+ let callback = async move {
1609+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1610+ move |editor: &mut Editor, compositor: &mut Compositor| {
1611+ let content = ui::Markdown::new(content, editor.syn_loader.clone());
1612+ let popup = Popup::new("hover", content).auto_close(true);
1613+ compositor.replace_or_push("hover", popup);
1614+ },
1615+ ));
1616+ Ok(call)
1617+ };
1618+
1619+ cx.jobs.callback(callback);
1620+
1621+ Ok(())
1622+}
1623+
15331624fn vsplit(
15341625 cx: &mut compositor::Context,
15351626 args: &[Cow<str>],
@@ -1539,10 +1630,8 @@ fn vsplit(
15391630 return Ok(());
15401631 }
15411632
1542− let id = view!(cx.editor).doc;
1543−
15441633 if args.is_empty() {
1545− cx.editor.switch(id, Action::VerticalSplit);
1634+ split(cx.editor, Action::VerticalSplit);
15461635 } else {
15471636 for arg in args {
15481637 cx.editor
@@ -1562,10 +1651,8 @@ fn hsplit(
15621651 return Ok(());
15631652 }
15641653
1565− let id = view!(cx.editor).doc;
1566−
15671654 if args.is_empty() {
1568− cx.editor.switch(id, Action::HorizontalSplit);
1655+ split(cx.editor, Action::HorizontalSplit);
15691656 } else {
15701657 for arg in args {
15711658 cx.editor
@@ -1680,7 +1767,7 @@ fn tutor(
16801767 let path = helix_loader::runtime_file(Path::new("tutor"));
16811768 cx.editor.open(&path, Action::Replace)?;
16821769 // Unset path to prevent accidentally saving to the original tutor file.
1683− doc_mut!(cx.editor).set_path(None)?;
1770+ doc_mut!(cx.editor).set_path(None);
16841771 Ok(())
16851772}
16861773
@@ -1857,14 +1944,29 @@ fn toggle_option(
18571944 .to_string(),
18581945 )
18591946 }
1860− Value::Null | Value::Object(_) | Value::Array(_) | Value::Number(_) => {
1947+ Value::Number(ref value) => {
1948+ ensure!(
1949+ args.len() > 2,
1950+ "Bad arguments. For number configurations use: `:toggle key val1 val2 ...`",
1951+ );
1952+
1953+ Value::Number(
1954+ args[1..]
1955+ .iter()
1956+ .skip_while(|&e| value.to_string() != *e.to_string())
1957+ .nth(1)
1958+ .unwrap_or_else(|| &args[1])
1959+ .parse()?,
1960+ )
1961+ }
1962+ Value::Null | Value::Object(_) | Value::Array(_) => {
18611963 anyhow::bail!("Configuration {key} does not support toggle yet")
18621964 }
18631965 };
18641966
18651967 let status = format!("'{key}' is now set to {value}");
18661968 let config = serde_json::from_value(config)
1867− .map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?;
1969+ .map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?;
18681970
18691971 cx.editor
18701972 .config_events
@@ -2273,606 +2375,721 @@ fn clear_register(
22732375 format!("Invalid register {}", args[0])
22742376 );
22752377 let register = args[0].chars().next().unwrap_or_default();
2276− match cx.editor.registers.remove(register) {
2277− Some(_) => cx
2278− .editor
2279− .set_status(format!("Register {} cleared", register)),
2280− None => cx
2281− .editor
2282− .set_error(format!("Register {} not found", register)),
2378+ if cx.editor.registers.remove(register) {
2379+ cx.editor
2380+ .set_status(format!("Register {} cleared", register));
2381+ } else {
2382+ cx.editor
2383+ .set_error(format!("Register {} not found", register));
2384+ }
2385+ Ok(())
2386+}
2387+
2388+fn redraw(
2389+ cx: &mut compositor::Context,
2390+ _args: &[Cow<str>],
2391+ event: PromptEvent,
2392+) -> anyhow::Result<()> {
2393+ if event != PromptEvent::Validate {
2394+ return Ok(());
22832395 }
2396+
2397+ let callback = Box::pin(async move {
2398+ let call: job::Callback =
2399+ job::Callback::EditorCompositor(Box::new(|_editor, compositor| {
2400+ compositor.need_full_redraw();
2401+ }));
2402+
2403+ Ok(call)
2404+ });
2405+
2406+ cx.jobs.callback(callback);
2407+
2408+ Ok(())
2409+}
2410+
2411+fn move_buffer(
2412+ cx: &mut compositor::Context,
2413+ args: &[Cow<str>],
2414+ event: PromptEvent,
2415+) -> anyhow::Result<()> {
2416+ if event != PromptEvent::Validate {
2417+ return Ok(());
2418+ }
2419+
2420+ ensure!(args.len() == 1, format!(":move takes one argument"));
2421+ let doc = doc!(cx.editor);
2422+
2423+ let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
2424+ let old_path = doc
2425+ .path()
2426+ .ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
2427+ .clone();
2428+ let old_path_as_url = doc.url().unwrap();
2429+ let new_path_as_url = Url::from_file_path(&new_path).unwrap();
2430+
2431+ let edits: Vec<(
2432+ helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
2433+ OffsetEncoding,
2434+ String,
2435+ )> = doc
2436+ .language_servers()
2437+ .map(|lsp| {
2438+ (
2439+ lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
2440+ lsp.offset_encoding(),
2441+ lsp.name().to_owned(),
2442+ )
2443+ })
2444+ .filter(|(f, _, _)| f.is_some())
2445+ .map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
2446+ .collect();
2447+
2448+ for (lsp_reply, encoding, name) in edits {
2449+ match lsp_reply {
2450+ Ok(edit) => {
2451+ if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
2452+ log::error!(
2453+ ":move command failed to apply edits from lsp {}: {:?}",
2454+ name,
2455+ e
2456+ );
2457+ };
2458+ }
2459+ Err(e) => {
2460+ log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
2461+ }
2462+ };
2463+ }
2464+
2465+ let doc = doc_mut!(cx.editor);
2466+
2467+ doc.set_path(Some(new_path.as_path()));
2468+ if let Err(e) = std::fs::rename(&old_path, &new_path) {
2469+ doc.set_path(Some(old_path.as_path()));
2470+ bail!("Could not move file: {}", e);
2471+ };
2472+
2473+ doc.language_servers().for_each(|lsp| {
2474+ lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
2475+ });
2476+
2477+ cx.editor
2478+ .language_servers
2479+ .file_event_handler
2480+ .file_changed(new_path);
2481+
22842482 Ok(())
22852483}
22862484
22872485pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2288− TypableCommand {
2289− name: "quit",
2290− aliases: &["q"],
2291− doc: "Close the current view.",
2292− fun: quit,
2293− signature: CommandSignature::none(),
2294− },
2295− TypableCommand {
2296− name: "quit!",
2297− aliases: &["q!"],
2298− doc: "Force close the current view, ignoring unsaved changes.",
2299− fun: force_quit,
2300− signature: CommandSignature::none(),
2301− },
2302− TypableCommand {
2303− name: "open",
2304− aliases: &["o"],
2305− doc: "Open a file from disk into the current view.",
2306− fun: open,
2307− signature: CommandSignature::all(completers::filename),
2308− },
2309− TypableCommand {
2310− name: "buffer-close",
2311− aliases: &["bc", "bclose"],
2312− doc: "Close the current buffer.",
2313− fun: buffer_close,
2314− signature: CommandSignature::all(completers::buffer),
2315− },
2316− TypableCommand {
2317− name: "buffer-close!",
2318− aliases: &["bc!", "bclose!"],
2319− doc: "Close the current buffer forcefully, ignoring unsaved changes.",
2320− fun: force_buffer_close,
2321− signature: CommandSignature::all(completers::buffer)
2322− },
2323− TypableCommand {
2324− name: "buffer-close-others",
2325− aliases: &["bco", "bcloseother"],
2326− doc: "Close all buffers but the currently focused one.",
2327− fun: buffer_close_others,
2328− signature: CommandSignature::none(),
2329− },
2330− TypableCommand {
2331− name: "buffer-close-others!",
2332− aliases: &["bco!", "bcloseother!"],
2333− doc: "Force close all buffers but the currently focused one.",
2334− fun: force_buffer_close_others,
2335− signature: CommandSignature::none(),
2336− },
2337− TypableCommand {
2338− name: "buffer-close-all",
2339− aliases: &["bca", "bcloseall"],
2340− doc: "Close all buffers without quitting.",
2341− fun: buffer_close_all,
2342− signature: CommandSignature::none(),
2343− },
2344− TypableCommand {
2345− name: "buffer-close-all!",
2346− aliases: &["bca!", "bcloseall!"],
2347− doc: "Force close all buffers ignoring unsaved changes without quitting.",
2348− fun: force_buffer_close_all,
2349− signature: CommandSignature::none(),
2350− },
2351− TypableCommand {
2352− name: "buffer-next",
2353− aliases: &["bn", "bnext"],
2354− doc: "Goto next buffer.",
2355− fun: buffer_next,
2356− signature: CommandSignature::none(),
2357− },
2358− TypableCommand {
2359− name: "buffer-previous",
2360− aliases: &["bp", "bprev"],
2361− doc: "Goto previous buffer.",
2362− fun: buffer_previous,
2363− signature: CommandSignature::none(),
2364− },
2365− TypableCommand {
2366− name: "write",
2367− aliases: &["w"],
2368− doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2369− fun: write,
2370− signature: CommandSignature::positional(&[completers::filename]),
2371− },
2372− TypableCommand {
2373− name: "write!",
2374− aliases: &["w!"],
2375− doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
2376− fun: force_write,
2377− signature: CommandSignature::positional(&[completers::filename]),
2378− },
2379− TypableCommand {
2380− name: "write-buffer-close",
2381− aliases: &["wbc"],
2382− doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
2383− fun: write_buffer_close,
2384− signature: CommandSignature::positional(&[completers::filename]),
2385− },
2386− TypableCommand {
2387− name: "write-buffer-close!",
2388− aliases: &["wbc!"],
2389− doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
2390− fun: force_write_buffer_close,
2391− signature: CommandSignature::positional(&[completers::filename]),
2392− },
2393− TypableCommand {
2394− name: "new",
2395− aliases: &["n"],
2396− doc: "Create a new scratch buffer.",
2397− fun: new_file,
2398− // TODO: This seems to complete with a filename, but doesn't use that filename to
2399− // set the path of the newly created buffer.
2400− signature: CommandSignature::positional(&[completers::filename]),
2401− },
2402− TypableCommand {
2403− name: "format",
2404− aliases: &["fmt"],
2405− doc: "Format the file using the LSP formatter.",
2406− fun: format,
2407− signature: CommandSignature::none(),
2408− },
2409− TypableCommand {
2410− name: "indent-style",
2411− aliases: &[],
2412− doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
2413− fun: set_indent_style,
2414− signature: CommandSignature::none(),
2415− },
2416− TypableCommand {
2417− name: "line-ending",
2418− aliases: &[],
2419− #[cfg(not(feature = "unicode-lines"))]
2420− doc: "Set the document's default line ending. Options: crlf, lf.",
2421− #[cfg(feature = "unicode-lines")]
2422− doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2423− fun: set_line_ending,
2424− signature: CommandSignature::none(),
2425− },
2426− TypableCommand {
2427− name: "earlier",
2428− aliases: &["ear"],
2429− doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2430− fun: earlier,
2431− signature: CommandSignature::none(),
2432− },
2433− TypableCommand {
2434− name: "later",
2435− aliases: &["lat"],
2436− doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2437− fun: later,
2438− signature: CommandSignature::none(),
2439− },
2440− TypableCommand {
2441− name: "write-quit",
2442− aliases: &["wq", "x"],
2443− doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2444− fun: write_quit,
2445− signature: CommandSignature::positional(&[completers::filename]),
2446− },
2447− TypableCommand {
2448− name: "write-quit!",
2449− aliases: &["wq!", "x!"],
2450− doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2451− fun: force_write_quit,
2452− signature: CommandSignature::positional(&[completers::filename]),
2453− },
2454− TypableCommand {
2455− name: "write-all",
2456− aliases: &["wa"],
2457− doc: "Write changes from all buffers to disk.",
2458− fun: write_all,
2459− signature: CommandSignature::none(),
2460− },
2461− TypableCommand {
2462− name: "write-all!",
2463− aliases: &["wa!"],
2464− doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
2465− fun: force_write_all,
2466− signature: CommandSignature::none(),
2467− },
2468− TypableCommand {
2469− name: "write-quit-all",
2470− aliases: &["wqa", "xa"],
2471− doc: "Write changes from all buffers to disk and close all views.",
2472− fun: write_all_quit,
2473− signature: CommandSignature::none(),
2474− },
2475− TypableCommand {
2476− name: "write-quit-all!",
2477− aliases: &["wqa!", "xa!"],
2478− doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
2479− fun: force_write_all_quit,
2480− signature: CommandSignature::none(),
2481− },
2482− TypableCommand {
2483− name: "quit-all",
2484− aliases: &["qa"],
2485− doc: "Close all views.",
2486− fun: quit_all,
2487− signature: CommandSignature::none(),
2488− },
2489− TypableCommand {
2490− name: "quit-all!",
2491− aliases: &["qa!"],
2492− doc: "Force close all views ignoring unsaved changes.",
2493− fun: force_quit_all,
2494− signature: CommandSignature::none(),
2495− },
2496− TypableCommand {
2497− name: "cquit",
2498− aliases: &["cq"],
2499− doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2500− fun: cquit,
2501− signature: CommandSignature::none(),
2502− },
2503− TypableCommand {
2504− name: "cquit!",
2505− aliases: &["cq!"],
2506− doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
2507− fun: force_cquit,
2508− signature: CommandSignature::none(),
2509− },
2510− TypableCommand {
2511− name: "theme",
2512− aliases: &[],
2513− doc: "Change the editor theme (show current theme if no name specified).",
2514− fun: theme,
2515− signature: CommandSignature::positional(&[completers::theme]),
2516− },
2517− TypableCommand {
2518− name: "yank-join",
2519− aliases: &[],
2520− doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
2521− fun: yank_joined,
2522− signature: CommandSignature::none(),
2523− },
2524− TypableCommand {
2525− name: "clipboard-yank",
2526− aliases: &[],
2527− doc: "Yank main selection into system clipboard.",
2528− fun: yank_main_selection_to_clipboard,
2529− signature: CommandSignature::none(),
2530− },
2531− TypableCommand {
2532− name: "clipboard-yank-join",
2533− aliases: &[],
2534− doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2535− fun: yank_joined_to_clipboard,
2536− signature: CommandSignature::none(),
2537− },
2538− TypableCommand {
2539− name: "primary-clipboard-yank",
2540− aliases: &[],
2541− doc: "Yank main selection into system primary clipboard.",
2542− fun: yank_main_selection_to_primary_clipboard,
2543− signature: CommandSignature::none(),
2544− },
2545− TypableCommand {
2546− name: "primary-clipboard-yank-join",
2547− aliases: &[],
2548− doc: "Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2549− fun: yank_joined_to_primary_clipboard,
2550− signature: CommandSignature::none(),
2551− },
2552− TypableCommand {
2553− name: "clipboard-paste-after",
2554− aliases: &[],
2555− doc: "Paste system clipboard after selections.",
2556− fun: paste_clipboard_after,
2557− signature: CommandSignature::none(),
2558− },
2559− TypableCommand {
2560− name: "clipboard-paste-before",
2561− aliases: &[],
2562− doc: "Paste system clipboard before selections.",
2563− fun: paste_clipboard_before,
2564− signature: CommandSignature::none(),
2565− },
2566− TypableCommand {
2567− name: "clipboard-paste-replace",
2568− aliases: &[],
2569− doc: "Replace selections with content of system clipboard.",
2570− fun: replace_selections_with_clipboard,
2571− signature: CommandSignature::none(),
2572− },
2573− TypableCommand {
2574− name: "primary-clipboard-paste-after",
2575− aliases: &[],
2576− doc: "Paste primary clipboard after selections.",
2577− fun: paste_primary_clipboard_after,
2578− signature: CommandSignature::none(),
2579− },
2580− TypableCommand {
2581− name: "primary-clipboard-paste-before",
2582− aliases: &[],
2583− doc: "Paste primary clipboard before selections.",
2584− fun: paste_primary_clipboard_before,
2585− signature: CommandSignature::none(),
2586− },
2587− TypableCommand {
2588− name: "primary-clipboard-paste-replace",
2589− aliases: &[],
2590− doc: "Replace selections with content of system primary clipboard.",
2591− fun: replace_selections_with_primary_clipboard,
2592− signature: CommandSignature::none(),
2593− },
2594− TypableCommand {
2595− name: "show-clipboard-provider",
2596− aliases: &[],
2597− doc: "Show clipboard provider name in status bar.",
2598− fun: show_clipboard_provider,
2599− signature: CommandSignature::none(),
2600− },
2601− TypableCommand {
2602− name: "change-current-directory",
2603− aliases: &["cd"],
2604− doc: "Change the current working directory.",
2605− fun: change_current_directory,
2606− signature: CommandSignature::positional(&[completers::directory]),
2607− },
2608− TypableCommand {
2609− name: "show-directory",
2610− aliases: &["pwd"],
2611− doc: "Show the current working directory.",
2612− fun: show_current_directory,
2613− signature: CommandSignature::none(),
2614− },
2615− TypableCommand {
2616− name: "encoding",
2617− aliases: &[],
2618− doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
2619− fun: set_encoding,
2620− signature: CommandSignature::none(),
2621− },
2622− TypableCommand {
2623− name: "character-info",
2624− aliases: &["char"],
2625− doc: "Get info about the character under the primary cursor.",
2626− fun: get_character_info,
2627− signature: CommandSignature::none(),
2628− },
2629− TypableCommand {
2630− name: "reload",
2631− aliases: &["rl"],
2632− doc: "Discard changes and reload from the source file.",
2633− fun: reload,
2634− signature: CommandSignature::none(),
2635− },
2636− TypableCommand {
2637− name: "reload-all",
2638− aliases: &["rla"],
2639− doc: "Discard changes and reload all documents from the source files.",
2640− fun: reload_all,
2641− signature: CommandSignature::none(),
2642− },
2643− TypableCommand {
2644− name: "update",
2645− aliases: &["u"],
2646− doc: "Write changes only if the file has been modified.",
2647− fun: update,
2648− signature: CommandSignature::none(),
2649− },
2650− TypableCommand {
2651− name: "lsp-workspace-command",
2652− aliases: &[],
2653− doc: "Open workspace command picker",
2654− fun: lsp_workspace_command,
2655− signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
2656− },
2657− TypableCommand {
2658− name: "lsp-restart",
2659− aliases: &[],
2660− doc: "Restarts the language servers used by the current doc",
2661− fun: lsp_restart,
2662− signature: CommandSignature::none(),
2663− },
2664− TypableCommand {
2665− name: "lsp-stop",
2666− aliases: &[],
2667− doc: "Stops the language servers that are used by the current doc",
2668− fun: lsp_stop,
2669− signature: CommandSignature::none(),
2670− },
2671− TypableCommand {
2672− name: "tree-sitter-scopes",
2673− aliases: &[],
2674− doc: "Display tree sitter scopes, primarily for theming and development.",
2675− fun: tree_sitter_scopes,
2676− signature: CommandSignature::none(),
2677− },
2678− TypableCommand {
2679− name: "debug-start",
2680− aliases: &["dbg"],
2681− doc: "Start a debug session from a given template with given parameters.",
2682− fun: debug_start,
2683− signature: CommandSignature::none(),
2684− },
2685− TypableCommand {
2686− name: "debug-remote",
2687− aliases: &["dbg-tcp"],
2688− doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
2689− fun: debug_remote,
2690− signature: CommandSignature::none(),
2691− },
2692− TypableCommand {
2693− name: "debug-eval",
2694− aliases: &[],
2695− doc: "Evaluate expression in current debug context.",
2696− fun: debug_eval,
2697− signature: CommandSignature::none(),
2698− },
2699− TypableCommand {
2700− name: "vsplit",
2701− aliases: &["vs"],
2702− doc: "Open the file in a vertical split.",
2703− fun: vsplit,
2704− signature: CommandSignature::all(completers::filename)
2705− },
2706− TypableCommand {
2707− name: "vsplit-new",
2708− aliases: &["vnew"],
2709− doc: "Open a scratch buffer in a vertical split.",
2710− fun: vsplit_new,
2711− signature: CommandSignature::none(),
2712− },
2713− TypableCommand {
2714− name: "hsplit",
2715− aliases: &["hs", "sp"],
2716− doc: "Open the file in a horizontal split.",
2717− fun: hsplit,
2718− signature: CommandSignature::all(completers::filename)
2719− },
2720− TypableCommand {
2721− name: "hsplit-new",
2722− aliases: &["hnew"],
2723− doc: "Open a scratch buffer in a horizontal split.",
2724− fun: hsplit_new,
2725− signature: CommandSignature::none(),
2726− },
2727− TypableCommand {
2728− name: "tutor",
2729− aliases: &[],
2730− doc: "Open the tutorial.",
2731− fun: tutor,
2732− signature: CommandSignature::none(),
2733− },
2734− TypableCommand {
2735− name: "goto",
2736− aliases: &["g"],
2737− doc: "Goto line number.",
2738− fun: goto_line_number,
2739− signature: CommandSignature::none(),
2740− },
2741− TypableCommand {
2742− name: "set-language",
2743− aliases: &["lang"],
2744− doc: "Set the language of current buffer (show current language if no value specified).",
2745− fun: language,
2746− signature: CommandSignature::positional(&[completers::language]),
2747− },
2748− TypableCommand {
2749− name: "set-option",
2750− aliases: &["set"],
2751− doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
2752− fun: set_option,
2753− // TODO: Add support for completion of the options value(s), when appropriate.
2754− signature: CommandSignature::positional(&[completers::setting]),
2755− },
2756− TypableCommand {
2757− name: "toggle-option",
2758− aliases: &["toggle"],
2759− doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
2760− fun: toggle_option,
2761− signature: CommandSignature::positional(&[completers::setting]),
2762− },
2763− TypableCommand {
2764− name: "get-option",
2765− aliases: &["get"],
2766− doc: "Get the current value of a config option.",
2767− fun: get_option,
2768− signature: CommandSignature::positional(&[completers::setting]),
2769− },
2770− TypableCommand {
2771− name: "sort",
2772− aliases: &[],
2773− doc: "Sort ranges in selection.",
2774− fun: sort,
2775− signature: CommandSignature::none(),
2776− },
2777− TypableCommand {
2778− name: "rsort",
2779− aliases: &[],
2780− doc: "Sort ranges in selection in reverse order.",
2781− fun: sort_reverse,
2782− signature: CommandSignature::none(),
2783− },
2784− TypableCommand {
2785− name: "reflow",
2786− aliases: &[],
2787− doc: "Hard-wrap the current selection of lines to a given width.",
2788− fun: reflow,
2789− signature: CommandSignature::none(),
2790− },
2791− TypableCommand {
2792− name: "tree-sitter-subtree",
2793− aliases: &["ts-subtree"],
2794− doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
2795− fun: tree_sitter_subtree,
2796− signature: CommandSignature::none(),
2797− },
2798− TypableCommand {
2799− name: "config-reload",
2800− aliases: &[],
2801− doc: "Refresh user config.",
2802− fun: refresh_config,
2803− signature: CommandSignature::none(),
2804− },
2805− TypableCommand {
2806− name: "config-open",
2807− aliases: &[],
2808− doc: "Open the user config.toml file.",
2809− fun: open_config,
2810− signature: CommandSignature::none(),
2811− },
2812− TypableCommand {
2813− name: "config-open-workspace",
2814− aliases: &[],
2815− doc: "Open the workspace config.toml file.",
2816− fun: open_workspace_config,
2817− signature: CommandSignature::none(),
2818− },
2819− TypableCommand {
2820− name: "log-open",
2821− aliases: &[],
2822− doc: "Open the helix log file.",
2823− fun: open_log,
2824− signature: CommandSignature::none(),
2825− },
2826− TypableCommand {
2827− name: "insert-output",
2828− aliases: &[],
2829− doc: "Run shell command, inserting output before each selection.",
2830− fun: insert_output,
2831− signature: CommandSignature::none(),
2832− },
2833− TypableCommand {
2834− name: "append-output",
2835− aliases: &[],
2836− doc: "Run shell command, appending output after each selection.",
2837− fun: append_output,
2838− signature: CommandSignature::none(),
2839− },
2840− TypableCommand {
2841− name: "pipe",
2842− aliases: &[],
2843− doc: "Pipe each selection to the shell command.",
2844− fun: pipe,
2845− signature: CommandSignature::none(),
2846− },
2847− TypableCommand {
2848− name: "pipe-to",
2849− aliases: &[],
2850− doc: "Pipe each selection to the shell command, ignoring output.",
2851− fun: pipe_to,
2852− signature: CommandSignature::none(),
2853− },
2854− TypableCommand {
2855− name: "run-shell-command",
2856− aliases: &["sh"],
2857− doc: "Run a shell command",
2858− fun: run_shell_command,
2859− signature: CommandSignature::all(completers::filename)
2860− },
2861− TypableCommand {
2862− name: "reset-diff-change",
2863− aliases: &["diffget", "diffg"],
2864− doc: "Reset the diff change at the cursor position.",
2865− fun: reset_diff_change,
2866− signature: CommandSignature::none(),
2867− },
2868− TypableCommand {
2869− name: "clear-register",
2870− aliases: &[],
2871− doc: "Clear given register. If no argument is provided, clear all registers.",
2872− fun: clear_register,
2873− signature: CommandSignature::none(),
2874− },
2875− ];
2486+ TypableCommand {
2487+ name: "quit",
2488+ aliases: &["q"],
2489+ doc: "Close the current view.",
2490+ fun: quit,
2491+ signature: CommandSignature::none(),
2492+ },
2493+ TypableCommand {
2494+ name: "quit!",
2495+ aliases: &["q!"],
2496+ doc: "Force close the current view, ignoring unsaved changes.",
2497+ fun: force_quit,
2498+ signature: CommandSignature::none(),
2499+ },
2500+ TypableCommand {
2501+ name: "open",
2502+ aliases: &["o"],
2503+ doc: "Open a file from disk into the current view.",
2504+ fun: open,
2505+ signature: CommandSignature::all(completers::filename),
2506+ },
2507+ TypableCommand {
2508+ name: "buffer-close",
2509+ aliases: &["bc", "bclose"],
2510+ doc: "Close the current buffer.",
2511+ fun: buffer_close,
2512+ signature: CommandSignature::all(completers::buffer),
2513+ },
2514+ TypableCommand {
2515+ name: "buffer-close!",
2516+ aliases: &["bc!", "bclose!"],
2517+ doc: "Close the current buffer forcefully, ignoring unsaved changes.",
2518+ fun: force_buffer_close,
2519+ signature: CommandSignature::all(completers::buffer)
2520+ },
2521+ TypableCommand {
2522+ name: "buffer-close-others",
2523+ aliases: &["bco", "bcloseother"],
2524+ doc: "Close all buffers but the currently focused one.",
2525+ fun: buffer_close_others,
2526+ signature: CommandSignature::none(),
2527+ },
2528+ TypableCommand {
2529+ name: "buffer-close-others!",
2530+ aliases: &["bco!", "bcloseother!"],
2531+ doc: "Force close all buffers but the currently focused one.",
2532+ fun: force_buffer_close_others,
2533+ signature: CommandSignature::none(),
2534+ },
2535+ TypableCommand {
2536+ name: "buffer-close-all",
2537+ aliases: &["bca", "bcloseall"],
2538+ doc: "Close all buffers without quitting.",
2539+ fun: buffer_close_all,
2540+ signature: CommandSignature::none(),
2541+ },
2542+ TypableCommand {
2543+ name: "buffer-close-all!",
2544+ aliases: &["bca!", "bcloseall!"],
2545+ doc: "Force close all buffers ignoring unsaved changes without quitting.",
2546+ fun: force_buffer_close_all,
2547+ signature: CommandSignature::none(),
2548+ },
2549+ TypableCommand {
2550+ name: "buffer-next",
2551+ aliases: &["bn", "bnext"],
2552+ doc: "Goto next buffer.",
2553+ fun: buffer_next,
2554+ signature: CommandSignature::none(),
2555+ },
2556+ TypableCommand {
2557+ name: "buffer-previous",
2558+ aliases: &["bp", "bprev"],
2559+ doc: "Goto previous buffer.",
2560+ fun: buffer_previous,
2561+ signature: CommandSignature::none(),
2562+ },
2563+ TypableCommand {
2564+ name: "write",
2565+ aliases: &["w"],
2566+ doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2567+ fun: write,
2568+ signature: CommandSignature::positional(&[completers::filename]),
2569+ },
2570+ TypableCommand {
2571+ name: "write!",
2572+ aliases: &["w!"],
2573+ doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
2574+ fun: force_write,
2575+ signature: CommandSignature::positional(&[completers::filename]),
2576+ },
2577+ TypableCommand {
2578+ name: "write-buffer-close",
2579+ aliases: &["wbc"],
2580+ doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
2581+ fun: write_buffer_close,
2582+ signature: CommandSignature::positional(&[completers::filename]),
2583+ },
2584+ TypableCommand {
2585+ name: "write-buffer-close!",
2586+ aliases: &["wbc!"],
2587+ doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
2588+ fun: force_write_buffer_close,
2589+ signature: CommandSignature::positional(&[completers::filename]),
2590+ },
2591+ TypableCommand {
2592+ name: "new",
2593+ aliases: &["n"],
2594+ doc: "Create a new scratch buffer.",
2595+ fun: new_file,
2596+ signature: CommandSignature::none(),
2597+ },
2598+ TypableCommand {
2599+ name: "format",
2600+ aliases: &["fmt"],
2601+ doc: "Format the file using the LSP formatter.",
2602+ fun: format,
2603+ signature: CommandSignature::none(),
2604+ },
2605+ TypableCommand {
2606+ name: "indent-style",
2607+ aliases: &[],
2608+ doc: "Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.)",
2609+ fun: set_indent_style,
2610+ signature: CommandSignature::none(),
2611+ },
2612+ TypableCommand {
2613+ name: "line-ending",
2614+ aliases: &[],
2615+ #[cfg(not(feature = "unicode-lines"))]
2616+ doc: "Set the document's default line ending. Options: crlf, lf.",
2617+ #[cfg(feature = "unicode-lines")]
2618+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2619+ fun: set_line_ending,
2620+ signature: CommandSignature::none(),
2621+ },
2622+ TypableCommand {
2623+ name: "earlier",
2624+ aliases: &["ear"],
2625+ doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2626+ fun: earlier,
2627+ signature: CommandSignature::none(),
2628+ },
2629+ TypableCommand {
2630+ name: "later",
2631+ aliases: &["lat"],
2632+ doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2633+ fun: later,
2634+ signature: CommandSignature::none(),
2635+ },
2636+ TypableCommand {
2637+ name: "write-quit",
2638+ aliases: &["wq", "x"],
2639+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2640+ fun: write_quit,
2641+ signature: CommandSignature::positional(&[completers::filename]),
2642+ },
2643+ TypableCommand {
2644+ name: "write-quit!",
2645+ aliases: &["wq!", "x!"],
2646+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2647+ fun: force_write_quit,
2648+ signature: CommandSignature::positional(&[completers::filename]),
2649+ },
2650+ TypableCommand {
2651+ name: "write-all",
2652+ aliases: &["wa"],
2653+ doc: "Write changes from all buffers to disk.",
2654+ fun: write_all,
2655+ signature: CommandSignature::none(),
2656+ },
2657+ TypableCommand {
2658+ name: "write-all!",
2659+ aliases: &["wa!"],
2660+ doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
2661+ fun: force_write_all,
2662+ signature: CommandSignature::none(),
2663+ },
2664+ TypableCommand {
2665+ name: "write-quit-all",
2666+ aliases: &["wqa", "xa"],
2667+ doc: "Write changes from all buffers to disk and close all views.",
2668+ fun: write_all_quit,
2669+ signature: CommandSignature::none(),
2670+ },
2671+ TypableCommand {
2672+ name: "write-quit-all!",
2673+ aliases: &["wqa!", "xa!"],
2674+ doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
2675+ fun: force_write_all_quit,
2676+ signature: CommandSignature::none(),
2677+ },
2678+ TypableCommand {
2679+ name: "quit-all",
2680+ aliases: &["qa"],
2681+ doc: "Close all views.",
2682+ fun: quit_all,
2683+ signature: CommandSignature::none(),
2684+ },
2685+ TypableCommand {
2686+ name: "quit-all!",
2687+ aliases: &["qa!"],
2688+ doc: "Force close all views ignoring unsaved changes.",
2689+ fun: force_quit_all,
2690+ signature: CommandSignature::none(),
2691+ },
2692+ TypableCommand {
2693+ name: "cquit",
2694+ aliases: &["cq"],
2695+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2696+ fun: cquit,
2697+ signature: CommandSignature::none(),
2698+ },
2699+ TypableCommand {
2700+ name: "cquit!",
2701+ aliases: &["cq!"],
2702+ doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
2703+ fun: force_cquit,
2704+ signature: CommandSignature::none(),
2705+ },
2706+ TypableCommand {
2707+ name: "theme",
2708+ aliases: &[],
2709+ doc: "Change the editor theme (show current theme if no name specified).",
2710+ fun: theme,
2711+ signature: CommandSignature::positional(&[completers::theme]),
2712+ },
2713+ TypableCommand {
2714+ name: "yank-join",
2715+ aliases: &[],
2716+ doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
2717+ fun: yank_joined,
2718+ signature: CommandSignature::none(),
2719+ },
2720+ TypableCommand {
2721+ name: "clipboard-yank",
2722+ aliases: &[],
2723+ doc: "Yank main selection into system clipboard.",
2724+ fun: yank_main_selection_to_clipboard,
2725+ signature: CommandSignature::none(),
2726+ },
2727+ TypableCommand {
2728+ name: "clipboard-yank-join",
2729+ aliases: &[],
2730+ doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2731+ fun: yank_joined_to_clipboard,
2732+ signature: CommandSignature::none(),
2733+ },
2734+ TypableCommand {
2735+ name: "primary-clipboard-yank",
2736+ aliases: &[],
2737+ doc: "Yank main selection into system primary clipboard.",
2738+ fun: yank_main_selection_to_primary_clipboard,
2739+ signature: CommandSignature::none(),
2740+ },
2741+ TypableCommand {
2742+ name: "primary-clipboard-yank-join",
2743+ aliases: &[],
2744+ doc: "Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2745+ fun: yank_joined_to_primary_clipboard,
2746+ signature: CommandSignature::none(),
2747+ },
2748+ TypableCommand {
2749+ name: "clipboard-paste-after",
2750+ aliases: &[],
2751+ doc: "Paste system clipboard after selections.",
2752+ fun: paste_clipboard_after,
2753+ signature: CommandSignature::none(),
2754+ },
2755+ TypableCommand {
2756+ name: "clipboard-paste-before",
2757+ aliases: &[],
2758+ doc: "Paste system clipboard before selections.",
2759+ fun: paste_clipboard_before,
2760+ signature: CommandSignature::none(),
2761+ },
2762+ TypableCommand {
2763+ name: "clipboard-paste-replace",
2764+ aliases: &[],
2765+ doc: "Replace selections with content of system clipboard.",
2766+ fun: replace_selections_with_clipboard,
2767+ signature: CommandSignature::none(),
2768+ },
2769+ TypableCommand {
2770+ name: "primary-clipboard-paste-after",
2771+ aliases: &[],
2772+ doc: "Paste primary clipboard after selections.",
2773+ fun: paste_primary_clipboard_after,
2774+ signature: CommandSignature::none(),
2775+ },
2776+ TypableCommand {
2777+ name: "primary-clipboard-paste-before",
2778+ aliases: &[],
2779+ doc: "Paste primary clipboard before selections.",
2780+ fun: paste_primary_clipboard_before,
2781+ signature: CommandSignature::none(),
2782+ },
2783+ TypableCommand {
2784+ name: "primary-clipboard-paste-replace",
2785+ aliases: &[],
2786+ doc: "Replace selections with content of system primary clipboard.",
2787+ fun: replace_selections_with_primary_clipboard,
2788+ signature: CommandSignature::none(),
2789+ },
2790+ TypableCommand {
2791+ name: "show-clipboard-provider",
2792+ aliases: &[],
2793+ doc: "Show clipboard provider name in status bar.",
2794+ fun: show_clipboard_provider,
2795+ signature: CommandSignature::none(),
2796+ },
2797+ TypableCommand {
2798+ name: "change-current-directory",
2799+ aliases: &["cd"],
2800+ doc: "Change the current working directory.",
2801+ fun: change_current_directory,
2802+ signature: CommandSignature::positional(&[completers::directory]),
2803+ },
2804+ TypableCommand {
2805+ name: "show-directory",
2806+ aliases: &["pwd"],
2807+ doc: "Show the current working directory.",
2808+ fun: show_current_directory,
2809+ signature: CommandSignature::none(),
2810+ },
2811+ TypableCommand {
2812+ name: "encoding",
2813+ aliases: &[],
2814+ doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
2815+ fun: set_encoding,
2816+ signature: CommandSignature::none(),
2817+ },
2818+ TypableCommand {
2819+ name: "character-info",
2820+ aliases: &["char"],
2821+ doc: "Get info about the character under the primary cursor.",
2822+ fun: get_character_info,
2823+ signature: CommandSignature::none(),
2824+ },
2825+ TypableCommand {
2826+ name: "reload",
2827+ aliases: &["rl"],
2828+ doc: "Discard changes and reload from the source file.",
2829+ fun: reload,
2830+ signature: CommandSignature::none(),
2831+ },
2832+ TypableCommand {
2833+ name: "reload-all",
2834+ aliases: &["rla"],
2835+ doc: "Discard changes and reload all documents from the source files.",
2836+ fun: reload_all,
2837+ signature: CommandSignature::none(),
2838+ },
2839+ TypableCommand {
2840+ name: "update",
2841+ aliases: &["u"],
2842+ doc: "Write changes only if the file has been modified.",
2843+ fun: update,
2844+ signature: CommandSignature::none(),
2845+ },
2846+ TypableCommand {
2847+ name: "lsp-workspace-command",
2848+ aliases: &[],
2849+ doc: "Open workspace command picker",
2850+ fun: lsp_workspace_command,
2851+ signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
2852+ },
2853+ TypableCommand {
2854+ name: "lsp-restart",
2855+ aliases: &[],
2856+ doc: "Restarts the language servers used by the current doc",
2857+ fun: lsp_restart,
2858+ signature: CommandSignature::none(),
2859+ },
2860+ TypableCommand {
2861+ name: "lsp-stop",
2862+ aliases: &[],
2863+ doc: "Stops the language servers that are used by the current doc",
2864+ fun: lsp_stop,
2865+ signature: CommandSignature::none(),
2866+ },
2867+ TypableCommand {
2868+ name: "tree-sitter-scopes",
2869+ aliases: &[],
2870+ doc: "Display tree sitter scopes, primarily for theming and development.",
2871+ fun: tree_sitter_scopes,
2872+ signature: CommandSignature::none(),
2873+ },
2874+ TypableCommand {
2875+ name: "tree-sitter-highlight-name",
2876+ aliases: &[],
2877+ doc: "Display name of tree-sitter highlight scope under the cursor.",
2878+ fun: tree_sitter_highlight_name,
2879+ signature: CommandSignature::none(),
2880+ },
2881+ TypableCommand {
2882+ name: "debug-start",
2883+ aliases: &["dbg"],
2884+ doc: "Start a debug session from a given template with given parameters.",
2885+ fun: debug_start,
2886+ signature: CommandSignature::none(),
2887+ },
2888+ TypableCommand {
2889+ name: "debug-remote",
2890+ aliases: &["dbg-tcp"],
2891+ doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
2892+ fun: debug_remote,
2893+ signature: CommandSignature::none(),
2894+ },
2895+ TypableCommand {
2896+ name: "debug-eval",
2897+ aliases: &[],
2898+ doc: "Evaluate expression in current debug context.",
2899+ fun: debug_eval,
2900+ signature: CommandSignature::none(),
2901+ },
2902+ TypableCommand {
2903+ name: "vsplit",
2904+ aliases: &["vs"],
2905+ doc: "Open the file in a vertical split.",
2906+ fun: vsplit,
2907+ signature: CommandSignature::all(completers::filename)
2908+ },
2909+ TypableCommand {
2910+ name: "vsplit-new",
2911+ aliases: &["vnew"],
2912+ doc: "Open a scratch buffer in a vertical split.",
2913+ fun: vsplit_new,
2914+ signature: CommandSignature::none(),
2915+ },
2916+ TypableCommand {
2917+ name: "hsplit",
2918+ aliases: &["hs", "sp"],
2919+ doc: "Open the file in a horizontal split.",
2920+ fun: hsplit,
2921+ signature: CommandSignature::all(completers::filename)
2922+ },
2923+ TypableCommand {
2924+ name: "hsplit-new",
2925+ aliases: &["hnew"],
2926+ doc: "Open a scratch buffer in a horizontal split.",
2927+ fun: hsplit_new,
2928+ signature: CommandSignature::none(),
2929+ },
2930+ TypableCommand {
2931+ name: "tutor",
2932+ aliases: &[],
2933+ doc: "Open the tutorial.",
2934+ fun: tutor,
2935+ signature: CommandSignature::none(),
2936+ },
2937+ TypableCommand {
2938+ name: "goto",
2939+ aliases: &["g"],
2940+ doc: "Goto line number.",
2941+ fun: goto_line_number,
2942+ signature: CommandSignature::none(),
2943+ },
2944+ TypableCommand {
2945+ name: "set-language",
2946+ aliases: &["lang"],
2947+ doc: "Set the language of current buffer (show current language if no value specified).",
2948+ fun: language,
2949+ signature: CommandSignature::positional(&[completers::language]),
2950+ },
2951+ TypableCommand {
2952+ name: "set-option",
2953+ aliases: &["set"],
2954+ doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
2955+ fun: set_option,
2956+ // TODO: Add support for completion of the options value(s), when appropriate.
2957+ signature: CommandSignature::positional(&[completers::setting]),
2958+ },
2959+ TypableCommand {
2960+ name: "toggle-option",
2961+ aliases: &["toggle"],
2962+ doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
2963+ fun: toggle_option,
2964+ signature: CommandSignature::positional(&[completers::setting]),
2965+ },
2966+ TypableCommand {
2967+ name: "get-option",
2968+ aliases: &["get"],
2969+ doc: "Get the current value of a config option.",
2970+ fun: get_option,
2971+ signature: CommandSignature::positional(&[completers::setting]),
2972+ },
2973+ TypableCommand {
2974+ name: "sort",
2975+ aliases: &[],
2976+ doc: "Sort ranges in selection.",
2977+ fun: sort,
2978+ signature: CommandSignature::none(),
2979+ },
2980+ TypableCommand {
2981+ name: "rsort",
2982+ aliases: &[],
2983+ doc: "Sort ranges in selection in reverse order.",
2984+ fun: sort_reverse,
2985+ signature: CommandSignature::none(),
2986+ },
2987+ TypableCommand {
2988+ name: "reflow",
2989+ aliases: &[],
2990+ doc: "Hard-wrap the current selection of lines to a given width.",
2991+ fun: reflow,
2992+ signature: CommandSignature::none(),
2993+ },
2994+ TypableCommand {
2995+ name: "tree-sitter-subtree",
2996+ aliases: &["ts-subtree"],
2997+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
2998+ fun: tree_sitter_subtree,
2999+ signature: CommandSignature::none(),
3000+ },
3001+ TypableCommand {
3002+ name: "config-reload",
3003+ aliases: &[],
3004+ doc: "Refresh user config.",
3005+ fun: refresh_config,
3006+ signature: CommandSignature::none(),
3007+ },
3008+ TypableCommand {
3009+ name: "config-open",
3010+ aliases: &[],
3011+ doc: "Open the user config.toml file.",
3012+ fun: open_config,
3013+ signature: CommandSignature::none(),
3014+ },
3015+ TypableCommand {
3016+ name: "config-open-workspace",
3017+ aliases: &[],
3018+ doc: "Open the workspace config.toml file.",
3019+ fun: open_workspace_config,
3020+ signature: CommandSignature::none(),
3021+ },
3022+ TypableCommand {
3023+ name: "log-open",
3024+ aliases: &[],
3025+ doc: "Open the helix log file.",
3026+ fun: open_log,
3027+ signature: CommandSignature::none(),
3028+ },
3029+ TypableCommand {
3030+ name: "insert-output",
3031+ aliases: &[],
3032+ doc: "Run shell command, inserting output before each selection.",
3033+ fun: insert_output,
3034+ signature: CommandSignature::none(),
3035+ },
3036+ TypableCommand {
3037+ name: "append-output",
3038+ aliases: &[],
3039+ doc: "Run shell command, appending output after each selection.",
3040+ fun: append_output,
3041+ signature: CommandSignature::none(),
3042+ },
3043+ TypableCommand {
3044+ name: "pipe",
3045+ aliases: &[],
3046+ doc: "Pipe each selection to the shell command.",
3047+ fun: pipe,
3048+ signature: CommandSignature::none(),
3049+ },
3050+ TypableCommand {
3051+ name: "pipe-to",
3052+ aliases: &[],
3053+ doc: "Pipe each selection to the shell command, ignoring output.",
3054+ fun: pipe_to,
3055+ signature: CommandSignature::none(),
3056+ },
3057+ TypableCommand {
3058+ name: "run-shell-command",
3059+ aliases: &["sh"],
3060+ doc: "Run a shell command",
3061+ fun: run_shell_command,
3062+ signature: CommandSignature::all(completers::filename)
3063+ },
3064+ TypableCommand {
3065+ name: "reset-diff-change",
3066+ aliases: &["diffget", "diffg"],
3067+ doc: "Reset the diff change at the cursor position.",
3068+ fun: reset_diff_change,
3069+ signature: CommandSignature::none(),
3070+ },
3071+ TypableCommand {
3072+ name: "clear-register",
3073+ aliases: &[],
3074+ doc: "Clear given register. If no argument is provided, clear all registers.",
3075+ fun: clear_register,
3076+ signature: CommandSignature::none(),
3077+ },
3078+ TypableCommand {
3079+ name: "redraw",
3080+ aliases: &[],
3081+ doc: "Clear and re-render the whole UI",
3082+ fun: redraw,
3083+ signature: CommandSignature::none(),
3084+ },
3085+ TypableCommand {
3086+ name: "move",
3087+ aliases: &[],
3088+ doc: "Move the current buffer and its corresponding file to a different path",
3089+ fun: move_buffer,
3090+ signature: CommandSignature::positional(&[completers::filename]),
3091+ },
3092+];
28763093
28773094pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
28783095 Lazy::new(|| {
@@ -2891,28 +3108,18 @@ pub(super) fn command_mode(cx: &mut Context) {
28913108 ":".into(),
28923109 Some(':'),
28933110 |editor: &Editor, input: &str| {
2894− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2895− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2896−
28973111 let shellwords = Shellwords::from(input);
28983112 let words = shellwords.words();
28993113
29003114 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
2901− // If the command has not been finished yet, complete commands.
2902− let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2903− .iter()
2904− .filter_map(|command| {
2905− FUZZY_MATCHER
2906− .fuzzy_match(command.name, input)
2907− .map(|score| (command.name, score))
2908− })
2909− .collect();
2910−
2911− matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
2912− matches
2913− .into_iter()
2914− .map(|(name, _)| (0.., name.into()))
2915− .collect()
3115+ fuzzy_match(
3116+ input,
3117+ TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
3118+ false,
3119+ )
3120+ .into_iter()
3121+ .map(|(name, _)| (0.., name.into()))
3122+ .collect()
29163123 } else {
29173124 // Otherwise, use the command's completer and the last shellword
29183125 // as completion input.
helix-term/src/compositor.rs+12
@@ -16,6 +16,7 @@ pub enum EventResult {
1616}
1717
1818use crate::job::Jobs;
19+use crate::ui::picker;
1920use helix_view::Editor;
2021
2122pub use helix_view::input::Event;
@@ -79,6 +80,7 @@ pub struct Compositor {
7980 area: Rect,
8081
8182 pub(crate) last_picker: Option<Box<dyn Component>>,
83+ pub(crate) full_redraw: bool,
8284}
8385
8486impl Compositor {
@@ -87,6 +89,7 @@ impl Compositor {
8789 layers: Vec::new(),
8890 area,
8991 last_picker: None,
92+ full_redraw: false,
9093 }
9194 }
9295
@@ -100,6 +103,11 @@ impl Compositor {
100103
101104 /// Add a layer to be rendered in front of all existing layers.
102105 pub fn push(&mut self, mut layer: Box<dyn Component>) {
106+ // immediately clear last_picker field to avoid excessive memory
107+ // consumption for picker with many items
108+ if layer.id() == Some(picker::ID) {
109+ self.last_picker = None;
110+ }
103111 let size = self.size();
104112 // trigger required_size on init
105113 layer.required_size((size.width, size.height));
@@ -200,6 +208,10 @@ impl Compositor {
200208 .find(|component| component.id() == Some(id))
201209 .and_then(|component| component.as_any_mut().downcast_mut())
202210 }
211+
212+ pub fn need_full_redraw(&mut self) {
213+ self.full_redraw = true;
214+ }
203215}
204216
205217// View casting, taken straight from Cursive
helix-term/src/config.rs+1
@@ -109,6 +109,7 @@ impl Config {
109109 )?,
110110 }
111111 }
112+
112113 // these are just two io errors return the one for the global config
113114 (Err(err), Err(_)) => return Err(err),
114115 };
helix-term/src/health.rs+44−16
@@ -181,8 +181,8 @@ pub fn languages_all() -> std::io::Result<()> {
181181 .language
182182 .sort_unstable_by_key(|l| l.language_id.clone());
183183
184− let check_binary = |cmd: Option<String>| match cmd {
185− Some(cmd) => match which::which(&cmd) {
184+ let check_binary = |cmd: Option<&str>| match cmd {
185+ Some(cmd) => match which::which(cmd) {
186186 Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
187187 Err(_) => column(&format!("✘ {}", cmd), Color::Red),
188188 },
@@ -192,17 +192,15 @@ pub fn languages_all() -> std::io::Result<()> {
192192 for lang in &syn_loader_conf.language {
193193 column(&lang.language_id, Color::Reset);
194194
195− // TODO multiple language servers (check binary for each supported language server, not just the first)
196−
197− let lsp = lang.language_servers.first().and_then(|ls| {
195+ let mut cmds = lang.language_servers.iter().filter_map(|ls| {
198196 syn_loader_conf
199197 .language_server
200198 .get(&ls.name)
201− .map(|config| config.command.clone())
199+ .map(|config| config.command.as_str())
202200 });
203− check_binary(lsp);
201+ check_binary(cmds.next());
204202
205− let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
203+ let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
206204 check_binary(dap);
207205
208206 for ts_feat in TsFeature::all() {
@@ -213,6 +211,12 @@ pub fn languages_all() -> std::io::Result<()> {
213211 }
214212
215213 writeln!(stdout)?;
214+
215+ for cmd in cmds {
216+ column("", Color::Reset);
217+ check_binary(Some(cmd));
218+ writeln!(stdout)?;
219+ }
216220 }
217221
218222 Ok(())
@@ -268,15 +272,12 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
268272 }
269273 };
270274
271− // TODO multiple language servers
272− probe_protocol(
275+ probe_protocols(
273276 "language server",
274− lang.language_servers.first().and_then(|ls| {
275− syn_loader_conf
276− .language_server
277− .get(&ls.name)
278− .map(|config| config.command.clone())
279− }),
277+ lang.language_servers
278+ .iter()
279+ .filter_map(|ls| syn_loader_conf.language_server.get(&ls.name))
280+ .map(|config| config.command.as_str()),
280281 )?;
281282
282283 probe_protocol(
@@ -291,6 +292,33 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
291292 Ok(())
292293}
293294
295+/// Display diagnostics about multiple LSPs and DAPs.
296+fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
297+ protocol_name: &str,
298+ server_cmds: I,
299+) -> std::io::Result<()> {
300+ let stdout = std::io::stdout();
301+ let mut stdout = stdout.lock();
302+ let mut server_cmds = server_cmds.peekable();
303+
304+ write!(stdout, "Configured {}s:", protocol_name)?;
305+ if server_cmds.peek().is_none() {
306+ writeln!(stdout, "{}", " None".yellow())?;
307+ return Ok(());
308+ }
309+ writeln!(stdout)?;
310+
311+ for cmd in server_cmds {
312+ let (path, icon) = match which::which(cmd) {
313+ Ok(path) => (path.display().to_string().green(), "✓".green()),
314+ Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "✘".red()),
315+ };
316+ writeln!(stdout, " {} {}: {}", icon, cmd, path)?;
317+ }
318+
319+ Ok(())
320+}
321+
294322/// Display diagnostics about LSP and DAP.
295323fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::Result<()> {
296324 let stdout = std::io::stdout();
helix-term/src/keymap.rs+14−14
@@ -401,14 +401,14 @@ mod tests {
401401 fn merge_partial_keys() {
402402 let keymap = hashmap! {
403403 Mode::Normal => keymap!({ "Normal mode"
404− "i" => normal_mode,
405− "无" => insert_mode,
406− "z" => jump_backward,
407− "g" => { "Merge into goto mode"
408− "$" => goto_line_end,
409− "g" => delete_char_forward,
410− },
411− })
404+ "i" => normal_mode,
405+ "无" => insert_mode,
406+ "z" => jump_backward,
407+ "g" => { "Merge into goto mode"
408+ "$" => goto_line_end,
409+ "g" => delete_char_forward,
410+ },
411+ })
412412 };
413413 let mut merged_keyamp = default();
414414 merge_keys(&mut merged_keyamp, keymap.clone());
@@ -474,13 +474,13 @@ mod tests {
474474 fn order_should_be_set() {
475475 let keymap = hashmap! {
476476 Mode::Normal => keymap!({ "Normal mode"
477− "space" => { ""
478− "s" => { ""
479− "v" => vsplit,
480− "c" => hsplit,
481− },
477+ "space" => { ""
478+ "s" => { ""
479+ "v" => vsplit,
480+ "c" => hsplit,
482481 },
483− })
482+ },
483+ })
484484 };
485485 let mut merged_keyamp = default();
486486 merge_keys(&mut merged_keyamp, keymap.clone());
helix-term/src/keymap/default.rs+8−2
@@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
8888 "A-i" | "A-down" => shrink_selection,
8989 "A-p" | "A-left" => select_prev_sibling,
9090 "A-n" | "A-right" => select_next_sibling,
91+ "A-e" => move_parent_node_end,
92+ "A-b" => move_parent_node_start,
9193
9294 "%" => select_all,
9395 "x" => extend_line_below,
@@ -265,7 +267,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
265267 "C-v" | "v" => vsplit_new,
266268 },
267269 },
268− "y" => yank_joined_to_clipboard,
270+ "y" => yank_to_clipboard,
269271 "Y" => yank_main_selection_to_clipboard,
270272 "p" => paste_clipboard_after,
271273 "P" => paste_clipboard_before,
@@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
336338 "B" => extend_prev_long_word_start,
337339 "E" => extend_next_long_word_end,
338340
341+ "A-e" => extend_parent_node_end,
342+ "A-b" => extend_parent_node_start,
343+
339344 "n" => extend_search_next,
340345 "N" => extend_search_prev,
341346
@@ -368,7 +373,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
368373 "C-h" | "backspace" | "S-backspace" => delete_char_backward,
369374 "C-d" | "del" => delete_char_forward,
370375 "C-j" | "ret" => insert_newline,
371− "tab" => insert_tab,
376+ "tab" => smart_tab,
377+ "S-tab" => insert_tab,
372378
373379 "up" => move_visual_line_up,
374380 "down" => move_visual_line_down,
helix-term/src/main.rs+17−1
@@ -66,6 +66,8 @@ FLAGS:
6666 -V, --version Prints version information
6767 --vsplit Splits all given files vertically into different windows
6868 --hsplit Splits all given files horizontally into different windows
69+ -w, --working-dir <path> Specify an initial working directory
70+ +N Open the first given file at line number N
6971",
7072 env!("CARGO_PKG_NAME"),
7173 VERSION_AND_GIT_HASH,
@@ -74,7 +76,7 @@ FLAGS:
7476 helix_loader::default_log_file().display(),
7577 );
7678
77− let args = Args::parse_args().context("could not parse arguments")?;
79+ let mut args = Args::parse_args().context("could not parse arguments")?;
7880
7981 helix_loader::initialize_config_file(args.config_file.clone());
8082 helix_loader::initialize_log_file(args.log_file.clone());
@@ -114,6 +116,20 @@ FLAGS:
114116
115117 setup_logging(args.verbosity).context("failed to initialize logging")?;
116118
119+ // Before setting the working directory, resolve all the paths in args.files
120+ for (path, _) in args.files.iter_mut() {
121+ *path = helix_core::path::get_canonicalized_path(path);
122+ }
123+
124+ // NOTE: Set the working directory early so the correct configuration is loaded. Be aware that
125+ // Application::new() depends on this logic so it must be updated if this changes.
126+ if let Some(path) = &args.working_directory {
127+ helix_loader::set_current_working_dir(path)?;
128+ } else if let Some((path, _)) = args.files.first().filter(|p| p.0.is_dir()) {
129+ // If the first file is a directory, it will be the working directory unless -w was specified
130+ helix_loader::set_current_working_dir(path)?;
131+ }
132+
117133 let config = match Config::load_default() {
118134 Ok(config) => config,
119135 Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
helix-term/src/ui/completion.rs+32−4
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
22use helix_view::{
33 document::SavePoint,
44 editor::CompleteAction,
5+ graphics::Margin,
56 theme::{Modifier, Style},
67 ViewId,
78};
@@ -144,7 +145,9 @@ impl Completion {
144145 }
145146 };
146147
147− let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
148+ let Some(range) =
149+ util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
150+ else {
148151 return Transaction::new(doc.text());
149152 };
150153
@@ -292,6 +295,8 @@ impl Completion {
292295 };
293296 // if more text was entered, remove it
294297 doc.restore(view, &savepoint, true);
298+ // save an undo checkpoint before the completion
299+ doc.append_changes_to_history(view);
295300 let transaction = item_to_transaction(
296301 doc,
297302 view.id,
@@ -322,9 +327,18 @@ impl Completion {
322327 }
323328 };
324329 });
330+
331+ let margin = if editor.menu_border() {
332+ Margin::vertical(1)
333+ } else {
334+ Margin::none()
335+ };
336+
325337 let popup = Popup::new(Self::ID, menu)
326338 .with_scrollbar(false)
327− .ignore_escape_key(true);
339+ .ignore_escape_key(true)
340+ .margin(margin);
341+
328342 let mut completion = Self {
329343 popup,
330344 start_offset,
@@ -411,10 +425,18 @@ impl Completion {
411425 _ => return false,
412426 };
413427
414− let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
428+ let Some(language_server) = cx
429+ .editor
430+ .language_server_by_id(current_item.language_server_id)
431+ else {
432+ return false;
433+ };
415434
416435 // This method should not block the compositor so we handle the response asynchronously.
417− let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
436+ let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
437+ else {
438+ return false;
439+ };
418440
419441 cx.callback(
420442 future,
@@ -557,6 +579,12 @@ impl Component for Completion {
557579 // clear area
558580 let background = cx.editor.theme.get("ui.popup");
559581 surface.clear_with(doc_area, background);
582+
583+ if cx.editor.popup_border() {
584+ use tui::widgets::{Block, Borders, Widget};
585+ Widget::render(Block::default().borders(Borders::ALL), doc_area, surface);
586+ }
587+
560588 markdown_doc.render(doc_area, surface, cx);
561589 }
562590}
helix-term/src/ui/document.rs+60−24
@@ -97,7 +97,8 @@ pub fn render_document(
9797 doc: &Document,
9898 offset: ViewPosition,
9999 doc_annotations: &TextAnnotations,
100− highlight_iter: impl Iterator<Item = HighlightEvent>,
100+ syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
101+ overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
101102 theme: &Theme,
102103 line_decoration: &mut [Box<dyn LineDecoration + '_>],
103104 translated_positions: &mut [TranslatedPosition],
@@ -109,7 +110,8 @@ pub fn render_document(
109110 offset,
110111 &doc.text_format(viewport.width, Some(theme)),
111112 doc_annotations,
112− highlight_iter,
113+ syntax_highlight_iter,
114+ overlay_highlight_iter,
113115 theme,
114116 line_decoration,
115117 translated_positions,
@@ -157,7 +159,8 @@ pub fn render_text<'t>(
157159 offset: ViewPosition,
158160 text_fmt: &TextFormat,
159161 text_annotations: &TextAnnotations,
160− highlight_iter: impl Iterator<Item = HighlightEvent>,
162+ syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
163+ overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
161164 theme: &Theme,
162165 line_decorations: &mut [Box<dyn LineDecoration + '_>],
163166 translated_positions: &mut [TranslatedPosition],
@@ -178,10 +181,16 @@ pub fn render_text<'t>(
178181
179182 let (mut formatter, mut first_visible_char_idx) =
180183 DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
181− let mut styles = StyleIter {
184+ let mut syntax_styles = StyleIter {
182185 text_style: renderer.text_style,
183186 active_highlights: Vec::with_capacity(64),
184− highlight_iter,
187+ highlight_iter: syntax_highlight_iter,
188+ theme,
189+ };
190+ let mut overlay_styles = StyleIter {
191+ text_style: Style::default(),
192+ active_highlights: Vec::with_capacity(64),
193+ highlight_iter: overlay_highlight_iter,
185194 theme,
186195 };
187196
@@ -193,7 +202,10 @@ pub fn render_text<'t>(
193202 };
194203 let mut is_in_indent_area = true;
195204 let mut last_line_indent_level = 0;
196− let mut style_span = styles
205+ let mut syntax_style_span = syntax_styles
206+ .next()
207+ .unwrap_or_else(|| (Style::default(), usize::MAX));
208+ let mut overlay_style_span = overlay_styles
197209 .next()
198210 .unwrap_or_else(|| (Style::default(), usize::MAX));
199211
@@ -221,9 +233,16 @@ pub fn render_text<'t>(
221233
222234 // skip any graphemes on visual lines before the block start
223235 if pos.row < row_off {
224− if char_pos >= style_span.1 {
225− style_span = if let Some(style_span) = styles.next() {
226− style_span
236+ if char_pos >= syntax_style_span.1 {
237+ syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() {
238+ syntax_style_span
239+ } else {
240+ break;
241+ }
242+ }
243+ if char_pos >= overlay_style_span.1 {
244+ overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() {
245+ overlay_style_span
227246 } else {
228247 break;
229248 }
@@ -260,8 +279,15 @@ pub fn render_text<'t>(
260279 }
261280
262281 // acquire the correct grapheme style
263− if char_pos >= style_span.1 {
264− style_span = styles.next().unwrap_or((Style::default(), usize::MAX));
282+ if char_pos >= syntax_style_span.1 {
283+ syntax_style_span = syntax_styles
284+ .next()
285+ .unwrap_or((Style::default(), usize::MAX));
286+ }
287+ if char_pos >= overlay_style_span.1 {
288+ overlay_style_span = overlay_styles
289+ .next()
290+ .unwrap_or((Style::default(), usize::MAX));
265291 }
266292 char_pos += grapheme.doc_chars();
267293
@@ -275,22 +301,25 @@ pub fn render_text<'t>(
275301 pos,
276302 );
277303
278− let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
279− let style = renderer.text_style;
280− if let Some(highlight) = highlight {
281− style.patch(theme.highlight(highlight.0))
304+ let (syntax_style, overlay_style) =
305+ if let GraphemeSource::VirtualText { highlight } = grapheme.source {
306+ let mut style = renderer.text_style;
307+ if let Some(highlight) = highlight {
308+ style = style.patch(theme.highlight(highlight.0))
309+ }
310+ (style, Style::default())
282311 } else {
283− style
284− }
285− } else {
286− style_span.0
287− };
312+ (syntax_style_span.0, overlay_style_span.0)
313+ };
288314
289− let virt = grapheme.is_virtual();
315+ let is_virtual = grapheme.is_virtual();
290316 renderer.draw_grapheme(
291317 grapheme.grapheme,
292− grapheme_style,
293− virt,
318+ GraphemeStyle {
319+ syntax_style,
320+ overlay_style,
321+ },
322+ is_virtual,
294323 &mut last_line_indent_level,
295324 &mut is_in_indent_area,
296325 pos,
@@ -322,6 +351,11 @@ pub struct TextRenderer<'a> {
322351 pub viewport: Rect,
323352}
324353
354+pub struct GraphemeStyle {
355+ syntax_style: Style,
356+ overlay_style: Style,
357+}
358+
325359impl<'a> TextRenderer<'a> {
326360 pub fn new(
327361 surface: &'a mut Surface,
@@ -395,7 +429,7 @@ impl<'a> TextRenderer<'a> {
395429 pub fn draw_grapheme(
396430 &mut self,
397431 grapheme: Grapheme,
398− mut style: Style,
432+ grapheme_style: GraphemeStyle,
399433 is_virtual: bool,
400434 last_indent_level: &mut usize,
401435 is_in_indent_area: &mut bool,
@@ -405,9 +439,11 @@ impl<'a> TextRenderer<'a> {
405439 let is_whitespace = grapheme.is_whitespace();
406440
407441 // TODO is it correct to apply the whitespace style to all unicode white spaces?
442+ let mut style = grapheme_style.syntax_style;
408443 if is_whitespace {
409444 style = style.patch(self.whitespace_style);
410445 }
446+ style = style.patch(grapheme_style.overlay_style);
411447
412448 let width = grapheme.width();
413449 let space = if is_virtual { " " } else { &self.space };
helix-term/src/ui/editor.rs+102−54
@@ -43,6 +43,8 @@ pub struct EditorView {
4343 pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
4444 pub(crate) completion: Option<Completion>,
4545 spinners: ProgressSpinners,
46+ /// Tracks if the terminal window is focused by reaction to terminal focus events
47+ terminal_focused: bool,
4648}
4749
4850#[derive(Debug, Clone)]
@@ -71,6 +73,7 @@ impl EditorView {
7173 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
7274 completion: None,
7375 spinners: ProgressSpinners::default(),
76+ terminal_focused: true,
7477 }
7578 }
7679
@@ -121,16 +124,20 @@ impl EditorView {
121124 line_decorations.push(Box::new(line_decoration));
122125 }
123126
124− let mut highlights =
127+ let syntax_highlights =
125128 Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
126− let overlay_highlights = Self::overlay_syntax_highlights(
129+
130+ let mut overlay_highlights =
131+ Self::empty_highlight_iter(doc, view.offset.anchor, inner.height);
132+ let overlay_syntax_highlights = Self::overlay_syntax_highlights(
127133 doc,
128134 view.offset.anchor,
129135 inner.height,
130136 &text_annotations,
131137 );
132− if !overlay_highlights.is_empty() {
133− highlights = Box::new(syntax::merge(highlights, overlay_highlights));
138+ if !overlay_syntax_highlights.is_empty() {
139+ overlay_highlights =
140+ Box::new(syntax::merge(overlay_highlights, overlay_syntax_highlights));
134141 }
135142
136143 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
@@ -139,39 +146,41 @@ impl EditorView {
139146 if diagnostic.is_empty() {
140147 continue;
141148 }
142− highlights = Box::new(syntax::merge(highlights, diagnostic));
149+ overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic));
143150 }
144151
145− let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
152+ if is_focused {
146153 let highlights = syntax::merge(
147− highlights,
154+ overlay_highlights,
148155 Self::doc_selection_highlights(
149156 editor.mode(),
150157 doc,
151158 view,
152159 theme,
153160 &config.cursor_shape,
161+ self.terminal_focused,
154162 ),
155163 );
156164 let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
157165 if focused_view_elements.is_empty() {
158− Box::new(highlights)
166+ overlay_highlights = Box::new(highlights)
159167 } else {
160− Box::new(syntax::merge(highlights, focused_view_elements))
168+ overlay_highlights = Box::new(syntax::merge(highlights, focused_view_elements))
161169 }
162− } else {
163− Box::new(highlights)
164− };
170+ }
165171
166− Self::render_gutter(
167− editor,
168− doc,
169− view,
170− view.area,
171− theme,
172− is_focused,
173− &mut line_decorations,
174− );
172+ let gutter_overflow = view.gutter_offset(doc) == 0;
173+ if !gutter_overflow {
174+ Self::render_gutter(
175+ editor,
176+ doc,
177+ view,
178+ view.area,
179+ theme,
180+ is_focused & self.terminal_focused,
181+ &mut line_decorations,
182+ );
183+ }
175184
176185 if is_focused {
177186 let cursor = doc
@@ -191,7 +200,8 @@ impl EditorView {
191200 doc,
192201 view.offset,
193202 &text_annotations,
194− highlights,
203+ syntax_highlights,
204+ overlay_highlights,
195205 theme,
196206 &mut line_decorations,
197207 &mut translated_positions,
@@ -251,27 +261,39 @@ impl EditorView {
251261 .for_each(|area| surface.set_style(area, ruler_theme))
252262 }
253263
254− pub fn overlay_syntax_highlights(
264+ fn viewport_byte_range(
265+ text: helix_core::RopeSlice,
266+ row: usize,
267+ height: u16,
268+ ) -> std::ops::Range<usize> {
269+ // Calculate viewport byte ranges:
270+ // Saturating subs to make it inclusive zero indexing.
271+ let last_line = text.len_lines().saturating_sub(1);
272+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
273+ let start = text.line_to_byte(row.min(last_line));
274+ let end = text.line_to_byte(last_visible_line + 1);
275+
276+ start..end
277+ }
278+
279+ pub fn empty_highlight_iter(
255280 doc: &Document,
256281 anchor: usize,
257282 height: u16,
258− text_annotations: &TextAnnotations,
259− ) -> Vec<(usize, std::ops::Range<usize>)> {
283+ ) -> Box<dyn Iterator<Item = HighlightEvent>> {
260284 let text = doc.text().slice(..);
261285 let row = text.char_to_line(anchor.min(text.len_chars()));
262286
263− let range = {
264− // Calculate viewport byte ranges:
265− // Saturating subs to make it inclusive zero indexing.
266− let last_line = text.len_lines().saturating_sub(1);
267− let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
268− let start = text.line_to_byte(row.min(last_line));
269− let end = text.line_to_byte(last_visible_line + 1);
270−
271− start..end
272− };
273−
274− text_annotations.collect_overlay_highlights(range)
287+ // Calculate viewport byte ranges:
288+ // Saturating subs to make it inclusive zero indexing.
289+ let range = Self::viewport_byte_range(text, row, height);
290+ Box::new(
291+ [HighlightEvent::Source {
292+ start: text.byte_to_char(range.start),
293+ end: text.byte_to_char(range.end),
294+ }]
295+ .into_iter(),
296+ )
275297 }
276298
277299 /// Get syntax highlights for a document in a view represented by the first line
@@ -286,16 +308,7 @@ impl EditorView {
286308 let text = doc.text().slice(..);
287309 let row = text.char_to_line(anchor.min(text.len_chars()));
288310
289− let range = {
290− // Calculate viewport byte ranges:
291− // Saturating subs to make it inclusive zero indexing.
292− let last_line = text.len_lines().saturating_sub(1);
293− let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
294− let start = text.line_to_byte(row.min(last_line));
295− let end = text.line_to_byte(last_visible_line + 1);
296−
297− start..end
298− };
311+ let range = Self::viewport_byte_range(text, row, height);
299312
300313 match doc.syntax() {
301314 Some(syntax) => {
@@ -328,6 +341,20 @@ impl EditorView {
328341 }
329342 }
330343
344+ pub fn overlay_syntax_highlights(
345+ doc: &Document,
346+ anchor: usize,
347+ height: u16,
348+ text_annotations: &TextAnnotations,
349+ ) -> Vec<(usize, std::ops::Range<usize>)> {
350+ let text = doc.text().slice(..);
351+ let row = text.char_to_line(anchor.min(text.len_chars()));
352+
353+ let range = Self::viewport_byte_range(text, row, height);
354+
355+ text_annotations.collect_overlay_highlights(range)
356+ }
357+
331358 /// Get highlight spans for document diagnostics
332359 pub fn doc_diagnostics_highlights(
333360 doc: &Document,
@@ -359,7 +386,7 @@ impl EditorView {
359386 let mut warning_vec = Vec::new();
360387 let mut error_vec = Vec::new();
361388
362− for diagnostic in doc.diagnostics() {
389+ for diagnostic in doc.shown_diagnostics() {
363390 // Separate diagnostics into different Vecs by severity.
364391 let (vec, scope) = match diagnostic.severity {
365392 Some(Severity::Info) => (&mut info_vec, info),
@@ -394,6 +421,7 @@ impl EditorView {
394421 view: &View,
395422 theme: &Theme,
396423 cursor_shape_config: &CursorShapeConfig,
424+ is_terminal_focused: bool,
397425 ) -> Vec<(usize, std::ops::Range<usize>)> {
398426 let text = doc.text().slice(..);
399427 let selection = doc.selection(view.id);
@@ -441,7 +469,7 @@ impl EditorView {
441469
442470 // Special-case: cursor at end of the rope.
443471 if range.head == range.anchor && range.head == text.len_chars() {
444− if !selection_is_primary || cursor_is_block {
472+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
445473 // Bar and underline cursors are drawn by the terminal
446474 // BUG: If the editor area loses focus while having a bar or
447475 // underline cursor (eg. when a regex prompt has focus) then
@@ -464,13 +492,17 @@ impl EditorView {
464492 cursor_start
465493 };
466494 spans.push((selection_scope, range.anchor..selection_end));
467− if !selection_is_primary || cursor_is_block {
495+ // add block cursors
496+ // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
497+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
468498 spans.push((cursor_scope, cursor_start..range.head));
469499 }
470500 } else {
471501 // Reverse case.
472502 let cursor_end = next_grapheme_boundary(text, range.head);
473− if !selection_is_primary || cursor_is_block {
503+ // add block cursors
504+ // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
505+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
474506 spans.push((cursor_scope, range.head..cursor_end));
475507 }
476508 // non block cursors look like they exclude the cursor
@@ -1067,6 +1099,7 @@ impl EditorView {
10671099 let editor = &mut cxt.editor;
10681100
10691101 if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
1102+ let prev_view_id = view!(editor).id;
10701103 let doc = doc_mut!(editor, &view!(editor, view_id).doc);
10711104
10721105 if modifiers == KeyModifiers::ALT {
@@ -1076,6 +1109,10 @@ impl EditorView {
10761109 doc.set_selection(view_id, Selection::point(pos));
10771110 }
10781111
1112+ if view_id != prev_view_id {
1113+ self.clear_completion(editor);
1114+ }
1115+
10791116 editor.focus(view_id);
10801117 editor.ensure_cursor_in_view(view_id);
10811118
@@ -1369,13 +1406,17 @@ impl Component for EditorView {
13691406
13701407 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
13711408 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1372− Event::FocusGained => EventResult::Ignored(None),
1409+ Event::FocusGained => {
1410+ self.terminal_focused = true;
1411+ EventResult::Consumed(None)
1412+ }
13731413 Event::FocusLost => {
13741414 if context.editor.config().auto_save {
13751415 if let Err(e) = commands::typed::write_all_impl(context, false, false) {
13761416 context.editor.set_error(format!("{}", e));
13771417 }
13781418 }
1419+ self.terminal_focused = false;
13791420 EventResult::Consumed(None)
13801421 }
13811422 }
@@ -1485,8 +1526,15 @@ impl Component for EditorView {
14851526
14861527 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
14871528 match editor.cursor() {
1488− // All block cursors are drawn manually
1489− (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
1529+ // all block cursors are drawn manually
1530+ (pos, CursorKind::Block) => {
1531+ if self.terminal_focused {
1532+ (pos, CursorKind::Hidden)
1533+ } else {
1534+ // use crossterm cursor when terminal loses focus
1535+ (pos, CursorKind::Underline)
1536+ }
1537+ }
14901538 cursor => cursor,
14911539 }
14921540 }
helix-term/src/ui/fuzzy_match.rsdeleted−239
@@ -1,239 +0,0 @@
1−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2−use fuzzy_matcher::FuzzyMatcher;
3−
4−#[cfg(test)]
5−mod test;
6−
7−struct QueryAtom {
8− kind: QueryAtomKind,
9− atom: String,
10− ignore_case: bool,
11− inverse: bool,
12−}
13−impl QueryAtom {
14− fn new(atom: &str) -> Option<QueryAtom> {
15− let mut atom = atom.to_string();
16− let inverse = atom.starts_with('!');
17− if inverse {
18− atom.remove(0);
19− }
20−
21− let mut kind = match atom.chars().next() {
22− Some('^') => QueryAtomKind::Prefix,
23− Some('\'') => QueryAtomKind::Substring,
24− _ if inverse => QueryAtomKind::Substring,
25− _ => QueryAtomKind::Fuzzy,
26− };
27−
28− if atom.starts_with(['^', '\'']) {
29− atom.remove(0);
30− }
31−
32− if atom.is_empty() {
33− return None;
34− }
35−
36− if atom.ends_with('$') && !atom.ends_with("\\$") {
37− atom.pop();
38− kind = if kind == QueryAtomKind::Prefix {
39− QueryAtomKind::Exact
40− } else {
41− QueryAtomKind::Postfix
42− }
43− }
44−
45− Some(QueryAtom {
46− kind,
47− atom: atom.replace('\\', ""),
48− // not ideal but fuzzy_matches only knows ascii uppercase so more consistent
49− // to behave the same
50− ignore_case: kind != QueryAtomKind::Fuzzy
51− && atom.chars().all(|c| c.is_ascii_lowercase()),
52− inverse,
53− })
54− }
55−
56− fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
57− // for inverse there are no indices to return
58− // just return whether we matched
59− if self.inverse {
60− return self.matches(matcher, item);
61− }
62− let buf;
63− let item = if self.ignore_case {
64− buf = item.to_ascii_lowercase();
65− &buf
66− } else {
67− item
68− };
69− let off = match self.kind {
70− QueryAtomKind::Fuzzy => {
71− if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
72− indices.extend_from_slice(&fuzzy_indices);
73− return true;
74− } else {
75− return false;
76− }
77− }
78− QueryAtomKind::Substring => {
79− if let Some(off) = item.find(&self.atom) {
80− off
81− } else {
82− return false;
83− }
84− }
85− QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
86− QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
87− QueryAtomKind::Exact if item == self.atom => 0,
88− _ => return false,
89− };
90−
91− indices.extend(off..(off + self.atom.len()));
92− true
93− }
94−
95− fn matches(&self, matcher: &Matcher, item: &str) -> bool {
96− let buf;
97− let item = if self.ignore_case {
98− buf = item.to_ascii_lowercase();
99− &buf
100− } else {
101− item
102− };
103− let mut res = match self.kind {
104− QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
105− QueryAtomKind::Substring => item.contains(&self.atom),
106− QueryAtomKind::Prefix => item.starts_with(&self.atom),
107− QueryAtomKind::Postfix => item.ends_with(&self.atom),
108− QueryAtomKind::Exact => item == self.atom,
109− };
110− if self.inverse {
111− res = !res;
112− }
113− res
114− }
115−}
116−
117−#[derive(Debug, PartialEq, Eq, Clone, Copy)]
118−enum QueryAtomKind {
119− /// Item is a fuzzy match of this behaviour
120− ///
121− /// Usage: `foo`
122− Fuzzy,
123− /// Item contains query atom as a continuous substring
124− ///
125− /// Usage `'foo`
126− Substring,
127− /// Item starts with query atom
128− ///
129− /// Usage: `^foo`
130− Prefix,
131− /// Item ends with query atom
132− ///
133− /// Usage: `foo$`
134− Postfix,
135− /// Item is equal to query atom
136− ///
137− /// Usage `^foo$`
138− Exact,
139−}
140−
141−#[derive(Default)]
142−pub struct FuzzyQuery {
143− first_fuzzy_atom: Option<String>,
144− query_atoms: Vec<QueryAtom>,
145−}
146−
147−fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
148− let mut saw_backslash = false;
149− query.split(move |c| {
150− saw_backslash = match c {
151− ' ' if !saw_backslash => return true,
152− '\\' => true,
153− _ => false,
154− };
155− false
156− })
157−}
158−
159−impl FuzzyQuery {
160− pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
161− // TODO: we could be a lot smarter about this
162− let new_query = Self::new(query);
163− let mut is_refinement = query.starts_with(old_query);
164−
165− // if the last atom is an inverse atom adding more text to it
166− // will actually increase the number of matches and we can not refine
167− // the matches.
168− if is_refinement && !self.query_atoms.is_empty() {
169− let last_idx = self.query_atoms.len() - 1;
170− if self.query_atoms[last_idx].inverse
171− && self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
172− {
173− is_refinement = false;
174− }
175− }
176−
177− (new_query, is_refinement)
178− }
179−
180− pub fn new(query: &str) -> FuzzyQuery {
181− let mut first_fuzzy_query = None;
182− let query_atoms = query_atoms(query)
183− .filter_map(|atom| {
184− let atom = QueryAtom::new(atom)?;
185− if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
186− first_fuzzy_query = Some(atom.atom);
187− None
188− } else {
189− Some(atom)
190− }
191− })
192− .collect();
193− FuzzyQuery {
194− first_fuzzy_atom: first_fuzzy_query,
195− query_atoms,
196− }
197− }
198−
199− pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
200− // use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
201− // this behaviour matches fzf and skim
202− let score = self
203− .first_fuzzy_atom
204− .as_ref()
205− .map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
206− if self
207− .query_atoms
208− .iter()
209− .any(|atom| !atom.matches(matcher, item))
210− {
211− return None;
212− }
213− Some(score)
214− }
215−
216− pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
217− let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
218− || Some((0, Vec::new())),
219− |atom| matcher.fuzzy_indices(item, atom),
220− )?;
221−
222− // fast path for the common case of just a single atom
223− if self.query_atoms.is_empty() {
224− return Some((score, indices));
225− }
226−
227− for atom in &self.query_atoms {
228− if !atom.indices(matcher, item, &mut indices) {
229− return None;
230− }
231− }
232−
233− // deadup and remove duplicate matches
234− indices.sort_unstable();
235− indices.dedup();
236−
237− Some((score, indices))
238− }
239−}
helix-term/src/ui/fuzzy_match/test.rsdeleted−47
@@ -1,47 +0,0 @@
1−use crate::ui::fuzzy_match::FuzzyQuery;
2−use crate::ui::fuzzy_match::Matcher;
3−
4−fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
5− let query = FuzzyQuery::new(query);
6− let matcher = Matcher::default();
7− items
8− .iter()
9− .filter_map(|item| {
10− let (_, indices) = query.fuzzy_indices(item, &matcher)?;
11− let matched_string = indices
12− .iter()
13− .map(|&pos| item.chars().nth(pos).unwrap())
14− .collect();
15− Some(matched_string)
16− })
17− .collect()
18−}
19−
20−#[test]
21−fn match_single_value() {
22− let matches = run_test("foo", &["foobar", "foo", "bar"]);
23− assert_eq!(matches, &["foo", "foo"])
24−}
25−
26−#[test]
27−fn match_multiple_values() {
28− let matches = run_test(
29− "foo bar",
30− &["foo bar", "foo bar", "bar foo", "bar", "foo"],
31− );
32− assert_eq!(matches, &["foobar", "foobar", "barfoo"])
33−}
34−
35−#[test]
36−fn space_escape() {
37− let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
38− assert_eq!(matches, &["foo bar"])
39−}
40−
41−#[test]
42−fn trim() {
43− let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
44− assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
45− let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
46− assert_eq!(matches, &["bar foo"])
47−}
helix-term/src/ui/lsp.rs+5−3
@@ -62,7 +62,7 @@ impl Component for SignatureHelp {
6262 });
6363
6464 let sig_text = crate::ui::markdown::highlighted_code_block(
65− self.signature.clone(),
65+ &self.signature,
6666 &self.language,
6767 Some(&cx.editor.theme),
6868 Arc::clone(&self.config_loader),
@@ -92,7 +92,9 @@ impl Component for SignatureHelp {
9292 Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
9393 };
9494 let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
95− let sig_doc_area = area.clip_top(sig_text_area.height + 2);
95+ let sig_doc_area = area
96+ .clip_top(sig_text_area.height + 2)
97+ .clip_bottom(u16::from(cx.editor.popup_border()));
9698 let sig_doc_para = Paragraph::new(sig_doc)
9799 .wrap(Wrap { trim: false })
98100 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
@@ -109,7 +111,7 @@ impl Component for SignatureHelp {
109111 let max_text_width = (viewport.0 - PADDING).min(120);
110112
111113 let signature_text = crate::ui::markdown::highlighted_code_block(
112− self.signature.clone(),
114+ &self.signature,
113115 &self.language,
114116 None,
115117 Arc::clone(&self.config_loader),
helix-term/src/ui/markdown.rs+19−12
@@ -10,14 +10,15 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
1010
1111use helix_core::{
1212 syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
13− Rope,
13+ RopeSlice,
1414};
1515use helix_view::{
1616 graphics::{Margin, Rect, Style},
17+ theme::Modifier,
1718 Theme,
1819};
1920
20−fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
21+fn styled_multiline_text<'a>(text: &str, style: Style) -> Text<'a> {
2122 let spans: Vec<_> = text
2223 .lines()
2324 .map(|line| Span::styled(line.to_string(), style))
@@ -27,7 +28,7 @@ fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
2728}
2829
2930pub fn highlighted_code_block<'a>(
30− text: String,
31+ text: &str,
3132 language: &str,
3233 theme: Option<&Theme>,
3334 config_loader: Arc<syntax::Loader>,
@@ -45,13 +46,13 @@ pub fn highlighted_code_block<'a>(
4546 None => return styled_multiline_text(text, code_style),
4647 };
4748
48− let rope = Rope::from(text.as_ref());
49+ let ropeslice = RopeSlice::from(text);
4950 let syntax = config_loader
5051 .language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
5152 language.into(),
5253 ))
5354 .and_then(|config| config.highlight_config(theme.scopes()))
54− .and_then(|config| Syntax::new(&rope, config, Arc::clone(&config_loader)));
55+ .and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
5556
5657 let syntax = match syntax {
5758 Some(s) => s,
@@ -59,7 +60,7 @@ pub fn highlighted_code_block<'a>(
5960 };
6061
6162 let highlight_iter = syntax
62− .highlight_iter(rope.slice(..), None, None)
63+ .highlight_iter(ropeslice, None, None)
6364 .map(|e| e.unwrap());
6465 let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
6566 if let Some(spans) = additional_highlight_spans {
@@ -183,7 +184,9 @@ impl Markdown {
183184 // Transform text in `<code>` blocks into `Event::Code`
184185 let mut in_code = false;
185186 let parser = parser.filter_map(|event| match event {
186− Event::Html(tag) if *tag == *"<code>" => {
187+ Event::Html(tag)
188+ if tag.starts_with("<code") && matches!(tag.chars().nth(5), Some(' ' | '>')) =>
189+ {
187190 in_code = true;
188191 None
189192 }
@@ -267,7 +270,7 @@ impl Markdown {
267270 CodeBlockKind::Indented => "",
268271 };
269272 let tui_text = highlighted_code_block(
270− text.to_string(),
273+ &text,
271274 language,
272275 theme,
273276 Arc::clone(&self.config_loader),
@@ -275,17 +278,21 @@ impl Markdown {
275278 );
276279 lines.extend(tui_text.lines.into_iter());
277280 } else {
278− let style = if let Some(Tag::Heading(level, ..)) = tags.last() {
279− match level {
281+ let style = match tags.last() {
282+ Some(Tag::Heading(level, ..)) => match level {
280283 HeadingLevel::H1 => heading_styles[0],
281284 HeadingLevel::H2 => heading_styles[1],
282285 HeadingLevel::H3 => heading_styles[2],
283286 HeadingLevel::H4 => heading_styles[3],
284287 HeadingLevel::H5 => heading_styles[4],
285288 HeadingLevel::H6 => heading_styles[5],
289+ },
290+ Some(Tag::Emphasis) => text_style.add_modifier(Modifier::ITALIC),
291+ Some(Tag::Strong) => text_style.add_modifier(Modifier::BOLD),
292+ Some(Tag::Strikethrough) => {
293+ text_style.add_modifier(Modifier::CROSSED_OUT)
286294 }
287− } else {
288− text_style
295+ _ => text_style,
289296 };
290297 spans.push(Span::styled(text, style));
291298 }
helix-term/src/ui/menu.rs+73−40
@@ -1,22 +1,29 @@
1−use std::{borrow::Cow, path::PathBuf};
1+use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
22
33use crate::{
44 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
55 ctrl, key, shift,
66};
7−use tui::{buffer::Buffer as Surface, widgets::Table};
7+use helix_core::fuzzy::MATCHER;
8+use nucleo::pattern::{Atom, AtomKind, CaseMatching};
9+use nucleo::{Config, Utf32Str};
10+use tui::{
11+ buffer::Buffer as Surface,
12+ widgets::{Block, Borders, Table, Widget},
13+};
814
915pub use tui::widgets::{Cell, Row};
1016
11−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12−use fuzzy_matcher::FuzzyMatcher;
13−
14−use helix_view::{graphics::Rect, Editor};
17+use helix_view::{
18+ editor::SmartTabConfig,
19+ graphics::{Margin, Rect},
20+ Editor,
21+};
1522use tui::layout::Constraint;
1623
17−pub trait Item {
24+pub trait Item: Sync + Send + 'static {
1825 /// Additional editor state that is used for label calculation.
19− type Data;
26+ type Data: Sync + Send + 'static;
2027
2128 fn format(&self, data: &Self::Data) -> Row;
2229
@@ -51,9 +58,8 @@ pub struct Menu<T: Item> {
5158
5259 cursor: Option<usize>,
5360
54− matcher: Box<Matcher>,
5561 /// (index, score)
56− matches: Vec<(usize, i64)>,
62+ matches: Vec<(u32, u32)>,
5763
5864 widths: Vec<Constraint>,
5965
@@ -75,11 +81,10 @@ impl<T: Item> Menu<T> {
7581 editor_data: <T as Item>::Data,
7682 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
7783 ) -> Self {
78− let matches = (0..options.len()).map(|i| (i, 0)).collect();
84+ let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
7985 Self {
8086 options,
8187 editor_data,
82− matcher: Box::new(Matcher::default().ignore_case()),
8388 matches,
8489 cursor: None,
8590 widths: Vec::new(),
@@ -94,20 +99,19 @@ impl<T: Item> Menu<T> {
9499 pub fn score(&mut self, pattern: &str) {
95100 // reuse the matches allocation
96101 self.matches.clear();
97− self.matches.extend(
98− self.options
99− .iter()
100− .enumerate()
101− .filter_map(|(index, option)| {
102− let text = option.filter_text(&self.editor_data);
103− // TODO: using fuzzy_indices could give us the char idx for match highlighting
104− self.matcher
105− .fuzzy_match(&text, pattern)
106− .map(|score| (index, score))
107− }),
108− );
109− // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority
110− self.matches.sort_by_key(|(_, score)| -score);
102+ let mut matcher = MATCHER.lock();
103+ matcher.config = Config::DEFAULT;
104+ let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
105+ let mut buf = Vec::new();
106+ let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
107+ let text = option.filter_text(&self.editor_data);
108+ pattern
109+ .score(Utf32Str::new(&text, &mut buf), &mut matcher)
110+ .map(|score| (i as u32, score as u32))
111+ });
112+ self.matches.extend(matches);
113+ self.matches
114+ .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
111115
112116 // reset cursor position
113117 self.cursor = None;
@@ -201,7 +205,7 @@ impl<T: Item> Menu<T> {
201205 self.cursor.and_then(|cursor| {
202206 self.matches
203207 .get(cursor)
204− .map(|(index, _score)| &self.options[*index])
208+ .map(|(index, _score)| &self.options[*index as usize])
205209 })
206210 }
207211
@@ -209,7 +213,7 @@ impl<T: Item> Menu<T> {
209213 self.cursor.and_then(|cursor| {
210214 self.matches
211215 .get(cursor)
212− .map(|(index, _score)| &mut self.options[*index])
216+ .map(|(index, _score)| &mut self.options[*index as usize])
213217 })
214218 }
215219
@@ -247,6 +251,21 @@ impl<T: Item + 'static> Component for Menu<T> {
247251 compositor.pop();
248252 }));
249253
254+ // Ignore tab key when supertab is turned on in order not to interfere
255+ // with it. (Is there a better way to do this?)
256+ if (event == key!(Tab) || event == shift!(Tab))
257+ && cx.editor.config().auto_completion
258+ && matches!(
259+ cx.editor.config().smart_tab,
260+ Some(SmartTabConfig {
261+ enable: true,
262+ supersede_menu: true,
263+ })
264+ )
265+ {
266+ return EventResult::Ignored(None);
267+ }
268+
250269 match event {
251270 // esc or ctrl-c aborts the completion and closes the menu
252271 key!(Esc) | ctrl!('c') => {
@@ -310,6 +329,15 @@ impl<T: Item + 'static> Component for Menu<T> {
310329 let selected = theme.get("ui.menu.selected");
311330 surface.clear_with(area, style);
312331
332+ let render_borders = cx.editor.menu_border();
333+
334+ let area = if render_borders {
335+ Widget::render(Block::default().borders(Borders::ALL), area, surface);
336+ area.inner(&Margin::vertical(1))
337+ } else {
338+ area
339+ };
340+
313341 let scroll = self.scroll;
314342
315343 let options: Vec<_> = self
@@ -317,7 +345,7 @@ impl<T: Item + 'static> Component for Menu<T> {
317345 .iter()
318346 .map(|(index, _score)| {
319347 // (index, self.options.get(*index).unwrap()) // get_unchecked
320− &self.options[*index] // get_unchecked
348+ &self.options[*index as usize] // get_unchecked
321349 })
322350 .collect();
323351
@@ -350,15 +378,19 @@ impl<T: Item + 'static> Component for Menu<T> {
350378 false,
351379 );
352380
353− if let Some(cursor) = self.cursor {
354− let offset_from_top = cursor - scroll;
355− let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
356− left.set_style(selected);
357− let right = &mut surface[(
358− area.right().saturating_sub(1),
359− area.y + offset_from_top as u16,
360− )];
361− right.set_style(selected);
381+ let render_borders = cx.editor.menu_border();
382+
383+ if !render_borders {
384+ if let Some(cursor) = self.cursor {
385+ let offset_from_top = cursor - scroll;
386+ let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
387+ left.set_style(selected);
388+ let right = &mut surface[(
389+ area.right().saturating_sub(1),
390+ area.y + offset_from_top as u16,
391+ )];
392+ right.set_style(selected);
393+ }
362394 }
363395
364396 let fits = len <= win_height;
@@ -373,12 +405,13 @@ impl<T: Item + 'static> Component for Menu<T> {
373405 for i in 0..win_height {
374406 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
375407
376− cell.set_symbol("▐"); // right half block
408+ let half_block = if render_borders { "▌" } else { "▐" };
377409
378410 if scroll_line <= i && i < scroll_line + scroll_height {
379411 // Draw scroll thumb
412+ cell.set_symbol(half_block);
380413 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
381− } else {
414+ } else if !render_borders {
382415 // Draw scroll track
383416 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
384417 }
helix-term/src/ui/mod.rs+90−149
@@ -1,13 +1,12 @@
11mod completion;
22mod document;
33pub(crate) mod editor;
4−mod fuzzy_match;
54mod info;
65pub mod lsp;
76mod markdown;
87pub mod menu;
98pub mod overlay;
10−mod picker;
9+pub mod picker;
1110pub mod popup;
1211mod prompt;
1312mod spinner;
@@ -64,7 +63,7 @@ pub fn regex_prompt(
6463 prompt: std::borrow::Cow<'static, str>,
6564 history_register: Option<char>,
6665 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
67− fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
66+ fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
6867) {
6968 let (view, doc) = current!(cx.editor);
7069 let doc_id = view.doc;
@@ -111,7 +110,7 @@ pub fn regex_prompt(
111110 view.jumps.push((doc_id, snapshot.clone()));
112111 }
113112
114− fun(cx.editor, regex, event);
113+ fun(cx, regex, event);
115114
116115 let (view, doc) = current!(cx.editor);
117116 view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -142,16 +141,14 @@ pub fn regex_prompt(
142141 };
143142
144143 cx.jobs.callback(callback);
145− } else {
146− // Update
147− // TODO: mark command line as error
148144 }
149145 }
150146 }
151147 }
152148 }
153149 },
154− );
150+ )
151+ .with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader));
155152 // Calculate initial completion
156153 prompt.recalculate_completion(cx.editor);
157154 // prompt
@@ -176,9 +173,13 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
176173 .git_ignore(config.file_picker.git_ignore)
177174 .git_global(config.file_picker.git_global)
178175 .git_exclude(config.file_picker.git_exclude)
176+ .sort_by_file_name(|name1, name2| name1.cmp(name2))
179177 .max_depth(config.file_picker.max_depth)
180178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
181179
180+ walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
181+ walk_builder.add_custom_ignore_filename(".helix/ignore");
182+
182183 // We want to exclude files that the editor can't handle yet
183184 let mut type_builder = TypesBuilder::new();
184185 type_builder
@@ -192,32 +193,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
192193 .build()
193194 .expect("failed to build excluded_types");
194195 walk_builder.types(excluded_types);
195−
196− // We want files along with their modification date for sorting
197− let files = walk_builder.build().filter_map(|entry| {
196+ let mut files = walk_builder.build().filter_map(|entry| {
198197 let entry = entry.ok()?;
199− // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
200− if entry.file_type()?.is_file() {
201− Some(entry.into_path())
202− } else {
203− None
198+ if !entry.file_type()?.is_file() {
199+ return None;
204200 }
201+ Some(entry.into_path())
205202 });
206−
207− // Cap the number of files if we aren't in a git project, preventing
208− // hangs when using the picker in your home directory
209− let mut files: Vec<PathBuf> = if root.join(".git").exists() {
210− files.collect()
211− } else {
212− // const MAX: usize = 8192;
213− const MAX: usize = 100_000;
214− files.take(MAX).collect()
215− };
216− files.sort();
217−
218203 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
219204
220− Picker::new(files, root, move |cx, path: &PathBuf, action| {
205+ let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
221206 if let Err(e) = cx.editor.open(path, action) {
222207 let err = if let Some(err) = e.source() {
223208 format!("{}", err)
@@ -227,20 +212,41 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
227212 cx.editor.set_error(err);
228213 }
229214 })
230− .with_preview(|_editor, path| Some((path.clone().into(), None)))
215+ .with_preview(|_editor, path| Some((path.clone().into(), None)));
216+ let injector = picker.injector();
217+ let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
218+
219+ let mut hit_timeout = false;
220+ for file in &mut files {
221+ if injector.push(file).is_err() {
222+ break;
223+ }
224+ if std::time::Instant::now() >= timeout {
225+ hit_timeout = true;
226+ break;
227+ }
228+ }
229+ if hit_timeout {
230+ std::thread::spawn(move || {
231+ for file in files {
232+ if injector.push(file).is_err() {
233+ break;
234+ }
235+ }
236+ });
237+ }
238+ picker
231239}
232240
233241pub mod completers {
234242 use crate::ui::prompt::Completion;
235− use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
236− use fuzzy_matcher::FuzzyMatcher;
243+ use helix_core::fuzzy::fuzzy_match;
237244 use helix_core::syntax::LanguageServerFeature;
238245 use helix_view::document::SCRATCH_BUFFER_NAME;
239246 use helix_view::theme;
240247 use helix_view::{editor::Config, Editor};
241248 use once_cell::sync::Lazy;
242249 use std::borrow::Cow;
243− use std::cmp::Reverse;
244250
245251 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
246252
@@ -249,31 +255,16 @@ pub mod completers {
249255 }
250256
251257 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
252− let mut names: Vec<_> = editor
253− .documents
254− .values()
255− .map(|doc| {
256− let name = doc
257− .relative_path()
258− .map(|p| p.display().to_string())
259− .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
260− ((0..), Cow::from(name))
261− })
262− .collect();
263−
264− let matcher = Matcher::default();
265−
266− let mut matches: Vec<_> = names
267− .into_iter()
268− .filter_map(|(_range, name)| {
269− matcher.fuzzy_match(&name, input).map(|score| (name, score))
270− })
271− .collect();
272−
273− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
274− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
258+ let names = editor.documents.values().map(|doc| {
259+ doc.relative_path()
260+ .map(|p| p.display().to_string().into())
261+ .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
262+ });
275263
276− names
264+ fuzzy_match(input, names, true)
265+ .into_iter()
266+ .map(|(name, _)| ((0..), name))
267+ .collect()
277268 }
278269
279270 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@@ -286,26 +277,10 @@ pub mod completers {
286277 names.sort();
287278 names.dedup();
288279
289− let mut names: Vec<_> = names
280+ fuzzy_match(input, names, false)
290281 .into_iter()
291− .map(|name| ((0..), Cow::from(name)))
292− .collect();
293−
294− let matcher = Matcher::default();
295−
296− let mut matches: Vec<_> = names
297− .into_iter()
298− .filter_map(|(_range, name)| {
299− matcher.fuzzy_match(&name, input).map(|score| (name, score))
300− })
301− .collect();
302−
303− matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
304− (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
305− });
306− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
307−
308− names
282+ .map(|(name, _)| ((0..), name.into()))
283+ .collect()
309284 }
310285
311286 /// Recursive function to get all keys from this value and add them to vec
@@ -332,22 +307,22 @@ pub mod completers {
332307 keys
333308 });
334309
335− let matcher = Matcher::default();
336−
337− let mut matches: Vec<_> = KEYS
338− .iter()
339− .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
340− .collect();
341−
342− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
343− matches
310+ fuzzy_match(input, &*KEYS, false)
344311 .into_iter()
345312 .map(|(name, _)| ((0..), name.into()))
346313 .collect()
347314 }
348315
349316 pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> {
350− filename_impl(editor, input, |entry| {
317+ filename_with_git_ignore(editor, input, true)
318+ }
319+
320+ pub fn filename_with_git_ignore(
321+ editor: &Editor,
322+ input: &str,
323+ git_ignore: bool,
324+ ) -> Vec<Completion> {
325+ filename_impl(editor, input, git_ignore, |entry| {
351326 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
352327
353328 if is_dir {
@@ -359,8 +334,6 @@ pub mod completers {
359334 }
360335
361336 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
362− let matcher = Matcher::default();
363−
364337 let text: String = "text".into();
365338
366339 let language_ids = editor
@@ -369,27 +342,13 @@ pub mod completers {
369342 .map(|config| &config.language_id)
370343 .chain(std::iter::once(&text));
371344
372− let mut matches: Vec<_> = language_ids
373− .filter_map(|language_id| {
374− matcher
375− .fuzzy_match(language_id, input)
376− .map(|score| (language_id, score))
377− })
378− .collect();
379−
380− matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
381− (Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
382− });
383−
384− matches
345+ fuzzy_match(input, language_ids, false)
385346 .into_iter()
386− .map(|(language, _score)| ((0..), language.clone().into()))
347+ .map(|(name, _)| ((0..), name.to_owned().into()))
387348 .collect()
388349 }
389350
390351 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
391− let matcher = Matcher::default();
392−
393352 let Some(options) = doc!(editor)
394353 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
395354 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@@ -397,28 +356,22 @@ pub mod completers {
397356 return vec![];
398357 };
399358
400− let mut matches: Vec<_> = options
401− .commands
402− .iter()
403− .filter_map(|command| {
404− matcher
405− .fuzzy_match(command, input)
406− .map(|score| (command, score))
407− })
408− .collect();
409−
410− matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
411− (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
412− });
413−
414− matches
359+ fuzzy_match(input, &options.commands, false)
415360 .into_iter()
416− .map(|(command, _score)| ((0..), command.clone().into()))
361+ .map(|(name, _)| ((0..), name.to_owned().into()))
417362 .collect()
418363 }
419364
420365 pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
421− filename_impl(editor, input, |entry| {
366+ directory_with_git_ignore(editor, input, true)
367+ }
368+
369+ pub fn directory_with_git_ignore(
370+ editor: &Editor,
371+ input: &str,
372+ git_ignore: bool,
373+ ) -> Vec<Completion> {
374+ filename_impl(editor, input, git_ignore, |entry| {
422375 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
423376
424377 if is_dir {
@@ -441,7 +394,12 @@ pub mod completers {
441394 }
442395
443396 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
444− fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion>
397+ fn filename_impl<F>(
398+ _editor: &Editor,
399+ input: &str,
400+ git_ignore: bool,
401+ filter_fn: F,
402+ ) -> Vec<Completion>
445403 where
446404 F: Fn(&ignore::DirEntry) -> FileMatch,
447405 {
@@ -481,9 +439,10 @@ pub mod completers {
481439
482440 let end = input.len()..;
483441
484− let mut files: Vec<_> = WalkBuilder::new(&dir)
442+ let files = WalkBuilder::new(&dir)
485443 .hidden(false)
486444 .follow_links(false) // We're scanning over depth 1
445+ .git_ignore(git_ignore)
487446 .max_depth(Some(1))
488447 .build()
489448 .filter_map(|file| {
@@ -512,43 +471,25 @@ pub mod completers {
512471 path.push("");
513472 }
514473
515− let path = path.to_str()?.to_owned();
516− Some((end.clone(), Cow::from(path)))
474+ let path = path.into_os_string().into_string().ok()?;
475+ Some(Cow::from(path))
517476 })
518477 }) // TODO: unwrap or skip
519− .filter(|(_, path)| !path.is_empty()) // TODO
520− .collect();
478+ .filter(|path| !path.is_empty());
521479
522480 // if empty, return a list of dirs and files in current dir
523481 if let Some(file_name) = file_name {
524− let matcher = Matcher::default();
525−
526− // inefficient, but we need to calculate the scores, filter out None, then sort.
527− let mut matches: Vec<_> = files
528− .into_iter()
529− .filter_map(|(_range, file)| {
530− matcher
531− .fuzzy_match(&file, &file_name)
532− .map(|score| (file, score))
533− })
534− .collect();
535−
536482 let range = (input.len().saturating_sub(file_name.len()))..;
537−
538− matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
539− (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
540− });
541−
542− files = matches
483+ fuzzy_match(&file_name, files, true)
543484 .into_iter()
544− .map(|(file, _)| (range.clone(), file))
545− .collect();
485+ .map(|(name, _)| (range.clone(), name))
486+ .collect()
546487
547488 // TODO: complete to longest common match
548489 } else {
490+ let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
549491 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
492+ files
550493 }
551−
552− files
553494 }
554495}
helix-term/src/ui/picker.rs+362−325
@@ -7,11 +7,12 @@ use crate::{
77 ui::{
88 self,
99 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
1110 EditorView,
1211 },
1312};
1413use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
1516use tui::{
1617 buffer::Buffer as Surface,
1718 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
1920 widgets::{Block, BorderType, Borders, Cell, Table},
2021};
2122
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2323use tui::widgets::Widget;
2424
25−use std::cmp::{self, Ordering};
26−use std::{collections::HashMap, io::Read, path::PathBuf};
25+use std::{
26+ collections::HashMap,
27+ io::Read,
28+ path::PathBuf,
29+ sync::{
30+ atomic::{self, AtomicBool},
31+ Arc,
32+ },
33+};
2734
2835use crate::ui::{Prompt, PromptEvent};
2936use helix_core::{
30− movement::Direction, text_annotations::TextAnnotations,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39+ Syntax,
3240};
3341use helix_view::{
3442 editor::Action,
@@ -38,6 +46,7 @@ use helix_view::{
3846 Document, DocumentId, Editor,
3947};
4048
49+pub const ID: &str = "picker";
4150use super::{menu::Item, overlay::Overlay};
4251
4352pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
@@ -51,12 +60,12 @@ pub enum PathOrId {
5160}
5261
5362impl PathOrId {
54− fn get_canonicalized(self) -> std::io::Result<Self> {
63+ fn get_canonicalized(self) -> Self {
5564 use PathOrId::*;
56− Ok(match self {
57− Path(path) => Path(helix_core::path::get_canonicalized_path(&path)?),
65+ match self {
66+ Path(path) => Path(helix_core::path::get_canonicalized_path(&path)),
5867 Id(id) => Id(id),
59− })
68+ }
6069 }
6170}
6271
@@ -103,9 +112,9 @@ impl Preview<'_, '_> {
103112 /// Alternate text to show for the preview.
104113 fn placeholder(&self) -> &str {
105114 match *self {
106− Self::EditorDocument(_) => "<File preview>",
115+ Self::EditorDocument(_) => "<Invalid file location>",
107116 Self::Cached(preview) => match preview {
108− CachedPreview::Document(_) => "<File preview>",
117+ CachedPreview::Document(_) => "<Invalid file location>",
109118 CachedPreview::Binary => "<Binary file>",
110119 CachedPreview::LargeFile => "<File too large to preview>",
111120 CachedPreview::NotFound => "<File not found>",
@@ -114,20 +123,71 @@ impl Preview<'_, '_> {
114123 }
115124}
116125
126+fn item_to_nucleo<T: Item>(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> {
127+ let row = item.format(editor_data);
128+ let mut cells = row.cells.iter();
129+ let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum());
130+ let cell = cells.next()?;
131+ if let Some(cell) = cell.content.lines.first() {
132+ for span in &cell.0 {
133+ text.push_str(&span.content);
134+ }
135+ }
136+
137+ for cell in cells {
138+ text.push(' ');
139+ if let Some(cell) = cell.content.lines.first() {
140+ for span in &cell.0 {
141+ text.push_str(&span.content);
142+ }
143+ }
144+ }
145+ Some((item, text.into()))
146+}
147+
148+pub struct Injector<T: Item> {
149+ dst: nucleo::Injector<T>,
150+ editor_data: Arc<T::Data>,
151+ shutown: Arc<AtomicBool>,
152+}
153+
154+impl<T: Item> Clone for Injector<T> {
155+ fn clone(&self) -> Self {
156+ Injector {
157+ dst: self.dst.clone(),
158+ editor_data: self.editor_data.clone(),
159+ shutown: self.shutown.clone(),
160+ }
161+ }
162+}
163+
164+pub struct InjectorShutdown;
165+
166+impl<T: Item> Injector<T> {
167+ pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
168+ if self.shutown.load(atomic::Ordering::Relaxed) {
169+ return Err(InjectorShutdown);
170+ }
171+
172+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
173+ self.dst.push(item, |dst| dst[0] = matcher_text);
174+ }
175+ Ok(())
176+ }
177+}
178+
117179pub struct Picker<T: Item> {
118− options: Vec<T>,
119− editor_data: T::Data,
120− // filter: String,
121− matcher: Box<Matcher>,
122− matches: Vec<PickerMatch>,
180+ editor_data: Arc<T::Data>,
181+ shutdown: Arc<AtomicBool>,
182+ matcher: Nucleo<T>,
123183
124184 /// Current height of the completions box
125185 completion_height: u16,
126186
127− cursor: usize,
128− // pattern: String,
187+ cursor: u32,
129188 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
189+ previous_pattern: String,
190+
131191 /// Whether to show the preview panel (default true)
132192 show_preview: bool,
133193 /// Constraints for tabular formatting
@@ -144,10 +204,59 @@ pub struct Picker<T: Item> {
144204}
145205
146206impl<T: Item + 'static> Picker<T> {
207+ pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
208+ let matcher = Nucleo::new(
209+ Config::DEFAULT,
210+ Arc::new(helix_event::request_redraw),
211+ None,
212+ 1,
213+ );
214+ let streamer = Injector {
215+ dst: matcher.injector(),
216+ editor_data: Arc::new(editor_data),
217+ shutown: Arc::new(AtomicBool::new(false)),
218+ };
219+ (matcher, streamer)
220+ }
221+
147222 pub fn new(
148223 options: Vec<T>,
149224 editor_data: T::Data,
150225 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
226+ ) -> Self {
227+ let matcher = Nucleo::new(
228+ Config::DEFAULT,
229+ Arc::new(helix_event::request_redraw),
230+ None,
231+ 1,
232+ );
233+ let injector = matcher.injector();
234+ for item in options {
235+ if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) {
236+ injector.push(item, |dst| dst[0] = matcher_text);
237+ }
238+ }
239+ Self::with(
240+ matcher,
241+ Arc::new(editor_data),
242+ Arc::new(AtomicBool::new(false)),
243+ callback_fn,
244+ )
245+ }
246+
247+ pub fn with_stream(
248+ matcher: Nucleo<T>,
249+ injector: Injector<T>,
250+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
251+ ) -> Self {
252+ Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
253+ }
254+
255+ fn with(
256+ matcher: Nucleo<T>,
257+ editor_data: Arc<T::Data>,
258+ shutdown: Arc<AtomicBool>,
259+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
151260 ) -> Self {
152261 let prompt = Prompt::new(
153262 "".into(),
@@ -156,14 +265,13 @@ impl<T: Item + 'static> Picker<T> {
156265 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
157266 );
158267
159− let mut picker = Self {
160− options,
268+ Self {
269+ matcher,
161270 editor_data,
162− matcher: Box::default(),
163− matches: Vec::new(),
271+ shutdown,
164272 cursor: 0,
165273 prompt,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
274+ previous_pattern: String::new(),
167275 truncate_start: true,
168276 show_preview: true,
169277 callback_fn: Box::new(callback_fn),
@@ -172,24 +280,15 @@ impl<T: Item + 'static> Picker<T> {
172280 preview_cache: HashMap::new(),
173281 read_buffer: Vec::with_capacity(1024),
174282 file_fn: None,
175− };
176−
177− picker.calculate_column_widths();
178−
179− // scoring on empty input
180− // TODO: just reuse score()
181− picker
182− .matches
183− .extend(picker.options.iter().enumerate().map(|(index, option)| {
184− let text = option.filter_text(&picker.editor_data);
185− PickerMatch {
186− index,
187− score: 0,
188− len: text.chars().count(),
189− }
190− }));
283+ }
284+ }
191285
192− picker
286+ pub fn injector(&self) -> Injector<T> {
287+ Injector {
288+ dst: self.matcher.injector(),
289+ editor_data: self.editor_data.clone(),
290+ shutown: self.shutdown.clone(),
291+ }
193292 }
194293
195294 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +301,25 @@ impl<T: Item + 'static> Picker<T> {
202301 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203302 ) -> Self {
204303 self.file_fn = Some(Box::new(preview_fn));
304+ // assumption: if we have a preview we are matching paths... If this is ever
305+ // not true this could be a separate builder function
306+ self.matcher.update_config(Config::DEFAULT.match_paths());
205307 self
206308 }
207309
208310 pub fn set_options(&mut self, new_options: Vec<T>) {
209− self.options = new_options;
210− self.cursor = 0;
211− self.force_score();
212− self.calculate_column_widths();
213− }
214−
215− /// Calculate the width constraints using the maximum widths of each column
216− /// for the current options.
217− fn calculate_column_widths(&mut self) {
218− let n = self
219− .options
220− .first()
221− .map(|option| option.format(&self.editor_data).cells.len())
222− .unwrap_or_default();
223− let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
224− let row = option.format(&self.editor_data);
225− // maintain max for each column
226− for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
227− let width = cell.content.width();
228− if width > *acc {
229− *acc = width;
230− }
311+ self.matcher.restart(false);
312+ let injector = self.matcher.injector();
313+ for item in new_options {
314+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
315+ injector.push(item, |dst| dst[0] = matcher_text);
231316 }
232− acc
233− });
234− self.widths = max_lens
235− .into_iter()
236− .map(|len| Constraint::Length(len as u16))
237− .collect();
238− }
239−
240− pub fn score(&mut self) {
241− let pattern = self.prompt.line();
242−
243− if pattern == &self.previous_pattern.0 {
244− return;
245317 }
246−
247− let (query, is_refined) = self
248− .previous_pattern
249− .1
250− .refine(pattern, &self.previous_pattern.0);
251−
252− if pattern.is_empty() {
253− // Fast path for no pattern.
254− self.matches.clear();
255− self.matches
256− .extend(self.options.iter().enumerate().map(|(index, option)| {
257− let text = option.filter_text(&self.editor_data);
258− PickerMatch {
259− index,
260− score: 0,
261− len: text.chars().count(),
262− }
263− }));
264− } else if is_refined {
265− // optimization: if the pattern is a more specific version of the previous one
266− // then we can score the filtered set.
267− self.matches.retain_mut(|pmatch| {
268− let option = &self.options[pmatch.index];
269− let text = option.sort_text(&self.editor_data);
270−
271− match query.fuzzy_match(&text, &self.matcher) {
272− Some(s) => {
273− // Update the score
274− pmatch.score = s;
275− true
276− }
277− None => false,
278− }
279− });
280−
281− self.matches.sort_unstable();
282− } else {
283− self.force_score();
284− }
285−
286− // reset cursor position
287− self.cursor = 0;
288− let pattern = self.prompt.line();
289− self.previous_pattern.0.clone_from(pattern);
290− self.previous_pattern.1 = query;
291− }
292−
293− pub fn force_score(&mut self) {
294− let pattern = self.prompt.line();
295−
296− let query = FuzzyQuery::new(pattern);
297− self.matches.clear();
298− self.matches.extend(
299− self.options
300− .iter()
301− .enumerate()
302− .filter_map(|(index, option)| {
303− let text = option.filter_text(&self.editor_data);
304−
305− query
306− .fuzzy_match(&text, &self.matcher)
307− .map(|score| PickerMatch {
308− index,
309− score,
310− len: text.chars().count(),
311− })
312− }),
313− );
314−
315− self.matches.sort_unstable();
316318 }
317319
318320 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319− pub fn move_by(&mut self, amount: usize, direction: Direction) {
320− let len = self.matches.len();
321+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
322+ let len = self.matcher.snapshot().matched_item_count();
321323
322324 if len == 0 {
323325 // No results, can't move.
@@ -336,12 +338,12 @@ impl<T: Item + 'static> Picker<T> {
336338
337339 /// Move the cursor down by exactly one page. After the last page comes the first page.
338340 pub fn page_up(&mut self) {
339− self.move_by(self.completion_height as usize, Direction::Backward);
341+ self.move_by(self.completion_height as u32, Direction::Backward);
340342 }
341343
342344 /// Move the cursor up by exactly one page. After the first page comes the last page.
343345 pub fn page_down(&mut self) {
344− self.move_by(self.completion_height as usize, Direction::Forward);
346+ self.move_by(self.completion_height as u32, Direction::Forward);
345347 }
346348
347349 /// Move the cursor to the first entry
@@ -351,13 +353,18 @@ impl<T: Item + 'static> Picker<T> {
351353
352354 /// Move the cursor to the last entry
353355 pub fn to_end(&mut self) {
354− self.cursor = self.matches.len().saturating_sub(1);
356+ self.cursor = self
357+ .matcher
358+ .snapshot()
359+ .matched_item_count()
360+ .saturating_sub(1);
355361 }
356362
357363 pub fn selection(&self) -> Option<&T> {
358− self.matches
359− .get(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
364+ self.matcher
365+ .snapshot()
366+ .get_matched_item(self.cursor)
367+ .map(|item| item.data)
361368 }
362369
363370 pub fn toggle_preview(&mut self) {
@@ -366,8 +373,17 @@ impl<T: Item + 'static> Picker<T> {
366373
367374 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368375 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
369− // TODO: recalculate only if pattern changed
370− self.score();
376+ let pattern = self.prompt.line();
377+ // TODO: better track how the pattern has changed
378+ if pattern != &self.previous_pattern {
379+ self.matcher.pattern.reparse(
380+ 0,
381+ pattern,
382+ CaseMatching::Smart,
383+ pattern.starts_with(&self.previous_pattern),
384+ );
385+ self.previous_pattern = pattern.clone();
386+ }
371387 }
372388 EventResult::Consumed(None)
373389 }
@@ -375,7 +391,7 @@ impl<T: Item + 'static> Picker<T> {
375391 fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
376392 self.selection()
377393 .and_then(|current| (self.file_fn.as_ref()?)(editor, current))
378− .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
394+ .map(|(path_or_id, line)| (path_or_id.get_canonicalized(), line))
379395 }
380396
381397 /// Get (cached) preview for a given path. If a document corresponding
@@ -411,12 +427,9 @@ impl<T: Item + 'static> Picker<T> {
411427 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412428 CachedPreview::LargeFile
413429 }
414− _ => {
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
419− }
430+ _ => Document::open(path, None, None, editor.config.clone())
431+ .map(|doc| CachedPreview::Document(Box::new(doc)))
432+ .unwrap_or(CachedPreview::NotFound),
420433 },
421434 )
422435 .unwrap_or(CachedPreview::NotFound);
@@ -432,7 +445,7 @@ impl<T: Item + 'static> Picker<T> {
432445
433446 fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
434447 let Some((current_file, _)) = self.current_file(cx.editor) else {
435− return EventResult::Consumed(None)
448+ return EventResult::Consumed(None);
436449 };
437450
438451 // Try to find a document in the cache
@@ -453,17 +466,24 @@ impl<T: Item + 'static> Picker<T> {
453466 let text = doc.text().clone();
454467 let loader = cx.editor.syn_loader.clone();
455468 let job = tokio::task::spawn_blocking(move || {
456− let syntax = language_config
457− .highlight_config(&loader.scopes())
458− .and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
469+ let syntax = language_config.highlight_config(&loader.scopes()).and_then(
470+ |highlight_config| Syntax::new(text.slice(..), highlight_config, loader),
471+ );
459472 let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
460473 let Some(syntax) = syntax else {
461474 log::info!("highlighting picker item failed");
462− return
475+ return;
476+ };
477+ let picker = match compositor.find::<Overlay<Self>>() {
478+ Some(Overlay { content, .. }) => Some(content),
479+ None => compositor
480+ .find::<Overlay<DynamicPicker<T>>>()
481+ .map(|overlay| &mut overlay.content.file_picker),
463482 };
464− let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
483+ let Some(picker) = picker
484+ else {
465485 log::info!("picker closed before syntax highlighting finished");
466− return
486+ return;
467487 };
468488 // Try to find a document in the cache
469489 let doc = match current_file {
@@ -492,6 +512,14 @@ impl<T: Item + 'static> Picker<T> {
492512 }
493513
494514 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
515+ let status = self.matcher.tick(10);
516+ let snapshot = self.matcher.snapshot();
517+ if status.changed {
518+ self.cursor = self
519+ .cursor
520+ .min(snapshot.matched_item_count().saturating_sub(1))
521+ }
522+
495523 let text_style = cx.editor.theme.get("ui.text");
496524 let selected = cx.editor.theme.get("ui.text.focus");
497525 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -512,8 +540,15 @@ impl<T: Item + 'static> Picker<T> {
512540 // -- Render the input bar:
513541
514542 let area = inner.clip_left(1).with_height(1);
543+ // render the prompt first since it will clear its background
544+ self.prompt.render(area, surface, cx);
515545
516− let count = format!("{}/{}", self.matches.len(), self.options.len());
546+ let count = format!(
547+ "{}{}/{}",
548+ if status.running { "(running) " } else { "" },
549+ snapshot.matched_item_count(),
550+ snapshot.item_count(),
551+ );
517552 surface.set_stringn(
518553 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
519554 area.y,
@@ -522,8 +557,6 @@ impl<T: Item + 'static> Picker<T> {
522557 text_style,
523558 );
524559
525− self.prompt.render(area, surface, cx);
526−
527560 // -- Separator
528561 let sep_style = cx.editor.theme.get("ui.background.separator");
529562 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -536,106 +569,89 @@ impl<T: Item + 'static> Picker<T> {
536569 // -- Render the contents:
537570 // subtract area of prompt from top
538571 let inner = inner.clip_top(2);
539−
540− let rows = inner.height;
541− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
572+ let rows = inner.height as u32;
573+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
542574 let cursor = self.cursor.saturating_sub(offset);
575+ let end = offset
576+ .saturating_add(rows)
577+ .min(snapshot.matched_item_count());
578+ let mut indices = Vec::new();
579+ let mut matcher = MATCHER.lock();
580+ matcher.config = Config::DEFAULT;
581+ if self.file_fn.is_some() {
582+ matcher.config.set_match_paths()
583+ }
543584
544− let options = self
545− .matches
546− .iter()
547− .skip(offset)
548− .take(rows as usize)
549− .map(|pmatch| &self.options[pmatch.index])
550− .map(|option| option.format(&self.editor_data))
551− .map(|mut row| {
552− const TEMP_CELL_SEP: &str = " ";
553−
554− let line = row.cell_text().fold(String::new(), |mut s, frag| {
555− s.push_str(&frag);
556− s.push_str(TEMP_CELL_SEP);
557− s
558− });
559−
560− // Items are filtered by using the text returned by menu::Item::filter_text
561− // but we do highlighting here using the text in Row and therefore there
562− // might be inconsistencies. This is the best we can do since only the
563− // text in Row is displayed to the end user.
564− let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
565− .fuzzy_indices(&line, &self.matcher)
566− .unwrap_or_default();
567−
568− let highlight_byte_ranges: Vec<_> = line
569− .char_indices()
570− .enumerate()
571− .filter_map(|(char_idx, (byte_offset, ch))| {
572− highlights
573− .contains(&char_idx)
574− .then(|| byte_offset..byte_offset + ch.len_utf8())
575− })
576− .collect();
577−
578− // The starting byte index of the current (iterating) cell
579− let mut cell_start_byte_offset = 0;
580− for cell in row.cells.iter_mut() {
581− let spans = match cell.content.lines.get(0) {
582− Some(s) => s,
583− None => {
584− cell_start_byte_offset += TEMP_CELL_SEP.len();
585− continue;
586− }
587− };
585+ let options = snapshot.matched_items(offset..end).map(|item| {
586+ snapshot.pattern().column_pattern(0).indices(
587+ item.matcher_columns[0].slice(..),
588+ &mut matcher,
589+ &mut indices,
590+ );
591+ indices.sort_unstable();
592+ indices.dedup();
593+ let mut row = item.data.format(&self.editor_data);
594+
595+ let mut grapheme_idx = 0u32;
596+ let mut indices = indices.drain(..);
597+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
598+ if self.widths.len() < row.cells.len() {
599+ self.widths.resize(row.cells.len(), Constraint::Length(0));
600+ }
601+ let mut widths = self.widths.iter_mut();
602+ for cell in &mut row.cells {
603+ let Some(Constraint::Length(max_width)) = widths.next() else {
604+ unreachable!();
605+ };
588606
589− let mut cell_len = 0;
590−
591− let graphemes_with_style: Vec<_> = spans
592− .0
593− .iter()
594− .flat_map(|span| {
595− span.content
596− .grapheme_indices(true)
597− .zip(std::iter::repeat(span.style))
598− })
599− .map(|((grapheme_byte_offset, grapheme), style)| {
600− cell_len += grapheme.len();
601− let start = cell_start_byte_offset;
602−
603− let grapheme_byte_range =
604− grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
605−
606− if highlight_byte_ranges.iter().any(|hl_rng| {
607− hl_rng.start >= start + grapheme_byte_range.start
608− && hl_rng.end <= start + grapheme_byte_range.end
609− }) {
610− (grapheme, style.patch(highlight_style))
611− } else {
612− (grapheme, style)
613− }
614− })
615− .collect();
616−
617− let mut span_list: Vec<(String, Style)> = Vec::new();
618− for (grapheme, style) in graphemes_with_style {
619− if span_list.last().map(|(_, sty)| sty) == Some(&style) {
620− let (string, _) = span_list.last_mut().unwrap();
621− string.push_str(grapheme);
607+ // merge index highlights on top of existing hightlights
608+ let mut span_list = Vec::new();
609+ let mut current_span = String::new();
610+ let mut current_style = Style::default();
611+ let mut width = 0;
612+
613+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
614+ for span in spans {
615+ // this looks like a bug on first glance, we are iterating
616+ // graphemes but treating them as char indices. The reason that
617+ // this is correct is that nucleo will only ever consider the first char
618+ // of a grapheme (and discard the rest of the grapheme) so the indices
619+ // returned by nucleo are essentially grapheme indecies
620+ for grapheme in span.content.graphemes(true) {
621+ let style = if grapheme_idx == next_highlight_idx {
622+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
623+ span.style.patch(highlight_style)
622624 } else {
623− span_list.push((String::from(grapheme), style))
625+ span.style
626+ };
627+ if style != current_style {
628+ if !current_span.is_empty() {
629+ span_list.push(Span::styled(current_span, current_style))
630+ }
631+ current_span = String::new();
632+ current_style = style;
624633 }
634+ current_span.push_str(grapheme);
635+ grapheme_idx += 1;
625636 }
637+ width += span.width();
638+ }
626639
627− let spans: Vec<Span> = span_list
628− .into_iter()
629− .map(|(string, style)| Span::styled(string, style))
630− .collect();
631− let spans: Spans = spans.into();
632− *cell = Cell::from(spans);
640+ span_list.push(Span::styled(current_span, current_style));
641+ if width as u16 > *max_width {
642+ *max_width = width as u16;
643+ }
644+ *cell = Cell::from(Spans::from(span_list));
633645
634− cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
646+ // spacer
647+ if grapheme_idx == next_highlight_idx {
648+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
635649 }
650+ grapheme_idx += 1;
651+ }
636652
637− row
638− });
653+ row
654+ });
639655
640656 let table = Table::new(options)
641657 .style(text_style)
@@ -651,7 +667,7 @@ impl<T: Item + 'static> Picker<T> {
651667 surface,
652668 &mut TableState {
653669 offset: 0,
654− selected: Some(cursor),
670+ selected: Some(cursor as usize),
655671 },
656672 self.truncate_start,
657673 );
@@ -677,8 +693,14 @@ impl<T: Item + 'static> Picker<T> {
677693 if let Some((path, range)) = self.current_file(cx.editor) {
678694 let preview = self.get_preview(path, cx.editor);
679695 let doc = match preview.document() {
680− Some(doc) => doc,
681− None => {
696+ Some(doc)
697+ if range.map_or(true, |(start, end)| {
698+ start <= end && end <= doc.text().len_lines()
699+ }) =>
700+ {
701+ doc
702+ }
703+ _ => {
682704 let alt_text = preview.placeholder();
683705 let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
684706 let y = inner.y + inner.height / 2;
@@ -687,32 +709,47 @@ impl<T: Item + 'static> Picker<T> {
687709 }
688710 };
689711
690− // align to middle
691− let first_line = range
692− .map(|(start, end)| {
693− let height = end.saturating_sub(start) + 1;
694− let middle = start + (height.saturating_sub(1) / 2);
695− middle.saturating_sub(inner.height as usize / 2).min(start)
696− })
697− .unwrap_or(0);
698−
699− let offset = ViewPosition {
700− anchor: doc.text().line_to_char(first_line),
701− horizontal_offset: 0,
702− vertical_offset: 0,
703− };
712+ let mut offset = ViewPosition::default();
713+ if let Some((start_line, end_line)) = range {
714+ let height = end_line - start_line;
715+ let text = doc.text().slice(..);
716+ let start = text.line_to_char(start_line);
717+ let middle = text.line_to_char(start_line + height / 2);
718+ if height < inner.height as usize {
719+ let text_fmt = doc.text_format(inner.width, None);
720+ let annotations = TextAnnotations::default();
721+ (offset.anchor, offset.vertical_offset) = char_idx_at_visual_offset(
722+ text,
723+ middle,
724+ // align to middle
725+ -(inner.height as isize / 2),
726+ 0,
727+ &text_fmt,
728+ &annotations,
729+ );
730+ if start < offset.anchor {
731+ offset.anchor = start;
732+ offset.vertical_offset = 0;
733+ }
734+ } else {
735+ offset.anchor = start;
736+ }
737+ }
704738
705− let mut highlights = EditorView::doc_syntax_highlights(
739+ let syntax_highlights = EditorView::doc_syntax_highlights(
706740 doc,
707741 offset.anchor,
708742 area.height,
709743 &cx.editor.theme,
710744 );
745+
746+ let mut overlay_highlights =
747+ EditorView::empty_highlight_iter(doc, offset.anchor, area.height);
711748 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
712749 if spans.is_empty() {
713750 continue;
714751 }
715− highlights = Box::new(helix_core::syntax::merge(highlights, spans));
752+ overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
716753 }
717754 let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
718755
@@ -743,7 +780,8 @@ impl<T: Item + 'static> Picker<T> {
743780 offset,
744781 // TODO: compute text annotations asynchronously here (like inlay hints)
745782 &TextAnnotations::default(),
746− highlights,
783+ syntax_highlights,
784+ overlay_highlights,
747785 &cx.editor.theme,
748786 &mut decorations,
749787 &mut [],
@@ -752,7 +790,7 @@ impl<T: Item + 'static> Picker<T> {
752790 }
753791}
754792
755−impl<T: Item + 'static> Component for Picker<T> {
793+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
756794 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
757795 // +---------+ +---------+
758796 // |prompt | |preview |
@@ -761,7 +799,8 @@ impl<T: Item + 'static> Component for Picker<T> {
761799 // | | | |
762800 // +---------+ +---------+
763801
764− let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
802+ let render_preview =
803+ self.show_preview && self.file_fn.is_some() && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
765804
766805 let picker_width = if render_preview {
767806 area.width / 2
@@ -791,11 +830,28 @@ impl<T: Item + 'static> Component for Picker<T> {
791830 _ => return EventResult::Ignored(None),
792831 };
793832
794− let close_fn =
795− EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
796− // remove the layer
797− compositor.last_picker = compositor.pop();
798− })));
833+ let close_fn = |picker: &mut Self| {
834+ // if the picker is very large don't store it as last_picker to avoid
835+ // excessive memory consumption
836+ let callback: compositor::Callback = if picker.matcher.snapshot().item_count() > 100_000
837+ {
838+ Box::new(|compositor: &mut Compositor, _ctx| {
839+ // remove the layer
840+ compositor.pop();
841+ })
842+ } else {
843+ // stop streaming in new items in the background, really we should
844+ // be restarting the stream somehow once the picker gets
845+ // reopened instead (like for an FS crawl) that would also remove the
846+ // need for the special case above but that is pretty tricky
847+ picker.shutdown.store(true, atomic::Ordering::Relaxed);
848+ Box::new(|compositor: &mut Compositor, _ctx| {
849+ // remove the layer
850+ compositor.last_picker = compositor.pop();
851+ })
852+ };
853+ EventResult::Consumed(Some(callback))
854+ };
799855
800856 // So that idle timeout retriggers
801857 ctx.editor.reset_idle_timer();
@@ -819,9 +875,7 @@ impl<T: Item + 'static> Component for Picker<T> {
819875 key!(End) => {
820876 self.to_end();
821877 }
822− key!(Esc) | ctrl!('c') => {
823− return close_fn;
824− }
878+ key!(Esc) | ctrl!('c') => return close_fn(self),
825879 alt!(Enter) => {
826880 if let Some(option) = self.selection() {
827881 (self.callback_fn)(ctx, option, Action::Load);
@@ -831,19 +885,19 @@ impl<T: Item + 'static> Component for Picker<T> {
831885 if let Some(option) = self.selection() {
832886 (self.callback_fn)(ctx, option, Action::Replace);
833887 }
834− return close_fn;
888+ return close_fn(self);
835889 }
836890 ctrl!('s') => {
837891 if let Some(option) = self.selection() {
838892 (self.callback_fn)(ctx, option, Action::HorizontalSplit);
839893 }
840− return close_fn;
894+ return close_fn(self);
841895 }
842896 ctrl!('v') => {
843897 if let Some(option) = self.selection() {
844898 (self.callback_fn)(ctx, option, Action::VerticalSplit);
845899 }
846− return close_fn;
900+ return close_fn(self);
847901 }
848902 ctrl!('t') => {
849903 self.toggle_preview();
@@ -871,30 +925,15 @@ impl<T: Item + 'static> Component for Picker<T> {
871925 self.completion_height = height.saturating_sub(4);
872926 Some((width, height))
873927 }
874−}
875−
876−#[derive(PartialEq, Eq, Debug)]
877−struct PickerMatch {
878− score: i64,
879− index: usize,
880− len: usize,
881−}
882−
883−impl PickerMatch {
884− fn key(&self) -> impl Ord {
885− (cmp::Reverse(self.score), self.len, self.index)
886− }
887−}
888928
889−impl PartialOrd for PickerMatch {
890− fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
891− Some(self.cmp(other))
929+ fn id(&self) -> Option<&'static str> {
930+ Some(ID)
892931 }
893932}
894−
895−impl Ord for PickerMatch {
896− fn cmp(&self, other: &Self) -> Ordering {
897− self.key().cmp(&other.key())
933+impl<T: Item> Drop for Picker<T> {
934+ fn drop(&mut self) {
935+ // ensure we cancel any ongoing background threads streaming into the picker
936+ self.shutdown.store(true, atomic::Ordering::Relaxed)
898937 }
899938}
900939
@@ -907,15 +946,13 @@ pub type DynQueryCallback<T> =
907946
908947/// A picker that updates its contents via a callback whenever the
909948/// query string changes. Useful for live grep, workspace symbols, etc.
910−pub struct DynamicPicker<T: ui::menu::Item + Send> {
949+pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
911950 file_picker: Picker<T>,
912951 query_callback: DynQueryCallback<T>,
913952 query: String,
914953}
915954
916−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
917− pub const ID: &'static str = "dynamic-picker";
918−
955+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
919956 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
920957 Self {
921958 file_picker,
@@ -925,7 +962,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
925962 }
926963}
927964
928−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
965+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
929966 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
930967 self.file_picker.render(area, surface, cx);
931968 }
@@ -947,7 +984,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
947984 let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
948985 // Wrapping of pickers in overlay is done outside the picker code,
949986 // so this is fragile and will break if wrapped in some other widget.
950− let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
987+ let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
951988 Some(overlay) => &mut overlay.content.file_picker,
952989 None => return,
953990 };
@@ -968,6 +1005,6 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
9681005 }
9691006
9701007 fn id(&self) -> Option<&'static str> {
971− Some(Self::ID)
1008+ Some(ID)
9721009 }
9731010}
helix-term/src/ui/popup.rs+27−7
@@ -3,7 +3,10 @@ use crate::{
33 compositor::{Callback, Component, Context, Event, EventResult},
44 ctrl, key,
55};
6−use tui::buffer::Buffer as Surface;
6+use tui::{
7+ buffer::Buffer as Surface,
8+ widgets::{Block, Borders, Widget},
9+};
710
811use helix_core::Position;
912use helix_view::{
@@ -252,13 +255,29 @@ impl<T: Component> Component for Popup<T> {
252255 let background = cx.editor.theme.get("ui.popup");
253256 surface.clear_with(area, background);
254257
255− let inner = area.inner(&self.margin);
258+ let render_borders = cx.editor.popup_border();
259+
260+ let inner = if self
261+ .contents
262+ .type_name()
263+ .starts_with("helix_term::ui::menu::Menu")
264+ {
265+ area
266+ } else {
267+ area.inner(&self.margin)
268+ };
269+
270+ let border = usize::from(render_borders);
271+ if render_borders {
272+ Widget::render(Block::default().borders(Borders::ALL), area, surface);
273+ }
274+
256275 self.contents.render(inner, surface, cx);
257276
258277 // render scrollbar if contents do not fit
259278 if self.has_scrollbar {
260− let win_height = inner.height as usize;
261− let len = self.child_size.1 as usize;
279+ let win_height = (inner.height as usize).saturating_sub(2 * border);
280+ let len = (self.child_size.1 as usize).saturating_sub(2 * border);
262281 let fits = len <= win_height;
263282 let scroll = self.scroll;
264283 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -274,14 +293,15 @@ impl<T: Component> Component for Popup<T> {
274293
275294 let mut cell;
276295 for i in 0..win_height {
277− cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
296+ cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
278297
279− cell.set_symbol("▐"); // right half block
298+ let half_block = if render_borders { "▌" } else { "▐" };
280299
281300 if scroll_line <= i && i < scroll_line + scroll_height {
282301 // Draw scroll thumb
302+ cell.set_symbol(half_block);
283303 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
284− } else {
304+ } else if !render_borders {
285305 // Draw scroll track
286306 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
287307 }
helix-term/src/ui/prompt.rs+53−48
@@ -1,7 +1,9 @@
11use crate::compositor::{Component, Compositor, Context, Event, EventResult};
22use crate::{alt, ctrl, key, shift, ui};
3+use helix_core::syntax;
34use helix_view::input::KeyEvent;
45use helix_view::keyboard::KeyCode;
6+use std::sync::Arc;
57use std::{borrow::Cow, ops::RangeFrom};
68use tui::buffer::Buffer as Surface;
79use tui::widgets::{Block, Borders, Widget};
@@ -32,6 +34,7 @@ pub struct Prompt {
3234 callback_fn: CallbackFn,
3335 pub doc_fn: DocFn,
3436 next_char_handler: Option<PromptCharHandler>,
37+ language: Option<(&'static str, Arc<syntax::Loader>)>,
3538}
3639
3740#[derive(Clone, Copy, PartialEq, Eq)]
@@ -83,6 +86,7 @@ impl Prompt {
8386 callback_fn: Box::new(callback_fn),
8487 doc_fn: Box::new(|_| None),
8588 next_char_handler: None,
89+ language: None,
8690 }
8791 }
8892
@@ -94,6 +98,11 @@ impl Prompt {
9498 self
9599 }
96100
101+ pub fn with_language(mut self, language: &'static str, loader: Arc<syntax::Loader>) -> Self {
102+ self.language = Some((language, loader));
103+ self
104+ }
105+
97106 pub fn line(&self) -> &String {
98107 &self.line
99108 }
@@ -297,8 +306,8 @@ impl Prompt {
297306 direction: CompletionDirection,
298307 ) {
299308 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
300− let values = match cx.editor.registers.read(register) {
301− Some(values) if !values.is_empty() => values,
309+ let mut values = match cx.editor.registers.read(register, cx.editor) {
310+ Some(values) if values.len() > 0 => values.rev(),
302311 _ => return,
303312 };
304313
@@ -306,13 +315,16 @@ impl Prompt {
306315
307316 let index = match direction {
308317 CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
309− CompletionDirection::Backward => {
310− self.history_pos.unwrap_or(values.len()).saturating_sub(1)
311− }
318+ CompletionDirection::Backward => self
319+ .history_pos
320+ .unwrap_or_else(|| values.len())
321+ .saturating_sub(1),
312322 }
313323 .min(end);
314324
315− self.line = values[index].clone();
325+ self.line = values.nth(index).unwrap().to_string();
326+ // Appease the borrow checker.
327+ drop(values);
316328
317329 self.history_pos = Some(index);
318330
@@ -456,30 +468,28 @@ impl Prompt {
456468 // render buffer text
457469 surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
458470
459− let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() {
460− // latest value in the register list
461− match self
471+ let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
472+ if self.line.is_empty() {
473+ // Show the most recently entered value as a suggestion.
474+ if let Some(suggestion) = self
462475 .history_register
463− .and_then(|reg| cx.editor.registers.last(reg))
464− .map(|entry| entry.into())
476+ .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
465477 {
466− Some(value) => (value, true),
467− None => (Cow::from(""), false),
478+ surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
468479 }
480+ } else if let Some((language, loader)) = self.language.as_ref() {
481+ let mut text: ui::text::Text = crate::ui::markdown::highlighted_code_block(
482+ &self.line,
483+ language,
484+ Some(&cx.editor.theme),
485+ loader.clone(),
486+ None,
487+ )
488+ .into();
489+ text.render(line_area, surface, cx);
469490 } else {
470− (self.line.as_str().into(), false)
471− };
472−
473− surface.set_string(
474− area.x + self.prompt.len() as u16,
475− area.y + line,
476− &input,
477− if is_suggestion {
478− suggestion_color
479− } else {
480− prompt_color
481− },
482− );
491+ surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
492+ }
483493 }
484494}
485495
@@ -560,25 +570,29 @@ impl Component for Prompt {
560570 } else {
561571 let last_item = self
562572 .history_register
563− .and_then(|reg| cx.editor.registers.last(reg).cloned())
564− .map(|entry| entry.into())
565− .unwrap_or_else(|| Cow::from(""));
573+ .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
574+ .map(|entry| entry.to_string())
575+ .unwrap_or_else(|| String::from(""));
566576
567577 // handle executing with last command in history if nothing entered
568− let input: Cow<str> = if self.line.is_empty() {
569− last_item
578+ let input = if self.line.is_empty() {
579+ &last_item
570580 } else {
571581 if last_item != self.line {
572582 // store in history
573583 if let Some(register) = self.history_register {
574− cx.editor.registers.push(register, self.line.clone());
584+ if let Err(err) =
585+ cx.editor.registers.push(register, self.line.clone())
586+ {
587+ cx.editor.set_error(err.to_string());
588+ }
575589 };
576590 }
577591
578− self.line.as_str().into()
592+ &self.line
579593 };
580594
581− (self.callback_fn)(cx, &input, PromptEvent::Validate);
595+ (self.callback_fn)(cx, input, PromptEvent::Validate);
582596
583597 return close_fn;
584598 }
@@ -610,25 +624,16 @@ impl Component for Prompt {
610624 self.completion = cx
611625 .editor
612626 .registers
613− .inner()
614− .iter()
615− .map(|(ch, reg)| {
616− let content = reg
617− .read()
618− .get(0)
619− .and_then(|s| s.lines().next().to_owned())
620− .unwrap_or_default();
621− (0.., format!("{} {}", ch, &content).into())
622− })
627+ .iter_preview()
628+ .map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
623629 .collect();
624630 self.next_char_handler = Some(Box::new(|prompt, c, context| {
625631 prompt.insert_str(
626− context
632+ &context
627633 .editor
628634 .registers
629− .read(c)
630− .and_then(|r| r.first())
631− .map_or("", |r| r.as_str()),
635+ .first(c, context.editor)
636+ .unwrap_or_default(),
632637 context.editor,
633638 );
634639 }));
helix-term/src/ui/statusline.rs+14
@@ -145,6 +145,7 @@ where
145145 helix_view::editor::StatusLineElement::FileModificationIndicator => {
146146 render_file_modification_indicator
147147 }
148+ helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator,
148149 helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
149150 helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
150151 helix_view::editor::StatusLineElement::FileType => render_file_type,
@@ -442,6 +443,19 @@ where
442443 write(context, title, None);
443444}
444445
446+fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
447+where
448+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
449+{
450+ let title = if context.doc.readonly {
451+ " [readonly] "
452+ } else {
453+ ""
454+ }
455+ .to_string();
456+ write(context, title, None);
457+}
458+
445459fn render_file_base_name<F>(context: &mut RenderContext, write: F)
446460where
447461 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
helix-term/tests/integration.rs+1
@@ -18,6 +18,7 @@ mod test {
1818 mod auto_indent;
1919 mod auto_pairs;
2020 mod commands;
21+ mod languages;
2122 mod movement;
2223 mod prompt;
2324 mod splits;
helix-term/tests/test/commands.rs+47
@@ -2,6 +2,7 @@ use helix_term::application::Application;
22
33use super::*;
44
5+mod movement;
56mod write;
67
78#[tokio::test(flavor = "multi_thread")]
@@ -479,3 +480,49 @@ fn bar() {#(\n|)#\
479480
480481 Ok(())
481482}
483+
484+#[tokio::test(flavor = "multi_thread")]
485+async fn test_join_selections() -> anyhow::Result<()> {
486+ // normal join
487+ test((
488+ platform_line(indoc! {"\
489+ #[a|]#bc
490+ def
491+ "}),
492+ "J",
493+ platform_line(indoc! {"\
494+ #[a|]#bc def
495+ "}),
496+ ))
497+ .await?;
498+
499+ // join with empty line
500+ test((
501+ platform_line(indoc! {"\
502+ #[a|]#bc
503+
504+ def
505+ "}),
506+ "JJ",
507+ platform_line(indoc! {"\
508+ #[a|]#bc def
509+ "}),
510+ ))
511+ .await?;
512+
513+ // join with additional space in non-empty line
514+ test((
515+ platform_line(indoc! {"\
516+ #[a|]#bc
517+
518+ def
519+ "}),
520+ "JJ",
521+ platform_line(indoc! {"\
522+ #[a|]#bc def
523+ "}),
524+ ))
525+ .await?;
526+
527+ Ok(())
528+}
helix-term/tests/test/commands/movement.rsnew file+452
@@ -0,0 +1,452 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn test_move_parent_node_end() -> anyhow::Result<()> {
5+ let tests = vec![
6+ // single cursor stays single cursor, first goes to end of current
7+ // node, then parent
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ fn foo() {
11+ let result = if true {
12+ "yes"
13+ } else {
14+ "no#["|]#
15+ }
16+ }
17+ "##}),
18+ "<A-e>",
19+ helpers::platform_line(indoc! {"\
20+ fn foo() {
21+ let result = if true {
22+ \"yes\"
23+ } else {
24+ \"no\"#[\n|]#
25+ }
26+ }
27+ "}),
28+ ),
29+ (
30+ helpers::platform_line(indoc! {"\
31+ fn foo() {
32+ let result = if true {
33+ \"yes\"
34+ } else {
35+ \"no\"#[\n|]#
36+ }
37+ }
38+ "}),
39+ "<A-e>",
40+ helpers::platform_line(indoc! {"\
41+ fn foo() {
42+ let result = if true {
43+ \"yes\"
44+ } else {
45+ \"no\"
46+ }#[\n|]#
47+ }
48+ "}),
49+ ),
50+ // select mode extends
51+ (
52+ helpers::platform_line(indoc! {r##"
53+ fn foo() {
54+ let result = if true {
55+ "yes"
56+ } else {
57+ #["no"|]#
58+ }
59+ }
60+ "##}),
61+ "v<A-e><A-e>",
62+ helpers::platform_line(indoc! {"\
63+ fn foo() {
64+ let result = if true {
65+ \"yes\"
66+ } else {
67+ #[\"no\"
68+ }\n|]#
69+ }
70+ "}),
71+ ),
72+ ];
73+
74+ for test in tests {
75+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
76+ }
77+
78+ Ok(())
79+}
80+
81+#[tokio::test(flavor = "multi_thread")]
82+async fn test_move_parent_node_start() -> anyhow::Result<()> {
83+ let tests = vec![
84+ // single cursor stays single cursor, first goes to end of current
85+ // node, then parent
86+ (
87+ helpers::platform_line(indoc! {r##"
88+ fn foo() {
89+ let result = if true {
90+ "yes"
91+ } else {
92+ "no#["|]#
93+ }
94+ }
95+ "##}),
96+ "<A-b>",
97+ helpers::platform_line(indoc! {"\
98+ fn foo() {
99+ let result = if true {
100+ \"yes\"
101+ } else {
102+ #[\"|]#no\"
103+ }
104+ }
105+ "}),
106+ ),
107+ (
108+ helpers::platform_line(indoc! {"\
109+ fn foo() {
110+ let result = if true {
111+ \"yes\"
112+ } else {
113+ \"no\"#[\n|]#
114+ }
115+ }
116+ "}),
117+ "<A-b>",
118+ helpers::platform_line(indoc! {"\
119+ fn foo() {
120+ let result = if true {
121+ \"yes\"
122+ } else #[{|]#
123+ \"no\"
124+ }
125+ }
126+ "}),
127+ ),
128+ (
129+ helpers::platform_line(indoc! {"\
130+ fn foo() {
131+ let result = if true {
132+ \"yes\"
133+ } else #[{|]#
134+ \"no\"
135+ }
136+ }
137+ "}),
138+ "<A-b>",
139+ helpers::platform_line(indoc! {"\
140+ fn foo() {
141+ let result = if true {
142+ \"yes\"
143+ } #[e|]#lse {
144+ \"no\"
145+ }
146+ }
147+ "}),
148+ ),
149+ // select mode extends
150+ (
151+ helpers::platform_line(indoc! {r##"
152+ fn foo() {
153+ let result = if true {
154+ "yes"
155+ } else {
156+ #["no"|]#
157+ }
158+ }
159+ "##}),
160+ "v<A-b><A-b>",
161+ helpers::platform_line(indoc! {"\
162+ fn foo() {
163+ let result = if true {
164+ \"yes\"
165+ } else #[|{
166+ ]#\"no\"
167+ }
168+ }
169+ "}),
170+ ),
171+ (
172+ helpers::platform_line(indoc! {r##"
173+ fn foo() {
174+ let result = if true {
175+ "yes"
176+ } else {
177+ #["no"|]#
178+ }
179+ }
180+ "##}),
181+ "v<A-b><A-b><A-b>",
182+ helpers::platform_line(indoc! {"\
183+ fn foo() {
184+ let result = if true {
185+ \"yes\"
186+ } #[|else {
187+ ]#\"no\"
188+ }
189+ }
190+ "}),
191+ ),
192+ ];
193+
194+ for test in tests {
195+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
196+ }
197+
198+ Ok(())
199+}
200+
201+#[tokio::test(flavor = "multi_thread")]
202+async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
203+ let tests = vec![
204+ // single cursor stays single cursor, first goes to end of current
205+ // node, then parent
206+ (
207+ helpers::platform_line(indoc! {r##"
208+ fn foo() {
209+ let result = if true {
210+ "yes"
211+ } else {
212+ "no#["|]#
213+ }
214+ }
215+ "##}),
216+ "i<tab>",
217+ helpers::platform_line(indoc! {"\
218+ fn foo() {
219+ let result = if true {
220+ \"yes\"
221+ } else {
222+ \"no\"#[|\n]#
223+ }
224+ }
225+ "}),
226+ ),
227+ (
228+ helpers::platform_line(indoc! {"\
229+ fn foo() {
230+ let result = if true {
231+ \"yes\"
232+ } else {
233+ \"no\"#[\n|]#
234+ }
235+ }
236+ "}),
237+ "i<tab>",
238+ helpers::platform_line(indoc! {"\
239+ fn foo() {
240+ let result = if true {
241+ \"yes\"
242+ } else {
243+ \"no\"
244+ }#[|\n]#
245+ }
246+ "}),
247+ ),
248+ // appending to the end of a line should still look at the current
249+ // line, not the next one
250+ (
251+ helpers::platform_line(indoc! {"\
252+ fn foo() {
253+ let result = if true {
254+ \"yes\"
255+ } else {
256+ \"no#[\"|]#
257+ }
258+ }
259+ "}),
260+ "a<tab>",
261+ helpers::platform_line(indoc! {"\
262+ fn foo() {
263+ let result = if true {
264+ \"yes\"
265+ } else {
266+ \"no\"
267+ }#[\n|]#
268+ }
269+ "}),
270+ ),
271+ // before cursor is all whitespace, so insert tab
272+ (
273+ helpers::platform_line(indoc! {"\
274+ fn foo() {
275+ let result = if true {
276+ \"yes\"
277+ } else {
278+ #[\"no\"|]#
279+ }
280+ }
281+ "}),
282+ "i<tab>",
283+ helpers::platform_line(indoc! {"\
284+ fn foo() {
285+ let result = if true {
286+ \"yes\"
287+ } else {
288+ #[|\"no\"]#
289+ }
290+ }
291+ "}),
292+ ),
293+ // if selection spans multiple lines, it should still only look at the
294+ // line on which the head is
295+ (
296+ helpers::platform_line(indoc! {"\
297+ fn foo() {
298+ let result = if true {
299+ #[\"yes\"
300+ } else {
301+ \"no\"|]#
302+ }
303+ }
304+ "}),
305+ "a<tab>",
306+ helpers::platform_line(indoc! {"\
307+ fn foo() {
308+ let result = if true {
309+ \"yes\"
310+ } else {
311+ \"no\"
312+ }#[\n|]#
313+ }
314+ "}),
315+ ),
316+ (
317+ helpers::platform_line(indoc! {"\
318+ fn foo() {
319+ let result = if true {
320+ #[\"yes\"
321+ } else {
322+ \"no\"|]#
323+ }
324+ }
325+ "}),
326+ "i<tab>",
327+ helpers::platform_line(indoc! {"\
328+ fn foo() {
329+ let result = if true {
330+ #[|\"yes\"
331+ } else {
332+ \"no\"]#
333+ }
334+ }
335+ "}),
336+ ),
337+ (
338+ helpers::platform_line(indoc! {"\
339+ fn foo() {
340+ #[l|]#et result = if true {
341+ #(\"yes\"
342+ } else {
343+ \"no\"|)#
344+ }
345+ }
346+ "}),
347+ "i<tab>",
348+ helpers::platform_line(indoc! {"\
349+ fn foo() {
350+ #[|l]#et result = if true {
351+ #(|\"yes\"
352+ } else {
353+ \"no\")#
354+ }
355+ }
356+ "}),
357+ ),
358+ (
359+ helpers::platform_line(indoc! {"\
360+ fn foo() {
361+ let result = if true {
362+ \"yes\"#[\n|]#
363+ } else {
364+ \"no\"#(\n|)#
365+ }
366+ }
367+ "}),
368+ "i<tab>",
369+ helpers::platform_line(indoc! {"\
370+ fn foo() {
371+ let result = if true {
372+ \"yes\"
373+ }#[| ]#else {
374+ \"no\"
375+ }#(|\n)#
376+ }
377+ "}),
378+ ),
379+ (
380+ helpers::platform_line(indoc! {"\
381+ fn foo() {
382+ let result = if true {
383+ #[\"yes\"|]#
384+ } else {
385+ #(\"no\"|)#
386+ }
387+ }
388+ "}),
389+ "i<tab>",
390+ helpers::platform_line(indoc! {"\
391+ fn foo() {
392+ let result = if true {
393+ #[|\"yes\"]#
394+ } else {
395+ #(|\"no\")#
396+ }
397+ }
398+ "}),
399+ ),
400+ // if any cursors are not preceded by all whitespace, then do the
401+ // smart_tab action
402+ (
403+ helpers::platform_line(indoc! {"\
404+ fn foo() {
405+ let result = if true {
406+ #[\"yes\"\n|]#
407+ } else {
408+ \"no#(\"\n|)#
409+ }
410+ }
411+ "}),
412+ "i<tab>",
413+ helpers::platform_line(indoc! {"\
414+ fn foo() {
415+ let result = if true {
416+ \"yes\"
417+ }#[| ]#else {
418+ \"no\"
419+ }#(|\n)#
420+ }
421+ "}),
422+ ),
423+ // Ctrl-tab always inserts a tab
424+ (
425+ helpers::platform_line(indoc! {"\
426+ fn foo() {
427+ let result = if true {
428+ #[\"yes\"\n|]#
429+ } else {
430+ \"no#(\"\n|)#
431+ }
432+ }
433+ "}),
434+ "i<S-tab>",
435+ helpers::platform_line(indoc! {"\
436+ fn foo() {
437+ let result = if true {
438+ #[|\"yes\"\n]#
439+ } else {
440+ \"no #(|\"\n)#
441+ }
442+ }
443+ "}),
444+ ),
445+ ];
446+
447+ for test in tests {
448+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
449+ }
450+
451+ Ok(())
452+}
helix-term/tests/test/commands/write.rs+122−3
@@ -93,7 +93,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
9393 )
9494 .await?;
9595
96− helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
96+ helpers::assert_file_has_content(file.as_file_mut(), &platform_line(&RANGE.end().to_string()))?;
9797
9898 Ok(())
9999}
@@ -209,7 +209,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
209209
210210 let mut file_content = String::new();
211211 file.as_file_mut().read_to_string(&mut file_content)?;
212− assert_eq!(RANGE.end().to_string(), file_content);
212+ assert_eq!(platform_line(&RANGE.end().to_string()), file_content);
213213
214214 Ok(())
215215}
@@ -424,13 +424,132 @@ async fn test_write_utf_bom_file() -> anyhow::Result<()> {
424424 Ok(())
425425}
426426
427+#[tokio::test(flavor = "multi_thread")]
428+async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<()> {
429+ let mut file = tempfile::NamedTempFile::new()?;
430+ let mut app = helpers::AppBuilder::new()
431+ .with_file(file.path(), None)
432+ .with_input_text("#[h|]#ave you tried chamomile tea?")
433+ .build()?;
434+
435+ test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
436+
437+ helpers::assert_file_has_content(
438+ file.as_file_mut(),
439+ &helpers::platform_line("have you tried chamomile tea?\n"),
440+ )?;
441+
442+ Ok(())
443+}
444+
445+#[tokio::test(flavor = "multi_thread")]
446+async fn test_write_insert_final_newline_unchanged_if_not_missing() -> anyhow::Result<()> {
447+ let mut file = tempfile::NamedTempFile::new()?;
448+ let mut app = helpers::AppBuilder::new()
449+ .with_file(file.path(), None)
450+ .with_input_text(&helpers::platform_line("#[t|]#en minutes, please\n"))
451+ .build()?;
452+
453+ test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
454+
455+ helpers::assert_file_has_content(
456+ file.as_file_mut(),
457+ &helpers::platform_line("ten minutes, please\n"),
458+ )?;
459+
460+ Ok(())
461+}
462+
463+#[tokio::test(flavor = "multi_thread")]
464+async fn test_write_insert_final_newline_unchanged_if_missing_and_false() -> anyhow::Result<()> {
465+ let mut file = tempfile::NamedTempFile::new()?;
466+ let mut app = helpers::AppBuilder::new()
467+ .with_config(Config {
468+ editor: helix_view::editor::Config {
469+ insert_final_newline: false,
470+ ..Default::default()
471+ },
472+ ..Default::default()
473+ })
474+ .with_file(file.path(), None)
475+ .with_input_text("#[t|]#he quiet rain continued through the night")
476+ .build()?;
477+
478+ test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
479+
480+ helpers::assert_file_has_content(
481+ file.as_file_mut(),
482+ "the quiet rain continued through the night",
483+ )?;
484+
485+ Ok(())
486+}
487+
488+#[tokio::test(flavor = "multi_thread")]
489+async fn test_write_all_insert_final_newline_add_if_missing_and_modified() -> anyhow::Result<()> {
490+ let mut file1 = tempfile::NamedTempFile::new()?;
491+ let mut file2 = tempfile::NamedTempFile::new()?;
492+ let mut app = helpers::AppBuilder::new()
493+ .with_file(file1.path(), None)
494+ .with_input_text("#[w|]#e don't serve time travelers here")
495+ .build()?;
496+
497+ test_key_sequence(
498+ &mut app,
499+ Some(&format!(
500+ ":o {}<ret>ia time traveler walks into a bar<esc>:wa<ret>",
501+ file2.path().to_string_lossy()
502+ )),
503+ None,
504+ false,
505+ )
506+ .await?;
507+
508+ helpers::assert_file_has_content(
509+ file1.as_file_mut(),
510+ &helpers::platform_line("we don't serve time travelers here\n"),
511+ )?;
512+
513+ helpers::assert_file_has_content(
514+ file2.as_file_mut(),
515+ &helpers::platform_line("a time traveler walks into a bar\n"),
516+ )?;
517+
518+ Ok(())
519+}
520+
521+#[tokio::test(flavor = "multi_thread")]
522+async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyhow::Result<()> {
523+ let mut file = tempfile::NamedTempFile::new()?;
524+ let mut app = helpers::AppBuilder::new()
525+ .with_file(file.path(), None)
526+ .build()?;
527+
528+ file.write_all(b"i lost on Jeopardy!")?;
529+ file.rewind()?;
530+
531+ test_key_sequence(&mut app, Some(":wa<ret>"), None, false).await?;
532+
533+ helpers::assert_file_has_content(file.as_file_mut(), "i lost on Jeopardy!")?;
534+
535+ Ok(())
536+}
537+
427538async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
428539 let mut file = tempfile::NamedTempFile::new()?;
429540
430541 file.as_file_mut().write_all(&file_content)?;
431542
432543 helpers::test_key_sequence(
433− &mut helpers::AppBuilder::new().build()?,
544+ &mut helpers::AppBuilder::new()
545+ .with_config(Config {
546+ editor: helix_view::editor::Config {
547+ insert_final_newline: false,
548+ ..Default::default()
549+ },
550+ ..Default::default()
551+ })
552+ .build()?,
434553 Some(&format!(":o {}<ret>:x<ret>", file.path().to_string_lossy())),
435554 None,
436555 true,
helix-term/tests/test/helpers.rs+9−1
@@ -320,6 +320,14 @@ impl AppBuilder {
320320 }
321321
322322 pub fn build(self) -> anyhow::Result<Application> {
323+ if let Some(path) = &self.args.working_directory {
324+ bail!("Changing the working directory to {path:?} is not yet supported for integration tests");
325+ }
326+
327+ if let Some((path, _)) = self.args.files.first().filter(|p| p.0.is_dir()) {
328+ bail!("Having the directory {path:?} in args.files[0] is not yet supported for integration tests");
329+ }
330+
323331 let mut app = Application::new(self.args, self.config, self.syn_conf)?;
324332
325333 if let Some((text, selection)) = self.input {
@@ -350,7 +358,7 @@ pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result
350358
351359 let mut file_content = String::new();
352360 file.read_to_string(&mut file_content)?;
353− assert_eq!(content, file_content);
361+ assert_eq!(file_content, content);
354362
355363 Ok(())
356364}
helix-term/tests/test/languages/go.rsnew file+41
@@ -0,0 +1,41 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn auto_indent() -> anyhow::Result<()> {
5+ let app = || AppBuilder::new().with_file("foo.go", None);
6+
7+ let enter_tests = [
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ type Test struct {#[}|]#
11+ "##}),
12+ "i<ret>",
13+ helpers::platform_line(indoc! {"\
14+ type Test struct {
15+ \t#[|\n]#
16+ }
17+ "}),
18+ ),
19+ (
20+ helpers::platform_line(indoc! {"\
21+ func main() {
22+ \tswitch nil {#[}|]#
23+ }
24+ "}),
25+ "i<ret>",
26+ helpers::platform_line(indoc! {"\
27+ func main() {
28+ \tswitch nil {
29+ \t\t#[|\n]#
30+ \t}
31+ }
32+ "}),
33+ ),
34+ ];
35+
36+ for test in enter_tests {
37+ test_with_config(app(), test).await?;
38+ }
39+
40+ Ok(())
41+}
helix-term/tests/test/languages/mod.rsnew file+4
@@ -0,0 +1,4 @@
1+use super::*;
2+
3+mod go;
4+mod yaml;
helix-term/tests/test/languages/yaml.rsnew file+819
@@ -0,0 +1,819 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn auto_indent() -> anyhow::Result<()> {
5+ let app = || AppBuilder::new().with_file("foo.yaml", None);
6+
7+ let below_tests = [
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ #[t|]#op:
11+ baz: foo
12+ bazi:
13+ more: yes
14+ why: because
15+ quux:
16+ - 1
17+ - 2
18+ bax: foox
19+ fook:
20+ "##}),
21+ "o",
22+ helpers::platform_line(indoc! {"\
23+ top:
24+ #[\n|]#
25+ baz: foo
26+ bazi:
27+ more: yes
28+ why: because
29+ quux:
30+ - 1
31+ - 2
32+ bax: foox
33+ fook:
34+ "}),
35+ ),
36+ (
37+ helpers::platform_line(indoc! {r##"
38+ top:
39+ b#[a|]#z: foo
40+ bazi:
41+ more: yes
42+ why: because
43+ quux:
44+ - 1
45+ - 2
46+ bax: foox
47+ fook:
48+ "##}),
49+ "o",
50+ helpers::platform_line(indoc! {"\
51+ top:
52+ baz: foo
53+ #[\n|]#
54+ bazi:
55+ more: yes
56+ why: because
57+ quux:
58+ - 1
59+ - 2
60+ bax: foox
61+ fook:
62+ "}),
63+ ),
64+ (
65+ helpers::platform_line(indoc! {r##"
66+ top:
67+ baz: foo
68+ bazi#[:|]#
69+ more: yes
70+ why: because
71+ quux:
72+ - 1
73+ - 2
74+ bax: foox
75+ fook:
76+ "##}),
77+ "o",
78+ helpers::platform_line(indoc! {"\
79+ top:
80+ baz: foo
81+ bazi:
82+ #[\n|]#
83+ more: yes
84+ why: because
85+ quux:
86+ - 1
87+ - 2
88+ bax: foox
89+ fook:
90+ "}),
91+ ),
92+ (
93+ helpers::platform_line(indoc! {r##"
94+ top:
95+ baz: foo
96+ bazi:
97+ more: #[yes|]#
98+ why: because
99+ quux:
100+ - 1
101+ - 2
102+ bax: foox
103+ fook:
104+ "##}),
105+ "o",
106+ helpers::platform_line(indoc! {"\
107+ top:
108+ baz: foo
109+ bazi:
110+ more: yes
111+ #[\n|]#
112+ why: because
113+ quux:
114+ - 1
115+ - 2
116+ bax: foox
117+ fook:
118+ "}),
119+ ),
120+ (
121+ helpers::platform_line(indoc! {r##"
122+ top:
123+ baz: foo
124+ bazi:
125+ more: yes
126+ why: becaus#[e|]#
127+ quux:
128+ - 1
129+ - 2
130+ bax: foox
131+ fook:
132+ "##}),
133+ "o",
134+ helpers::platform_line(indoc! {"\
135+ top:
136+ baz: foo
137+ bazi:
138+ more: yes
139+ why: because
140+ #[\n|]#
141+ quux:
142+ - 1
143+ - 2
144+ bax: foox
145+ fook:
146+ "}),
147+ ),
148+ (
149+ helpers::platform_line(indoc! {"\
150+ top:
151+ baz: foo
152+ bazi:
153+ more: yes
154+ why: because
155+ quux:#[\n|]#
156+ - 1
157+ - 2
158+ bax: foox
159+ fook:
160+ "}),
161+ "o",
162+ helpers::platform_line(indoc! {"\
163+ top:
164+ baz: foo
165+ bazi:
166+ more: yes
167+ why: because
168+ quux:
169+ #[\n|]#
170+ - 1
171+ - 2
172+ bax: foox
173+ fook:
174+ "}),
175+ ),
176+ (
177+ helpers::platform_line(indoc! {"\
178+ top:
179+ baz: foo
180+ bazi:
181+ more: yes
182+ why: because
183+ quux:
184+ - 1#[\n|]#
185+ - 2
186+ bax: foox
187+ fook:
188+ "}),
189+ "o",
190+ helpers::platform_line(indoc! {"\
191+ top:
192+ baz: foo
193+ bazi:
194+ more: yes
195+ why: because
196+ quux:
197+ - 1
198+ #[\n|]#
199+ - 2
200+ bax: foox
201+ fook:
202+ "}),
203+ ),
204+ (
205+ helpers::platform_line(indoc! {"\
206+ top:
207+ baz: foo
208+ bazi:
209+ more: yes
210+ why: because
211+ quux:
212+ - 1
213+ - 2
214+ bax: foox
215+ fook:#[\n|]#
216+ "}),
217+ "o",
218+ helpers::platform_line(indoc! {"\
219+ top:
220+ baz: foo
221+ bazi:
222+ more: yes
223+ why: because
224+ quux:
225+ - 1
226+ - 2
227+ bax: foox
228+ fook:
229+ #[\n|]#
230+ "}),
231+ ),
232+ (
233+ helpers::platform_line(indoc! {"\
234+ top:
235+ baz: foo
236+ bax: |
237+ some
238+ multi
239+ line
240+ string#[\n|]#
241+ fook:
242+ "}),
243+ "o",
244+ helpers::platform_line(indoc! {"\
245+ top:
246+ baz: foo
247+ bax: |
248+ some
249+ multi
250+ line
251+ string
252+ #[\n|]#
253+ fook:
254+ "}),
255+ ),
256+ (
257+ helpers::platform_line(indoc! {"\
258+ top:
259+ baz: foo
260+ bax: >
261+ some
262+ multi
263+ line#[\n|]#
264+ string
265+ fook:
266+ "}),
267+ "o",
268+ helpers::platform_line(indoc! {"\
269+ top:
270+ baz: foo
271+ bax: >
272+ some
273+ multi
274+ line
275+ #[\n|]#
276+ string
277+ fook:
278+ "}),
279+ ),
280+ (
281+ helpers::platform_line(indoc! {"\
282+ top:
283+ baz: foo
284+ bax: >#[\n|]#
285+ fook:
286+ "}),
287+ "o",
288+ helpers::platform_line(indoc! {"\
289+ top:
290+ baz: foo
291+ bax: >
292+ #[\n|]#
293+ fook:
294+ "}),
295+ ),
296+ (
297+ helpers::platform_line(indoc! {"\
298+ - top:#[\n|]#
299+ baz: foo
300+ bax: foox
301+ fook:
302+ "}),
303+ "o",
304+ helpers::platform_line(indoc! {"\
305+ - top:
306+ #[\n|]#
307+ baz: foo
308+ bax: foox
309+ fook:
310+ "}),
311+ ),
312+ (
313+ helpers::platform_line(indoc! {"\
314+ - top:
315+ baz: foo#[\n|]#
316+ bax: foox
317+ fook:
318+ "}),
319+ "o",
320+ helpers::platform_line(indoc! {"\
321+ - top:
322+ baz: foo
323+ #[\n|]#
324+ bax: foox
325+ fook:
326+ "}),
327+ ),
328+ (
329+ helpers::platform_line(indoc! {"\
330+ - top:
331+ baz: foo
332+ bax: foox#[\n|]#
333+ fook:
334+ "}),
335+ "o",
336+ helpers::platform_line(indoc! {"\
337+ - top:
338+ baz: foo
339+ bax: foox
340+ #[\n|]#
341+ fook:
342+ "}),
343+ ),
344+ (
345+ helpers::platform_line(indoc! {"\
346+ top:
347+ baz:
348+ - one: two#[\n|]#
349+ three: four
350+ - top:
351+ baz: foo
352+ bax: foox
353+ "}),
354+ "o",
355+ helpers::platform_line(indoc! {"\
356+ top:
357+ baz:
358+ - one: two
359+ #[\n|]#
360+ three: four
361+ - top:
362+ baz: foo
363+ bax: foox
364+ "}),
365+ ),
366+ // yaml map without a key
367+ (
368+ helpers::platform_line(indoc! {"\
369+ top:#[\n|]#
370+ "}),
371+ "o",
372+ helpers::platform_line(indoc! {"\
373+ top:
374+ #[\n|]#
375+ "}),
376+ ),
377+ (
378+ helpers::platform_line(indoc! {"\
379+ top#[:|]#
380+ bottom: withvalue
381+ "}),
382+ "o",
383+ helpers::platform_line(indoc! {"\
384+ top:
385+ #[\n|]#
386+ bottom: withvalue
387+ "}),
388+ ),
389+ (
390+ helpers::platform_line(indoc! {"\
391+ bottom: withvalue
392+ top#[:|]#
393+ "}),
394+ "o",
395+ helpers::platform_line(indoc! {"\
396+ bottom: withvalue
397+ top:
398+ #[\n|]#
399+ "}),
400+ ),
401+ ];
402+
403+ for test in below_tests {
404+ test_with_config(app(), test).await?;
405+ }
406+
407+ let above_tests = [
408+ (
409+ helpers::platform_line(indoc! {r##"
410+ #[t|]#op:
411+ baz: foo
412+ bazi:
413+ more: yes
414+ why: because
415+ quux:
416+ - 1
417+ - 2
418+ bax: foox
419+ fook:
420+ "##}),
421+ "O",
422+ helpers::platform_line(indoc! {"\
423+ #[\n|]#
424+ top:
425+ baz: foo
426+ bazi:
427+ more: yes
428+ why: because
429+ quux:
430+ - 1
431+ - 2
432+ bax: foox
433+ fook:
434+ "}),
435+ ),
436+ (
437+ helpers::platform_line(indoc! {r##"
438+ top:
439+ b#[a|]#z: foo
440+ bazi:
441+ more: yes
442+ why: because
443+ quux:
444+ - 1
445+ - 2
446+ bax: foox
447+ fook:
448+ "##}),
449+ "O",
450+ helpers::platform_line(indoc! {"\
451+ top:
452+ #[\n|]#
453+ baz: foo
454+ bazi:
455+ more: yes
456+ why: because
457+ quux:
458+ - 1
459+ - 2
460+ bax: foox
461+ fook:
462+ "}),
463+ ),
464+ (
465+ helpers::platform_line(indoc! {r##"
466+ top:
467+ baz: foo
468+ bazi#[:|]#
469+ more: yes
470+ why: because
471+ quux:
472+ - 1
473+ - 2
474+ bax: foox
475+ fook:
476+ "##}),
477+ "O",
478+ helpers::platform_line(indoc! {"\
479+ top:
480+ baz: foo
481+ #[\n|]#
482+ bazi:
483+ more: yes
484+ why: because
485+ quux:
486+ - 1
487+ - 2
488+ bax: foox
489+ fook:
490+ "}),
491+ ),
492+ (
493+ helpers::platform_line(indoc! {r##"
494+ top:
495+ baz: foo
496+ bazi:
497+ more: #[yes|]#
498+ why: because
499+ quux:
500+ - 1
501+ - 2
502+ bax: foox
503+ fook:
504+ "##}),
505+ "O",
506+ helpers::platform_line(indoc! {"\
507+ top:
508+ baz: foo
509+ bazi:
510+ #[\n|]#
511+ more: yes
512+ why: because
513+ quux:
514+ - 1
515+ - 2
516+ bax: foox
517+ fook:
518+ "}),
519+ ),
520+ (
521+ helpers::platform_line(indoc! {r##"
522+ top:
523+ baz: foo
524+ bazi:
525+ more: yes
526+ why: becaus#[e|]#
527+ quux:
528+ - 1
529+ - 2
530+ bax: foox
531+ fook:
532+ "##}),
533+ "O",
534+ helpers::platform_line(indoc! {"\
535+ top:
536+ baz: foo
537+ bazi:
538+ more: yes
539+ #[\n|]#
540+ why: because
541+ quux:
542+ - 1
543+ - 2
544+ bax: foox
545+ fook:
546+ "}),
547+ ),
548+ (
549+ helpers::platform_line(indoc! {"\
550+ top:
551+ baz: foo
552+ bazi:
553+ more: yes
554+ why: because
555+ quux:#[\n|]#
556+ - 1
557+ - 2
558+ bax: foox
559+ fook:
560+ "}),
561+ "O",
562+ helpers::platform_line(indoc! {"\
563+ top:
564+ baz: foo
565+ bazi:
566+ more: yes
567+ why: because
568+ #[\n|]#
569+ quux:
570+ - 1
571+ - 2
572+ bax: foox
573+ fook:
574+ "}),
575+ ),
576+ (
577+ helpers::platform_line(indoc! {"\
578+ top:
579+ baz: foo
580+ bazi:
581+ more: yes
582+ why: because
583+ quux:
584+ - 1#[\n|]#
585+ - 2
586+ bax: foox
587+ fook:
588+ "}),
589+ "O",
590+ helpers::platform_line(indoc! {"\
591+ top:
592+ baz: foo
593+ bazi:
594+ more: yes
595+ why: because
596+ quux:
597+ #[\n|]#
598+ - 1
599+ - 2
600+ bax: foox
601+ fook:
602+ "}),
603+ ),
604+ (
605+ helpers::platform_line(indoc! {"\
606+ top:
607+ baz: foo
608+ bazi:
609+ more: yes
610+ why: because
611+ quux:
612+ - 1
613+ - 2
614+ bax: foox
615+ fook:#[\n|]#
616+ "}),
617+ "O",
618+ helpers::platform_line(indoc! {"\
619+ top:
620+ baz: foo
621+ bazi:
622+ more: yes
623+ why: because
624+ quux:
625+ - 1
626+ - 2
627+ bax: foox
628+ #[\n|]#
629+ fook:
630+ "}),
631+ ),
632+ (
633+ helpers::platform_line(indoc! {"\
634+ top:
635+ baz: foo
636+ bax: |
637+ some
638+ multi
639+ line
640+ string#[\n|]#
641+ fook:
642+ "}),
643+ "O",
644+ helpers::platform_line(indoc! {"\
645+ top:
646+ baz: foo
647+ bax: |
648+ some
649+ multi
650+ line
651+ #[\n|]#
652+ string
653+ fook:
654+ "}),
655+ ),
656+ (
657+ helpers::platform_line(indoc! {"\
658+ top:
659+ baz: foo
660+ bax: >
661+ some#[\n|]#
662+ multi
663+ line
664+ string
665+ fook:
666+ "}),
667+ "O",
668+ helpers::platform_line(indoc! {"\
669+ top:
670+ baz: foo
671+ bax: >
672+ #[\n|]#
673+ some
674+ multi
675+ line
676+ string
677+ fook:
678+ "}),
679+ ),
680+ (
681+ helpers::platform_line(indoc! {"\
682+ top:
683+ baz: foo
684+ bax: >
685+ fook:#[\n|]#
686+ "}),
687+ "O",
688+ helpers::platform_line(indoc! {"\
689+ top:
690+ baz: foo
691+ bax: >
692+ #[\n|]#
693+ fook:
694+ "}),
695+ ),
696+ (
697+ helpers::platform_line(indoc! {"\
698+ - top:
699+ baz: foo#[\n|]#
700+ bax: foox
701+ fook:
702+ "}),
703+ "O",
704+ helpers::platform_line(indoc! {"\
705+ - top:
706+ #[\n|]#
707+ baz: foo
708+ bax: foox
709+ fook:
710+ "}),
711+ ),
712+ (
713+ helpers::platform_line(indoc! {"\
714+ - top:
715+ baz: foo
716+ bax: foox
717+ fook:#[\n|]#
718+ "}),
719+ "O",
720+ helpers::platform_line(indoc! {"\
721+ - top:
722+ baz: foo
723+ bax: foox
724+ #[\n|]#
725+ fook:
726+ "}),
727+ ),
728+ (
729+ helpers::platform_line(indoc! {"\
730+ top:
731+ baz:
732+ - one: two#[\n|]#
733+ three: four
734+ - top:
735+ baz: foo
736+ bax: foox
737+ "}),
738+ "O",
739+ helpers::platform_line(indoc! {"\
740+ top:
741+ baz:
742+ #[\n|]#
743+ - one: two
744+ three: four
745+ - top:
746+ baz: foo
747+ bax: foox
748+ "}),
749+ ),
750+ // yaml map without a key
751+ (
752+ helpers::platform_line(indoc! {"\
753+ top:#[\n|]#
754+ "}),
755+ "O",
756+ helpers::platform_line(indoc! {"\
757+ #[\n|]#
758+ top:
759+ "}),
760+ ),
761+ (
762+ helpers::platform_line(indoc! {"\
763+ bottom: withvalue
764+ top#[:|]#
765+ "}),
766+ "O",
767+ helpers::platform_line(indoc! {"\
768+ bottom: withvalue
769+ #[\n|]#
770+ top:
771+ "}),
772+ ),
773+ (
774+ helpers::platform_line(indoc! {"\
775+ top:
776+ bottom:#[ |]#withvalue
777+ "}),
778+ "O",
779+ helpers::platform_line(indoc! {"\
780+ top:
781+ #[\n|]#
782+ bottom: withvalue
783+ "}),
784+ ),
785+ ];
786+
787+ for test in above_tests {
788+ test_with_config(app(), test).await?;
789+ }
790+
791+ let enter_tests = [
792+ (
793+ helpers::platform_line(indoc! {r##"
794+ foo: #[b|]#ar
795+ "##}),
796+ "i<ret>",
797+ helpers::platform_line(indoc! {"\
798+ foo:
799+ #[|b]#ar
800+ "}),
801+ ),
802+ (
803+ helpers::platform_line(indoc! {"\
804+ foo:#[\n|]#
805+ "}),
806+ "i<ret>",
807+ helpers::platform_line(indoc! {"\
808+ foo:
809+ #[|\n]#
810+ "}),
811+ ),
812+ ];
813+
814+ for test in enter_tests {
815+ test_with_config(app(), test).await?;
816+ }
817+
818+ Ok(())
819+}
helix-term/tests/test/movement.rs+39
@@ -513,3 +513,42 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
513513
514514 Ok(())
515515}
516+
517+#[tokio::test(flavor = "multi_thread")]
518+async fn find_char_line_ending() -> anyhow::Result<()> {
519+ test((
520+ helpers::platform_line(indoc! {
521+ "\
522+ one
523+ #[|t]#wo
524+ three"
525+ }),
526+ "T<ret>gll2f<ret>",
527+ helpers::platform_line(indoc! {
528+ "\
529+ one
530+ two#[
531+ |]#three"
532+ }),
533+ ))
534+ .await?;
535+
536+ test((
537+ helpers::platform_line(indoc! {
538+ "\
539+ #[|o]#ne
540+ two
541+ three"
542+ }),
543+ "f<ret>2t<ret>ghT<ret>F<ret>",
544+ helpers::platform_line(indoc! {
545+ "\
546+ one#[|
547+ t]#wo
548+ three"
549+ }),
550+ ))
551+ .await?;
552+
553+ Ok(())
554+}
helix-term/tests/test/splits.rs+3−3
@@ -62,9 +62,9 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
6262 )
6363 .await?;
6464
65− helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
66− helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
67− helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
65+ helpers::assert_file_has_content(file1.as_file_mut(), &platform_line("hello1"))?;
66+ helpers::assert_file_has_content(file2.as_file_mut(), &platform_line("hello2"))?;
67+ helpers::assert_file_has_content(file3.as_file_mut(), &platform_line("hello3"))?;
6868
6969 Ok(())
7070}
helix-tui/Cargo.toml+15−15
@@ -1,28 +1,28 @@
11[package]
22name = "helix-tui"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−description = """
6−A library to build rich terminal user interfaces or dashboards
7−"""
8−edition = "2021"
9−license = "MPL-2.0"
10−categories = ["editor"]
11−repository = "https://github.com/helix-editor/helix"
12−homepage = "https://helix-editor.com"
3+description = """A library to build rich terminal user interfaces or dashboards"""
134include = ["src/**/*", "README.md"]
5+version.workspace = true
6+authors.workspace = true
7+edition.workspace = true
8+license.workspace = true
9+rust-version.workspace = true
10+categories.workspace = true
11+repository.workspace = true
12+homepage.workspace = true
1413
1514[features]
1615default = ["crossterm"]
1716
1817[dependencies]
19−bitflags = "2.3"
18+helix-view = { path = "../helix-view", features = ["term"] }
19+helix-core = { path = "../helix-core" }
20+
21+bitflags = "2.4"
2022cassowary = "0.3"
2123unicode-segmentation = "1.10"
22−crossterm = { version = "0.26", optional = true }
24+crossterm = { version = "0.27", optional = true }
2325termini = "1.0"
2426serde = { version = "1", "optional" = true, features = ["derive"]}
25−once_cell = "1.18"
27+once_cell = "1.19"
2628log = "~0.4"
27−helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
28−helix-core = { version = "0.6", path = "../helix-core" }
helix-tui/src/backend/crossterm.rs+51−33
@@ -32,10 +32,21 @@ fn vte_version() -> Option<usize> {
3232}
3333
3434/// Describes terminal capabilities like extended underline, truecolor, etc.
35−#[derive(Copy, Clone, Debug, Default)]
35+#[derive(Clone, Debug)]
3636struct Capabilities {
3737 /// Support for undercurled, underdashed, etc.
3838 has_extended_underlines: bool,
39+ /// Support for resetting the cursor style back to normal.
40+ reset_cursor_command: String,
41+}
42+
43+impl Default for Capabilities {
44+ fn default() -> Self {
45+ Self {
46+ has_extended_underlines: false,
47+ reset_cursor_command: "\x1B[0 q".to_string(),
48+ }
49+ }
3950}
4051
4152impl Capabilities {
@@ -54,6 +65,10 @@ impl Capabilities {
5465 || t.extended_cap("Su").is_some()
5566 || vte_version() >= Some(5102)
5667 || matches!(term_program().as_deref(), Some("WezTerm")),
68+ reset_cursor_command: t
69+ .utf8_string_cap(termini::StringCapability::CursorNormal)
70+ .unwrap_or("\x1B[0 q")
71+ .to_string(),
5772 },
5873 }
5974 }
@@ -154,7 +169,8 @@ where
154169
155170 fn restore(&mut self, config: Config) -> io::Result<()> {
156171 // reset cursor shape
157− write!(self.buffer, "\x1B[0 q")?;
172+ self.buffer
173+ .write_all(self.capabilities.reset_cursor_command.as_bytes())?;
158174 if config.enable_mouse_capture {
159175 execute!(self.buffer, DisableMouseCapture)?;
160176 }
@@ -201,7 +217,7 @@ where
201217 for (x, y, cell) in content {
202218 // Move the cursor if the previous location was not (x - 1, y)
203219 if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
204− map_error(queue!(self.buffer, MoveTo(x, y)))?;
220+ queue!(self.buffer, MoveTo(x, y))?;
205221 }
206222 last_pos = Some((x, y));
207223 if cell.modifier != modifier {
@@ -214,12 +230,12 @@ where
214230 }
215231 if cell.fg != fg {
216232 let color = CColor::from(cell.fg);
217− map_error(queue!(self.buffer, SetForegroundColor(color)))?;
233+ queue!(self.buffer, SetForegroundColor(color))?;
218234 fg = cell.fg;
219235 }
220236 if cell.bg != bg {
221237 let color = CColor::from(cell.bg);
222− map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
238+ queue!(self.buffer, SetBackgroundColor(color))?;
223239 bg = cell.bg;
224240 }
225241
@@ -227,7 +243,7 @@ where
227243 if self.capabilities.has_extended_underlines {
228244 if cell.underline_color != underline_color {
229245 let color = CColor::from(cell.underline_color);
230− map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
246+ queue!(self.buffer, SetUnderlineColor(color))?;
231247 underline_color = cell.underline_color;
232248 }
233249 } else {
@@ -239,24 +255,24 @@ where
239255
240256 if new_underline_style != underline_style {
241257 let attr = CAttribute::from(new_underline_style);
242− map_error(queue!(self.buffer, SetAttribute(attr)))?;
258+ queue!(self.buffer, SetAttribute(attr))?;
243259 underline_style = new_underline_style;
244260 }
245261
246− map_error(queue!(self.buffer, Print(&cell.symbol)))?;
262+ queue!(self.buffer, Print(&cell.symbol))?;
247263 }
248264
249− map_error(queue!(
265+ queue!(
250266 self.buffer,
251267 SetUnderlineColor(CColor::Reset),
252268 SetForegroundColor(CColor::Reset),
253269 SetBackgroundColor(CColor::Reset),
254270 SetAttribute(CAttribute::Reset)
255− ))
271+ )
256272 }
257273
258274 fn hide_cursor(&mut self) -> io::Result<()> {
259− map_error(execute!(self.buffer, Hide))
275+ execute!(self.buffer, Hide)
260276 }
261277
262278 fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
@@ -266,7 +282,7 @@ where
266282 CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
267283 CursorKind::Hidden => unreachable!(),
268284 };
269− map_error(execute!(self.buffer, Show, shape))
285+ execute!(self.buffer, Show, shape)
270286 }
271287
272288 fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -275,11 +291,11 @@ where
275291 }
276292
277293 fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
278− map_error(execute!(self.buffer, MoveTo(x, y)))
294+ execute!(self.buffer, MoveTo(x, y))
279295 }
280296
281297 fn clear(&mut self) -> io::Result<()> {
282− map_error(execute!(self.buffer, Clear(ClearType::All)))
298+ execute!(self.buffer, Clear(ClearType::All))
283299 }
284300
285301 fn size(&self) -> io::Result<Rect> {
@@ -294,10 +310,6 @@ where
294310 }
295311}
296312
297−fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
298− error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
299−}
300−
301313#[derive(Debug)]
302314struct ModifierDiff {
303315 pub from: Modifier,
@@ -312,48 +324,54 @@ impl ModifierDiff {
312324 //use crossterm::Attribute;
313325 let removed = self.from - self.to;
314326 if removed.contains(Modifier::REVERSED) {
315− map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
327+ queue!(w, SetAttribute(CAttribute::NoReverse))?;
316328 }
317329 if removed.contains(Modifier::BOLD) {
318− map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
330+ queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
319331 if self.to.contains(Modifier::DIM) {
320− map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
332+ queue!(w, SetAttribute(CAttribute::Dim))?;
321333 }
322334 }
323335 if removed.contains(Modifier::ITALIC) {
324− map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
336+ queue!(w, SetAttribute(CAttribute::NoItalic))?;
325337 }
326338 if removed.contains(Modifier::DIM) {
327− map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
339+ queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
328340 }
329341 if removed.contains(Modifier::CROSSED_OUT) {
330− map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
342+ queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
331343 }
332344 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
333− map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
345+ queue!(w, SetAttribute(CAttribute::NoBlink))?;
346+ }
347+ if removed.contains(Modifier::HIDDEN) {
348+ queue!(w, SetAttribute(CAttribute::NoHidden))?;
334349 }
335350
336351 let added = self.to - self.from;
337352 if added.contains(Modifier::REVERSED) {
338− map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
353+ queue!(w, SetAttribute(CAttribute::Reverse))?;
339354 }
340355 if added.contains(Modifier::BOLD) {
341− map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
356+ queue!(w, SetAttribute(CAttribute::Bold))?;
342357 }
343358 if added.contains(Modifier::ITALIC) {
344− map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
359+ queue!(w, SetAttribute(CAttribute::Italic))?;
345360 }
346361 if added.contains(Modifier::DIM) {
347− map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
362+ queue!(w, SetAttribute(CAttribute::Dim))?;
348363 }
349364 if added.contains(Modifier::CROSSED_OUT) {
350− map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
365+ queue!(w, SetAttribute(CAttribute::CrossedOut))?;
351366 }
352367 if added.contains(Modifier::SLOW_BLINK) {
353− map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
368+ queue!(w, SetAttribute(CAttribute::SlowBlink))?;
354369 }
355370 if added.contains(Modifier::RAPID_BLINK) {
356− map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
371+ queue!(w, SetAttribute(CAttribute::RapidBlink))?;
372+ }
373+ if added.contains(Modifier::HIDDEN) {
374+ queue!(w, SetAttribute(CAttribute::Hidden))?;
357375 }
358376
359377 Ok(())
@@ -407,7 +425,7 @@ impl Command for SetUnderlineColor {
407425 }
408426
409427 #[cfg(windows)]
410− fn execute_winapi(&self) -> crossterm::Result<()> {
428+ fn execute_winapi(&self) -> io::Result<()> {
411429 Err(std::io::Error::new(
412430 std::io::ErrorKind::Other,
413431 "SetUnderlineColor not supported by winapi.",
helix-tui/src/buffer.rs+2−2
@@ -213,7 +213,7 @@ impl Buffer {
213213 && y < self.area.bottom()
214214 }
215215
216− /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
216+ /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
217217 ///
218218 /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
219219 ///
@@ -242,7 +242,7 @@ impl Buffer {
242242 ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize)
243243 }
244244
245− /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates,
245+ /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates,
246246 /// or `None` if the coordinates are outside the buffer's area.
247247 fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
248248 if self.in_bounds(x, y) {
helix-tui/src/terminal.rs−14
@@ -65,20 +65,6 @@ where
6565 viewport: Viewport,
6666}
6767
68−impl<B> Drop for Terminal<B>
69−where
70− B: Backend,
71−{
72− fn drop(&mut self) {
73− // Attempt to restore the cursor state
74− if self.cursor_kind == CursorKind::Hidden {
75− if let Err(err) = self.show_cursor(CursorKind::Block) {
76− eprintln!("Failed to show the cursor: {}", err);
77− }
78− }
79− }
80−}
81−
8268impl<B> Terminal<B>
8369where
8470 B: Backend,
helix-vcs/Cargo.toml+12−10
@@ -1,23 +1,25 @@
11[package]
22name = "helix-vcs"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
7−categories = ["editor"]
8−repository = "https://github.com/helix-editor/helix"
9−homepage = "https://helix-editor.com"
3+version.workspace = true
4+authors.workspace = true
5+edition.workspace = true
6+license.workspace = true
7+rust-version.workspace = true
8+categories.workspace = true
9+repository.workspace = true
10+homepage.workspace = true
1011
1112# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1213
1314[dependencies]
14−helix-core = { version = "0.6", path = "../helix-core" }
15+helix-core = { path = "../helix-core" }
16+helix-event = { path = "../helix-event" }
1517
1618tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
1719parking_lot = "0.12"
1820arc-swap = { version = "1.6.0" }
1921
20−gix = { version = "0.48.0", default-features = false , optional = true }
22+gix = { version = "0.57.0", default-features = false , optional = true }
2123imara-diff = "0.1.5"
2224anyhow = "1"
2325
@@ -27,4 +29,4 @@ log = "0.4"
2729git = ["gix"]
2830
2931[dev-dependencies]
30−tempfile = "3.6"
32+tempfile = "3.8"
helix-vcs/src/diff.rs+6−19
@@ -2,10 +2,10 @@ use std::ops::Range;
22use std::sync::Arc;
33
44use helix_core::Rope;
5+use helix_event::RenderLockGuard;
56use imara_diff::Algorithm;
67use parking_lot::{Mutex, MutexGuard};
78use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8−use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
99use tokio::task::JoinHandle;
1010use tokio::time::Instant;
1111
@@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker;
1414mod line_cache;
1515mod worker;
1616
17−type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18−
1917/// A rendering lock passed to the differ the prevents redraws from occurring
2018struct RenderLock {
21− pub lock: OwnedRwLockReadGuard<()>,
19+ pub lock: RenderLockGuard,
2220 pub timeout: Option<Instant>,
2321}
2422
@@ -38,28 +36,22 @@ struct DiffInner {
3836#[derive(Clone, Debug)]
3937pub struct DiffHandle {
4038 channel: UnboundedSender<Event>,
41− render_lock: Arc<RwLock<()>>,
4239 diff: Arc<Mutex<DiffInner>>,
4340 inverted: bool,
4441}
4542
4643impl DiffHandle {
47− pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
48− DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
44+ pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
45+ DiffHandle::new_with_handle(diff_base, doc).0
4946 }
5047
51− fn new_with_handle(
52− diff_base: Rope,
53− doc: Rope,
54− redraw_handle: RedrawHandle,
55− ) -> (DiffHandle, JoinHandle<()>) {
48+ fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) {
5649 let (sender, receiver) = unbounded_channel();
5750 let diff: Arc<Mutex<DiffInner>> = Arc::default();
5851 let worker = DiffWorker {
5952 channel: receiver,
6053 diff: diff.clone(),
6154 new_hunks: Vec::default(),
62− redraw_notify: redraw_handle.0,
6355 diff_finished_notify: Arc::default(),
6456 };
6557 let handle = tokio::spawn(worker.run(diff_base, doc));
@@ -67,7 +59,6 @@ impl DiffHandle {
6759 channel: sender,
6860 diff,
6961 inverted: false,
70− render_lock: redraw_handle.1,
7162 };
7263 (differ, handle)
7364 }
@@ -87,11 +78,7 @@ impl DiffHandle {
8778 /// This function is only intended to be called from within the rendering loop
8879 /// if called from elsewhere it may fail to acquire the render lock and panic
8980 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
90− // unwrap is ok here because the rendering lock is
91− // only exclusively locked during redraw.
92− // This function is only intended to be called
93− // from the core rendering loop where no redraw can happen in parallel
94− let lock = self.render_lock.clone().try_read_owned().unwrap();
81+ let lock = helix_event::lock_frame();
9582 let timeout = if block {
9683 None
9784 } else {
helix-vcs/src/diff/worker.rs+3−9
@@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
2323 pub channel: UnboundedReceiver<Event>,
2424 pub diff: Arc<Mutex<DiffInner>>,
2525 pub new_hunks: Vec<Hunk>,
26− pub redraw_notify: Arc<Notify>,
2726 pub diff_finished_notify: Arc<Notify>,
2827}
2928
@@ -32,11 +31,7 @@ impl DiffWorker {
3231 let mut accumulator = EventAccumulator::new();
3332 accumulator.handle_event(event).await;
3433 accumulator
35− .accumulate_debounced_events(
36− &mut self.channel,
37− self.redraw_notify.clone(),
38− self.diff_finished_notify.clone(),
39− )
34+ .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
4035 .await;
4136 (accumulator.doc, accumulator.diff_base)
4237 }
@@ -137,7 +132,6 @@ impl<'a> EventAccumulator {
137132 async fn accumulate_debounced_events(
138133 &mut self,
139134 channel: &mut UnboundedReceiver<Event>,
140− redraw_notify: Arc<Notify>,
141135 diff_finished_notify: Arc<Notify>,
142136 ) {
143137 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@@ -164,7 +158,7 @@ impl<'a> EventAccumulator {
164158 None => {
165159 tokio::spawn(async move {
166160 diff_finished_notify.notified().await;
167− redraw_notify.notify_one();
161+ helix_event::request_redraw();
168162 });
169163 }
170164 // diff is performed inside the rendering loop
@@ -190,7 +184,7 @@ impl<'a> EventAccumulator {
190184 // and wait until the diff occurs to trigger an async redraw
191185 log::info!("Diff computation timed out, update of diffs might appear delayed");
192186 diff_finished_notify.notified().await;
193− redraw_notify.notify_one();
187+ helix_event::request_redraw()
194188 });
195189 }
196190 // a blocking diff is performed inside the rendering loop
helix-vcs/src/diff/worker/test.rs+1−5
@@ -5,11 +5,7 @@ use crate::diff::{DiffHandle, Hunk};
55
66impl DiffHandle {
77 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8− DiffHandle::new_with_handle(
9− Rope::from_str(diff_base),
10− Rope::from_str(doc),
11− Default::default(),
12− )
8+ DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
139 }
1410 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
1511 let diff = self.diff;
helix-vcs/src/git.rs+5−5
@@ -3,7 +3,7 @@ use arc_swap::ArcSwap;
33use std::path::Path;
44use std::sync::Arc;
55
6−use gix::objs::tree::EntryMode;
6+use gix::objs::tree::EntryKind;
77use gix::sec::trust::DefaultForLevel;
88use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
99
@@ -126,14 +126,14 @@ fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Resul
126126 let rel_path = file.strip_prefix(repo_dir)?;
127127 let tree = commit.tree()?;
128128 let tree_entry = tree
129− .lookup_entry_by_path(rel_path)?
129+ .lookup_entry_by_path(rel_path, &mut Vec::new())?
130130 .context("file is untracked")?;
131− match tree_entry.mode() {
131+ match tree_entry.mode().kind() {
132132 // not a file, everything is new, do not show diff
133− mode @ (EntryMode::Tree | EntryMode::Commit | EntryMode::Link) => {
133+ mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
134134 bail!("entry at {} is not a file but a {mode:?}", file.display())
135135 }
136136 // found a file
137− EntryMode::Blob | EntryMode::BlobExecutable => Ok(tree_entry.object_id()),
137+ EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
138138 }
139139}
helix-view/Cargo.toml+22−18
@@ -1,31 +1,34 @@
11[package]
22name = "helix-view"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
73description = "UI abstractions for use in backends"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
4+version.workspace = true
5+authors.workspace = true
6+edition.workspace = true
7+license.workspace = true
8+rust-version.workspace = true
9+categories.workspace = true
10+repository.workspace = true
11+homepage.workspace = true
1112
1213[features]
1314default = []
1415term = ["crossterm"]
1516
1617[dependencies]
17−bitflags = "2.3"
18+helix-core = { path = "../helix-core" }
19+helix-event = { path = "../helix-event" }
20+helix-loader = { path = "../helix-loader" }
21+helix-lsp = { path = "../helix-lsp" }
22+helix-dap = { path = "../helix-dap" }
23+helix-vcs = { path = "../helix-vcs" }
24+
25+bitflags = "2.4"
1826anyhow = "1"
19−helix-core = { version = "0.6", path = "../helix-core" }
20−helix-loader = { version = "0.6", path = "../helix-loader" }
21−helix-lsp = { version = "0.6", path = "../helix-lsp" }
22−helix-dap = { version = "0.6", path = "../helix-dap" }
23−crossterm = { version = "0.26", optional = true }
24−helix-vcs = { version = "0.6", path = "../helix-vcs" }
27+crossterm = { version = "0.27", optional = true }
2528
2629# Conversion traits
27−once_cell = "1.18"
28−url = "2"
30+once_cell = "1.19"
31+url = "2.5.0"
2932
3033arc-swap = { version = "1.6.0" }
3134
@@ -42,15 +45,16 @@ serde_json = "1.0"
4245toml = "0.7"
4346log = "~0.4"
4447
45−which = "4.4"
48+which = "5.0.0"
4649parking_lot = "0.12.1"
4750
4851
4952[target.'cfg(windows)'.dependencies]
50−clipboard-win = { version = "4.5", features = ["std"] }
53+clipboard-win = { version = "5.0", features = ["std"] }
5154
5255[target.'cfg(unix)'.dependencies]
5356libc = "0.2"
57+rustix = { version = "0.38", features = ["fs"] }
5458
5559[dev-dependencies]
5660helix-tui = { path = "../helix-tui" }
helix-view/src/clipboard.rs+7−2
@@ -73,9 +73,14 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
7373
7474#[cfg(target_os = "macos")]
7575pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
76− use crate::env::binary_exists;
76+ use crate::env::{binary_exists, env_var_is_set};
7777
78− if binary_exists("pbcopy") && binary_exists("pbpaste") {
78+ if env_var_is_set("TMUX") && binary_exists("tmux") {
79+ command_provider! {
80+ paste => "tmux", "save-buffer", "-";
81+ copy => "tmux", "load-buffer", "-w", "-";
82+ }
83+ } else if binary_exists("pbcopy") && binary_exists("pbpaste") {
7984 command_provider! {
8085 paste => "pbpaste";
8186 copy => "pbcopy";
helix-view/src/document.rs+122−46
@@ -33,7 +33,7 @@ use helix_core::{
3333 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
3434};
3535
36−use crate::editor::{Config, RedrawHandle};
36+use crate::editor::Config;
3737use crate::{DocumentId, Editor, Theme, View, ViewId};
3838
3939/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -185,6 +185,8 @@ pub struct Document {
185185
186186 // when document was used for most-recent-used buffer picker
187187 pub focused_at: std::time::Instant,
188+
189+ pub readonly: bool,
188190}
189191
190192/// Inlay hints for a single `(Document, View)` combo.
@@ -642,7 +644,7 @@ impl Document {
642644 ) -> Self {
643645 let (encoding, has_bom) = encoding_with_bom_info.unwrap_or((encoding::UTF_8, false));
644646 let line_ending = config.load().default_line_ending.into();
645− let changes = ChangeSet::new(&text);
647+ let changes = ChangeSet::new(text.slice(..));
646648 let old_state = None;
647649
648650 Self {
@@ -673,6 +675,7 @@ impl Document {
673675 config,
674676 version_control_head: None,
675677 focused_at: std::time::Instant::now(),
678+ readonly: false,
676679 }
677680 }
678681
@@ -705,7 +708,7 @@ impl Document {
705708 let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
706709
707710 // set the path and try detecting the language
708− doc.set_path(Some(path))?;
711+ doc.set_path(Some(path));
709712 if let Some(loader) = config_loader {
710713 doc.detect_language(loader);
711714 }
@@ -730,16 +733,16 @@ impl Document {
730733 // We can't use anyhow::Result here since the output of the future has to be
731734 // clonable to be used as shared future. So use a custom error type.
732735 pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
733− if let Some(formatter) = self
736+ if let Some((fmt_cmd, fmt_args)) = self
734737 .language_config()
735− .and_then(|c| c.formatter.clone())
736− .filter(|formatter| which::which(&formatter.command).is_ok())
738+ .and_then(|c| c.formatter.as_ref())
739+ .and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args)))
737740 {
738741 use std::process::Stdio;
739742 let text = self.text().clone();
740− let mut process = tokio::process::Command::new(&formatter.command);
743+ let mut process = tokio::process::Command::new(&fmt_cmd);
741744 process
742− .args(&formatter.args)
745+ .args(fmt_args)
743746 .stdin(Stdio::piped())
744747 .stdout(Stdio::piped())
745748 .stderr(Stdio::piped());
@@ -748,7 +751,7 @@ impl Document {
748751 let mut process = process
749752 .spawn()
750753 .map_err(|e| FormatterError::SpawningFailed {
751− command: formatter.command.clone(),
754+ command: fmt_cmd.to_string_lossy().into(),
752755 error: e.kind(),
753756 })?;
754757 {
@@ -850,7 +853,7 @@ impl Document {
850853 let text = self.text().clone();
851854
852855 let path = match path {
853− Some(path) => helix_core::path::get_canonicalized_path(&path)?,
856+ Some(path) => helix_core::path::get_canonicalized_path(&path),
854857 None => {
855858 if self.path.is_none() {
856859 bail!("Can't save with no path set!");
@@ -938,7 +941,7 @@ impl Document {
938941 ) -> Option<Arc<helix_core::syntax::LanguageConfiguration>> {
939942 config_loader
940943 .language_config_for_file_name(self.path.as_ref()?)
941− .or_else(|| config_loader.language_config_for_shebang(self.text()))
944+ .or_else(|| config_loader.language_config_for_shebang(self.text().slice(..)))
942945 }
943946
944947 /// Detect the indentation used in the file, or otherwise defaults to the language indentation
@@ -955,12 +958,43 @@ impl Document {
955958 }
956959 }
957960
961+ #[cfg(unix)]
962+ // Detect if the file is readonly and change the readonly field if necessary (unix only)
963+ pub fn detect_readonly(&mut self) {
964+ use rustix::fs::{access, Access};
965+ // Allows setting the flag for files the user cannot modify, like root files
966+ self.readonly = match &self.path {
967+ None => false,
968+ Some(p) => match access(p, Access::WRITE_OK) {
969+ Ok(_) => false,
970+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
971+ Err(_) => true,
972+ },
973+ };
974+ }
975+
976+ #[cfg(not(unix))]
977+ // Detect if the file is readonly and change the readonly field if necessary (non-unix os)
978+ pub fn detect_readonly(&mut self) {
979+ // TODO Use the Windows' function `CreateFileW` to check if a file is readonly
980+ // Discussion: https://github.com/helix-editor/helix/pull/7740#issuecomment-1656806459
981+ // Vim implementation: https://github.com/vim/vim/blob/4c0089d696b8d1d5dc40568f25ea5738fa5bbffb/src/os_win32.c#L7665
982+ // Windows binding: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/FileSystem/fn.CreateFileW.html
983+ self.readonly = match &self.path {
984+ None => false,
985+ Some(p) => match std::fs::metadata(p) {
986+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
987+ Err(_) => false,
988+ Ok(metadata) => metadata.permissions().readonly(),
989+ },
990+ };
991+ }
992+
958993 /// Reload the document from its path.
959994 pub fn reload(
960995 &mut self,
961996 view: &mut View,
962997 provider_registry: &DiffProviderRegistry,
963− redraw_handle: RedrawHandle,
964998 ) -> Result<(), Error> {
965999 let encoding = self.encoding;
9661000 let path = self
@@ -969,6 +1003,9 @@ impl Document {
9691003 .ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
9701004 .to_owned();
9711005
1006+ // Once we have a valid path we check if its readonly status has changed
1007+ self.detect_readonly();
1008+
9721009 let mut file = std::fs::File::open(&path)?;
9731010 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
9741011
@@ -985,7 +1022,7 @@ impl Document {
9851022 self.detect_indent_and_line_ending();
9861023
9871024 match provider_registry.get_diff_base(&path) {
988− Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
1025+ Some(diff_base) => self.set_diff_base(diff_base),
9891026 None => self.diff_handle = None,
9901027 }
9911028
@@ -1009,16 +1046,14 @@ impl Document {
10091046 self.encoding
10101047 }
10111048
1012− pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
1013− let path = path
1014− .map(helix_core::path::get_canonicalized_path)
1015− .transpose()?;
1049+ pub fn set_path(&mut self, path: Option<&Path>) {
1050+ let path = path.map(helix_core::path::get_canonicalized_path);
10161051
10171052 // if parent doesn't exist we still want to open the document
10181053 // and error out when document is saved
10191054 self.path = path;
10201055
1021− Ok(())
1056+ self.detect_readonly();
10221057 }
10231058
10241059 /// Set the programming language for the file and load associated data (e.g. highlighting)
@@ -1030,7 +1065,7 @@ impl Document {
10301065 ) {
10311066 if let (Some(language_config), Some(loader)) = (language_config, loader) {
10321067 if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
1033− self.syntax = Syntax::new(&self.text, highlight_config, loader);
1068+ self.syntax = Syntax::new(self.text.slice(..), highlight_config, loader);
10341069 }
10351070
10361071 self.language = Some(language_config);
@@ -1165,32 +1200,53 @@ impl Document {
11651200 // update tree-sitter syntax tree
11661201 if let Some(syntax) = &mut self.syntax {
11671202 // TODO: no unwrap
1168− let res = syntax.update(&old_doc, &self.text, transaction.changes());
1203+ let res = syntax.update(
1204+ old_doc.slice(..),
1205+ self.text.slice(..),
1206+ transaction.changes(),
1207+ );
11691208 if res.is_err() {
1170− log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
1209+ log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
11711210 self.syntax = None;
11721211 }
11731212 }
11741213
11751214 let changes = transaction.changes();
11761215
1177− changes.update_positions(
1178− self.diagnostics
1179− .iter_mut()
1180− .map(|diagnostic| (&mut diagnostic.range.start, Assoc::After)),
1181− );
1182− changes.update_positions(
1183− self.diagnostics
1184− .iter_mut()
1185− .map(|diagnostic| (&mut diagnostic.range.end, Assoc::After)),
1186− );
1187− // map state.diagnostics over changes::map_pos too
1188− for diagnostic in &mut self.diagnostics {
1216+ // map diagnostics over changes too
1217+ changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
1218+ let assoc = if diagnostic.starts_at_word {
1219+ Assoc::BeforeWord
1220+ } else {
1221+ Assoc::After
1222+ };
1223+ (&mut diagnostic.range.start, assoc)
1224+ }));
1225+ changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
1226+ let assoc = if diagnostic.ends_at_word {
1227+ Assoc::AfterWord
1228+ } else {
1229+ Assoc::Before
1230+ };
1231+ (&mut diagnostic.range.end, assoc)
1232+ }));
1233+ self.diagnostics.retain_mut(|diagnostic| {
1234+ if diagnostic.range.start > diagnostic.range.end
1235+ || (!diagnostic.zero_width && diagnostic.range.start == diagnostic.range.end)
1236+ {
1237+ return false;
1238+ }
11891239 diagnostic.line = self.text.char_to_line(diagnostic.range.start);
1190− }
1240+ true
1241+ });
11911242
1192− self.diagnostics
1193− .sort_unstable_by_key(|diagnostic| diagnostic.range);
1243+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
1244+ (
1245+ diagnostic.range,
1246+ diagnostic.severity,
1247+ diagnostic.language_server_id,
1248+ )
1249+ });
11941250
11951251 // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
11961252 let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
@@ -1288,7 +1344,7 @@ impl Document {
12881344
12891345 if success {
12901346 // reset changeset to fix len
1291− self.changes = ChangeSet::new(self.text());
1347+ self.changes = ChangeSet::new(self.text().slice(..));
12921348 // Sync with changes with the jumplist selections.
12931349 view.sync_changes(self);
12941350 }
@@ -1371,7 +1427,7 @@ impl Document {
13711427 }
13721428 if success {
13731429 // reset changeset to fix len
1374− self.changes = ChangeSet::new(self.text());
1430+ self.changes = ChangeSet::new(self.text().slice(..));
13751431 // Sync with changes with the jumplist selections.
13761432 view.sync_changes(self);
13771433 }
@@ -1394,7 +1450,7 @@ impl Document {
13941450 return;
13951451 }
13961452
1397− let new_changeset = ChangeSet::new(self.text());
1453+ let new_changeset = ChangeSet::new(self.text().slice(..));
13981454 let changes = std::mem::replace(&mut self.changes, new_changeset);
13991455 // Instead of doing this messy merge we could always commit, and based on transaction
14001456 // annotations either add a new layer or compose into the previous one.
@@ -1543,13 +1599,13 @@ impl Document {
15431599 }
15441600
15451601 /// Intialize/updates the differ for this document with a new base.
1546− pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
1602+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
15471603 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
15481604 if let Some(differ) = &self.diff_handle {
15491605 differ.update_diff_base(diff_base);
15501606 return;
15511607 }
1552− self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
1608+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone()))
15531609 } else {
15541610 self.diff_handle = None;
15551611 }
@@ -1667,13 +1723,33 @@ impl Document {
16671723
16681724 pub fn replace_diagnostics(
16691725 &mut self,
1670− mut diagnostics: Vec<Diagnostic>,
1726+ diagnostics: impl IntoIterator<Item = Diagnostic>,
1727+ unchanged_sources: &[String],
16711728 language_server_id: usize,
16721729 ) {
1673− self.clear_diagnostics(language_server_id);
1674− self.diagnostics.append(&mut diagnostics);
1675− self.diagnostics
1676− .sort_unstable_by_key(|diagnostic| diagnostic.range);
1730+ if unchanged_sources.is_empty() {
1731+ self.clear_diagnostics(language_server_id);
1732+ } else {
1733+ self.diagnostics.retain(|d| {
1734+ if d.language_server_id != language_server_id {
1735+ return true;
1736+ }
1737+
1738+ if let Some(source) = &d.source {
1739+ unchanged_sources.contains(source)
1740+ } else {
1741+ false
1742+ }
1743+ });
1744+ }
1745+ self.diagnostics.extend(diagnostics);
1746+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
1747+ (
1748+ diagnostic.range,
1749+ diagnostic.severity,
1750+ diagnostic.language_server_id,
1751+ )
1752+ });
16771753 }
16781754
16791755 pub fn clear_diagnostics(&mut self, language_server_id: usize) {
helix-view/src/editor.rs+154−77
@@ -1,10 +1,10 @@
11use crate::{
22 align_view,
3− clipboard::{get_clipboard_provider, ClipboardProvider},
43 document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
54 graphics::{CursorKind, Rect},
65 info::Info,
76 input::KeyEvent,
7+ register::Registers,
88 theme::{self, Theme},
99 tree::{self, Tree},
1010 view::ViewPosition,
@@ -32,7 +32,7 @@ use std::{
3232use tokio::{
3333 sync::{
3434 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
35− oneshot, Notify, RwLock,
35+ oneshot,
3636 },
3737 time::{sleep, Duration, Instant, Sleep},
3838};
@@ -40,13 +40,11 @@ use tokio::{
4040use anyhow::{anyhow, bail, Error};
4141
4242pub use helix_core::diagnostic::Severity;
43−pub use helix_core::register::Registers;
4443use helix_core::{
4544 auto_pairs::AutoPairs,
46− syntax::{self, AutoPairConfig, SoftWrap},
47− Change, LineEnding, NATIVE_LINE_ENDING,
45+ syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
46+ Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
4847};
49−use helix_core::{Position, Selection};
5048use helix_dap as dap;
5149use helix_lsp::lsp;
5250
@@ -245,7 +243,7 @@ pub struct Config {
245243 /// Set a global text_width
246244 pub text_width: usize,
247245 /// Time in milliseconds since last keypress before idle timers trigger.
248− /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
246+ /// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
249247 #[serde(
250248 serialize_with = "serialize_duration_millis",
251249 deserialize_with = "deserialize_duration_millis"
@@ -288,6 +286,31 @@ pub struct Config {
288286 pub workspace_lsp_roots: Vec<PathBuf>,
289287 /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
290288 pub default_line_ending: LineEndingConfig,
289+ /// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
290+ pub insert_final_newline: bool,
291+ /// Enables smart tab
292+ pub smart_tab: Option<SmartTabConfig>,
293+ /// Draw border around popups.
294+ pub popup_border: PopupBorderConfig,
295+ /// Which indent heuristic to use when a new line is inserted
296+ #[serde(default)]
297+ pub indent_heuristic: IndentationHeuristic,
298+}
299+
300+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
301+#[serde(rename_all = "kebab-case", default)]
302+pub struct SmartTabConfig {
303+ pub enable: bool,
304+ pub supersede_menu: bool,
305+}
306+
307+impl Default for SmartTabConfig {
308+ fn default() -> Self {
309+ SmartTabConfig {
310+ enable: true,
311+ supersede_menu: false,
312+ }
313+ }
291314}
292315
293316#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -404,6 +427,7 @@ impl Default for StatusLineConfig {
404427 E::Mode,
405428 E::Spinner,
406429 E::FileName,
430+ E::ReadOnlyIndicator,
407431 E::FileModificationIndicator,
408432 ],
409433 center: vec![],
@@ -447,15 +471,18 @@ pub enum StatusLineElement {
447471 /// The LSP activity spinner
448472 Spinner,
449473
450− /// The base file name, including a dirty flag if it's unsaved
474+ /// The file basename (the leaf of the open file's path)
451475 FileBaseName,
452476
453− /// The relative file path, including a dirty flag if it's unsaved
477+ /// The relative file path
454478 FileName,
455479
456480 // The file modification indicator
457481 FileModificationIndicator,
458482
483+ /// An indicator that shows `"[readonly]"` when a file cannot be written
484+ ReadOnlyIndicator,
485+
459486 /// The file encoding
460487 FileEncoding,
461488
@@ -704,7 +731,7 @@ pub struct WhitespaceCharacters {
704731impl Default for WhitespaceCharacters {
705732 fn default() -> Self {
706733 Self {
707− space: '·', // U+00B7
734+ space: '·', // U+00B7
708735 nbsp: '⍽', // U+237D
709736 tab: '→', // U+2192
710737 newline: '⏎', // U+23CE
@@ -732,12 +759,13 @@ impl Default for IndentGuidesConfig {
732759}
733760
734761/// Line ending configuration.
735−#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
762+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
736763#[serde(rename_all = "lowercase")]
737764pub enum LineEndingConfig {
738765 /// The platform's native line ending.
739766 ///
740767 /// `crlf` on Windows, otherwise `lf`.
768+ #[default]
741769 Native,
742770 /// Line feed.
743771 LF,
@@ -754,12 +782,6 @@ pub enum LineEndingConfig {
754782 Nel,
755783}
756784
757−impl Default for LineEndingConfig {
758− fn default() -> Self {
759− LineEndingConfig::Native
760− }
761−}
762−
763785impl From<LineEndingConfig> for LineEnding {
764786 fn from(line_ending: LineEndingConfig) -> Self {
765787 match line_ending {
@@ -776,6 +798,15 @@ impl From<LineEndingConfig> for LineEnding {
776798 }
777799}
778800
801+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802+#[serde(rename_all = "kebab-case")]
803+pub enum PopupBorderConfig {
804+ None,
805+ All,
806+ Popup,
807+ Menu,
808+}
809+
779810impl Default for Config {
780811 fn default() -> Self {
781812 Self {
@@ -796,7 +827,7 @@ impl Default for Config {
796827 auto_completion: true,
797828 auto_format: true,
798829 auto_save: false,
799− idle_timeout: Duration::from_millis(400),
830+ idle_timeout: Duration::from_millis(250),
800831 preview_completion_insert: true,
801832 completion_trigger_len: 2,
802833 auto_info: true,
@@ -821,6 +852,10 @@ impl Default for Config {
821852 completion_replace: false,
822853 workspace_lsp_roots: Vec::new(),
823854 default_line_ending: LineEndingConfig::default(),
855+ insert_final_newline: true,
856+ smart_tab: Some(SmartTabConfig::default()),
857+ popup_border: PopupBorderConfig::None,
858+ indent_heuristic: IndentationHeuristic::default(),
824859 }
825860 }
826861}
@@ -875,8 +910,6 @@ pub struct Editor {
875910 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
876911 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
877912
878− pub clipboard_provider: Box<dyn ClipboardProvider>,
879−
880913 pub syn_loader: Arc<syntax::Loader>,
881914 pub theme_loader: Arc<theme::Loader>,
882915 /// last_theme is used for theme previews. We store the current theme here,
@@ -898,16 +931,13 @@ pub struct Editor {
898931 pub auto_pairs: Option<AutoPairs>,
899932
900933 pub idle_timer: Pin<Box<Sleep>>,
934+ redraw_timer: Pin<Box<Sleep>>,
901935 last_motion: Option<Motion>,
902936 pub last_completion: Option<CompleteAction>,
903937
904938 pub exit_code: i32,
905939
906940 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
907− /// Allows asynchronous tasks to control the rendering
908− /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
909− /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be acquired
910− pub redraw_handle: RedrawHandle,
911941 pub needs_redraw: bool,
912942 /// Cached position of the cursor calculated during rendering.
913943 /// The content of `cursor_cache` is returned by `Editor::cursor` if
@@ -934,8 +964,6 @@ pub struct Editor {
934964
935965pub type Motion = Box<dyn Fn(&mut Editor)>;
936966
937−pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
938−
939967#[derive(Debug)]
940968pub enum EditorEvent {
941969 DocumentSaved(DocumentSavedEventResult),
@@ -943,6 +971,7 @@ pub enum EditorEvent {
943971 LanguageServerMessage((usize, Call)),
944972 DebuggerEvent(dap::Payload),
945973 IdleTimer,
974+ Redraw,
946975}
947976
948977#[derive(Debug, Clone)]
@@ -975,6 +1004,13 @@ pub enum Action {
9751004 VerticalSplit,
9761005}
9771006
1007+impl Action {
1008+ /// Whether to align the view to the cursor after executing this action
1009+ pub fn align_view(&self, view: &View, new_doc: DocumentId) -> bool {
1010+ !matches!((self, view.doc == new_doc), (Action::Load, false))
1011+ }
1012+}
1013+
9781014/// Error thrown on failed document closed
9791015pub enum CloseError {
9801016 /// Document doesn't exist
@@ -1023,23 +1059,32 @@ impl Editor {
10231059 last_theme: None,
10241060 last_selection: None,
10251061 registers: Registers::default(),
1026− clipboard_provider: get_clipboard_provider(),
10271062 status_msg: None,
10281063 autoinfo: None,
10291064 idle_timer: Box::pin(sleep(conf.idle_timeout)),
1065+ redraw_timer: Box::pin(sleep(Duration::MAX)),
10301066 last_motion: None,
10311067 last_completion: None,
10321068 config,
10331069 auto_pairs,
10341070 exit_code: 0,
10351071 config_events: unbounded_channel(),
1036− redraw_handle: Default::default(),
10371072 needs_redraw: false,
10381073 cursor_cache: Cell::new(None),
10391074 completion_request_handle: None,
10401075 }
10411076 }
10421077
1078+ pub fn popup_border(&self) -> bool {
1079+ self.config().popup_border == PopupBorderConfig::All
1080+ || self.config().popup_border == PopupBorderConfig::Popup
1081+ }
1082+
1083+ pub fn menu_border(&self) -> bool {
1084+ self.config().popup_border == PopupBorderConfig::All
1085+ || self.config().popup_border == PopupBorderConfig::Menu
1086+ }
1087+
10431088 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
10441089 motion(self);
10451090 self.last_motion = Some(Box::new(motion));
@@ -1163,71 +1208,80 @@ impl Editor {
11631208 }
11641209
11651210 /// Refreshes the language server for a given document
1166− pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1211+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
11671212 self.launch_language_servers(doc_id)
11681213 }
11691214
11701215 /// Launch a language server for a given document
1171− fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1216+ fn launch_language_servers(&mut self, doc_id: DocumentId) {
11721217 if !self.config().lsp.enable {
1173− return None;
1218+ return;
11741219 }
11751220 // if doc doesn't have a URL it's a scratch buffer, ignore it
1176− let doc = self.documents.get_mut(&doc_id)?;
1177− let doc_url = doc.url()?;
1221+ let Some(doc) = self.documents.get_mut(&doc_id) else {
1222+ return;
1223+ };
1224+ let Some(doc_url) = doc.url() else {
1225+ return;
1226+ };
11781227 let (lang, path) = (doc.language.clone(), doc.path().cloned());
11791228 let config = doc.config.load();
11801229 let root_dirs = &config.workspace_lsp_roots;
11811230
1182− // try to find language servers based on the language name
1183− let language_servers = lang.as_ref().and_then(|language| {
1231+ // store only successfully started language servers
1232+ let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| {
11841233 self.language_servers
11851234 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1186− .map_err(|e| {
1187− log::error!(
1188− "Failed to initialize the language servers for `{}` {{ {} }}",
1189− language.scope(),
1190− e
1191− )
1235+ .filter_map(|(lang, client)| match client {
1236+ Ok(client) => Some((lang, client)),
1237+ Err(err) => {
1238+ log::error!(
1239+ "Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
1240+ language.scope(),
1241+ lang,
1242+ err
1243+ );
1244+ None
1245+ }
11921246 })
1193− .ok()
1247+ .collect::<HashMap<_, _>>()
11941248 });
11951249
1196− if let Some(language_servers) = language_servers {
1197− let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1198−
1199− // only spawn new language servers if the servers aren't the same
1200−
1201− let doc_language_servers_not_in_registry =
1202− doc.language_servers.iter().filter(|(name, doc_ls)| {
1203− language_servers
1204− .get(*name)
1205− .map_or(true, |ls| ls.id() != doc_ls.id())
1206− });
1250+ if language_servers.is_empty() {
1251+ return;
1252+ }
12071253
1208− for (_, language_server) in doc_language_servers_not_in_registry {
1209− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1210− }
1254+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
12111255
1212− let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1213− doc.language_servers
1256+ // only spawn new language servers if the servers aren't the same
1257+ let doc_language_servers_not_in_registry =
1258+ doc.language_servers.iter().filter(|(name, doc_ls)| {
1259+ language_servers
12141260 .get(*name)
1215− .map_or(true, |doc_ls| ls.id() != doc_ls.id())
1261+ .map_or(true, |ls| ls.id() != doc_ls.id())
12161262 });
12171263
1218− for (_, language_server) in language_servers_not_in_doc {
1219− // TODO: this now races with on_init code if the init happens too quickly
1220− tokio::spawn(language_server.text_document_did_open(
1221− doc_url.clone(),
1222− doc.version(),
1223− doc.text(),
1224− language_id.clone(),
1225− ));
1226− }
1264+ for (_, language_server) in doc_language_servers_not_in_registry {
1265+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1266+ }
1267+
1268+ let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1269+ doc.language_servers
1270+ .get(*name)
1271+ .map_or(true, |doc_ls| ls.id() != doc_ls.id())
1272+ });
12271273
1228− doc.language_servers = language_servers;
1274+ for (_, language_server) in language_servers_not_in_doc {
1275+ // TODO: this now races with on_init code if the init happens too quickly
1276+ tokio::spawn(language_server.text_document_did_open(
1277+ doc_url.clone(),
1278+ doc.version(),
1279+ doc.text(),
1280+ language_id.clone(),
1281+ ));
12291282 }
1230− Some(())
1283+
1284+ doc.language_servers = language_servers;
12311285 }
12321286
12331287 fn _refresh(&mut self) {
@@ -1410,7 +1464,7 @@ impl Editor {
14101464
14111465 // ??? possible use for integration tests
14121466 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
1413− let path = helix_core::path::get_canonicalized_path(path)?;
1467+ let path = helix_core::path::get_canonicalized_path(path);
14141468 let id = self.document_by_path(&path).map(|doc| doc.id);
14151469
14161470 let id = if let Some(id) = id {
@@ -1424,12 +1478,12 @@ impl Editor {
14241478 )?;
14251479
14261480 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1427− doc.set_diff_base(diff_base, self.redraw_handle.clone());
1481+ doc.set_diff_base(diff_base);
14281482 }
14291483 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
14301484
14311485 let id = self.new_document(doc);
1432− let _ = self.launch_language_servers(id);
1486+ self.launch_language_servers(id);
14331487
14341488 id
14351489 };
@@ -1535,7 +1589,18 @@ impl Editor {
15351589
15361590 let path = path.map(|path| path.into());
15371591 let doc = doc_mut!(self, &doc_id);
1538− let future = doc.save(path, force)?;
1592+ let doc_save_future = doc.save(path, force)?;
1593+
1594+ // When a file is written to, notify the file event handler.
1595+ // Note: This can be removed once proper file watching is implemented.
1596+ let handler = self.language_servers.file_event_handler.clone();
1597+ let future = async move {
1598+ let res = doc_save_future.await;
1599+ if let Ok(event) = &res {
1600+ handler.file_changed(event.path.clone());
1601+ }
1602+ res
1603+ };
15391604
15401605 use futures_util::stream;
15411606
@@ -1671,6 +1736,14 @@ impl Editor {
16711736 &self,
16721737 timeout: Option<u64>,
16731738 ) -> Result<(), tokio::time::error::Elapsed> {
1739+ // Remove all language servers from the file event handler.
1740+ // Note: this is non-blocking.
1741+ for client in self.language_servers.iter_clients() {
1742+ self.language_servers
1743+ .file_event_handler
1744+ .remove_client(client.id());
1745+ }
1746+
16741747 tokio::time::timeout(
16751748 Duration::from_millis(timeout.unwrap_or(3000)),
16761749 future::join_all(
@@ -1704,16 +1777,20 @@ impl Editor {
17041777 return EditorEvent::DebuggerEvent(event)
17051778 }
17061779
1707− _ = self.redraw_handle.0.notified() => {
1780+ _ = helix_event::redraw_requested() => {
17081781 if !self.needs_redraw{
17091782 self.needs_redraw = true;
17101783 let timeout = Instant::now() + Duration::from_millis(33);
1711− if timeout < self.idle_timer.deadline(){
1712− self.idle_timer.as_mut().reset(timeout)
1784+ if timeout < self.idle_timer.deadline() && timeout < self.redraw_timer.deadline(){
1785+ self.redraw_timer.as_mut().reset(timeout)
17131786 }
17141787 }
17151788 }
17161789
1790+ _ = &mut self.redraw_timer => {
1791+ self.redraw_timer.as_mut().reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
1792+ return EditorEvent::Redraw
1793+ }
17171794 _ = &mut self.idle_timer => {
17181795 return EditorEvent::IdleTimer
17191796 }
helix-view/src/gutter.rs+4−5
@@ -9,8 +9,7 @@ use crate::{
99};
1010
1111fn count_digits(n: usize) -> usize {
12− // TODO: use checked_log10 when MSRV reaches 1.67
13− std::iter::successors(Some(n), |&n| (n >= 10).then_some(n / 10)).count()
12+ (usize::checked_ilog10(n).unwrap_or(0) + 1) as usize
1413}
1514
1615pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
@@ -94,9 +93,9 @@ pub fn diff<'doc>(
9493 theme: &Theme,
9594 _is_focused: bool,
9695) -> GutterFn<'doc> {
97− let added = theme.get("diff.plus");
98− let deleted = theme.get("diff.minus");
99− let modified = theme.get("diff.delta");
96+ let added = theme.get("diff.plus.gutter");
97+ let deleted = theme.get("diff.minus.gutter");
98+ let modified = theme.get("diff.delta.gutter");
10099 if let Some(diff_handle) = doc.diff_handle() {
101100 let hunks = diff_handle.load();
102101 let mut hunk_i = 0;
helix-view/src/handlers/dap.rs+1−3
@@ -369,9 +369,7 @@ impl Editor {
369369 {
370370 Ok(process) => process,
371371 Err(err) => {
372− // TODO replace the pretty print {:?} with a regular format {}
373− // when the MSRV is raised to 1.60.0
374− self.set_error(format!("Error starting external terminal: {:?}", err));
372+ self.set_error(format!("Error starting external terminal: {}", err));
375373 return true;
376374 }
377375 };
helix-view/src/info.rs+4−11
@@ -1,4 +1,5 @@
1−use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
1+use crate::register::Registers;
2+use helix_core::unicode::width::UnicodeWidthStr;
23use std::fmt::Write;
34
45#[derive(Debug)]
@@ -56,16 +57,8 @@ impl Info {
5657
5758 pub fn from_registers(registers: &Registers) -> Self {
5859 let body: Vec<_> = registers
59− .inner()
60− .iter()
61− .map(|(ch, reg)| {
62− let content = reg
63− .read()
64− .get(0)
65− .and_then(|s| s.lines().next())
66− .unwrap_or_default();
67− (ch.to_string(), content)
68− })
60+ .iter_preview()
61+ .map(|(ch, preview)| (ch.to_string(), preview))
6962 .collect();
7063
7164 let mut infobox = Self::new("Registers", &body);
helix-view/src/input.rs+67−3
@@ -43,6 +43,10 @@ pub enum MouseEventKind {
4343 ScrollDown,
4444 /// Scrolled mouse wheel upwards (away from the user).
4545 ScrollUp,
46+ /// Scrolled mouse wheel leftwards.
47+ ScrollLeft,
48+ /// Scrolled mouse wheel rightwards.
49+ ScrollRight,
4650}
4751
4852/// Represents a mouse button.
@@ -379,7 +383,7 @@ impl std::str::FromStr for KeyEvent {
379383 function if function.len() > 1 && function.starts_with('F') => {
380384 let function: String = function.chars().skip(1).collect();
381385 let function = str::parse::<u8>(&function)?;
382− (function > 0 && function < 13)
386+ (function > 0 && function < 25)
383387 .then_some(KeyCode::F(function))
384388 .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
385389 }
@@ -458,6 +462,8 @@ impl From<crossterm::event::MouseEventKind> for MouseEventKind {
458462 crossterm::event::MouseEventKind::Moved => Self::Moved,
459463 crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
460464 crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
465+ crossterm::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
466+ crossterm::event::MouseEventKind::ScrollRight => Self::ScrollRight,
461467 }
462468 }
463469}
@@ -543,7 +549,7 @@ pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
543549 if c == ">" {
544550 keys_res = Err(anyhow!("Unmatched '>'"));
545551 } else if c != "<" {
546− keys.push(c);
552+ keys.push(if c == "-" { keys::MINUS } else { c });
547553 i += end_i;
548554 } else {
549555 match s.find('>').context("'>' expected") {
@@ -682,7 +688,7 @@ mod test {
682688
683689 #[test]
684690 fn parsing_nonsensical_keys_fails() {
685− assert!(str::parse::<KeyEvent>("F13").is_err());
691+ assert!(str::parse::<KeyEvent>("F25").is_err());
686692 assert!(str::parse::<KeyEvent>("F0").is_err());
687693 assert!(str::parse::<KeyEvent>("aaa").is_err());
688694 assert!(str::parse::<KeyEvent>("S-S-a").is_err());
@@ -807,6 +813,64 @@ mod test {
807813 },
808814 ])
809815 );
816+
817+ assert_eq!(
818+ parse_macro(":w aa-bb.txt<ret>").ok(),
819+ Some(vec![
820+ KeyEvent {
821+ code: KeyCode::Char(':'),
822+ modifiers: KeyModifiers::NONE,
823+ },
824+ KeyEvent {
825+ code: KeyCode::Char('w'),
826+ modifiers: KeyModifiers::NONE,
827+ },
828+ KeyEvent {
829+ code: KeyCode::Char(' '),
830+ modifiers: KeyModifiers::NONE,
831+ },
832+ KeyEvent {
833+ code: KeyCode::Char('a'),
834+ modifiers: KeyModifiers::NONE,
835+ },
836+ KeyEvent {
837+ code: KeyCode::Char('a'),
838+ modifiers: KeyModifiers::NONE,
839+ },
840+ KeyEvent {
841+ code: KeyCode::Char('-'),
842+ modifiers: KeyModifiers::NONE,
843+ },
844+ KeyEvent {
845+ code: KeyCode::Char('b'),
846+ modifiers: KeyModifiers::NONE,
847+ },
848+ KeyEvent {
849+ code: KeyCode::Char('b'),
850+ modifiers: KeyModifiers::NONE,
851+ },
852+ KeyEvent {
853+ code: KeyCode::Char('.'),
854+ modifiers: KeyModifiers::NONE,
855+ },
856+ KeyEvent {
857+ code: KeyCode::Char('t'),
858+ modifiers: KeyModifiers::NONE,
859+ },
860+ KeyEvent {
861+ code: KeyCode::Char('x'),
862+ modifiers: KeyModifiers::NONE,
863+ },
864+ KeyEvent {
865+ code: KeyCode::Char('t'),
866+ modifiers: KeyModifiers::NONE,
867+ },
868+ KeyEvent {
869+ code: KeyCode::Enter,
870+ modifiers: KeyModifiers::NONE,
871+ },
872+ ])
873+ );
810874 }
811875
812876 #[test]
helix-view/src/lib.rs+1
@@ -15,6 +15,7 @@ pub mod base64;
1515pub mod info;
1616pub mod input;
1717pub mod keyboard;
18+pub mod register;
1819pub mod theme;
1920pub mod tree;
2021pub mod view;
helix-view/src/register.rsnew file+333
@@ -0,0 +1,333 @@
1+use std::{borrow::Cow, collections::HashMap, iter};
2+
3+use anyhow::Result;
4+use helix_core::NATIVE_LINE_ENDING;
5+
6+use crate::{
7+ clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
8+ document::SCRATCH_BUFFER_NAME,
9+ Editor,
10+};
11+
12+/// A key-value store for saving sets of values.
13+///
14+/// Each register corresponds to a `char`. Most chars can be used to store any set of
15+/// values but a few chars are "special registers". Special registers have unique
16+/// behaviors when read or written to:
17+///
18+/// * Black hole (`_`): all values read and written are discarded
19+/// * Selection indices (`#`): index number of each selection starting at 1
20+/// * Selection contents (`.`)
21+/// * Document path (`%`): filename of the current buffer
22+/// * System clipboard (`*`)
23+/// * Primary clipboard (`+`)
24+#[derive(Debug)]
25+pub struct Registers {
26+ /// The mapping of register to values.
27+ /// Values are stored in reverse order when inserted with `Registers::write`.
28+ /// The order is reversed again in `Registers::read`. This allows us to
29+ /// efficiently prepend new values in `Registers::push`.
30+ inner: HashMap<char, Vec<String>>,
31+ clipboard_provider: Box<dyn ClipboardProvider>,
32+ pub last_search_register: char,
33+}
34+
35+impl Default for Registers {
36+ fn default() -> Self {
37+ Self {
38+ inner: Default::default(),
39+ clipboard_provider: get_clipboard_provider(),
40+ last_search_register: '/',
41+ }
42+ }
43+}
44+
45+impl Registers {
46+ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
47+ match name {
48+ '_' => Some(RegisterValues::new(iter::empty())),
49+ '#' => {
50+ let (view, doc) = current_ref!(editor);
51+ let selections = doc.selection(view.id).len();
52+ // ExactSizeIterator is implemented for Range<usize> but
53+ // not RangeInclusive<usize>.
54+ Some(RegisterValues::new(
55+ (0..selections).map(|i| (i + 1).to_string().into()),
56+ ))
57+ }
58+ '.' => {
59+ let (view, doc) = current_ref!(editor);
60+ let text = doc.text().slice(..);
61+ Some(RegisterValues::new(doc.selection(view.id).fragments(text)))
62+ }
63+ '%' => {
64+ let doc = doc!(editor);
65+
66+ let path = doc
67+ .path()
68+ .as_ref()
69+ .map(|p| p.to_string_lossy())
70+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
71+
72+ Some(RegisterValues::new(iter::once(path)))
73+ }
74+ '*' | '+' => Some(read_from_clipboard(
75+ self.clipboard_provider.as_ref(),
76+ self.inner.get(&name),
77+ match name {
78+ '+' => ClipboardType::Clipboard,
79+ '*' => ClipboardType::Selection,
80+ _ => unreachable!(),
81+ },
82+ )),
83+ _ => self
84+ .inner
85+ .get(&name)
86+ .map(|values| RegisterValues::new(values.iter().map(Cow::from).rev())),
87+ }
88+ }
89+
90+ pub fn write(&mut self, name: char, mut values: Vec<String>) -> Result<()> {
91+ match name {
92+ '_' => Ok(()),
93+ '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
94+ '*' | '+' => {
95+ self.clipboard_provider.set_contents(
96+ values.join(NATIVE_LINE_ENDING.as_str()),
97+ match name {
98+ '+' => ClipboardType::Clipboard,
99+ '*' => ClipboardType::Selection,
100+ _ => unreachable!(),
101+ },
102+ )?;
103+ values.reverse();
104+ self.inner.insert(name, values);
105+ Ok(())
106+ }
107+ _ => {
108+ values.reverse();
109+ self.inner.insert(name, values);
110+ Ok(())
111+ }
112+ }
113+ }
114+
115+ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
116+ match name {
117+ '_' => Ok(()),
118+ '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")),
119+ '*' | '+' => {
120+ let clipboard_type = match name {
121+ '+' => ClipboardType::Clipboard,
122+ '*' => ClipboardType::Selection,
123+ _ => unreachable!(),
124+ };
125+ let contents = self.clipboard_provider.get_contents(clipboard_type)?;
126+ let saved_values = self.inner.entry(name).or_insert_with(Vec::new);
127+
128+ if !contents_are_saved(saved_values, &contents) {
129+ anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents");
130+ }
131+
132+ saved_values.push(value.clone());
133+ if !contents.is_empty() {
134+ value.push_str(NATIVE_LINE_ENDING.as_str());
135+ }
136+ value.push_str(&contents);
137+ self.clipboard_provider
138+ .set_contents(value, clipboard_type)?;
139+
140+ Ok(())
141+ }
142+ _ => {
143+ self.inner.entry(name).or_insert_with(Vec::new).push(value);
144+ Ok(())
145+ }
146+ }
147+ }
148+
149+ pub fn first<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> {
150+ self.read(name, editor).and_then(|mut values| values.next())
151+ }
152+
153+ pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> {
154+ self.read(name, editor).and_then(|values| values.last())
155+ }
156+
157+ pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
158+ self.inner
159+ .iter()
160+ .filter(|(name, _)| !matches!(name, '*' | '+'))
161+ .map(|(name, values)| {
162+ let preview = values
163+ .last()
164+ .and_then(|s| s.lines().next())
165+ .unwrap_or("<empty>");
166+
167+ (*name, preview)
168+ })
169+ .chain(
170+ [
171+ ('_', "<empty>"),
172+ ('#', "<selection indices>"),
173+ ('.', "<selection contents>"),
174+ ('%', "<document path>"),
175+ ('+', "<system clipboard>"),
176+ ('*', "<primary clipboard>"),
177+ ]
178+ .iter()
179+ .copied(),
180+ )
181+ }
182+
183+ pub fn clear(&mut self) {
184+ self.clear_clipboard(ClipboardType::Clipboard);
185+ self.clear_clipboard(ClipboardType::Selection);
186+ self.inner.clear()
187+ }
188+
189+ pub fn remove(&mut self, name: char) -> bool {
190+ match name {
191+ '*' | '+' => {
192+ self.clear_clipboard(match name {
193+ '+' => ClipboardType::Clipboard,
194+ '*' => ClipboardType::Selection,
195+ _ => unreachable!(),
196+ });
197+ self.inner.remove(&name);
198+
199+ true
200+ }
201+ '_' | '#' | '.' | '%' => false,
202+ _ => self.inner.remove(&name).is_some(),
203+ }
204+ }
205+
206+ fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
207+ if let Err(err) = self
208+ .clipboard_provider
209+ .set_contents("".into(), clipboard_type)
210+ {
211+ log::error!(
212+ "Failed to clear {} clipboard: {err}",
213+ match clipboard_type {
214+ ClipboardType::Clipboard => "system",
215+ ClipboardType::Selection => "primary",
216+ }
217+ )
218+ }
219+ }
220+
221+ pub fn clipboard_provider_name(&self) -> Cow<str> {
222+ self.clipboard_provider.name()
223+ }
224+}
225+
226+fn read_from_clipboard<'a>(
227+ provider: &dyn ClipboardProvider,
228+ saved_values: Option<&'a Vec<String>>,
229+ clipboard_type: ClipboardType,
230+) -> RegisterValues<'a> {
231+ match provider.get_contents(clipboard_type) {
232+ Ok(contents) => {
233+ // If we're pasting the same values that we just yanked, re-use
234+ // the saved values. This allows pasting multiple selections
235+ // even when yanked to a clipboard.
236+ let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) };
237+
238+ if contents_are_saved(values, &contents) {
239+ RegisterValues::new(values.iter().map(Cow::from).rev())
240+ } else {
241+ RegisterValues::new(iter::once(contents.into()))
242+ }
243+ }
244+ Err(err) => {
245+ log::error!(
246+ "Failed to read {} clipboard: {err}",
247+ match clipboard_type {
248+ ClipboardType::Clipboard => "system",
249+ ClipboardType::Selection => "primary",
250+ }
251+ );
252+
253+ RegisterValues::new(iter::empty())
254+ }
255+ }
256+}
257+
258+fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool {
259+ let line_ending = NATIVE_LINE_ENDING.as_str();
260+ let mut values = saved_values.iter().rev();
261+
262+ match values.next() {
263+ Some(first) if contents.starts_with(first) => {
264+ contents = &contents[first.len()..];
265+ }
266+ None if contents.is_empty() => return true,
267+ _ => return false,
268+ }
269+
270+ for value in values {
271+ if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) {
272+ contents = &contents[line_ending.len() + value.len()..];
273+ } else {
274+ return false;
275+ }
276+ }
277+
278+ true
279+}
280+
281+// This is a wrapper of an iterator that is both double ended and exact size,
282+// and can return either owned or borrowed values. Regular registers can
283+// return borrowed values while some special registers need to return owned
284+// values.
285+pub struct RegisterValues<'a> {
286+ iter: Box<dyn DoubleEndedExactSizeIterator<Item = Cow<'a, str>> + 'a>,
287+}
288+
289+impl<'a> RegisterValues<'a> {
290+ fn new(
291+ iter: impl DoubleEndedIterator<Item = Cow<'a, str>>
292+ + ExactSizeIterator<Item = Cow<'a, str>>
293+ + 'a,
294+ ) -> Self {
295+ Self {
296+ iter: Box::new(iter),
297+ }
298+ }
299+}
300+
301+impl<'a> Iterator for RegisterValues<'a> {
302+ type Item = Cow<'a, str>;
303+
304+ fn next(&mut self) -> Option<Self::Item> {
305+ self.iter.next()
306+ }
307+
308+ fn size_hint(&self) -> (usize, Option<usize>) {
309+ self.iter.size_hint()
310+ }
311+}
312+
313+impl<'a> DoubleEndedIterator for RegisterValues<'a> {
314+ fn next_back(&mut self) -> Option<Self::Item> {
315+ self.iter.next_back()
316+ }
317+}
318+
319+impl<'a> ExactSizeIterator for RegisterValues<'a> {
320+ fn len(&self) -> usize {
321+ self.iter.len()
322+ }
323+}
324+
325+// Each RegisterValues iterator is both double ended and exact size. We can't
326+// type RegisterValues as `Box<dyn DoubleEndedIterator + ExactSizeIterator>`
327+// because only one non-auto trait is allowed in trait objects. So we need to
328+// create a new trait that covers both. `RegisterValues` wraps that type so that
329+// trait only needs to live in this module and not be imported for all register
330+// callsites.
331+trait DoubleEndedExactSizeIterator: DoubleEndedIterator + ExactSizeIterator {}
332+
333+impl<I: DoubleEndedIterator + ExactSizeIterator> DoubleEndedExactSizeIterator for I {}
helix-view/src/theme.rs+6
@@ -297,6 +297,11 @@ impl Theme {
297297 self.highlights[index]
298298 }
299299
300+ #[inline]
301+ pub fn scope(&self, index: usize) -> &str {
302+ &self.scopes[index]
303+ }
304+
300305 pub fn name(&self) -> &str {
301306 &self.name
302307 }
@@ -359,6 +364,7 @@ impl Default for ThemePalette {
359364 fn default() -> Self {
360365 Self {
361366 palette: hashmap! {
367+ "default".to_string() => Color::Reset,
362368 "black".to_string() => Color::Black,
363369 "red".to_string() => Color::Red,
364370 "green".to_string() => Color::Green,
helix-view/src/view.rs+8−2
@@ -187,11 +187,17 @@ impl View {
187187 }
188188
189189 pub fn gutter_offset(&self, doc: &Document) -> u16 {
190− self.gutters
190+ let total_width = self
191+ .gutters
191192 .layout
192193 .iter()
193194 .map(|gutter| gutter.width(self, doc) as u16)
194− .sum()
195+ .sum();
196+ if total_width < self.area.width {
197+ total_width
198+ } else {
199+ 0
200+ }
195201 }
196202
197203 //
languages.toml+514−206
@@ -3,10 +3,12 @@
33
44[language-server]
55
6+als = { command = "als" }
67awk-language-server = { command = "awk-language-server" }
78bash-language-server = { command = "bash-language-server", args = ["start"] }
89bass = { command = "bass", args = ["--lsp"] }
910bicep-langserver = { command = "bicep-langserver" }
11+bufls = { command = "bufls", args = ["serve"] }
1012cairo-language-server = { command = "cairo-language-server", args = [] }
1113cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
1214clangd = { command = "clangd" }
@@ -14,6 +16,7 @@ clojure-lsp = { command = "clojure-lsp" }
1416cmake-language-server = { command = "cmake-language-server" }
1517crystalline = { command = "crystalline", args = ["--stdio"] }
1618cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
19+csharp-ls = { command = "csharp-ls" }
1720cuelsp = { command = "cuelsp" }
1821dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
1922dhall-lsp-server = { command = "dhall-lsp-server" }
@@ -28,6 +31,7 @@ forth-lsp = { command = "forth-lsp" }
2831fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
2932fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } }
3033gleam = { command = "gleam", args = ["lsp"] }
34+graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] }
3135haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
3236idris2-lsp = { command = "idris2-lsp" }
3337intelephense = { command = "intelephense", args = ["--stdio"] }
@@ -36,6 +40,7 @@ jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "-
3640julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] }
3741kotlin-language-server = { command = "kotlin-language-server" }
3842lean = { command = "lean", args = [ "--server" ] }
43+ltex-ls = { command = "ltex-ls" }
3944markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
4045marksman = { command = "marksman", args = ["server"] }
4146metals = { command = "metals", config = { "isHttpEnabled" = true } }
@@ -43,15 +48,19 @@ mint = { command = "mint", args = ["ls"] }
4348nil = { command = "nil" }
4449nimlangserver = { command = "nimlangserver" }
4550nls = { command = "nls" }
51+nu-lsp = { command = "nu", args = [ "--lsp" ] }
4652ocamllsp = { command = "ocamllsp" }
4753ols = { command = "ols", args = [] }
4854omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
4955openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
5056pasls = { command = "pasls", args = [] }
57+pbkit = { command = "pb", args = [ "lsp" ] }
5158perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
5259prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
5360purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
5461pylsp = { command = "pylsp" }
62+pyright = { command = "pyright-langserver", args = ["--stdio"], config = {} }
63+pylyzer = { command = "pylyzer", args = ["--server"] }
5564qmlls = { command = "qmlls" }
5665r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
5766racket = { command = "racket", args = ["-l", "racket-langserver"] }
@@ -65,24 +74,30 @@ solc = { command = "solc", args = ["--lsp"] }
6574sourcekit-lsp = { command = "sourcekit-lsp" }
6675svlangserver = { command = "svlangserver", args = [] }
6776swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
77+tailwindcss-ls = { command = "tailwindcss-language-server", args = ["--stdio"] }
6878taplo = { command = "taplo", args = ["lsp", "stdio"] }
79+templ = { command = "templ", args = ["lsp"] }
6980terraform-ls = { command = "terraform-ls", args = ["serve"] }
7081texlab = { command = "texlab" }
7182vala-language-server = { command = "vala-language-server" }
7283vhdl_ls = { command = "vhdl_ls", args = [] }
73−vlang-language-server = { command = "v", args = ["ls"] }
74−vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { "provideFormatter" = true }}
84+vlang-language-server = { command = "v-analyzer" }
85+vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { provideFormatter = true, css = { validate = { enable = true } } } }
7586vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
76−vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } }
87+vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true, json = { validate = { enable = true } } } }
7788vuels = { command = "vue-language-server", args = ["--stdio"], config = { typescript = { tsdk = "node_modules/typescript/lib/" } } }
7889wgsl_analyzer = { command = "wgsl_analyzer" }
7990yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
8091zls = { command = "zls" }
8192blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] }
93+typst-lsp = { command = "typst-lsp" }
8294
95+[language-server.ansible-language-server]
96+command = "ansible-language-server"
97+args = ["--stdio"]
8398
8499[language-server.lua-language-server]
85−command = "lua-language-server"
100+command = "lua-language-server"
86101
87102[language-server.lua-language-server.config.Lua.hint]
88103enable = true
@@ -104,6 +119,12 @@ functionTypeParameters = true
104119parameterNames = true
105120rangeVariableTypes = true
106121
122+[language-server.golangci-lint-lsp]
123+command = "golangci-lint-langserver"
124+
125+[language-server.golangci-lint-lsp.config]
126+command = ["golangci-lint", "run", "--out-format", "json", "--issues-exit-code=1"]
127+
107128
108129[language-server.rust-analyzer]
109130command = "rust-analyzer"
@@ -170,6 +191,7 @@ auto-format = true
170191comment-token = "//"
171192language-servers = [ "rust-analyzer" ]
172193indent = { tab-width = 4, unit = " " }
194+persistent-diagnostic-sources = ["rustc", "clippy"]
173195
174196[language.auto-pairs]
175197'(' = ')'
@@ -230,7 +252,6 @@ name = "toml"
230252scope = "source.toml"
231253injection-regex = "toml"
232254file-types = ["toml", "poetry.lock", "Cargo.lock"]
233−roots = []
234255comment-token = "#"
235256language-servers = [ "taplo" ]
236257indent = { tab-width = 2, unit = " " }
@@ -244,7 +265,6 @@ name = "awk"
244265scope = "source.awk"
245266injection-regex = "awk"
246267file-types = ["awk", "gawk", "nawk", "mawk"]
247−roots = []
248268comment-token = "#"
249269language-servers = [ "awk-language-server" ]
250270indent = { tab-width = 2, unit = " " }
@@ -258,7 +278,7 @@ name = "protobuf"
258278scope = "source.proto"
259279injection-regex = "protobuf"
260280file-types = ["proto"]
261−roots = []
281+language-servers = [ "bufls", "pbkit" ]
262282comment-token = "//"
263283indent = { tab-width = 2, unit = " " }
264284
@@ -287,7 +307,6 @@ scope = "source.fish"
287307injection-regex = "fish"
288308file-types = ["fish"]
289309shebangs = ["fish"]
290−roots = []
291310comment-token = "#"
292311indent = { tab-width = 4, unit = " " }
293312
@@ -301,7 +320,6 @@ scope = "source.mint"
301320injection-regex = "mint"
302321file-types = ["mint"]
303322shebangs = []
304−roots = []
305323comment-token = "//"
306324language-servers = [ "mint" ]
307325indent = { tab-width = 2, unit = " " }
@@ -310,8 +328,28 @@ indent = { tab-width = 2, unit = " " }
310328name = "json"
311329scope = "source.json"
312330injection-regex = "json"
313−file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
314−roots = []
331+file-types = [
332+ "json",
333+ "jsonc",
334+ "arb",
335+ "ipynb",
336+ "geojson",
337+ "gltf",
338+ "webmanifest",
339+ "flake.lock",
340+ ".babelrc",
341+ ".bowerrc",
342+ ".jscrc",
343+ "js.map",
344+ "ts.map",
345+ "css.map",
346+ ".jslintrc",
347+ "jsonld",
348+ ".vuerc",
349+ "composer.lock",
350+ ".watchmanconfig",
351+ "avsc"
352+]
315353language-servers = [ "vscode-json-language-server" ]
316354auto-format = true
317355indent = { tab-width = 2, unit = " " }
@@ -320,12 +358,26 @@ indent = { tab-width = 2, unit = " " }
320358name = "json"
321359source = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "73076754005a460947cafe8e03a8cf5fa4fa2938" }
322360
361+
362+[[language]]
363+name = "json5"
364+scope = "source.json5"
365+injection-regex = "json5"
366+file-types = ["json5"]
367+language-servers = []
368+comment-token = "//"
369+indent = { tab-width = 4, unit = " " }
370+# https://json5.org
371+
372+[[grammar]]
373+name = "json5"
374+source = { git = "https://github.com/Joakker/tree-sitter-json5", rev = "c23f7a9b1ee7d45f516496b1e0e4be067264fa0d" }
375+
323376[[language]]
324377name = "c"
325378scope = "source.c"
326379injection-regex = "c"
327380file-types = ["c"] # TODO: ["h"]
328−roots = []
329381comment-token = "//"
330382language-servers = [ "clangd" ]
331383indent = { tab-width = 2, unit = " " }
@@ -361,8 +413,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
361413name = "cpp"
362414scope = "source.cpp"
363415injection-regex = "cpp"
364−file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm"]
365−roots = []
416+file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { suffix = ".hpp.in" }, { suffix = ".h.in" }]
366417comment-token = "//"
367418language-servers = [ "clangd" ]
368419indent = { tab-width = 2, unit = " " }
@@ -408,7 +459,7 @@ language-servers = [ "crystalline" ]
408459name = "c-sharp"
409460scope = "source.csharp"
410461injection-regex = "c-?sharp"
411−file-types = ["cs"]
462+file-types = ["cs", "csx", "cake"]
412463roots = ["sln", "csproj"]
413464comment-token = "//"
414465indent = { tab-width = 4, unit = "\t" }
@@ -445,7 +496,7 @@ file-types = ["go"]
445496roots = ["go.work", "go.mod"]
446497auto-format = true
447498comment-token = "//"
448−language-servers = [ "gopls" ]
499+language-servers = [ "gopls", "golangci-lint-lsp" ]
449500# TODO: gopls needs utf-8 offsets?
450501indent = { tab-width = 4, unit = "\t" }
451502
@@ -495,7 +546,6 @@ name = "gomod"
495546scope = "source.gomod"
496547injection-regex = "gomod"
497548file-types = ["go.mod"]
498−roots = []
499549auto-format = true
500550comment-token = "//"
501551language-servers = [ "gopls" ]
@@ -510,7 +560,6 @@ name = "gotmpl"
510560scope = "source.gotmpl"
511561injection-regex = "gotmpl"
512562file-types = ["gotmpl"]
513−roots = []
514563comment-token = "//"
515564language-servers = [ "gopls" ]
516565indent = { tab-width = 2, unit = " " }
@@ -524,7 +573,6 @@ name = "gowork"
524573scope = "source.gowork"
525574injection-regex = "gowork"
526575file-types = ["go.work"]
527−roots = []
528576auto-format = true
529577comment-token = "//"
530578language-servers = [ "gopls" ]
@@ -539,9 +587,8 @@ name = "javascript"
539587scope = "source.js"
540588injection-regex = "(js|javascript)"
541589language-id = "javascript"
542−file-types = ["js", "mjs", "cjs"]
590+file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", "jakefile"]
543591shebangs = ["node"]
544−roots = []
545592comment-token = "//"
546593language-servers = [ "typescript-language-server" ]
547594indent = { tab-width = 2, unit = " " }
@@ -560,7 +607,7 @@ args = { program = "{0}" }
560607
561608[[grammar]]
562609name = "javascript"
563−source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev = "4a95461c4761c624f2263725aca79eeaefd36cad" }
610+source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev = "f772967f7b7bc7c28f845be2420a38472b16a8ee" }
564611
565612[[language]]
566613name = "jsx"
@@ -568,7 +615,6 @@ scope = "source.jsx"
568615injection-regex = "jsx"
569616language-id = "javascriptreact"
570617file-types = ["jsx"]
571−roots = []
572618comment-token = "//"
573619language-servers = [ "typescript-language-server" ]
574620indent = { tab-width = 2, unit = " " }
@@ -580,14 +626,13 @@ scope = "source.ts"
580626injection-regex = "(ts|typescript)"
581627file-types = ["ts", "mts", "cts"]
582628language-id = "typescript"
583−shebangs = []
584−roots = []
629+shebangs = ["deno", "ts-node"]
585630language-servers = [ "typescript-language-server" ]
586631indent = { tab-width = 2, unit = " " }
587632
588633[[grammar]]
589634name = "typescript"
590−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", subpath = "typescript" }
635+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "b1bf4825d9eaa0f3bdeb1e52f099533328acfbdf", subpath = "typescript" }
591636
592637[[language]]
593638name = "tsx"
@@ -595,20 +640,18 @@ scope = "source.tsx"
595640injection-regex = "(tsx)" # |typescript
596641language-id = "typescriptreact"
597642file-types = ["tsx"]
598−roots = []
599643language-servers = [ "typescript-language-server" ]
600644indent = { tab-width = 2, unit = " " }
601645
602646[[grammar]]
603647name = "tsx"
604−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", subpath = "tsx" }
648+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "b1bf4825d9eaa0f3bdeb1e52f099533328acfbdf", subpath = "tsx" }
605649
606650[[language]]
607651name = "css"
608652scope = "source.css"
609653injection-regex = "css"
610654file-types = ["css", "scss"]
611−roots = []
612655language-servers = [ "vscode-css-language-server" ]
613656auto-format = true
614657indent = { tab-width = 2, unit = " " }
@@ -622,7 +665,6 @@ name = "scss"
622665scope = "source.scss"
623666injection-regex = "scss"
624667file-types = ["scss"]
625−roots = []
626668language-servers = [ "vscode-css-language-server" ]
627669auto-format = true
628670indent = { tab-width = 2, unit = " " }
@@ -635,8 +677,7 @@ source = { git = "https://github.com/serenadeai/tree-sitter-scss", rev = "c478c6
635677name = "html"
636678scope = "text.html.basic"
637679injection-regex = "html"
638−file-types = ["html"]
639−roots = []
680+file-types = ["html", "htm", "shtml", "xhtml", "xht", "jsp", "asp", "aspx", "jshtm", "volt", "rhtml"]
640681language-servers = [ "vscode-html-language-server" ]
641682auto-format = true
642683indent = { tab-width = 2, unit = " " }
@@ -649,9 +690,9 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53
649690name = "python"
650691scope = "source.python"
651692injection-regex = "python"
652−file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct"]
693+file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct", "rpy", "cpy", "ipy", "pyt", "SConscript"]
653694shebangs = ["python"]
654−roots = []
695+roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"]
655696comment-token = "#"
656697language-servers = [ "pylsp" ]
657698# TODO: pyls needs utf-8 offsets
@@ -659,7 +700,7 @@ indent = { tab-width = 4, unit = " " }
659700
660701[[grammar]]
661702name = "python"
662−source = { git = "https://github.com/tree-sitter/tree-sitter-python", rev = "de221eccf9a221f5b85474a553474a69b4b5784d" }
703+source = { git = "https://github.com/tree-sitter/tree-sitter-python", rev = "4bfdd9033a2225cc95032ce77066b7aeca9e2efc" }
663704
664705[[language]]
665706name = "nickel"
@@ -667,7 +708,6 @@ scope = "source.nickel"
667708injection-regex = "nickel"
668709file-types = ["ncl"]
669710shebangs = []
670−roots = []
671711comment-token = "#"
672712language-servers = [ "nls" ]
673713indent = { tab-width = 2, unit = " " }
@@ -688,7 +728,6 @@ scope = "source.nix"
688728injection-regex = "nix"
689729file-types = ["nix"]
690730shebangs = []
691−roots = []
692731comment-token = "#"
693732language-servers = [ "nil" ]
694733indent = { tab-width = 2, unit = " " }
@@ -701,9 +740,43 @@ source = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "1b69
701740name = "ruby"
702741scope = "source.ruby"
703742injection-regex = "ruby"
704−file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile", "Gemfile", "rabl", "jbuilder", "jb", "Podfile", "podspec", "Vagrantfile", "Brewfile"]
743+file-types = [
744+ "rb",
745+ "rake",
746+ "rakefile",
747+ "irb",
748+ "gemfile",
749+ "gemspec",
750+ "Rakefile",
751+ "Gemfile",
752+ "rabl",
753+ "jbuilder",
754+ "jb",
755+ "Podfile",
756+ "podspec",
757+ "Vagrantfile",
758+ "Brewfile",
759+ "rjs",
760+ "rbi",
761+ "Guardfile",
762+ "Capfile",
763+ "Cheffile",
764+ "Hobofile",
765+ "Appraisals",
766+ "Rantfile",
767+ "Berksfile",
768+ "Berksfile.lock",
769+ "Thorfile",
770+ "Puppetfile",
771+ "Fastfile",
772+ "Appfile",
773+ "Deliverfile",
774+ "Matchfile",
775+ "Scanfile",
776+ "Snapfile",
777+ "Gymfile"
778+]
705779shebangs = ["ruby"]
706−roots = []
707780comment-token = "#"
708781language-servers = [ "solargraph" ]
709782indent = { tab-width = 2, unit = " " }
@@ -716,9 +789,48 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "206c7
716789name = "bash"
717790scope = "source.bash"
718791injection-regex = "(shell|bash|zsh|sh)"
719−file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".profile", ".zshenv", ".zlogin", ".zlogout", ".zprofile", ".zshrc", ".zimrc", "APKBUILD", "PKGBUILD", "eclass", "ebuild", "bazelrc", ".bash_aliases", "Renviron", ".Renviron"]
792+file-types = [
793+ "sh",
794+ "bash",
795+ "zsh",
796+ ".bash_login",
797+ ".bash_logout",
798+ ".bash_profile",
799+ ".bashrc",
800+ ".profile",
801+ ".zshenv",
802+ "zshenv",
803+ ".zlogin",
804+ "zlogin",
805+ ".zlogout",
806+ "zlogout",
807+ ".zprofile",
808+ "zprofile",
809+ ".zshrc",
810+ "zshrc",
811+ ".zimrc",
812+ "APKBUILD",
813+ "PKGBUILD",
814+ "eclass",
815+ "ebuild",
816+ "bazelrc",
817+ ".bash_aliases",
818+ "Renviron",
819+ ".Renviron",
820+ ".xprofile",
821+ ".xsession",
822+ ".xsessionrc",
823+ "zsh-theme",
824+ "ksh",
825+ "cshrc",
826+ "tcshrc",
827+ ".yashrc",
828+ ".yash_profile",
829+ ".hushlogin",
830+ "bashrc_Apple_Terminal",
831+ "zshrc_Apple_Terminal"
832+]
720833shebangs = ["sh", "bash", "dash", "zsh"]
721−roots = []
722834comment-token = "#"
723835language-servers = [ "bash-language-server" ]
724836indent = { tab-width = 2, unit = " " }
@@ -731,7 +843,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "275ef
731843name = "php"
732844scope = "source.php"
733845injection-regex = "php"
734−file-types = ["php", "inc"]
846+file-types = ["php", "inc", "php4", "php5", "phtml", "ctp"]
735847shebangs = ["php"]
736848roots = ["composer.json", "index.php"]
737849language-servers = [ "intelephense" ]
@@ -746,7 +858,6 @@ name = "twig"
746858scope = "source.twig"
747859injection-regex = "twig"
748860file-types = ["twig"]
749−roots = []
750861indent = { tab-width = 2, unit = " " }
751862
752863[[grammar]]
@@ -757,8 +868,7 @@ source = { git = "https://github.com/gbprod/tree-sitter-twig", rev = "807b293fec
757868name = "latex"
758869scope = "source.tex"
759870injection-regex = "tex"
760−file-types = ["tex", "sty", "cls", "Rd"]
761−roots = []
871+file-types = ["tex", "sty", "cls", "Rd", "bbx", "cbx"]
762872comment-token = "%"
763873language-servers = [ "texlab" ]
764874indent = { tab-width = 4, unit = "\t" }
@@ -772,7 +882,6 @@ name = "bibtex"
772882scope = "source.bib"
773883injection-regex = "bib"
774884file-types = ["bib"]
775−roots = []
776885comment-token = "%"
777886language-servers = [ "texlab" ]
778887indent = { tab-width = 4, unit = "\t" }
@@ -809,11 +918,23 @@ indent = { tab-width = 2, unit = " " }
809918name = "lean"
810919source = { git = "https://github.com/Julian/tree-sitter-lean", rev = "d98426109258b266e1e92358c5f11716d2e8f638" }
811920
921+
922+[[language]]
923+name = "lpf"
924+comment-token = "#"
925+scope = "source.lpf"
926+file-types = ["lpf"]
927+
928+[[grammar]]
929+name = "lpf"
930+source = { git = "https://gitlab.com/TheZoq2/tree-sitter-lpf", rev = "db7372e60c722ca7f12ab359e57e6bf7611ab126" }
931+
812932[[language]]
813933name = "julia"
814934scope = "source.julia"
815935injection-regex = "julia"
816936file-types = ["jl"]
937+shebangs = ["julia"]
817938roots = ["Manifest.toml", "Project.toml"]
818939comment-token = "#"
819940language-servers = [ "julia" ]
@@ -827,21 +948,33 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-julia", rev = "8fb3
827948name = "java"
828949scope = "source.java"
829950injection-regex = "java"
830−file-types = ["java"]
951+file-types = ["java", "jav"]
831952roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
832953language-servers = [ "jdtls" ]
833−indent = { tab-width = 4, unit = " " }
954+indent = { tab-width = 2, unit = " " }
834955
835956[[grammar]]
836957name = "java"
837958source = { git = "https://github.com/tree-sitter/tree-sitter-java", rev = "09d650def6cdf7f479f4b78f595e9ef5b58ce31e" }
838959
960+[[language]]
961+name = "smali"
962+scope = "source.smali"
963+injection-regex = "smali"
964+file-types = ["smali"]
965+comment-token = "#"
966+roots = []
967+indent = { tab-width = 4, unit = " " }
968+
969+[[grammar]]
970+name = "smali"
971+source = { git = "https://github.com/amaanq/tree-sitter-smali", rev = "5ae51e15c4d1ac93cba6127caf3d1f0a072c140c" }
972+
839973[[language]]
840974name = "ledger"
841975scope = "source.ledger"
842976injection-regex = "ledger"
843977file-types = ["ldg", "ledger", "journal"]
844−roots = []
845978comment-token = ";"
846979indent = { tab-width = 4, unit = " " }
847980
@@ -854,21 +987,19 @@ name = "beancount"
854987scope = "source.beancount"
855988injection-regex = "beancount"
856989file-types = ["beancount", "bean"]
857−roots = []
858990comment-token = ";"
859991indent = { tab-width = 2, unit = " " }
860992
861993[[grammar]]
862994name = "beancount"
863−source = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "4cbd1f09cd07c1f1fabf867c2cf354f9da53cc4c" }
995+source = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "f3741a3a68ade59ec894ed84a64673494d2ba8f3" }
864996
865997[[language]]
866998name = "ocaml"
867999scope = "source.ocaml"
8681000injection-regex = "ocaml"
8691001file-types = ["ml"]
870−shebangs = []
871−roots = []
1002+shebangs = ["ocaml", "ocamlrun", "ocamlscript"]
8721003comment-token = "(**)"
8731004language-servers = [ "ocamllsp" ]
8741005indent = { tab-width = 2, unit = " " }
@@ -878,7 +1009,6 @@ indent = { tab-width = 2, unit = " " }
8781009'{' = '}'
8791010'[' = ']'
8801011'"' = '"'
881−'`' = '`'
8821012
8831013[[grammar]]
8841014name = "ocaml"
@@ -889,7 +1019,6 @@ name = "ocaml-interface"
8891019scope = "source.ocaml.interface"
8901020file-types = ["mli"]
8911021shebangs = []
892−roots = []
8931022comment-token = "(**)"
8941023language-servers = [ "ocamllsp" ]
8951024indent = { tab-width = 2, unit = " " }
@@ -899,7 +1028,6 @@ indent = { tab-width = 2, unit = " " }
8991028'{' = '}'
9001029'[' = ']'
9011030'"' = '"'
902−'`' = '`'
9031031
9041032[[grammar]]
9051033name = "ocaml-interface"
@@ -925,7 +1053,6 @@ name = "svelte"
9251053scope = "source.svelte"
9261054injection-regex = "svelte"
9271055file-types = ["svelte"]
928−roots = []
9291056indent = { tab-width = 2, unit = " " }
9301057language-servers = [ "svelteserver" ]
9311058
@@ -950,10 +1077,9 @@ source = { git = "https://github.com/ikatyang/tree-sitter-vue", rev = "91fe27547
9501077name = "yaml"
9511078scope = "source.yaml"
9521079file-types = ["yml", "yaml"]
953−roots = []
9541080comment-token = "#"
9551081indent = { tab-width = 2, unit = " " }
956−language-servers = [ "yaml-language-server" ]
1082+language-servers = [ "yaml-language-server", "ansible-language-server" ]
9571083injection-regex = "yml|yaml"
9581084
9591085[[grammar]]
@@ -972,13 +1098,12 @@ indent = { tab-width = 2, unit = " " }
9721098
9731099[[grammar]]
9741100name = "haskell"
975−source = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "98fc7f59049aeb713ab9b72a8ff25dcaaef81087" }
1101+source = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "d7ac98f49e3ed7e17541256fe3881a967d7ffdd3" }
9761102
9771103[[language]]
9781104name = "haskell-persistent"
9791105scope = "source.persistentmodels"
9801106file-types = ["persistentmodels"]
981−roots = []
9821107comment-token = "--"
9831108indent = { tab-width = 2, unit = " " }
9841109
@@ -997,7 +1122,10 @@ language-servers = [ "purescript-language-server" ]
9971122indent = { tab-width = 2, unit = " " }
9981123auto-format = true
9991124formatter = { command = "purs-tidy", args = ["format"] }
1000−grammar = "haskell"
1125+
1126+[[grammar]]
1127+name = "purescript"
1128+source = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "5ef5592674ea42de75fc2792972e4ea0b6e3da6c" }
10011129
10021130[[language]]
10031131name = "zig"
@@ -1036,12 +1164,11 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
10361164
10371165[[grammar]]
10381166name = "zig"
1039−source = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "8d3224c3bd0890fe08358886ebf54fca2ed448a6" }
1167+source = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
10401168
10411169[[language]]
10421170name = "prolog"
10431171scope = "source.prolog"
1044−roots = []
10451172file-types = ["pl", "prolog"]
10461173shebangs = ["swipl"]
10471174comment-token = "%"
@@ -1050,8 +1177,7 @@ language-servers = [ "swipl" ]
10501177[[language]]
10511178name = "tsq"
10521179scope = "source.tsq"
1053−file-types = ["scm"]
1054−roots = []
1180+file-types = ["tsq"]
10551181comment-token = ";"
10561182injection-regex = "tsq"
10571183indent = { tab-width = 2, unit = " " }
@@ -1064,7 +1190,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-tsq", rev = "48b5
10641190name = "cmake"
10651191scope = "source.cmake"
10661192file-types = ["cmake", "CMakeLists.txt"]
1067−roots = []
10681193comment-token = "#"
10691194indent = { tab-width = 2, unit = " " }
10701195language-servers = [ "cmake-language-server" ]
@@ -1077,9 +1202,9 @@ source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "6e51463ef30
10771202[[language]]
10781203name = "make"
10791204scope = "source.make"
1080−file-types = ["Makefile", "makefile", "make", "mk"]
1205+file-types = ["Makefile", "makefile", "make", "mk", "mak", "GNUmakefile", "OCamlMakefile"]
1206+shebangs = ["make", "gmake"]
10811207injection-regex = "(make|makefile|Makefile|mk)"
1082−roots = []
10831208comment-token = "#"
10841209indent = { tab-width = 4, unit = "\t" }
10851210
@@ -1091,7 +1216,6 @@ source = { git = "https://github.com/alemuller/tree-sitter-make", rev = "a4b9187
10911216name = "glsl"
10921217scope = "source.glsl"
10931218file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
1094−roots = []
10951219comment-token = "//"
10961220indent = { tab-width = 4, unit = " " }
10971221injection-regex = "glsl"
@@ -1103,31 +1227,39 @@ source = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "88408ff
11031227[[language]]
11041228name = "perl"
11051229scope = "source.perl"
1106−file-types = ["pl", "pm", "t"]
1230+file-types = ["pl", "pm", "t", "psgi", "raku", "rakumod", "rakutest", "rakudoc", "nqp", "p6", "pl6", "pm6"]
11071231shebangs = ["perl"]
1108−roots = []
11091232comment-token = "#"
11101233language-servers = [ "perlnavigator" ]
11111234indent = { tab-width = 2, unit = " " }
11121235
11131236[[grammar]]
11141237name = "perl"
1115−source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "ed21ecbcc128a6688770ebafd3ef68a1c9bc1ea9" }
1238+source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "e99bb5283805db4cb86c964722d709df21b0ac16" }
1239+
1240+[[language]]
1241+name = "pod"
1242+scope = "source.pod"
1243+injection-regex = "pod"
1244+file-types = ["pod"]
1245+
1246+[[grammar]]
1247+name = "pod"
1248+source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "d466b84009a63986834498073ec05d58d727d55f" }
11161249
11171250[[language]]
11181251name = "racket"
11191252scope = "source.racket"
1120−roots = []
11211253file-types = ["rkt", "rktd", "rktl", "scrbl"]
11221254shebangs = ["racket"]
11231255comment-token = ";"
1256+indent = { tab-width = 2, unit = " " }
11241257language-servers = [ "racket" ]
11251258grammar = "scheme"
11261259
11271260[[language]]
11281261name = "common-lisp"
11291262scope = "source.lisp"
1130−roots = []
11311263file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
11321264shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
11331265comment-token = ";"
@@ -1144,19 +1276,17 @@ grammar = "scheme"
11441276[[language]]
11451277name = "comment"
11461278scope = "scope.comment"
1147−roots = []
11481279file-types = []
11491280injection-regex = "comment"
11501281
11511282[[grammar]]
11521283name = "comment"
1153−source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "5dd3c62f1bbe378b220fe16b317b85247898639e" }
1284+source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "a37ca370310ac6f89b6e0ebf2b86b2219780494e" }
11541285
11551286[[language]]
11561287name = "wgsl"
11571288scope = "source.wgsl"
11581289file-types = ["wgsl"]
1159−roots = []
11601290comment-token = "//"
11611291language-servers = [ "wgsl_analyzer" ]
11621292indent = { tab-width = 4, unit = " " }
@@ -1168,7 +1298,6 @@ source = { git = "https://github.com/szebniok/tree-sitter-wgsl", rev = "272e89ef
11681298[[language]]
11691299name = "llvm"
11701300scope = "source.llvm"
1171−roots = []
11721301file-types = ["ll"]
11731302comment-token = ";"
11741303indent = { tab-width = 2, unit = " " }
@@ -1176,12 +1305,11 @@ injection-regex = "llvm"
11761305
11771306[[grammar]]
11781307name = "llvm"
1179−source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "3b213925b9c4f42c1acfe2e10bfbb438d9c6834d" }
1308+source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "e9948edc41e9e5869af99dddb2b5ff5cc5581af6" }
11801309
11811310[[language]]
11821311name = "llvm-mir"
11831312scope = "source.llvm_mir"
1184−roots = []
11851313file-types = []
11861314comment-token = ";"
11871315indent = { tab-width = 2, unit = " " }
@@ -1197,7 +1325,6 @@ name = "llvm-mir-yaml"
11971325#
11981326# grammar = "yaml"
11991327scope = "source.yaml"
1200−roots = []
12011328file-types = ["mir"]
12021329comment-token = "#"
12031330indent = { tab-width = 2, unit = " " }
@@ -1205,7 +1332,6 @@ indent = { tab-width = 2, unit = " " }
12051332[[language]]
12061333name = "tablegen"
12071334scope = "source.tablegen"
1208−roots = []
12091335file-types = ["td"]
12101336comment-token = "//"
12111337indent = { tab-width = 2, unit = " " }
@@ -1219,26 +1345,25 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd
12191345name = "markdown"
12201346scope = "source.md"
12211347injection-regex = "md|markdown"
1222−file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1348+file-types = ["md", "markdown", "PULLREQ_EDITMSG", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook"]
12231349roots = [".marksman.toml"]
12241350language-servers = [ "marksman" ]
12251351indent = { tab-width = 2, unit = " " }
12261352
12271353[[grammar]]
12281354name = "markdown"
1229−source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "fa6bfd51727e4bef99f7eec5f43947f73d64ea7d", subpath = "tree-sitter-markdown" }
1355+source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown" }
12301356
12311357[[language]]
12321358name = "markdown.inline"
12331359scope = "source.markdown.inline"
12341360injection-regex = "markdown\\.inline"
12351361file-types = []
1236−roots = []
12371362grammar = "markdown_inline"
12381363
12391364[[grammar]]
12401365name = "markdown_inline"
1241−source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "fa6bfd51727e4bef99f7eec5f43947f73d64ea7d", subpath = "tree-sitter-markdown-inline" }
1366+source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown-inline" }
12421367
12431368[[language]]
12441369name = "dart"
@@ -1284,7 +1409,6 @@ source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev =
12841409[[language]]
12851410name = "git-commit"
12861411scope = "git.commitmsg"
1287−roots = []
12881412file-types = ["COMMIT_EDITMSG"]
12891413comment-token = "#"
12901414indent = { tab-width = 2, unit = " " }
@@ -1293,13 +1417,12 @@ text-width = 72
12931417
12941418[[grammar]]
12951419name = "git-commit"
1296−source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "db88cffa3952dd2328b741af5d0fc69bdb76704f" }
1420+source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "6f193a66e9aa872760823dff020960c6cedc37b3" }
12971421
12981422[[language]]
12991423name = "diff"
13001424scope = "source.diff"
1301−roots = []
1302−file-types = ["diff", "patch"]
1425+file-types = ["diff", "patch", "rej"]
13031426injection-regex = "diff"
13041427comment-token = "#"
13051428indent = { tab-width = 2, unit = " " }
@@ -1311,7 +1434,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-diff", rev = "fd7
13111434[[language]]
13121435name = "git-rebase"
13131436scope = "source.gitrebase"
1314−roots = []
13151437file-types = ["git-rebase-todo"]
13161438injection-regex = "git-rebase"
13171439comment-token = "#"
@@ -1326,7 +1448,6 @@ name = "regex"
13261448scope = "source.regex"
13271449injection-regex = "regex"
13281450file-types = ["regex", ".Rbuildignore"]
1329−roots = []
13301451
13311452[[grammar]]
13321453name = "regex"
@@ -1335,7 +1456,6 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf
13351456[[language]]
13361457name = "git-config"
13371458scope = "source.gitconfig"
1338−roots = []
13391459file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }]
13401460injection-regex = "git-config"
13411461comment-token = "#"
@@ -1348,7 +1468,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev
13481468[[language]]
13491469name = "git-attributes"
13501470scope = "source.gitattributes"
1351−roots = []
13521471file-types = [".gitattributes"]
13531472injection-regex = "git-attributes"
13541473comment-token = "#"
@@ -1361,8 +1480,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
13611480[[language]]
13621481name = "git-ignore"
13631482scope = "source.gitignore"
1364−roots = []
1365−file-types = [".gitignore", ".gitignore_global"]
1483+file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"]
13661484injection-regex = "git-ignore"
13671485comment-token = "#"
13681486grammar = "gitignore"
@@ -1376,7 +1494,7 @@ name = "graphql"
13761494scope = "source.graphql"
13771495injection-regex = "graphql"
13781496file-types = ["gql", "graphql", "graphqls"]
1379−roots = []
1497+language-servers = [ "graphql-language-service" ]
13801498indent = { tab-width = 2, unit = " " }
13811499
13821500[[grammar]]
@@ -1403,7 +1521,6 @@ name = "iex"
14031521scope = "source.iex"
14041522injection-regex = "iex"
14051523file-types = ["iex"]
1406−roots = []
14071524
14081525[[grammar]]
14091526name = "iex"
@@ -1422,7 +1539,7 @@ indent = { tab-width = 2, unit = " " }
14221539
14231540[[grammar]]
14241541name = "rescript"
1425−source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "65609807c628477f3b94052e7ef895885ac51c3c" }
1542+source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "467dcf99f68c47823d7b378779a6b282d7ef9782" }
14261543
14271544[[language]]
14281545name = "erlang"
@@ -1466,7 +1583,6 @@ scope = "source.hcl"
14661583injection-regex = "(hcl|tf|nomad)"
14671584language-id = "terraform"
14681585file-types = ["hcl", "tf", "nomad"]
1469−roots = []
14701586comment-token = "#"
14711587indent = { tab-width = 2, unit = " " }
14721588language-servers = [ "terraform-ls" ]
@@ -1481,7 +1597,6 @@ name = "tfvars"
14811597scope = "source.tfvars"
14821598language-id = "terraform-vars"
14831599file-types = ["tfvars"]
1484−roots = []
14851600comment-token = "#"
14861601indent = { tab-width = 2, unit = " " }
14871602language-servers = [ "terraform-ls" ]
@@ -1493,7 +1608,6 @@ name = "org"
14931608scope = "source.org"
14941609injection-regex = "org"
14951610file-types = ["org"]
1496−roots = []
14971611indent = { tab-width = 2, unit = " " }
14981612
14991613[[grammar]]
@@ -1505,7 +1619,6 @@ name = "solidity"
15051619scope = "source.sol"
15061620injection-regex = "(sol|solidity)"
15071621file-types = ["sol"]
1508−roots = []
15091622comment-token = "//"
15101623indent = { tab-width = 4, unit = " " }
15111624language-servers = [ "solc" ]
@@ -1523,20 +1636,23 @@ roots = ["gleam.toml"]
15231636comment-token = "//"
15241637indent = { tab-width = 2, unit = " " }
15251638language-servers = [ "gleam" ]
1639+auto-format = true
15261640
15271641[[grammar]]
15281642name = "gleam"
1529−source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "ae79782c00656945db69641378e688cdb78d52c1" }
1643+source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "b2afa4fd6bb41a7bf912b034c653c90af7ae5122" }
15301644
15311645[[language]]
15321646name = "ron"
15331647scope = "source.ron"
15341648injection-regex = "ron"
15351649file-types = ["ron"]
1536−roots = []
15371650comment-token = "//"
15381651indent = { tab-width = 4, unit = " " }
1539−grammar = "rust"
1652+
1653+[[grammar]]
1654+name = "ron"
1655+source = { git = "https://github.com/zee-editor/tree-sitter-ron", rev = "7762d709a0f7c1f9e269d0125a2e8a7a69006146" }
15401656
15411657[[language]]
15421658name = "robot"
@@ -1544,21 +1660,19 @@ scope = "source.robot"
15441660injection-regex = "robot"
15451661file-types = ["robot", "resource"]
15461662comment-token = "#"
1547−roots = []
15481663indent = { tab-width = 4, unit = " " }
15491664language-servers = [ "robotframework_ls" ]
15501665
15511666[[grammar]]
15521667name = "robot"
1553−source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "f1142bfaa6acfce95e25d2c6d18d218f4f533927" }
1668+source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "322e4cc65754d2b3fdef4f2f8a71e0762e3d13af" }
15541669
15551670[[language]]
15561671name = "r"
15571672scope = "source.r"
15581673injection-regex = "(r|R)"
1559−file-types = ["r", "R", ".Rprofile", "Rprofile.site"]
1674+file-types = ["r", "R", ".Rprofile", "Rprofile.site", ".RHistory"]
15601675shebangs = ["r", "R"]
1561−roots = []
15621676comment-token = "#"
15631677indent = { tab-width = 2, unit = " " }
15641678language-servers = [ "r" ]
@@ -1570,9 +1684,9 @@ source = { git = "https://github.com/r-lib/tree-sitter-r", rev = "cc04302e1bff76
15701684[[language]]
15711685name = "rmarkdown"
15721686scope = "source.rmd"
1687+language-id = "rmd"
15731688injection-regex = "(r|R)md"
15741689file-types = ["rmd", "Rmd"]
1575−roots = []
15761690indent = { tab-width = 2, unit = " " }
15771691grammar = "markdown"
15781692language-servers = [ "r" ]
@@ -1596,7 +1710,6 @@ name = "erb"
15961710scope = "text.html.erb"
15971711injection-regex = "erb"
15981712file-types = ["erb"]
1599−roots = []
16001713indent = { tab-width = 2, unit = " " }
16011714grammar = "embedded-template"
16021715
@@ -1605,7 +1718,6 @@ name = "ejs"
16051718scope = "text.html.ejs"
16061719injection-regex = "ejs"
16071720file-types = ["ejs"]
1608−roots = []
16091721indent = { tab-width = 2, unit = " " }
16101722grammar = "embedded-template"
16111723
@@ -1641,15 +1753,14 @@ source = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "
16411753[[language]]
16421754name = "sql"
16431755scope = "source.sql"
1644−file-types = ["sql"]
1645−roots = []
1756+file-types = ["sql", "dsql"]
16461757comment-token = "--"
16471758indent = { tab-width = 4, unit = " " }
16481759injection-regex = "sql"
16491760
16501761[[grammar]]
16511762name = "sql"
1652−source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "7cbac0472e5b8f8486ce64ffbcf1982d5cd5fc8d" }
1763+source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "25be0b8f17e9189ad9e1b875869d025c5aec1286" }
16531764
16541765[[language]]
16551766name = "gdscript"
@@ -1688,20 +1799,19 @@ scope = "source.nu"
16881799injection-regex = "nu"
16891800file-types = ["nu"]
16901801shebangs = ["nu"]
1691−roots = []
16921802comment-token = "#"
16931803indent = { tab-width = 2, unit = " " }
1804+language-servers = [ "nu-lsp" ]
16941805
16951806[[grammar]]
16961807name = "nu"
1697−source = { git = "https://github.com/LhKipp/tree-sitter-nu", rev = "eb95bdac3abd73ef47e53f19c63e74a31405ebd2" }
1808+source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "98c11c491e3405c75affa1cf004097692da3dda2" }
16981809
16991810[[language]]
17001811name = "vala"
17011812scope = "source.vala"
17021813injection-regex = "vala"
17031814file-types = ["vala", "vapi"]
1704−roots = []
17051815comment-token = "//"
17061816indent = { tab-width = 2, unit = " " }
17071817language-servers = [ "vala-language-server" ]
@@ -1715,20 +1825,18 @@ name = "hare"
17151825scope = "source.hare"
17161826injection-regex = "hare"
17171827file-types = ["ha"]
1718−roots = []
17191828comment-token = "//"
17201829indent = { tab-width = 8, unit = "\t" }
17211830
17221831[[grammar]]
17231832name = "hare"
1724−source = { git = "https://git.sr.ht/~ecmma/tree-sitter-hare", rev = "bc26a6a949f2e0d98b7bfc437d459b250900a165" }
1833+source = { git = "https://git.sr.ht/~ecmma/tree-sitter-hare", rev = "2495958aaf3f93581c87ec020164255e80655331" }
17251834
17261835[[language]]
17271836name = "devicetree"
17281837scope = "source.devicetree"
17291838injection-regex = "(dtsi?|devicetree|fdt)"
17301839file-types = ["dts", "dtsi"]
1731−roots = []
17321840comment-token = "//"
17331841indent = { tab-width = 4, unit = "\t" }
17341842
@@ -1741,7 +1849,6 @@ name = "cairo"
17411849scope = "source.cairo"
17421850injection-regex = "cairo"
17431851file-types = ["cairo"]
1744−roots = []
17451852comment-token = "//"
17461853indent = { tab-width = 4, unit = " " }
17471854# auto-format = true
@@ -1753,7 +1860,6 @@ name = "cpon"
17531860scope = "scope.cpon"
17541861injection-regex = "cpon"
17551862file-types = ["cpon", "cp"]
1756−roots = []
17571863auto-format = true
17581864comment-token = "//"
17591865indent = { tab-width = 2, unit = " " }
@@ -1780,8 +1886,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4
17801886name = "meson"
17811887scope = "source.meson"
17821888injection-regex = "meson"
1783−file-types = ["meson.build"]
1784−roots = []
1889+file-types = ["meson.build", "meson_options.txt"]
17851890comment-token = "#"
17861891indent = { tab-width = 2, unit = " " }
17871892
@@ -1793,7 +1898,6 @@ source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8
17931898name = "sshclientconfig"
17941899scope = "source.sshclientconfig"
17951900file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }]
1796−roots = []
17971901comment-token = "#"
17981902
17991903[[grammar]]
@@ -1804,14 +1908,14 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev =
18041908name = "scheme"
18051909scope = "source.scheme"
18061910injection-regex = "scheme"
1807−file-types = ["ss"] # "scm",
1808−roots = []
1911+file-types = ["ss", "scm"]
1912+shebangs = ["scheme", "guile", "chicken"]
18091913comment-token = ";"
18101914indent = { tab-width = 2, unit = " " }
18111915
18121916[[grammar]]
18131917name = "scheme"
1814−source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "c0741320bfca6b7b5b7a13b5171275951e96a842" }
1918+source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af3af6c9356b936f8a515a1e449c32e804c2b1a8" }
18151919
18161920[[language]]
18171921name = "v"
@@ -1826,13 +1930,12 @@ indent = { tab-width = 4, unit = "\t" }
18261930
18271931[[grammar]]
18281932name = "v"
1829−source = { git = "https://github.com/vlang/vls", subpath = "tree_sitter_v", rev = "66cf9d3086fb5ecc827cb32c64c5d812ab17d2c6" }
1933+source = {git = "https://github.com/v-analyzer/v-analyzer", subpath = "tree_sitter_v", rev = "e14fdf6e661b10edccc744102e4ccf0b187aa8ad"}
18301934
18311935[[language]]
18321936name = "verilog"
18331937scope = "source.verilog"
18341938file-types = ["v", "vh", "sv", "svh"]
1835−roots = []
18361939comment-token = "//"
18371940language-servers = [ "svlangserver" ]
18381941indent = { tab-width = 2, unit = " " }
@@ -1847,7 +1950,6 @@ name = "edoc"
18471950scope = "source.edoc"
18481951file-types = ["edoc", "edoc.in"]
18491952injection-regex = "edoc"
1850−roots = []
18511953indent = { tab-width = 4, unit = " " }
18521954
18531955[[grammar]]
@@ -1859,7 +1961,6 @@ name = "jsdoc"
18591961scope = "source.jsdoc"
18601962injection-regex = "jsdoc"
18611963file-types = ["jsdoc"]
1862−roots = []
18631964indent = { tab-width = 2, unit = " " }
18641965
18651966[[grammar]]
@@ -1871,7 +1972,6 @@ name = "openscad"
18711972scope = "source.openscad"
18721973injection-regex = "openscad"
18731974file-types = ["scad"]
1874−roots = []
18751975comment-token = "//"
18761976language-servers = [ "openscad-lsp" ]
18771977indent = { tab-width = 2, unit = "\t" }
@@ -1912,8 +2012,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569
19122012name = "starlark"
19132013scope = "source.starlark"
19142014injection-regex = "(starlark|bzl|bazel)"
1915−file-types = ["bzl", "bazel", "BUILD"]
1916−roots = []
2015+file-types = ["bzl", "bazel", "BUILD", "star"]
19172016comment-token = "#"
19182017indent = { tab-width = 4, unit = " " }
19192018grammar = "python"
@@ -1922,7 +2021,6 @@ grammar = "python"
19222021name = "elvish"
19232022scope = "source.elvish"
19242023file-types = ["elv"]
1925−roots = []
19262024comment-token = "#"
19272025indent = { tab-width = 2, unit = " " }
19282026language-servers = [ "elvish" ]
@@ -1938,7 +2036,6 @@ scope = "source.idr"
19382036injection-regex = "idr"
19392037file-types = ["idr"]
19402038shebangs = []
1941−roots = []
19422039comment-token = "--"
19432040indent = { tab-width = 2, unit = " " }
19442041language-servers = [ "idris2-lsp" ]
@@ -1962,20 +2059,18 @@ name = "ungrammar"
19622059scope = "source.ungrammar"
19632060injection-regex = "ungrammar"
19642061file-types = ["ungram", "ungrammar"]
1965−roots = []
19662062comment-token = "//"
19672063indent = { tab-width = 2, unit = " " }
19682064
19692065[[grammar]]
19702066name = "ungrammar"
1971−source = { git = "https://github.com/Philipp-M/tree-sitter-ungrammar", rev = "0113de880a58ea14f2a75802e9b99fcc25003d9c" }
2067+source = { git = "https://github.com/Philipp-M/tree-sitter-ungrammar", rev = "a7e104629cff5a8b7367187610631e8f5eb7c6ea" }
19722068
19732069[[language]]
19742070name = "dot"
19752071scope = "source.dot"
19762072injection-regex = "dot"
19772073file-types = ["dot"]
1978−roots = []
19792074comment-token = "//"
19802075indent = { tab-width = 4, unit = " " }
19812076language-servers = [ "dot-language-server" ]
@@ -2005,21 +2100,19 @@ name = "slint"
20052100scope = "source.slint"
20062101injection-regex = "slint"
20072102file-types = ["slint"]
2008−roots = []
20092103comment-token = "//"
20102104indent = { tab-width = 4, unit = " " }
20112105language-servers = [ "slint-lsp" ]
20122106
20132107[[grammar]]
20142108name = "slint"
2015−source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "0d4dda94f96623302dfc234e06be62a5717f47da" }
2109+source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "00c8a2d3645766f68c0d0460086c0a994e5b0d85" }
20162110
20172111[[language]]
20182112name = "task"
20192113scope = "source.task"
20202114injection-regex = "task"
20212115file-types = ["task"]
2022−roots = []
20232116comment-token = "#"
20242117indent = { tab-width = 2, unit = " " }
20252118
@@ -2032,7 +2125,6 @@ name = "xit"
20322125scope = "source.xit"
20332126injection-regex = "xit"
20342127file-types = ["xit"]
2035−roots = []
20362128indent = { tab-width = 4, unit = " " }
20372129
20382130[[grammar]]
@@ -2050,14 +2142,13 @@ roots = ["edgedb.toml"]
20502142
20512143[[grammar]]
20522144name ="esdl"
2053−source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "b840c8a8028127e0a7c6e6c45141adade2bd75cf" }
2145+source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "df83acc8cacd0cfb139eecee0e718dc32c4f92e2" }
20542146
20552147[[language]]
20562148name = "pascal"
20572149scope = "source.pascal"
20582150injection-regex = "pascal"
20592151file-types = ["pas", "pp", "inc", "lpr", "lfm"]
2060−roots = []
20612152comment-token = "//"
20622153indent = { tab-width = 2, unit = " " }
20632154language-servers = [ "pasls" ]
@@ -2072,7 +2163,6 @@ scope = "source.sml"
20722163injection-regex = "sml"
20732164file-types = ["sml"]
20742165comment-token = "(*"
2075−roots = []
20762166
20772167[[grammar]]
20782168name = "sml"
@@ -2096,19 +2186,17 @@ name = "astro"
20962186scope = "source.astro"
20972187injection-regex = "astro"
20982188file-types = ["astro"]
2099−roots = []
21002189indent = { tab-width = 2, unit = " " }
21012190
21022191[[grammar]]
21032192name = "astro"
2104−source = { git = "https://github.com/virchau13/tree-sitter-astro", rev = "5f5c3e73c45967df9aa42f861fad2d77cd4e0900" }
2193+source = { git = "https://github.com/virchau13/tree-sitter-astro", rev = "947e93089e60c66e681eba22283f4037841451e7" }
21052194
21062195[[language]]
21072196name = "bass"
21082197scope = "source.bass"
21092198injection-regex = "bass"
21102199file-types = ["bass"]
2111−roots = []
21122200comment-token = ";"
21132201indent = { tab-width = 2, unit = " " }
21142202language-servers = [ "bass" ]
@@ -2122,7 +2210,6 @@ name = "wat"
21222210scope = "source.wat"
21232211comment-token = ";;"
21242212file-types = ["wat"]
2125−roots = []
21262213
21272214[[grammar]]
21282215name = "wat"
@@ -2133,7 +2220,6 @@ name = "wast"
21332220scope = "source.wast"
21342221comment-token = ";;"
21352222file-types = ["wast"]
2136−roots = []
21372223
21382224[[grammar]]
21392225name = "wast"
@@ -2143,7 +2229,6 @@ source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f
21432229name = "d"
21442230scope = "source.d"
21452231file-types = [ "d", "dd" ]
2146−roots = []
21472232comment-token = "//"
21482233injection-regex = "d"
21492234indent = { tab-width = 4, unit = " "}
@@ -2152,13 +2237,12 @@ formatter = { command = "dfmt" }
21522237
21532238[[grammar]]
21542239name = "d"
2155−source = { git = "https://github.com/gdamore/tree-sitter-d", rev="601c4a1e8310fb2f3c43fa8a923d0d27497f3c04" }
2240+source = { git = "https://github.com/gdamore/tree-sitter-d", rev = "5566f8ce8fc24186fad06170bbb3c8d97c935d74" }
21562241
21572242[[language]]
21582243name = "vhs"
21592244scope = "source.vhs"
21602245file-types = ["tape"]
2161−roots = []
21622246comment-token = "#"
21632247indent = { tab-width = 2, unit = " " }
21642248grammar = "vhs"
@@ -2171,21 +2255,76 @@ source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d8
21712255name = "kdl"
21722256scope = "source.kdl"
21732257file-types = ["kdl"]
2174−roots = []
21752258comment-token = "//"
21762259injection-regex = "kdl"
21772260
21782261[[grammar]]
21792262name = "kdl"
2180−source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d15df6610484e1d4b5c987ecad52373" }
2263+source = { git = "https://github.com/amaanq/tree-sitter-kdl", rev = "3ca569b9f9af43593c24f9e7a21f02f43a13bb88" }
21812264
21822265[[language]]
21832266name = "xml"
21842267scope = "source.xml"
21852268injection-regex = "xml"
2186−file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd", "gml", "xaml", "gir"]
2269+file-types = [
2270+ "xml",
2271+ "mobileconfig",
2272+ "plist",
2273+ "xib",
2274+ "storyboard",
2275+ "svg",
2276+ "xsd",
2277+ "gml",
2278+ "xaml",
2279+ "gir",
2280+ "rss",
2281+ "atom",
2282+ "opml",
2283+ "policy",
2284+ "ascx",
2285+ "axml",
2286+ "axaml",
2287+ "bpmn",
2288+ "cpt",
2289+ "csl",
2290+ "csproj.user",
2291+ "dita",
2292+ "ditamap",
2293+ "dtml",
2294+ "fxml",
2295+ "iml",
2296+ "isml",
2297+ "jmx",
2298+ "launch",
2299+ "menu",
2300+ "mxml",
2301+ "nuspec",
2302+ "pt",
2303+ "publishsettings",
2304+ "pubxml",
2305+ "pubxml.user",
2306+ "rbxlx",
2307+ "rbxmx",
2308+ "rng",
2309+ "shproj",
2310+ "tld",
2311+ "tmx",
2312+ "vbproj.user",
2313+ "vcxproj",
2314+ "vcxproj.filters",
2315+ "wsdl",
2316+ "wxi",
2317+ "wxs",
2318+ "xbl",
2319+ "xlf",
2320+ "xliff",
2321+ "xpdl",
2322+ "xul",
2323+ "xoml",
2324+ "musicxml",
2325+ "glif"
2326+]
21872327indent = { tab-width = 2, unit = " " }
2188−roots = []
21892328
21902329[language.auto-pairs]
21912330'(' = ')'
@@ -2206,7 +2345,6 @@ scope = "source.dtd"
22062345injection-regex = "dtd"
22072346file-types = ["dtd", "ent"]
22082347indent = {tab-width = 2, unit = " "}
2209−roots = []
22102348
22112349[language.auto-pairs]
22122350'(' = ')'
@@ -2224,7 +2362,6 @@ name = "wit"
22242362scope = "source.wit"
22252363injection-regex = "wit"
22262364file-types = ["wit"]
2227−roots = []
22282365comment-token = "//"
22292366indent = { tab-width = 2, unit = " " }
22302367
@@ -2243,11 +2380,10 @@ source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9a
22432380[[language]]
22442381name = "env"
22452382scope = "source.env"
2246−file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc"]
2383+file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc", ".envrc.local", ".envrc.private"]
22472384injection-regex = "env"
22482385comment-token = "#"
22492386indent = { tab-width = 4, unit = "\t" }
2250−roots = []
22512387grammar = "bash"
22522388
22532389[[language]]
@@ -2271,12 +2407,15 @@ file-types = [
22712407 "container",
22722408 "volume",
22732409 "kube",
2274− "network"
2410+ "network",
2411+ ".editorconfig",
2412+ "properties",
2413+ "cfg",
2414+ "directory"
22752415]
22762416injection-regex = "ini"
22772417comment-token = "#"
22782418indent = { tab-width = 4, unit = "\t" }
2279−roots = []
22802419
22812420[[grammar]]
22822421name = "ini"
@@ -2286,7 +2425,6 @@ source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb87
22862425name = "bicep"
22872426scope = "source.bicep"
22882427file-types = ["bicep"]
2289−roots = []
22902428auto-format = true
22912429comment-token = "//"
22922430indent = { tab-width = 2, unit = " "}
@@ -2300,7 +2438,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-bicep", rev = "d8
23002438name = "qml"
23012439scope = "source.qml"
23022440file-types = ["qml"]
2303−roots = []
23042441language-servers = [ "qmlls" ]
23052442indent = { tab-width = 4, unit = " " }
23062443grammar = "qmljs"
@@ -2314,7 +2451,6 @@ name = "mermaid"
23142451scope = "source.mermaid"
23152452injection-regex = "mermaid"
23162453file-types = ["mermaid"]
2317−roots = []
23182454comment-token = "%%"
23192455indent = { tab-width = 4, unit = " " }
23202456
@@ -2328,12 +2464,11 @@ scope = "source.m"
23282464file-types = ["m"]
23292465comment-token = "%"
23302466shebangs = ["octave-cli", "matlab"]
2331−roots = []
23322467indent = { tab-width = 2, unit = " " }
23332468
23342469[[grammar]]
23352470name = "matlab"
2336−source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "676117eafa64afedc8380a921a77cd9f2244bc6b" }
2471+source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "6071891a8c39600203eba20513666cf93b4d650a" }
23372472
23382473[[language]]
23392474name = "ponylang"
@@ -2353,7 +2488,6 @@ name = "dhall"
23532488scope = "source.dhall"
23542489injection-regex = "dhall"
23552490file-types = ["dhall"]
2356−roots = []
23572491comment-token = "--"
23582492indent = { tab-width = 2, unit = " " }
23592493language-servers = [ "dhall-lsp-server" ]
@@ -2368,7 +2502,6 @@ name = "sage"
23682502scope = "source.sage"
23692503file-types = ["sage"]
23702504injection-regex = "sage"
2371−roots = []
23722505comment-token = "#"
23732506indent = { tab-width = 4, unit = " " }
23742507grammar = "python"
@@ -2379,7 +2512,6 @@ scope = "source.msbuild"
23792512injection-regex = "msbuild"
23802513file-types = ["proj", "vbproj", "csproj", "fsproj", "targets", "props"]
23812514indent = { tab-width = 2, unit = " " }
2382−roots = []
23832515grammar = "xml"
23842516
23852517[language.auto-pairs]
@@ -2395,7 +2527,6 @@ name = "pem"
23952527scope = "source.pem"
23962528file-types = ["pem", "cert", "crt"]
23972529injection-regex = "pem"
2398−roots = []
23992530grammar = "pem"
24002531
24012532[[grammar]]
@@ -2406,7 +2537,6 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-pem", rev = "be67a433
24062537name = "passwd"
24072538scope = "source.passwd"
24082539file-types = ["passwd"]
2409−roots = []
24102540
24112541[[grammar]]
24122542name = "passwd"
@@ -2416,7 +2546,6 @@ source = { git = "https://github.com/ath3/tree-sitter-passwd", rev = "20239395ea
24162546name = "hosts"
24172547scope = "source.hosts"
24182548file-types = ["hosts"]
2419−roots = []
24202549comment-token = "#"
24212550
24222551[[grammar]]
@@ -2428,20 +2557,18 @@ name = "uxntal"
24282557scope = "source.tal"
24292558injection-regex = "tal"
24302559file-types = ["tal"]
2431−roots = []
24322560auto-format = false
24332561comment-token = "("
24342562
24352563[[grammar]]
24362564name = "uxntal"
2437−source = { git = "https://github.com/Jummit/tree-sitter-uxntal", rev = "9297e95ef74380b0ad84c4fd98f91e9f6e4319e6" }
2565+source = { git = "https://github.com/Jummit/tree-sitter-uxntal", rev = "d68406066648cd6db4c6a2f11ec305af02079884" }
24382566
24392567[[language]]
24402568name = "yuck"
24412569scope = "source.yuck"
24422570injection-regex = "yuck"
24432571file-types = ["yuck"]
2444−roots = []
24452572comment-token = ";"
24462573indent = { tab-width = 2, unit = " " }
24472574
@@ -2454,19 +2581,17 @@ name = "prql"
24542581scope = "source.prql"
24552582injection-regex = "prql"
24562583file-types = ["prql"]
2457−roots = []
24582584comment-token = "#"
24592585indent = { tab-width = 4, unit = " " }
24602586
24612587[[grammar]]
24622588name = "prql"
2463−source = { git = "https://github.com/PRQL/tree-sitter-prql", rev = "3f27cac466f030ee7d985d91eba5470e01dd21ea" }
2589+source = { git = "https://github.com/PRQL/tree-sitter-prql", rev = "09e158cd3650581c0af4c49c2e5b10c4834c8646" }
24642590
24652591[[language]]
24662592name = "po"
24672593scope = "source.po"
24682594file-types = ["po", "pot"]
2469−roots = []
24702595comment-token = "#"
24712596
24722597[[grammar]]
@@ -2476,9 +2601,8 @@ source = { git = "https://github.com/erasin/tree-sitter-po", rev = "417cee9abb20
24762601[[language]]
24772602name = "nasm"
24782603scope = "source.nasm"
2479−file-types = ["asm", "s", "S", "nasm"]
2604+file-types = ["asm", "S", "nasm"]
24802605injection-regex = "n?asm"
2481−roots = []
24822606comment-token = ";"
24832607indent = { tab-width = 8, unit = " " }
24842608
@@ -2486,12 +2610,23 @@ indent = { tab-width = 8, unit = " " }
24862610name = "nasm"
24872611source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "a0db15db6fcfb1bf2cc8702500e55e558825c48b" }
24882612
2613+[[language]]
2614+name = "gas"
2615+scope = "source.gas"
2616+file-types = ["s"]
2617+injection-regex = "gas"
2618+comment-token = "#"
2619+indent = { tab-width = 8, unit = " " }
2620+
2621+[[grammar]]
2622+name = "gas"
2623+source = { git = "https://github.com/sirius94/tree-sitter-gas", rev = "60f443646b20edee3b7bf18f3a4fb91dc214259a" }
2624+
24892625[[language]]
24902626name = "rst"
24912627scope = "source.rst"
24922628comment-token = ".."
24932629file-types = ["rst"]
2494−roots = []
24952630
24962631[[grammar]]
24972632name = "rst"
@@ -2502,7 +2637,6 @@ name = "capnp"
25022637scope = "source.capnp"
25032638injection-regex = "capnp"
25042639file-types = ["capnp"]
2505−roots = []
25062640comment-token = "#"
25072641indent = { tab-width = 2, unit = " " }
25082642
@@ -2528,7 +2662,6 @@ source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "c
25282662name = "vhdl"
25292663scope = "source.vhdl"
25302664file-types = ["vhd", "vhdl"]
2531−roots = []
25322665comment-token = "--"
25332666language-servers = [ "vhdl_ls" ]
25342667indent = { tab-width = 2, unit = " " }
@@ -2540,7 +2673,6 @@ source = { git = "https://github.com/teburd/tree-sitter-vhdl", rev = "c57313adee
25402673
25412674[[language]]
25422675name = "rego"
2543−roots = []
25442676scope = "source.rego"
25452677injection-regex = "rego"
25462678file-types = ["rego"]
@@ -2551,7 +2683,7 @@ grammar = "rego"
25512683
25522684[[grammar]]
25532685name = "rego"
2554−source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "b2667c975f07b33be3ceb83bea5cfbad88095866" }
2686+source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "9ac75e71b2d791e0aadeef68098319d86a2a14cf" }
25552687
25562688[[language]]
25572689name = "nim"
@@ -2559,7 +2691,6 @@ scope = "source.nim"
25592691injection-regex = "nim"
25602692file-types = ["nim", "nims", "nimble"]
25612693shebangs = []
2562−roots = []
25632694comment-token = "#"
25642695indent = { tab-width = 2, unit = " " }
25652696language-servers = [ "nimlangserver" ]
@@ -2583,13 +2714,13 @@ file-types = [ "cabal" ]
25832714roots = ["cabal.project", "Setup.hs"]
25842715indent = { tab-width = 2, unit = " " }
25852716comment-token = "--"
2717+language-servers = [ "haskell-language-server" ]
25862718
25872719[[language]]
25882720name = "hurl"
25892721scope = "source.hurl"
25902722injection-regex = "hurl"
25912723file-types = ["hurl"]
2592−roots = []
25932724comment-token = "#"
25942725indent = { tab-width = 2, unit = " " }
25952726
@@ -2600,7 +2731,6 @@ source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c4206
26002731[[language]]
26012732name = "markdoc"
26022733scope = "text.markdoc"
2603−roots = []
26042734file-types = ["mdoc"]
26052735language-servers = [ "markdoc-ls" ]
26062736
@@ -2613,7 +2743,6 @@ name = "opencl"
26132743scope = "source.cl"
26142744injection-regex = "(cl|opencl)"
26152745file-types = ["cl"]
2616−roots = []
26172746comment-token = "//"
26182747language-servers = [ "clangd" ]
26192748
@@ -2626,7 +2755,6 @@ name = "just"
26262755scope = "source.just"
26272756file-types = ["justfile", "Justfile", ".justfile", ".Justfile"]
26282757injection-regex = "just"
2629−roots = []
26302758comment-token = "#"
26312759indent = { tab-width = 4, unit = "\t" }
26322760
@@ -2634,40 +2762,52 @@ indent = { tab-width = 4, unit = "\t" }
26342762name = "just"
26352763source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "8af0aab79854aaf25b620a52c39485849922f766" }
26362764
2765+[[language]]
2766+name = "gn"
2767+scope = "source.gn"
2768+injection-regex = "gn"
2769+file-types = ["gn", "gni"]
2770+roots = []
2771+comment-token = "#"
2772+indent = { tab-width = 2, unit = " " }
2773+formatter = { command = "gn", args = ["format", "--stdin"] }
2774+
2775+[[grammar]]
2776+name = "gn"
2777+source = { git = "https://github.com/willcassella/tree-sitter-gn", rev = "e18d6e36a79b20dafb58f19d407bd38b0e60260e" }
2778+
26372779[[language]]
26382780name = "blueprint"
26392781scope = "source.blueprint"
26402782injection-regex = "blueprint"
26412783file-types = ["blp"]
2642−roots = []
26432784comment-token = "//"
26442785language-servers = [ "blueprint-compiler" ]
26452786indent = { tab-width = 4, unit = " " }
26462787
26472788[[grammar]]
26482789name = "blueprint"
2649−source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "7f1a5df44861291d6951b6b2146a9fef4c226e14" }
2790+source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "863cea9f83ad5637300478e0559262f1e791684b" }
26502791
26512792[[language]]
26522793name = "forth"
26532794scope = "source.forth"
26542795injection-regex = "forth"
26552796file-types = ["fs", "forth", "fth", "4th"]
2656−roots = []
26572797comment-token = "\\"
26582798language-servers = [ "forth-lsp" ]
26592799indent = { tab-width = 3, unit = " " }
26602800
26612801[[grammar]]
26622802name = "forth"
2663−source = { git = "https://github.com/alexanderbrevig/tree-sitter-forth", rev = "304ed77beb113e37af38b20ff14e3c37bf350d10" }
2803+source = { git = "https://github.com/alexanderbrevig/tree-sitter-forth", rev = "90189238385cf636b9ee99ce548b9e5b5e569d48" }
26642804
26652805[[language]]
26662806name = "fsharp"
26672807scope = "source.fs"
26682808roots = ["sln", "fsproj"]
26692809injection-regex = "fsharp"
2670−file-types = ["fs", "fsx"]
2810+file-types = ["fs", "fsx", "fsi", "fsscript"]
26712811comment-token = "//"
26722812indent = { tab-width = 4, unit = " " }
26732813auto-format = true
@@ -2675,26 +2815,194 @@ language-servers = ["fsharp-ls"]
26752815
26762816[[grammar]]
26772817name = "fsharp"
2678−source = { git = "https://github.com/kaashyapan/tree-sitter-fsharp", rev = "1b5a315e003024db6181744f1d25b8f525732665" }
2818+source = { git = "https://github.com/kaashyapan/tree-sitter-fsharp", rev = "18da392fd9bd5e79f357abcce13f61f3a15e3951" }
26792819
26802820[[language]]
26812821name = "t32"
26822822scope = "source.t32"
26832823injection-regex = "t32"
26842824file-types = ["cmm", "t32"]
2685−roots = []
26862825comment-token = ";"
26872826indent = { tab-width = 2, unit = " " }
26882827
26892828[[grammar]]
26902829name = "t32"
2691−source = { git = "https://codeberg.org/xasc/tree-sitter-t32", rev = "1dd98248b01e4a3933c1b85b58bab0875e2ba437" }
2830+source = { git = "https://gitlab.com/xasc/tree-sitter-t32", rev = "6da5e3cbabd376b566d04282005e52ffe67ef74a" }
26922831
26932832[[language]]
26942833name = "webc"
26952834scope = "text.html.webc"
26962835injection-regex = "webc"
26972836file-types = ["webc"]
2698−roots = []
26992837indent = { tab-width = 2, unit = " " }
27002838grammar = "html"
2839+
2840+[[language]]
2841+name = "typst"
2842+scope = "source.typst"
2843+injection-regex = "typst"
2844+file-types = ["typst", "typ"]
2845+comment-token = "//"
2846+language-servers = ["typst-lsp"]
2847+indent = { tab-width = 2, unit = " " }
2848+
2849+[language.auto-pairs]
2850+'(' = ')'
2851+'{' = '}'
2852+'[' = ']'
2853+'$' = '$'
2854+'"' = '"'
2855+
2856+[[grammar]]
2857+name = "typst"
2858+source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "e35aa22395fdde82bbc4b5700c324ce346dfc9e5" }
2859+
2860+[[language]]
2861+name = "nunjucks"
2862+scope = "text.html.nunjucks"
2863+injection-regex = "nunjucks"
2864+file-types = ["njk"]
2865+indent = { tab-width = 2, unit = " " }
2866+grammar = "jinja2"
2867+
2868+[[language]]
2869+name = "jinja"
2870+scope = "text.html.jinja"
2871+injection-regex = "jinja"
2872+file-types = ["jinja", "jinja2", "j2"]
2873+indent = { tab-width = 2, unit = " " }
2874+grammar = "jinja2"
2875+
2876+[[grammar]]
2877+name = "jinja2"
2878+source = { git = "https://github.com/varpeti/tree-sitter-jinja2", rev = "a533cd3c33aea6acb0f9bf9a56f35dcfe6a8eb53" }
2879+
2880+[[grammar]]
2881+name = "wren"
2882+source = { git = "https://git.sr.ht/~jummit/tree-sitter-wren", rev = "793d58266924e6efcc40e411663393e9d72bec87"}
2883+
2884+[[language]]
2885+name = "wren"
2886+scope = "source.wren"
2887+injection-regex = "wren"
2888+file-types = ["wren"]
2889+indent = { tab-width = 2, unit = " "}
2890+
2891+[[language]]
2892+name = "unison"
2893+scope = "source.unison"
2894+injection-regex = "unison"
2895+file-types = ["u"]
2896+shebangs = []
2897+auto-format = false
2898+comment-token = "--"
2899+indent = { tab-width = 4, unit = " " }
2900+
2901+[language.auto-pairs]
2902+'(' = ')'
2903+'{' = '}'
2904+'[' = ']'
2905+'"' = '"'
2906+'`' = '`'
2907+
2908+[[grammar]]
2909+name = "unison"
2910+source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "98c4e8bc5c9f5989814a720457cf36963cf4043d" }
2911+
2912+[[language]]
2913+name = "todotxt"
2914+scope = "text.todotxt"
2915+file-types = [{ suffix = ".todo.txt" }, "todotxt"]
2916+formatter = { command = "sort" }
2917+auto-format = true
2918+
2919+[[grammar]]
2920+name = "todotxt"
2921+source = { git = "https://github.com/arnarg/tree-sitter-todotxt", rev = "0207f6a4ab6aeafc4b091914d31d8235049a2578" }
2922+
2923+[[language]]
2924+name = "strace"
2925+scope = "source.strace"
2926+file-types = ["strace"]
2927+
2928+[[grammar]]
2929+name = "strace"
2930+source = { git = "https://github.com/sigmaSd/tree-sitter-strace", rev = "2b18fdf9a01e7ec292cc6006724942c81beb7fd5" }
2931+
2932+[[language]]
2933+name = "gemini"
2934+scope = "source.gmi"
2935+file-types = ["gmi"]
2936+
2937+[[grammar]]
2938+name = "gemini"
2939+source = { git = "https://git.sr.ht/~nbsp/tree-sitter-gemini", rev = "3cc5e4bdf572d5df4277fc2e54d6299bd59a54b3" }
2940+
2941+[[language]]
2942+name = "agda"
2943+scope = "source.agda"
2944+injection-regex = "agda"
2945+file-types = ["agda"]
2946+roots = []
2947+comment-token = "--"
2948+# language-servers = [ "als" ]
2949+# the agda language server is of questionable functionality.
2950+auto-format = false
2951+indent = { tab-width = 2, unit = " " }
2952+
2953+[language.auto-pairs]
2954+'"' = '"'
2955+"'" = "'"
2956+'{' = '}'
2957+'(' = ')'
2958+'[' = ']'
2959+
2960+# [language.debugger]
2961+# ?? can this be used for proof assistant support? explore
2962+
2963+[[grammar]]
2964+name = "agda"
2965+source = { git = "https://github.com/tree-sitter/tree-sitter-agda", rev = "c21c3a0f996363ed17b8ac99d827fe5a4821f217" }
2966+
2967+[[language]]
2968+name = "templ"
2969+scope = "source.templ"
2970+file-types = ["templ"]
2971+roots = ["go.work", "go.mod"]
2972+comment-token = "//"
2973+indent = { tab-width = 2, unit = " " }
2974+language-servers = [ "templ" ]
2975+
2976+[[grammar]]
2977+name = "templ"
2978+source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "ea56ac0655243490a4929a988f4eaa91dfccc995" }
2979+
2980+[[language]]
2981+name = "dbml"
2982+scope = "source.dbml"
2983+injection-regex = "dbml"
2984+file-types = ["dbml"]
2985+comment-token = ";"
2986+indent = { tab-width = 2, unit = " " }
2987+
2988+[[grammar]]
2989+name = "dbml"
2990+source = { git = "https://github.com/dynamotn/tree-sitter-dbml", rev = "2e2fa5640268c33c3d3f27f7e676f631a9c68fd9" }
2991+
2992+[[language]]
2993+name = "log"
2994+scope = "source.log"
2995+file-types = ["log"]
2996+
2997+[[grammar]]
2998+name = "log"
2999+source = { git = "https://github.com/Tudyx/tree-sitter-log", rev = "62cfe307e942af3417171243b599cc7deac5eab9" }
3000+
3001+[[language]]
3002+name = "janet"
3003+scope = "source.janet"
3004+injection-regex = "janet"
3005+file-types = ["janet"]
3006+comment-token = "#"
3007+indent = { tab-width = 2, unit = " " }
3008+grammar = "clojure"
logo.svg+1−1
@@ -1 +1 @@
1−<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"> <g transform="matrix(1,0,0,1,-31352.7,-1817.25)"> <g transform="matrix(1,0,0,1,31062.7,-20.8972)"> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z" style="fill:#706bc8;"></path> </g> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z" style="fill:#55c5e4;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z" style="fill:#84ddea;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z" style="fill:#997bc8;"></path></g></g></g> </svg>
1+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"><g transform="matrix(1,0,0,1,-31352.7,-1817.25)"><g transform="matrix(1,0,0,1,31062.7,-20.8972)"><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#706bc8;" d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z"></path></g><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#55c5e4;" d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#84ddea;" d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#997bc8;" d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z"></path></g></g></g></svg>
runtime/queries/_jsx/highlights.scm+3−15
@@ -4,9 +4,6 @@
44(jsx_opening_element ((identifier) @constructor
55 (#match? @constructor "^[A-Z]")))
66
7−; Handle the dot operator effectively - <My.Component>
8−(jsx_opening_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
9−
107(jsx_opening_element (identifier) @tag)
118
129; Closing elements
@@ -15,9 +12,6 @@
1512(jsx_closing_element ((identifier) @constructor
1613 (#match? @constructor "^[A-Z]")))
1714
18−; Handle the dot operator effectively - </My.Component>
19−(jsx_closing_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
20−
2115(jsx_closing_element (identifier) @tag)
2216
2317; Self-closing elements
@@ -26,9 +20,6 @@
2620(jsx_self_closing_element ((identifier) @constructor
2721 (#match? @constructor "^[A-Z]")))
2822
29−; Handle the dot operator effectively - <My.Component />
30−(jsx_self_closing_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
31−
3223(jsx_self_closing_element (identifier) @tag)
3324
3425; Attributes
@@ -39,17 +30,14 @@
3930; Punctuation
4031; -----------
4132
42−; Handle attribute delimiter
33+; Handle attribute delimiter (<Component color="red"/>)
4334(jsx_attribute "=" @punctuation.delimiter)
4435
4536; <Component>
4637(jsx_opening_element ["<" ">"] @punctuation.bracket)
4738
4839; </Component>
49−(jsx_closing_element ["<" "/" ">"] @punctuation.bracket)
40+(jsx_closing_element ["</" ">"] @punctuation.bracket)
5041
5142; <Component />
52−(jsx_self_closing_element ["<" "/" ">"] @punctuation.bracket)
53−
54−; <> ... </>
55−(jsx_fragment ["<" "/" ">"] @punctuation.bracket)
43+(jsx_self_closing_element ["<" "/>"] @punctuation.braket)
runtime/queries/_jsx/indents.scm−1
@@ -1,5 +1,4 @@
11[
2− (jsx_fragment)
32 (jsx_element)
43 (jsx_self_closing_element)
54] @indent
runtime/queries/_typescript/highlights.scm+5−3
@@ -39,11 +39,11 @@
3939 (identifier) @variable.parameter))
4040
4141; (p?: t)
42−; (p?: t = 1) // Invalid but still posible to hihglight.
42+; (p?: t = 1) // Invalid but still possible to highlight.
4343(optional_parameter
4444 (identifier) @variable.parameter)
4545
46−; (...p?: t) // Invalid but still posible to hihglight.
46+; (...p?: t) // Invalid but still possible to highlight.
4747(optional_parameter
4848 (rest_pattern
4949 (identifier) @variable.parameter))
@@ -59,7 +59,7 @@
5959 (pair_pattern
6060 value: (identifier) @variable.parameter)))
6161
62−; ([ p ]?: t[]) // Invalid but still posible to hihglight.
62+; ([ p ]?: t[]) // Invalid but still possible to highlight.
6363(optional_parameter
6464 (array_pattern
6565 (identifier) @variable.parameter))
@@ -107,6 +107,8 @@
107107; Types
108108; -----
109109
110+(type_parameter
111+ name: (type_identifier) @type.parameter)
110112(type_identifier) @type
111113(predefined_type) @type.builtin
112114
runtime/queries/_typescript/locals.scm+17

This diff is too big to show here.

runtime/queries/agda/highlights.scmnew file+124

This diff is too big to show here.

runtime/queries/bash/indents.scm+1

This diff is too big to show here.

runtime/queries/bash/textobjects.scmnew file+9

This diff is too big to show here.

runtime/queries/blueprint/highlights.scm+5

This diff is too big to show here.

runtime/queries/c/indents.scm+18−1

This diff is too big to show here.

runtime/queries/comment/highlights.scm+8−8

This diff is too big to show here.

runtime/queries/common-lisp/indents.scmnew file+1

This diff is too big to show here.

runtime/queries/dart/highlights.scm+3−1

This diff is too big to show here.

runtime/queries/dbml/highlights.scmnew file+44

This diff is too big to show here.

runtime/queries/ecma/highlights.scm+1−1

This diff is too big to show here.

runtime/queries/fsharp/locals.scmnew file+25

This diff is too big to show here.

runtime/queries/gas/highlights.scmnew file+21

This diff is too big to show here.

runtime/queries/gas/injections.scmnew file+2

This diff is too big to show here.

runtime/queries/gas/textobjects.scmnew file+2

This diff is too big to show here.

runtime/queries/gemini/highlights.scmnew file+26

This diff is too big to show here.

runtime/queries/gleam/highlights.scm+16−5

This diff is too big to show here.

runtime/queries/gn/highlights.scmnew file+42

This diff is too big to show here.

runtime/queries/gn/injections.scmnew file+15

This diff is too big to show here.

runtime/queries/gn/locals.scmnew file+16

This diff is too big to show here.

runtime/queries/go/highlights.scm+13−5

This diff is too big to show here.

runtime/queries/go/locals.scm+10−2

This diff is too big to show here.

runtime/queries/hare/highlights.scm+4−1

This diff is too big to show here.

runtime/queries/haskell/highlights.scm+24−11

This diff is too big to show here.

runtime/queries/janet/highlights.scmnew file+1

This diff is too big to show here.

runtime/queries/java/indents.scmnew file+35

This diff is too big to show here.

runtime/queries/jinja/highlights.scmnew file+7

This diff is too big to show here.

runtime/queries/jinja/injections.scmnew file+4

This diff is too big to show here.

runtime/queries/json5/highlights.scmnew file+11

This diff is too big to show here.

runtime/queries/kdl/highlights.scm+4−4

This diff is too big to show here.

runtime/queries/kdl/indents.scmnew file+3

This diff is too big to show here.

runtime/queries/kdl/textobjects.scmnew file+27

This diff is too big to show here.

runtime/queries/kotlin/highlights.scm+4−1

This diff is too big to show here.

runtime/queries/kotlin/locals.scmnew file+15

This diff is too big to show here.

runtime/queries/log/highlights.scmnew file+10

This diff is too big to show here.

runtime/queries/lpf/highlights.scmnew file+19

This diff is too big to show here.

runtime/queries/markdown.inline/injections.scm+4−1

This diff is too big to show here.

runtime/queries/nix/injections.scm+14

This diff is too big to show here.

runtime/queries/nu/folds.scmdeleted−4

This diff is too big to show here.

runtime/queries/nu/highlights.scm+276−44

This diff is too big to show here.

runtime/queries/nu/injections.scm+1−1

This diff is too big to show here.

runtime/queries/nu/locals.scmdeleted−13

This diff is too big to show here.

runtime/queries/nunjucks/highlights.scmnew file+1

This diff is too big to show here.

runtime/queries/nunjucks/injections.scmnew file+1

This diff is too big to show here.

runtime/queries/ocaml/highlights.scm+2

This diff is too big to show here.

runtime/queries/perl/highlights.scm−2

This diff is too big to show here.

runtime/queries/perl/indents.scmnew file+29

This diff is too big to show here.

runtime/queries/perl/injections.scm+2

This diff is too big to show here.

runtime/queries/perl/textobjects.scmnew file+14

This diff is too big to show here.

runtime/queries/pod/highlights.scmnew file+61

This diff is too big to show here.

runtime/queries/prql/highlights.scm+19−12

This diff is too big to show here.

runtime/queries/purescript/highlights.scm+164−1

This diff is too big to show here.

runtime/queries/purescript/injections.scm+2−1

This diff is too big to show here.

runtime/queries/purescript/locals.scm+4−1

This diff is too big to show here.

runtime/queries/purescript/textobjects.scmnew file+13

This diff is too big to show here.

runtime/queries/python/highlights.scm+6

This diff is too big to show here.

runtime/queries/python/indents.scm+9

This diff is too big to show here.

runtime/queries/racket/indents.scmnew file+1

This diff is too big to show here.

runtime/queries/regex/highlights.scm+1−1

This diff is too big to show here.

runtime/queries/robot/highlights.scm+51−13

This diff is too big to show here.

runtime/queries/ron/highlights.scm+43−1

This diff is too big to show here.

runtime/queries/ron/indents.scm+12−1

This diff is too big to show here.

runtime/queries/ron/injections.scm+2−1

This diff is too big to show here.

runtime/queries/rust/highlights.scm+7

This diff is too big to show here.

runtime/queries/rust/indents.scm+69−2

This diff is too big to show here.

runtime/queries/rust/injections.scm+6

This diff is too big to show here.

runtime/queries/rust/locals.scm+14−1

This diff is too big to show here.

runtime/queries/rust/textobjects.scm+4−1

This diff is too big to show here.

runtime/queries/scheme/highlights.scm+5

This diff is too big to show here.

runtime/queries/scheme/indents.scmnew file+43

This diff is too big to show here.

runtime/queries/slint/highlights.scm+118−72

This diff is too big to show here.

runtime/queries/slint/indents.scm+6−5

This diff is too big to show here.

runtime/queries/slint/injections.scmnew file+2

This diff is too big to show here.

runtime/queries/slint/locals.scm+1−1

This diff is too big to show here.

runtime/queries/smali/folds.scmnew file+12

This diff is too big to show here.

runtime/queries/smali/highlights.scmnew file+216

This diff is too big to show here.

runtime/queries/smali/indents.scmnew file+31

This diff is too big to show here.

runtime/queries/smali/injections.scmnew file+2

This diff is too big to show here.

runtime/queries/smali/locals.scmnew file+42

This diff is too big to show here.

runtime/queries/sql/highlights.scm+317−231

This diff is too big to show here.

runtime/queries/strace/highlights.scmnew file+8

This diff is too big to show here.

runtime/queries/t32/highlights.scm+108−26

This diff is too big to show here.

runtime/queries/templ/highlights.scmnew file+99

This diff is too big to show here.

runtime/queries/templ/injections.scmnew file+4

This diff is too big to show here.

runtime/queries/todotxt/highlights.scmnew file+6

This diff is too big to show here.

runtime/queries/typst/highlights.scmnew file+77

This diff is too big to show here.

runtime/queries/typst/injections.scmnew file+6

This diff is too big to show here.

runtime/queries/unison/highlights.scmnew file+87

This diff is too big to show here.

runtime/queries/unison/injections.scmnew file+1

This diff is too big to show here.

runtime/queries/v/highlights.scm+85−51

This diff is too big to show here.

runtime/queries/v/indents.scm+2−3

This diff is too big to show here.

runtime/queries/v/textobjects.scm+3−3

This diff is too big to show here.

runtime/queries/wren/highlights.scmnew file+82

This diff is too big to show here.

runtime/queries/wren/indents.scmnew file+29

This diff is too big to show here.

runtime/queries/wren/injections.scmnew file+3

This diff is too big to show here.

runtime/queries/wren/locals.scmnew file+21

This diff is too big to show here.

runtime/queries/wren/textobjects.scmnew file+16

This diff is too big to show here.

runtime/queries/yaml/indents.scm+35−1

This diff is too big to show here.

runtime/queries/yaml/injections.scm+52

This diff is too big to show here.

runtime/queries/yuck/highlights.scm+1−1

This diff is too big to show here.

runtime/queries/zig/highlights.scm+5−2

This diff is too big to show here.

runtime/queries/zig/textobjects.scm+3−3

This diff is too big to show here.

runtime/themes/README.md+8

This diff is too big to show here.

runtime/themes/autumn.toml+62−65

This diff is too big to show here.

runtime/themes/autumn_night.toml+6−6

This diff is too big to show here.

runtime/themes/catppuccin_frappe.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_latte.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_macchiato.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_mocha.toml+11

This diff is too big to show here.

runtime/themes/cyan_light.tomlnew file+154

This diff is too big to show here.

runtime/themes/darcula-solid.toml+1−4

This diff is too big to show here.

runtime/themes/darcula.toml+10−4

This diff is too big to show here.

runtime/themes/dracula.toml+2

This diff is too big to show here.

runtime/themes/everblush.toml+1

This diff is too big to show here.

runtime/themes/everforest_dark.toml+4

This diff is too big to show here.

runtime/themes/gruber-darker.tomlnew file+114

This diff is too big to show here.

runtime/themes/gruvbox.toml+1

This diff is too big to show here.

runtime/themes/gruvbox_dark_hard.toml−2

This diff is too big to show here.

runtime/themes/gruvbox_light.toml−2

This diff is too big to show here.

runtime/themes/horizon-dark.tomlnew file+80

This diff is too big to show here.

runtime/themes/jellybeans.toml+1−1

This diff is too big to show here.

runtime/themes/kaolin-dark.tomlnew file+108

This diff is too big to show here.

runtime/themes/kaolin-light.tomlnew file+60

This diff is too big to show here.

runtime/themes/kaolin-valley-dark.tomlnew file+73

This diff is too big to show here.

runtime/themes/licenses/everforest.LICENSEnew file+21

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi.LICENSEnew file+5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_deuteranopia.LICENSEnew file+5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_tinted.LICENSEnew file+5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_tritanopia.LICENSEnew file+5

This diff is too big to show here.

runtime/themes/licenses/poimandres.LICENSEnew file+21

This diff is too big to show here.

runtime/themes/licenses/serika-syntax.LICENSEnew file+21

This diff is too big to show here.

runtime/themes/licenses/sonokai.LICENSEnew file+21

This diff is too big to show here.

runtime/themes/licenses/starlight.LICENSEnew file+24

This diff is too big to show here.

runtime/themes/material_darker.tomlnew file+20

This diff is too big to show here.

runtime/themes/material_deep_ocean.tomlnew file+123

This diff is too big to show here.

runtime/themes/material_oceanic.tomlnew file+21

This diff is too big to show here.

runtime/themes/material_palenight.tomlnew file+19

This diff is too big to show here.

runtime/themes/modus_operandi.tomlnew file+194

This diff is too big to show here.

runtime/themes/modus_operandi_deuteranopia.tomlnew file+46

This diff is too big to show here.

runtime/themes/modus_operandi_tinted.tomlnew file+45

This diff is too big to show here.

runtime/themes/modus_operandi_tritanopia.tomlnew file+73

This diff is too big to show here.

runtime/themes/modus_vivendi.tomlnew file+242

This diff is too big to show here.

runtime/themes/modus_vivendi_deuteranopia.tomlnew file+158

This diff is too big to show here.

runtime/themes/modus_vivendi_tinted.tomlnew file+144

This diff is too big to show here.

runtime/themes/modus_vivendi_tritanopia.tomlnew file+186

This diff is too big to show here.

runtime/themes/molokai.toml+1−1

This diff is too big to show here.

runtime/themes/monokai.toml+5

This diff is too big to show here.

runtime/themes/naysayer.toml+2−2

This diff is too big to show here.

runtime/themes/new_moon.tomlnew file+124

This diff is too big to show here.

runtime/themes/nightfox.toml+2−2

This diff is too big to show here.

runtime/themes/noctis.toml+26−26

This diff is too big to show here.

runtime/themes/nord-night.tomlnew file+23

This diff is too big to show here.

runtime/themes/nord.toml+23−13

This diff is too big to show here.

runtime/themes/onedark.toml+1

This diff is too big to show here.

runtime/themes/papercolor-dark.toml+67−116

This diff is too big to show here.

runtime/themes/papercolor-light.toml+194−103

This diff is too big to show here.

runtime/themes/poimandres.tomlnew file+113

This diff is too big to show here.

runtime/themes/poimandres_storm.tomlnew file+19

This diff is too big to show here.

runtime/themes/pop-dark.toml+4−4

This diff is too big to show here.

runtime/themes/rose_pine.toml+31−29

This diff is too big to show here.

runtime/themes/starlight.tomlnew file+113

This diff is too big to show here.

runtime/themes/vim_dark_high_contrast.tomlnew file+72

This diff is too big to show here.

runtime/themes/yellowed.tomlnew file+135

This diff is too big to show here.

runtime/themes/zenburn.toml+1−1

This diff is too big to show here.

runtime/tutor+157−3

This diff is too big to show here.

rust-toolchain.toml+2−2

This diff is too big to show here.

xtask/Cargo.toml+12−6

This diff is too big to show here.