on 62b2b63 help sign in
298 files changed+5481−15673
since splitsnapshots
unifiedsplit
.envrc−1 .github/ISSUE_TEMPLATE/bug_report.yaml−10 .github/workflows/build.yml+7−15 .github/workflows/cachix.yml+3−3 .github/workflows/gh-pages.yml+5−5 .github/workflows/release.yml+15−15 CHANGELOG.md−260 Cargo.lock+599−547 Cargo.toml−16 README.md+2−2 VERSION+1 book/book.toml−3 book/custom.css+1−1 book/src/configuration.md+11−38 book/src/generated/lang-support.md+15−35 book/src/generated/typable-cmd.md+1−4 book/src/guides/adding_languages.md−2 book/src/guides/indent.md+43−319 book/src/guides/injection.md+1−1 book/src/install.md+6−41 book/src/keymap.md+15−16 book/src/languages.md+1−3 book/src/themes.md+1−9 book/src/usage.md+5−21 book/theme/index.hbs+15−18 contrib/Helix.appdata.xml−15 contrib/Helix.desktop+1−3 docs/CONTRIBUTING.md+3−10 docs/releases.md+5−12 flake.lock+237−21 flake.nix+143−170 grammars.nix+21−24 helix-core/Cargo.toml+16−20 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+127−586 helix-core/src/lib.rs+1−3 helix-core/src/match_brackets.rs+2−6 helix-core/src/movement.rs+1−80 helix-core/src/path.rs+7−5 helix-core/src/register.rs+89 helix-core/src/rope_reader.rs−37 helix-core/src/selection.rs+2−10 helix-core/src/syntax.rs+80−88 helix-core/src/transaction.rs+21−84 helix-core/src/wrap.rs+1−3 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+25−153 helix-dap/Cargo.toml+9−11 helix-event/Cargo.toml−16 helix-event/src/lib.rs−8 helix-event/src/redraw.rs−49 helix-loader/Cargo.toml+12−14 helix-loader/build.rs+6−20 helix-loader/src/lib.rs+3−52 helix-lsp/Cargo.toml+13−15 helix-lsp/src/client.rs+11−123 helix-lsp/src/file_event.rs−193 helix-lsp/src/lib.rs+30−45 helix-parsec/Cargo.toml+7−8 helix-term/Cargo.toml+25−33 helix-term/src/application.rs+95−175 helix-term/src/args.rs−29 helix-term/src/commands.rs+356−617 helix-term/src/commands/dap.rs+5−14 helix-term/src/commands/lsp.rs+62−60 helix-term/src/commands/typed.rs+706−915 helix-term/src/compositor.rs−12 helix-term/src/config.rs−1 helix-term/src/health.rs+16−44 helix-term/src/keymap.rs+14−14 helix-term/src/keymap/default.rs+2−8 helix-term/src/main.rs+1−17 helix-term/src/ui/completion.rs+4−32 helix-term/src/ui/document.rs+24−60 helix-term/src/ui/editor.rs+54−102 helix-term/src/ui/fuzzy_match.rs+239 helix-term/src/ui/fuzzy_match/test.rs+47 helix-term/src/ui/lsp.rs+3−5 helix-term/src/ui/markdown.rs+12−19 helix-term/src/ui/menu.rs+40−73 helix-term/src/ui/mod.rs+150−91 helix-term/src/ui/picker.rs+325−362 helix-term/src/ui/popup.rs+7−27 helix-term/src/ui/prompt.rs+48−55 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+3−122 helix-term/tests/test/helpers.rs+1−9 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+33−51 helix-tui/src/buffer.rs+2−2 helix-tui/src/terminal.rs+14 helix-vcs/Cargo.toml+10−12 helix-vcs/src/diff.rs+19−6 helix-vcs/src/diff/worker.rs+9−3 helix-vcs/src/diff/worker/test.rs+5−1 helix-vcs/src/git.rs+5−5 helix-view/Cargo.toml+18−22 helix-view/src/clipboard.rs+2−7 helix-view/src/document.rs+46−122 helix-view/src/editor.rs+77−154 helix-view/src/gutter.rs+5−4 helix-view/src/handlers/dap.rs+3−1 helix-view/src/info.rs+11−4 helix-view/src/input.rs+3−67 helix-view/src/lib.rs−1 helix-view/src/register.rs−333 helix-view/src/theme.rs−6 helix-view/src/view.rs+2−8 languages.toml+207−569 logo.svg+1−1 runtime/queries/_jsx/highlights.scm+15−3 runtime/queries/_jsx/indents.scm+1 runtime/queries/_typescript/highlights.scm+3−6 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+1−18 runtime/queries/comment/highlights.scm+8−8 runtime/queries/common-lisp/indents.scm−1 runtime/queries/dart/highlights.scm+1−3 runtime/queries/dbml/highlights.scm−44 runtime/queries/ecma/highlights.scm+1−1 runtime/queries/fsharp/highlights.scm−196 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+5−16 runtime/queries/gn/highlights.scm−42 runtime/queries/gn/injections.scm−15 runtime/queries/gn/locals.scm−16 runtime/queries/go/highlights.scm+5−13 runtime/queries/go/locals.scm+2−10 runtime/queries/hare/highlights.scm+1−4 runtime/queries/haskell/highlights.scm+11−24 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+1−4 runtime/queries/kotlin/locals.scm−15 runtime/queries/log/highlights.scm−10 runtime/queries/lpf/highlights.scm−19 runtime/queries/markdown.inline/injections.scm+1−4 runtime/queries/nix/injections.scm−14 runtime/queries/nu/folds.scm+4 runtime/queries/nu/highlights.scm+44−276 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/fold.scm−19 runtime/queries/perl/highlights.scm+155−72 runtime/queries/perl/indents.scm+11−25 runtime/queries/perl/injections.scm+1−3 runtime/queries/perl/textobjects.scm+13−10 runtime/queries/pod/highlights.scm−61 runtime/queries/prql/highlights.scm+12−19 runtime/queries/purescript/highlights.scm+1−164 runtime/queries/purescript/injections.scm+1−2 runtime/queries/purescript/locals.scm+1−4 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+13−51 runtime/queries/ron/highlights.scm+1−43 runtime/queries/ron/indents.scm+1−12 runtime/queries/ron/injections.scm+1−2 runtime/queries/rust/highlights.scm−7 runtime/queries/rust/indents.scm+2−69 runtime/queries/rust/injections.scm−6 runtime/queries/rust/locals.scm+1−14 runtime/queries/rust/textobjects.scm+1−4 runtime/queries/scheme/highlights.scm−5 runtime/queries/scheme/indents.scm−43 runtime/queries/slint/highlights.scm+72−118 runtime/queries/slint/indents.scm+5−6 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+231−317 runtime/queries/strace/highlights.scm−8 runtime/queries/svelte/indents.scm−18 runtime/queries/svelte/injections.scm−10 runtime/queries/t32/highlights.scm+26−108 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+51−85 runtime/queries/v/indents.scm+3−2 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+1−35 runtime/queries/yaml/injections.scm−52 runtime/queries/yuck/highlights.scm+1−1 runtime/queries/zig/highlights.scm+2−5 runtime/queries/zig/textobjects.scm+3−3 runtime/themes/README.md−8 runtime/themes/autumn.toml+65−62 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+4−1 runtime/themes/darcula.toml+4−10 runtime/themes/dark_plus.toml+1−2 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−91 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+13−23 runtime/themes/onedark.toml−1 runtime/themes/papercolor-dark.toml+116−67 runtime/themes/papercolor-light.toml+103−194 runtime/themes/poimandres.toml−113 runtime/themes/poimandres_storm.toml−19 runtime/themes/pop-dark.toml+4−4 runtime/themes/rose_pine.toml+29−31 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+3−157 rust-toolchain.toml+2−2 xtask/Cargo.toml+6−12
.envrc−1
@@ -1,6 +1,5 @@
11watch_file shell.nix
22watch_file flake.lock
3−watch_file rust-toolchain.toml
43
54# try to use flakes, if it fails use normal nix (ie. shell.nix)
65use flake || use nix
.github/ISSUE_TEMPLATE/bug_report.yaml−10
@@ -55,16 +55,6 @@ 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
6858 - type: input
6959 id: helix-version
7060 attributes:
.github/workflows/build.yml+7−15
@@ -14,7 +14,7 @@ jobs:
1414 runs-on: ubuntu-latest
1515 steps:
1616 - name: Checkout sources
17− uses: actions/checkout@v4
17+ uses: actions/checkout@v3
1818 - name: Install stable toolchain
1919 uses: helix-editor/rust-toolchain@v1
2020 with:
@@ -22,8 +22,6 @@ jobs:
2222 override: true
2323
2424 - uses: Swatinem/rust-cache@v2
25− with:
26− shared-key: "build"
2725
2826 - name: Run cargo check
2927 run: cargo check
@@ -36,14 +34,12 @@ jobs:
3634 HELIX_LOG_LEVEL: info
3735 steps:
3836 - name: Checkout sources
39− uses: actions/checkout@v4
37+ uses: actions/checkout@v3
4038
4139 - name: Install stable toolchain
42− uses: dtolnay/rust-toolchain@1.70
40+ uses: dtolnay/rust-toolchain@1.65
4341
4442 - uses: Swatinem/rust-cache@v2
45− with:
46− shared-key: "build"
4743
4844 - name: Cache test tree-sitter grammar
4945 uses: actions/cache@v3
@@ -67,16 +63,14 @@ jobs:
6763 runs-on: ubuntu-latest
6864 steps:
6965 - name: Checkout sources
70− uses: actions/checkout@v4
66+ uses: actions/checkout@v3
7167
7268 - name: Install stable toolchain
73− uses: dtolnay/rust-toolchain@1.70
69+ uses: dtolnay/rust-toolchain@1.65
7470 with:
7571 components: rustfmt, clippy
7672
7773 - uses: Swatinem/rust-cache@v2
78− with:
79− shared-key: "build"
8074
8175 - name: Run cargo fmt
8276 run: cargo fmt --all --check
@@ -94,14 +88,12 @@ jobs:
9488 runs-on: ubuntu-latest
9589 steps:
9690 - name: Checkout sources
97− uses: actions/checkout@v4
91+ uses: actions/checkout@v3
9892
9993 - name: Install stable toolchain
100− uses: dtolnay/rust-toolchain@1.70
94+ uses: dtolnay/rust-toolchain@1.65
10195
10296 - uses: Swatinem/rust-cache@v2
103− with:
104− shared-key: "build"
10597
10698 - name: Validate queries
10799 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@v4
14+ uses: actions/checkout@v3
1515
1616 - name: Install nix
17− uses: cachix/install-nix-action@v24
17+ uses: cachix/install-nix-action@v22
1818
1919 - name: Authenticate with Cachix
20− uses: cachix/cachix-action@v13
20+ uses: cachix/cachix-action@v12
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@v4
14+ - uses: actions/checkout@v3
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 stable
29+ - name: Deploy
3030 uses: peaceiris/actions-gh-pages@v3
31− if: startswith(github.ref, 'refs/tags/')
3231 with:
3332 github_token: ${{ secrets.GITHUB_TOKEN }}
3433 publish_dir: ./book/book
34+ destination_dir: ./${{ env.OUTDIR }}
3535
36− - name: Deploy
36+ - name: Deploy stable
3737 uses: peaceiris/actions-gh-pages@v3
38+ if: startswith(github.ref, 'refs/tags/')
3839 with:
3940 github_token: ${{ secrets.GITHUB_TOKEN }}
4041 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@v4
26+ uses: actions/checkout@v3
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@v4
39+ - uses: actions/upload-artifact@v3
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@v4
106+ uses: actions/checkout@v3
107107
108108 - name: Download grammars
109− uses: actions/download-artifact@v4
109+ uses: actions/download-artifact@v3
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 == 'x86_64-linux'
163+ if: matrix.build == 'aarch64-linux' || 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@v4
223+ - uses: actions/upload-artifact@v3
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@v4
234+ uses: actions/checkout@v3
235235
236− - uses: actions/download-artifact@v4
236+ - uses: actions/download-artifact@v3
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" = "x86_64-linux" ]]; then
266+ if [[ "$platform" = "aarch64-linux" || "$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@v4
291+ uses: actions/upload-artifact@v3
292292 if: env.preview == 'true'
293293 with:
294294 name: release
CHANGELOG.md−260
@@ -1,263 +1,3 @@
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−
2611# 23.05 (2023-05-18)
2622
263323.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
Cargo.lock+599−547
@@ -19,22 +19,41 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
1919
2020[[package]]
2121name = "ahash"
22−version = "0.8.6"
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"
2334source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
35+checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
2536dependencies = [
2637 "cfg-if",
2738 "getrandom",
2839 "once_cell",
2940 "version_check",
30− "zerocopy",
3141]
3242
3343[[package]]
3444name = "aho-corasick"
35−version = "1.1.2"
45+version = "0.7.20"
3646source = "registry+https://github.com/rust-lang/crates.io-index"
37−checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
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"
3857dependencies = [
3958 "memchr",
4059]
@@ -62,9 +81,9 @@ dependencies = [
6281
6382[[package]]
6483name = "anyhow"
65−version = "1.0.76"
84+version = "1.0.71"
6685source = "registry+https://github.com/rust-lang/crates.io-index"
67−checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
86+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
6887
6988[[package]]
7089name = "arc-swap"
@@ -88,7 +107,7 @@ dependencies = [
88107 "cc",
89108 "cfg-if",
90109 "libc",
91− "miniz_oxide",
110+ "miniz_oxide 0.7.1",
92111 "object",
93112 "rustc-demangle",
94113]
@@ -101,17 +120,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
101120
102121[[package]]
103122name = "bitflags"
104−version = "2.4.1"
123+version = "2.3.3"
105124source = "registry+https://github.com/rust-lang/crates.io-index"
106−checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
125+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
107126
108127[[package]]
109128name = "bstr"
110−version = "1.8.0"
129+version = "1.4.0"
111130source = "registry+https://github.com/rust-lang/crates.io-index"
112−checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
131+checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
113132dependencies = [
114133 "memchr",
134+ "once_cell",
115135 "regex-automata",
116136 "serde",
117137]
@@ -131,6 +151,12 @@ version = "3.12.0"
131151source = "registry+https://github.com/rust-lang/crates.io-index"
132152checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
133153
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+
134160[[package]]
135161name = "bytes"
136162version = "1.4.0"
@@ -145,12 +171,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
145171
146172[[package]]
147173name = "cc"
148−version = "1.0.84"
174+version = "1.0.79"
149175source = "registry+https://github.com/rust-lang/crates.io-index"
150−checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
151−dependencies = [
152− "libc",
153−]
176+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
154177
155178[[package]]
156179name = "cfg-if"
@@ -171,23 +194,25 @@ dependencies = [
171194
172195[[package]]
173196name = "chrono"
174−version = "0.4.31"
197+version = "0.4.26"
175198source = "registry+https://github.com/rust-lang/crates.io-index"
176−checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
199+checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
177200dependencies = [
178201 "android-tzdata",
179202 "iana-time-zone",
180203 "num-traits",
181− "windows-targets 0.48.0",
204+ "winapi",
182205]
183206
184207[[package]]
185208name = "clipboard-win"
186−version = "5.0.0"
209+version = "4.5.0"
187210source = "registry+https://github.com/rust-lang/crates.io-index"
188−checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc"
211+checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
189212dependencies = [
190213 "error-code",
214+ "str-buf",
215+ "winapi",
191216]
192217
193218[[package]]
@@ -221,12 +246,6 @@ version = "0.8.4"
221246source = "registry+https://github.com/rust-lang/crates.io-index"
222247checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
223248
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−
230249[[package]]
231250name = "crc32fast"
232251version = "1.3.2"
@@ -236,58 +255,14 @@ dependencies = [
236255 "cfg-if",
237256]
238257
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−
282258[[package]]
283259name = "crossterm"
284−version = "0.27.0"
260+version = "0.26.1"
285261source = "registry+https://github.com/rust-lang/crates.io-index"
286−checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
262+checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
287263dependencies = [
288− "bitflags 2.4.1",
264+ "bitflags 1.3.2",
289265 "crossterm_winapi",
290− "filedescriptor",
291266 "futures-core",
292267 "libc",
293268 "mio",
@@ -299,9 +274,9 @@ dependencies = [
299274
300275[[package]]
301276name = "crossterm_winapi"
302−version = "0.9.1"
277+version = "0.9.0"
303278source = "registry+https://github.com/rust-lang/crates.io-index"
304−checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
279+checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
305280dependencies = [
306281 "winapi",
307282]
@@ -330,7 +305,7 @@ dependencies = [
330305 "proc-macro2",
331306 "quote",
332307 "scratch",
333− "syn 2.0.38",
308+ "syn 2.0.23",
334309]
335310
336311[[package]]
@@ -347,7 +322,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
347322dependencies = [
348323 "proc-macro2",
349324 "quote",
350− "syn 2.0.38",
325+ "syn 2.0.23",
351326]
352327
353328[[package]]
@@ -364,9 +339,9 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
364339
365340[[package]]
366341name = "encoding_rs"
367−version = "0.8.33"
342+version = "0.8.32"
368343source = "registry+https://github.com/rust-lang/crates.io-index"
369−checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
344+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
370345dependencies = [
371346 "cfg-if",
372347]
@@ -388,19 +363,34 @@ checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
388363
389364[[package]]
390365name = "errno"
391−version = "0.3.8"
366+version = "0.3.1"
367+source = "registry+https://github.com/rust-lang/crates.io-index"
368+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
369+dependencies = [
370+ "errno-dragonfly",
371+ "libc",
372+ "windows-sys 0.48.0",
373+]
374+
375+[[package]]
376+name = "errno-dragonfly"
377+version = "0.1.2"
392378source = "registry+https://github.com/rust-lang/crates.io-index"
393−checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
379+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
394380dependencies = [
381+ "cc",
395382 "libc",
396− "windows-sys 0.52.0",
397383]
398384
399385[[package]]
400386name = "error-code"
401−version = "3.0.0"
387+version = "2.3.1"
402388source = "registry+https://github.com/rust-lang/crates.io-index"
403−checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc"
389+checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
390+dependencies = [
391+ "libc",
392+ "str-buf",
393+]
404394
405395[[package]]
406396name = "etcetera"
@@ -414,12 +404,12 @@ dependencies = [
414404]
415405
416406[[package]]
417−name = "faster-hex"
418−version = "0.9.0"
407+name = "fastrand"
408+version = "1.9.0"
419409source = "registry+https://github.com/rust-lang/crates.io-index"
420−checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
410+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
421411dependencies = [
422− "serde",
412+ "instant",
423413]
424414
425415[[package]]
@@ -438,46 +428,53 @@ dependencies = [
438428]
439429
440430[[package]]
441−name = "filedescriptor"
442−version = "0.8.2"
431+name = "filetime"
432+version = "0.2.21"
443433source = "registry+https://github.com/rust-lang/crates.io-index"
444−checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
434+checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
445435dependencies = [
436+ "cfg-if",
446437 "libc",
447− "thiserror",
448− "winapi",
438+ "redox_syscall 0.2.16",
439+ "windows-sys 0.48.0",
449440]
450441
451442[[package]]
452443name = "flate2"
453−version = "1.0.27"
444+version = "1.0.25"
454445source = "registry+https://github.com/rust-lang/crates.io-index"
455−checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
446+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
456447dependencies = [
457448 "crc32fast",
458− "miniz_oxide",
449+ "miniz_oxide 0.6.2",
459450]
460451
452+[[package]]
453+name = "fnv"
454+version = "1.0.7"
455+source = "registry+https://github.com/rust-lang/crates.io-index"
456+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
457+
461458[[package]]
462459name = "form_urlencoded"
463−version = "1.2.1"
460+version = "1.2.0"
464461source = "registry+https://github.com/rust-lang/crates.io-index"
465−checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
462+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
466463dependencies = [
467464 "percent-encoding",
468465]
469466
470467[[package]]
471468name = "futures-core"
472−version = "0.3.30"
469+version = "0.3.28"
473470source = "registry+https://github.com/rust-lang/crates.io-index"
474−checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
471+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
475472
476473[[package]]
477474name = "futures-executor"
478−version = "0.3.29"
475+version = "0.3.28"
479476source = "registry+https://github.com/rust-lang/crates.io-index"
480−checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
477+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
481478dependencies = [
482479 "futures-core",
483480 "futures-task",
@@ -486,15 +483,15 @@ dependencies = [
486483
487484[[package]]
488485name = "futures-task"
489−version = "0.3.30"
486+version = "0.3.28"
490487source = "registry+https://github.com/rust-lang/crates.io-index"
491−checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
488+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
492489
493490[[package]]
494491name = "futures-util"
495−version = "0.3.30"
492+version = "0.3.28"
496493source = "registry+https://github.com/rust-lang/crates.io-index"
497−checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
494+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
498495dependencies = [
499496 "futures-core",
500497 "futures-task",
@@ -503,6 +500,15 @@ dependencies = [
503500 "slab",
504501]
505502
503+[[package]]
504+name = "fuzzy-matcher"
505+version = "0.3.7"
506+source = "registry+https://github.com/rust-lang/crates.io-index"
507+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
508+dependencies = [
509+ "thread_local",
510+]
511+
506512[[package]]
507513name = "getrandom"
508514version = "0.2.9"
@@ -522,13 +528,15 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
522528
523529[[package]]
524530name = "gix"
525−version = "0.57.0"
531+version = "0.48.0"
526532source = "registry+https://github.com/rust-lang/crates.io-index"
527−checksum = "721c7497ab24b665ed5a1eb2b3526936aa60068e61ba260651f7e77c52feec69"
533+checksum = "c1e74cea676de7f53a79f3c0365812b11f6814b81e671b8ee4abae6ca09c7881"
528534dependencies = [
529535 "gix-actor",
536+ "gix-attributes",
530537 "gix-commitgraph",
531538 "gix-config",
539+ "gix-credentials",
532540 "gix-date",
533541 "gix-diff",
534542 "gix-discover",
@@ -537,16 +545,19 @@ dependencies = [
537545 "gix-glob",
538546 "gix-hash",
539547 "gix-hashtable",
548+ "gix-ignore",
549+ "gix-index",
540550 "gix-lock",
541− "gix-macros",
551+ "gix-mailmap",
552+ "gix-negotiate",
542553 "gix-object",
543554 "gix-odb",
544555 "gix-pack",
545556 "gix-path",
557+ "gix-prompt",
546558 "gix-ref",
547559 "gix-refspec",
548560 "gix-revision",
549− "gix-revwalk",
550561 "gix-sec",
551562 "gix-tempfile",
552563 "gix-trace",
@@ -554,8 +565,10 @@ dependencies = [
554565 "gix-url",
555566 "gix-utils",
556567 "gix-validate",
568+ "gix-worktree",
569+ "log",
557570 "once_cell",
558− "parking_lot",
571+ "signal-hook",
559572 "smallvec",
560573 "thiserror",
561574 "unicode-normalization",
@@ -563,46 +576,81 @@ dependencies = [
563576
564577[[package]]
565578name = "gix-actor"
566−version = "0.29.0"
579+version = "0.23.0"
567580source = "registry+https://github.com/rust-lang/crates.io-index"
568−checksum = "886014c4865b93ce268f1d3eddd4fd3261242c3f3ee61eb36009f913016a9059"
581+checksum = "1969b77b9ee4cc1755c841987ec6f7622aaca95e952bcafb76973ae59d1b8716"
569582dependencies = [
570583 "bstr",
571584 "btoi",
572585 "gix-date",
573586 "itoa",
587+ "nom",
588+ "thiserror",
589+]
590+
591+[[package]]
592+name = "gix-attributes"
593+version = "0.14.1"
594+source = "registry+https://github.com/rust-lang/crates.io-index"
595+checksum = "e3772b0129dcd1fc73e985bbd08a1482d082097d2915cb1ee31ce8092b8e4434"
596+dependencies = [
597+ "bstr",
598+ "gix-glob",
599+ "gix-path",
600+ "gix-quote",
601+ "kstring",
602+ "log",
603+ "smallvec",
604+ "thiserror",
605+ "unicode-bom",
606+]
607+
608+[[package]]
609+name = "gix-bitmap"
610+version = "0.2.5"
611+source = "registry+https://github.com/rust-lang/crates.io-index"
612+checksum = "311e2fa997be6560c564b070c5da2d56d038b645a94e1e5796d5d85a350da33c"
613+dependencies = [
574614 "thiserror",
575− "winnow 0.5.28",
576615]
577616
578617[[package]]
579618name = "gix-chunk"
580−version = "0.4.6"
619+version = "0.4.3"
581620source = "registry+https://github.com/rust-lang/crates.io-index"
582−checksum = "2ade37ef69870de0ed966b97c57a3a947a22ff3482a52c3b99b205f77bcb08fb"
621+checksum = "39db5ed0fc0a2e9b1b8265993f7efdbc30379dec268f3b91b7af0c2de4672fdd"
583622dependencies = [
584623 "thiserror",
585624]
586625
626+[[package]]
627+name = "gix-command"
628+version = "0.2.6"
629+source = "registry+https://github.com/rust-lang/crates.io-index"
630+checksum = "bb49ab557a37b0abb2415bca2b10e541277dff0565deb5bd5e99fd95f93f51eb"
631+dependencies = [
632+ "bstr",
633+]
634+
587635[[package]]
588636name = "gix-commitgraph"
589−version = "0.23.0"
637+version = "0.17.1"
590638source = "registry+https://github.com/rust-lang/crates.io-index"
591−checksum = "e7559ea9cefee188cd88b05afcc8e3ef7a3cb4a5c647bccf06b981e591b02b77"
639+checksum = "ed42baa50075d41c1a0931074ce1a97c5797c7c6fe7591d9f1f2dcd448532c26"
592640dependencies = [
593641 "bstr",
594642 "gix-chunk",
595643 "gix-features",
596644 "gix-hash",
597− "memmap2",
645+ "memmap2 0.7.1",
598646 "thiserror",
599647]
600648
601649[[package]]
602650name = "gix-config"
603−version = "0.33.0"
651+version = "0.25.1"
604652source = "registry+https://github.com/rust-lang/crates.io-index"
605−checksum = "9c35dc7f9c00a42bbc9cfa1ca2ec0a78ad1b76ff0736d3d35dfd612962244467"
653+checksum = "817688c7005a716d9363e267913526adea402dabd947f4ba63842d10cc5132af"
606654dependencies = [
607655 "bstr",
608656 "gix-config-value",
@@ -611,32 +659,49 @@ dependencies = [
611659 "gix-path",
612660 "gix-ref",
613661 "gix-sec",
662+ "log",
614663 "memchr",
664+ "nom",
615665 "once_cell",
616666 "smallvec",
617667 "thiserror",
618668 "unicode-bom",
619− "winnow 0.5.28",
620669]
621670
622671[[package]]
623672name = "gix-config-value"
624−version = "0.14.2"
673+version = "0.12.3"
625674source = "registry+https://github.com/rust-lang/crates.io-index"
626−checksum = "39f388cb2396aee82d6f460a2e7770659bdf8854e9e5478f7d2b1324a9698284"
675+checksum = "83960be5e99266bcf55dae5a24731bbd39f643bfb68f27e939d6b06836b5b87d"
627676dependencies = [
628− "bitflags 2.4.1",
677+ "bitflags 2.3.3",
629678 "bstr",
630679 "gix-path",
631680 "libc",
632681 "thiserror",
633682]
634683
684+[[package]]
685+name = "gix-credentials"
686+version = "0.16.1"
687+source = "registry+https://github.com/rust-lang/crates.io-index"
688+checksum = "75a75565e0e6e7f80cfa4eb1b05cc448c6846ddd48dcf413a28875fbc11ee9af"
689+dependencies = [
690+ "bstr",
691+ "gix-command",
692+ "gix-config-value",
693+ "gix-path",
694+ "gix-prompt",
695+ "gix-sec",
696+ "gix-url",
697+ "thiserror",
698+]
699+
635700[[package]]
636701name = "gix-date"
637−version = "0.8.2"
702+version = "0.7.0"
638703source = "registry+https://github.com/rust-lang/crates.io-index"
639−checksum = "9d85f4a01e0d05c3de585e28bae514d4baf01655e3fc3f14ce6f30bf62405345"
704+checksum = "8e9a04a1d2387c955ec91059d56b673000dd24f3c07cad08ed253e36381782bf"
640705dependencies = [
641706 "bstr",
642707 "itoa",
@@ -646,21 +711,21 @@ dependencies = [
646711
647712[[package]]
648713name = "gix-diff"
649−version = "0.39.0"
714+version = "0.32.0"
650715source = "registry+https://github.com/rust-lang/crates.io-index"
651−checksum = "ac0e75f5afd2f6c47c800b6b0a000a08045739d0450d20482e8faa42543f62d1"
716+checksum = "aaf5d9b9b521b284ebe53ee69eee33341835ec70edc314f36b2100ea81396121"
652717dependencies = [
653− "bstr",
654718 "gix-hash",
655719 "gix-object",
720+ "imara-diff",
656721 "thiserror",
657722]
658723
659724[[package]]
660725name = "gix-discover"
661−version = "0.28.0"
726+version = "0.21.1"
662727source = "registry+https://github.com/rust-lang/crates.io-index"
663−checksum = "f0b63ce2ad81632ac1a84ac370f85a5e580c3261580bd85ea17ff35d3a0bba18"
728+checksum = "272aad20dc63dedba76615373dd8885fb5aebe4795e5b5b0aa2a24e63c82085c"
664729dependencies = [
665730 "bstr",
666731 "dunce",
@@ -673,9 +738,9 @@ dependencies = [
673738
674739[[package]]
675740name = "gix-features"
676−version = "0.37.0"
741+version = "0.31.1"
677742source = "registry+https://github.com/rust-lang/crates.io-index"
678−checksum = "befe7edea299a824504b5acc96d7a3a538125b38c42f3a8379f6912a29c90c81"
743+checksum = "06142d8cff5d17509399b04052b64d2f9b3a311d5cff0b1a32b220f62cd0d595"
679744dependencies = [
680745 "crc32fast",
681746 "flate2",
@@ -691,20 +756,20 @@ dependencies = [
691756
692757[[package]]
693758name = "gix-fs"
694−version = "0.9.0"
759+version = "0.3.0"
695760source = "registry+https://github.com/rust-lang/crates.io-index"
696−checksum = "007017ce93b819ea52c0ec68306f7d72212a2d307c3d70f1548c7141c015d0a1"
761+checksum = "bb15956bc0256594c62a2399fcf6958a02a11724217eddfdc2b49b21b6292496"
697762dependencies = [
698763 "gix-features",
699764]
700765
701766[[package]]
702767name = "gix-glob"
703−version = "0.15.0"
768+version = "0.9.1"
704769source = "registry+https://github.com/rust-lang/crates.io-index"
705−checksum = "61fb116c2516d3a1170e010118f639944a6389872c875a008960cdab2a44ac72"
770+checksum = "c18bdff83143d61e7d60da6183b87542a870d026b2a2d0b30170b8e9c0cd321a"
706771dependencies = [
707− "bitflags 2.4.1",
772+ "bitflags 2.3.3",
708773 "bstr",
709774 "gix-features",
710775 "gix-path",
@@ -712,30 +777,64 @@ dependencies = [
712777
713778[[package]]
714779name = "gix-hash"
715−version = "0.14.0"
780+version = "0.11.3"
716781source = "registry+https://github.com/rust-lang/crates.io-index"
717−checksum = "c52c3170a17b6031833cd2eb4ad7bc23f2755d06e6e70f78dec21d42e8fe1b30"
782+checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272"
718783dependencies = [
719− "faster-hex",
784+ "hex",
720785 "thiserror",
721786]
722787
723788[[package]]
724789name = "gix-hashtable"
725−version = "0.5.0"
790+version = "0.2.3"
726791source = "registry+https://github.com/rust-lang/crates.io-index"
727−checksum = "40a838e6366a5e5b84668b6997ce0981b833136468e8ba949f424c0ef2927eba"
792+checksum = "9e133bc56d938eaec1c675af7c681a51de9662b0ada779f45607b967a10da77a"
728793dependencies = [
729794 "gix-hash",
730− "hashbrown 0.14.3",
795+ "hashbrown 0.14.0",
731796 "parking_lot",
732797]
733798
799+[[package]]
800+name = "gix-ignore"
801+version = "0.4.1"
802+source = "registry+https://github.com/rust-lang/crates.io-index"
803+checksum = "ca801f2d0535210f77b33e2c067d565aedecacc82f1b3dbce26da1388ebc4634"
804+dependencies = [
805+ "bstr",
806+ "gix-glob",
807+ "gix-path",
808+ "unicode-bom",
809+]
810+
811+[[package]]
812+name = "gix-index"
813+version = "0.20.0"
814+source = "registry+https://github.com/rust-lang/crates.io-index"
815+checksum = "68099abdf6ee50ae3c897e8b05de96871cbe54d52a37cdf559101f911b883562"
816+dependencies = [
817+ "bitflags 2.3.3",
818+ "bstr",
819+ "btoi",
820+ "filetime",
821+ "gix-bitmap",
822+ "gix-features",
823+ "gix-hash",
824+ "gix-lock",
825+ "gix-object",
826+ "gix-traverse",
827+ "itoa",
828+ "memmap2 0.7.1",
829+ "smallvec",
830+ "thiserror",
831+]
832+
734833[[package]]
735834name = "gix-lock"
736−version = "12.0.0"
835+version = "7.0.0"
737836source = "registry+https://github.com/rust-lang/crates.io-index"
738−checksum = "6cf112ddee94223c119a8534dad027740dc3aba3365ac5edeef8a7f6660c74db"
837+checksum = "328f50aad713ab606caeaf834459ef915ccdfbb9133ac6cd54616d601aa9249f"
739838dependencies = [
740839 "gix-tempfile",
741840 "gix-utils",
@@ -743,21 +842,38 @@ dependencies = [
743842]
744843
745844[[package]]
746−name = "gix-macros"
747−version = "0.1.2"
845+name = "gix-mailmap"
846+version = "0.15.0"
748847source = "registry+https://github.com/rust-lang/crates.io-index"
749−checksum = "fc207b64189cf71bcb17fc841ab99a5d63d0845546b611e2703ce467f659323a"
848+checksum = "1787e3c37fc43b1f7c0e3be6196c6837b3ba5f869190dfeaa444b816f0a7f34b"
750849dependencies = [
751− "proc-macro2",
752− "quote",
753− "syn 2.0.38",
850+ "bstr",
851+ "gix-actor",
852+ "gix-date",
853+ "thiserror",
854+]
855+
856+[[package]]
857+name = "gix-negotiate"
858+version = "0.4.0"
859+source = "registry+https://github.com/rust-lang/crates.io-index"
860+checksum = "4e7bce64d4452dd609f44d04b14b29da2e0ad2c45fcdf4ce1472a5f5f8ec21c2"
861+dependencies = [
862+ "bitflags 2.3.3",
863+ "gix-commitgraph",
864+ "gix-date",
865+ "gix-hash",
866+ "gix-object",
867+ "gix-revwalk",
868+ "smallvec",
869+ "thiserror",
754870]
755871
756872[[package]]
757873name = "gix-object"
758−version = "0.40.0"
874+version = "0.32.0"
759875source = "registry+https://github.com/rust-lang/crates.io-index"
760−checksum = "e4c77e47ffba92127faf632a841fce23d19547e269bd8b88e68961a70eab4e93"
876+checksum = "a953f3d7ffad16734aa3ab1d05807972c80e339d1bd9dde03e0198716b99e2a6"
761877dependencies = [
762878 "bstr",
763879 "btoi",
@@ -766,17 +882,18 @@ dependencies = [
766882 "gix-features",
767883 "gix-hash",
768884 "gix-validate",
885+ "hex",
769886 "itoa",
887+ "nom",
770888 "smallvec",
771889 "thiserror",
772− "winnow 0.5.28",
773890]
774891
775892[[package]]
776893name = "gix-odb"
777−version = "0.56.0"
894+version = "0.49.1"
778895source = "registry+https://github.com/rust-lang/crates.io-index"
779−checksum = "a3254f2005cc7553ea78e85e816a09150c6f7a64e6b7627b8d1fdc56721bea73"
896+checksum = "f6418cff00ecc2713b58c8e04bff30dda808fbba1a080e7248b299d069894a01"
780897dependencies = [
781898 "arc-swap",
782899 "gix-date",
@@ -793,19 +910,21 @@ dependencies = [
793910
794911[[package]]
795912name = "gix-pack"
796−version = "0.46.0"
913+version = "0.39.1"
797914source = "registry+https://github.com/rust-lang/crates.io-index"
798−checksum = "02ebc8cd657eec207d82d8f876ca402361308ecd3c87a47935b0299506257b4f"
915+checksum = "414935138d90043ea5898de7a93f02c2558e52652492719470e203ef26a8fd0a"
799916dependencies = [
800917 "clru",
801918 "gix-chunk",
919+ "gix-diff",
802920 "gix-features",
803921 "gix-hash",
804922 "gix-hashtable",
805923 "gix-object",
806924 "gix-path",
807925 "gix-tempfile",
808− "memmap2",
926+ "gix-traverse",
927+ "memmap2 0.7.1",
809928 "parking_lot",
810929 "smallvec",
811930 "thiserror",
@@ -813,9 +932,9 @@ dependencies = [
813932
814933[[package]]
815934name = "gix-path"
816−version = "0.10.2"
935+version = "0.8.3"
817936source = "registry+https://github.com/rust-lang/crates.io-index"
818−checksum = "10931652a3da126990ac93bce1b1c600cc99d7c268d712b6360ed52174ce1b68"
937+checksum = "dfca182d2575ded2ed38280f1ebf75cd5d3790b77e0872de07854cf085821fbe"
819938dependencies = [
820939 "bstr",
821940 "gix-trace",
@@ -824,11 +943,24 @@ dependencies = [
824943 "thiserror",
825944]
826945
946+[[package]]
947+name = "gix-prompt"
948+version = "0.5.2"
949+source = "registry+https://github.com/rust-lang/crates.io-index"
950+checksum = "8dfd363fd89a40c1e7bff9c9c1b136cd2002480f724b0c627c1bc771cd5480ec"
951+dependencies = [
952+ "gix-command",
953+ "gix-config-value",
954+ "parking_lot",
955+ "rustix",
956+ "thiserror",
957+]
958+
827959[[package]]
828960name = "gix-quote"
829−version = "0.4.9"
961+version = "0.4.5"
830962source = "registry+https://github.com/rust-lang/crates.io-index"
831−checksum = "6c43530cb94a7759807e6f8d180e17ac9c65673b891645c6c433831dc0cf4342"
963+checksum = "3874de636c2526de26a3405b8024b23ef1a327bebf4845d770d00d48700b6a40"
832964dependencies = [
833965 "bstr",
834966 "btoi",
@@ -837,9 +969,9 @@ dependencies = [
837969
838970[[package]]
839971name = "gix-ref"
840−version = "0.40.0"
972+version = "0.32.1"
841973source = "registry+https://github.com/rust-lang/crates.io-index"
842−checksum = "baa951b3b850d6d1be4f900768e49af20b76bf9505beac22af723d57249a2f1d"
974+checksum = "39453f4e5f23cddc2e6e4cca2ba20adfdbec29379e3ca829714dfe98ae068ccd"
843975dependencies = [
844976 "gix-actor",
845977 "gix-date",
@@ -851,16 +983,16 @@ dependencies = [
851983 "gix-path",
852984 "gix-tempfile",
853985 "gix-validate",
854− "memmap2",
986+ "memmap2 0.7.1",
987+ "nom",
855988 "thiserror",
856− "winnow 0.5.28",
857989]
858990
859991[[package]]
860992name = "gix-refspec"
861−version = "0.21.0"
993+version = "0.13.0"
862994source = "registry+https://github.com/rust-lang/crates.io-index"
863−checksum = "88c61f849d58c06e3068a0b601cf10127d2a07cdad00a725ed66cf303f76f6b3"
995+checksum = "b8e76ff1f82fba295a121e31ab02f69642994e532c45c0c899aa393f4b740302"
864996dependencies = [
865997 "bstr",
866998 "gix-hash",
@@ -872,9 +1004,9 @@ dependencies = [
8721004
8731005[[package]]
8741006name = "gix-revision"
875−version = "0.25.0"
1007+version = "0.17.0"
8761008source = "registry+https://github.com/rust-lang/crates.io-index"
877−checksum = "bcbbf91f4200c5c76802ef5f057b96f5a336827508881fe55a780be71a794d22"
1009+checksum = "237428a7d3978e8572964e1e45d984027c2acc94df47e594baa6c4b0da7c9922"
8781010dependencies = [
8791011 "bstr",
8801012 "gix-date",
@@ -882,15 +1014,14 @@ dependencies = [
8821014 "gix-hashtable",
8831015 "gix-object",
8841016 "gix-revwalk",
885− "gix-trace",
8861017 "thiserror",
8871018]
8881019
8891020[[package]]
8901021name = "gix-revwalk"
891−version = "0.11.0"
1022+version = "0.3.0"
8921023source = "registry+https://github.com/rust-lang/crates.io-index"
893−checksum = "33ab66354d83f70f2730391747d0c75f94ef3c3dd40ebde2e206db9faaf7a0a7"
1024+checksum = "028d50fcaf8326a8f79a359490d9ca9fb4e2b51ac9ac86503560d0bcc888d2eb"
8941025dependencies = [
8951026 "gix-commitgraph",
8961027 "gix-date",
@@ -903,40 +1034,42 @@ dependencies = [
9031034
9041035[[package]]
9051036name = "gix-sec"
906−version = "0.10.2"
1037+version = "0.8.3"
9071038source = "registry+https://github.com/rust-lang/crates.io-index"
908−checksum = "a9963f38a42144253ed4d571882d7db5ef644c12e6726e4b75135597cc9d0e1a"
1039+checksum = "ede298863db2a0574a14070991710551e76d1f47c9783b62d4fcbca17f56371c"
9091040dependencies = [
910− "bitflags 2.4.1",
1041+ "bitflags 2.3.3",
9111042 "gix-path",
9121043 "libc",
913− "windows 0.52.0",
1044+ "windows",
9141045]
9151046
9161047[[package]]
9171048name = "gix-tempfile"
918−version = "12.0.0"
1049+version = "7.0.0"
9191050source = "registry+https://github.com/rust-lang/crates.io-index"
920−checksum = "e76a494bd530e1a1309188ff971825a24f159c76c2db0bf71fa5dfb469a2c915"
1051+checksum = "4fac8310c17406ea619af72f42ee46dac795110f68f41b4f4fa231b69889c6a2"
9211052dependencies = [
9221053 "gix-fs",
9231054 "libc",
9241055 "once_cell",
9251056 "parking_lot",
1057+ "signal-hook",
1058+ "signal-hook-registry",
9261059 "tempfile",
9271060]
9281061
9291062[[package]]
9301063name = "gix-trace"
931−version = "0.1.5"
1064+version = "0.1.2"
9321065source = "registry+https://github.com/rust-lang/crates.io-index"
933−checksum = "bda62acb44dd86a40c7c3762a5403cfc1ac789ea559df54085cedf79864f809e"
1066+checksum = "103eac621617be3ebe0605c9065ca51a223279a23218aaf67d10daa6e452f663"
9341067
9351068[[package]]
9361069name = "gix-traverse"
937−version = "0.36.0"
1070+version = "0.29.0"
9381071source = "registry+https://github.com/rust-lang/crates.io-index"
939−checksum = "8661fab39985c9214e56d81a63ceb5886ac948cec2fba76c39494d1e0e307ea8"
1072+checksum = "e3cdfd54598db4fae57d5ae6f52958422b2d13382d2745796bfe5c8015ffa86e"
9401073dependencies = [
9411074 "gix-commitgraph",
9421075 "gix-date",
@@ -950,9 +1083,9 @@ dependencies = [
9501083
9511084[[package]]
9521085name = "gix-url"
953−version = "0.26.0"
1086+version = "0.20.1"
9541087source = "registry+https://github.com/rust-lang/crates.io-index"
955−checksum = "10a0129c1e8b52736d7c5128300a4485dbc85863001371e2771ac1754bd89fd7"
1088+checksum = "beaede6dbc83f408b19adfd95bb52f1dbf01fb8862c3faf6c6243e2e67fcdfa1"
9561089dependencies = [
9571090 "bstr",
9581091 "gix-features",
@@ -964,71 +1097,94 @@ dependencies = [
9641097
9651098[[package]]
9661099name = "gix-utils"
967−version = "0.1.7"
1100+version = "0.1.4"
9681101source = "registry+https://github.com/rust-lang/crates.io-index"
969−checksum = "ab9277a5e32e85d53f738096d872a4a9b76067ad471894ad31bd99c8fa2da1dc"
1102+checksum = "7058c94f4164fcf5b8457d35f6d8f6e1007f9f7f938c9c7684a7e01d23c6ddde"
9701103dependencies = [
971− "fastrand",
1104+ "fastrand 2.0.0",
9721105]
9731106
9741107[[package]]
9751108name = "gix-validate"
976−version = "0.8.2"
1109+version = "0.7.6"
1110+source = "registry+https://github.com/rust-lang/crates.io-index"
1111+checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13"
1112+dependencies = [
1113+ "bstr",
1114+ "thiserror",
1115+]
1116+
1117+[[package]]
1118+name = "gix-worktree"
1119+version = "0.21.1"
9771120source = "registry+https://github.com/rust-lang/crates.io-index"
978−checksum = "f805ebbdbaa4bfd98e2ee43e6d14099b8a4d9141f5d7b8202fea4d48e44263e7"
1121+checksum = "c1363b9aa66b9e14412ac04e1f759827203f491729d92172535a8ce6cde02efa"
9791122dependencies = [
9801123 "bstr",
1124+ "filetime",
1125+ "gix-attributes",
1126+ "gix-features",
1127+ "gix-fs",
1128+ "gix-glob",
1129+ "gix-hash",
1130+ "gix-ignore",
1131+ "gix-index",
1132+ "gix-object",
1133+ "gix-path",
1134+ "io-close",
9811135 "thiserror",
9821136]
9831137
9841138[[package]]
9851139name = "globset"
986−version = "0.4.14"
1140+version = "0.4.10"
9871141source = "registry+https://github.com/rust-lang/crates.io-index"
988−checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
1142+checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
9891143dependencies = [
990− "aho-corasick",
1144+ "aho-corasick 0.7.20",
9911145 "bstr",
1146+ "fnv",
9921147 "log",
993− "regex-automata",
994− "regex-syntax",
1148+ "regex",
9951149]
9961150
9971151[[package]]
9981152name = "grep-matcher"
999−version = "0.1.7"
1153+version = "0.1.6"
10001154source = "registry+https://github.com/rust-lang/crates.io-index"
1001−checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02"
1155+checksum = "3902ca28f26945fe35cad349d776f163981d777fee382ccd6ef451126f51b319"
10021156dependencies = [
10031157 "memchr",
10041158]
10051159
10061160[[package]]
10071161name = "grep-regex"
1008−version = "0.1.12"
1162+version = "0.1.11"
10091163source = "registry+https://github.com/rust-lang/crates.io-index"
1010−checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d"
1164+checksum = "997598b41d53a37a2e3fc5300d5c11d825368c054420a9c65125b8fe1078463f"
10111165dependencies = [
1166+ "aho-corasick 0.7.20",
10121167 "bstr",
10131168 "grep-matcher",
10141169 "log",
1015− "regex-automata",
1016− "regex-syntax",
1170+ "regex",
1171+ "regex-syntax 0.6.29",
1172+ "thread_local",
10171173]
10181174
10191175[[package]]
10201176name = "grep-searcher"
1021−version = "0.1.13"
1177+version = "0.1.11"
10221178source = "registry+https://github.com/rust-lang/crates.io-index"
1023−checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54"
1179+checksum = "5601c4b9f480f0c9ebb40b1f6cbf447b8a50c5369223937a6c5214368c58779f"
10241180dependencies = [
10251181 "bstr",
1182+ "bytecount",
10261183 "encoding_rs",
10271184 "encoding_rs_io",
10281185 "grep-matcher",
10291186 "log",
1030− "memchr",
1031− "memmap2",
1187+ "memmap2 0.5.10",
10321188]
10331189
10341190[[package]]
@@ -1036,36 +1192,37 @@ name = "hashbrown"
10361192version = "0.12.3"
10371193source = "registry+https://github.com/rust-lang/crates.io-index"
10381194checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
1195+dependencies = [
1196+ "ahash 0.7.6",
1197+]
10391198
10401199[[package]]
10411200name = "hashbrown"
1042−version = "0.14.3"
1201+version = "0.14.0"
10431202source = "registry+https://github.com/rust-lang/crates.io-index"
1044−checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
1203+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
10451204dependencies = [
1046− "ahash",
1205+ "ahash 0.8.3",
10471206 "allocator-api2",
10481207]
10491208
10501209[[package]]
10511210name = "helix-core"
1052−version = "23.10.0"
1211+version = "0.6.0"
10531212dependencies = [
1054− "ahash",
1213+ "ahash 0.8.3",
10551214 "arc-swap",
1056− "bitflags 2.4.1",
1215+ "bitflags 2.3.3",
10571216 "chrono",
10581217 "dunce",
10591218 "encoding_rs",
10601219 "etcetera",
1061− "hashbrown 0.14.3",
1220+ "hashbrown 0.14.0",
10621221 "helix-loader",
10631222 "imara-diff",
10641223 "indoc",
10651224 "log",
1066− "nucleo",
10671225 "once_cell",
1068− "parking_lot",
10691226 "quickcheck",
10701227 "regex",
10711228 "ropey",
@@ -1084,7 +1241,7 @@ dependencies = [
10841241
10851242[[package]]
10861243name = "helix-dap"
1087−version = "23.10.0"
1244+version = "0.6.0"
10881245dependencies = [
10891246 "anyhow",
10901247 "fern",
@@ -1097,21 +1254,12 @@ dependencies = [
10971254 "which",
10981255]
10991256
1100−[[package]]
1101−name = "helix-event"
1102−version = "23.10.0"
1103−dependencies = [
1104− "parking_lot",
1105− "tokio",
1106−]
1107−
11081257[[package]]
11091258name = "helix-loader"
1110−version = "23.10.0"
1259+version = "0.6.0"
11111260dependencies = [
11121261 "anyhow",
11131262 "cc",
1114− "dunce",
11151263 "etcetera",
11161264 "libloading",
11171265 "log",
@@ -1126,12 +1274,11 @@ dependencies = [
11261274
11271275[[package]]
11281276name = "helix-lsp"
1129−version = "23.10.0"
1277+version = "0.6.0"
11301278dependencies = [
11311279 "anyhow",
11321280 "futures-executor",
11331281 "futures-util",
1134− "globset",
11351282 "helix-core",
11361283 "helix-loader",
11371284 "helix-parsec",
@@ -1148,11 +1295,11 @@ dependencies = [
11481295
11491296[[package]]
11501297name = "helix-parsec"
1151−version = "23.10.0"
1298+version = "0.6.0"
11521299
11531300[[package]]
11541301name = "helix-term"
1155−version = "23.10.0"
1302+version = "0.6.0"
11561303dependencies = [
11571304 "anyhow",
11581305 "arc-swap",
@@ -1161,11 +1308,11 @@ dependencies = [
11611308 "crossterm",
11621309 "fern",
11631310 "futures-util",
1311+ "fuzzy-matcher",
11641312 "grep-regex",
11651313 "grep-searcher",
11661314 "helix-core",
11671315 "helix-dap",
1168− "helix-event",
11691316 "helix-loader",
11701317 "helix-lsp",
11711318 "helix-tui",
@@ -1175,9 +1322,7 @@ dependencies = [
11751322 "indoc",
11761323 "libc",
11771324 "log",
1178− "nucleo",
11791325 "once_cell",
1180− "open",
11811326 "pulldown-cmark",
11821327 "serde",
11831328 "serde_json",
@@ -1188,15 +1333,14 @@ dependencies = [
11881333 "tokio",
11891334 "tokio-stream",
11901335 "toml",
1191− "url",
11921336 "which",
11931337]
11941338
11951339[[package]]
11961340name = "helix-tui"
1197−version = "23.10.0"
1341+version = "0.6.0"
11981342dependencies = [
1199− "bitflags 2.4.1",
1343+ "bitflags 2.3.3",
12001344 "cassowary",
12011345 "crossterm",
12021346 "helix-core",
@@ -1210,13 +1354,12 @@ dependencies = [
12101354
12111355[[package]]
12121356name = "helix-vcs"
1213−version = "23.10.0"
1357+version = "0.6.0"
12141358dependencies = [
12151359 "anyhow",
12161360 "arc-swap",
12171361 "gix",
12181362 "helix-core",
1219− "helix-event",
12201363 "imara-diff",
12211364 "log",
12221365 "parking_lot",
@@ -1226,18 +1369,17 @@ dependencies = [
12261369
12271370[[package]]
12281371name = "helix-view"
1229−version = "23.10.0"
1372+version = "0.6.0"
12301373dependencies = [
12311374 "anyhow",
12321375 "arc-swap",
1233− "bitflags 2.4.1",
1376+ "bitflags 2.3.3",
12341377 "chardetng",
12351378 "clipboard-win",
12361379 "crossterm",
12371380 "futures-util",
12381381 "helix-core",
12391382 "helix-dap",
1240− "helix-event",
12411383 "helix-loader",
12421384 "helix-lsp",
12431385 "helix-tui",
@@ -1246,7 +1388,6 @@ dependencies = [
12461388 "log",
12471389 "once_cell",
12481390 "parking_lot",
1249− "rustix",
12501391 "serde",
12511392 "serde_json",
12521393 "slotmap",
@@ -1266,13 +1407,25 @@ dependencies = [
12661407 "libc",
12671408]
12681409
1410+[[package]]
1411+name = "hermit-abi"
1412+version = "0.3.1"
1413+source = "registry+https://github.com/rust-lang/crates.io-index"
1414+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
1415+
1416+[[package]]
1417+name = "hex"
1418+version = "0.4.3"
1419+source = "registry+https://github.com/rust-lang/crates.io-index"
1420+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1421+
12691422[[package]]
12701423name = "home"
1271−version = "0.5.5"
1424+version = "0.5.4"
12721425source = "registry+https://github.com/rust-lang/crates.io-index"
1273−checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
1426+checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408"
12741427dependencies = [
1275− "windows-sys 0.48.0",
1428+ "winapi",
12761429]
12771430
12781431[[package]]
@@ -1286,7 +1439,7 @@ dependencies = [
12861439 "iana-time-zone-haiku",
12871440 "js-sys",
12881441 "wasm-bindgen",
1289− "windows 0.48.0",
1442+ "windows",
12901443]
12911444
12921445[[package]]
@@ -1301,9 +1454,9 @@ dependencies = [
13011454
13021455[[package]]
13031456name = "idna"
1304−version = "0.5.0"
1457+version = "0.4.0"
13051458source = "registry+https://github.com/rust-lang/crates.io-index"
1306−checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
1459+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
13071460dependencies = [
13081461 "unicode-bidi",
13091462 "unicode-normalization",
@@ -1311,16 +1464,17 @@ dependencies = [
13111464
13121465[[package]]
13131466name = "ignore"
1314−version = "0.4.21"
1467+version = "0.4.20"
13151468source = "registry+https://github.com/rust-lang/crates.io-index"
1316−checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
1469+checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
13171470dependencies = [
1318− "crossbeam-deque",
13191471 "globset",
1472+ "lazy_static",
13201473 "log",
13211474 "memchr",
1322− "regex-automata",
1475+ "regex",
13231476 "same-file",
1477+ "thread_local",
13241478 "walkdir",
13251479 "winapi-util",
13261480]
@@ -1331,7 +1485,7 @@ version = "0.1.5"
13311485source = "registry+https://github.com/rust-lang/crates.io-index"
13321486checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
13331487dependencies = [
1334− "ahash",
1488+ "ahash 0.8.3",
13351489 "hashbrown 0.12.3",
13361490]
13371491
@@ -1342,32 +1496,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
13421496checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
13431497dependencies = [
13441498 "equivalent",
1345− "hashbrown 0.14.3",
1499+ "hashbrown 0.14.0",
13461500]
13471501
13481502[[package]]
13491503name = "indoc"
1350−version = "2.0.4"
1504+version = "2.0.2"
13511505source = "registry+https://github.com/rust-lang/crates.io-index"
1352−checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
1506+checksum = "761cde40c27e2a9877f8c928fd248b7eec9dd48623dd514b256858ca593fbba7"
13531507
13541508[[package]]
1355−name = "is-docker"
1356−version = "0.2.0"
1509+name = "instant"
1510+version = "0.1.12"
13571511source = "registry+https://github.com/rust-lang/crates.io-index"
1358−checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
1512+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
13591513dependencies = [
1360− "once_cell",
1514+ "cfg-if",
13611515]
13621516
13631517[[package]]
1364−name = "is-wsl"
1365−version = "0.4.0"
1518+name = "io-close"
1519+version = "0.3.7"
13661520source = "registry+https://github.com/rust-lang/crates.io-index"
1367−checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
1521+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
13681522dependencies = [
1369− "is-docker",
1370− "once_cell",
1523+ "libc",
1524+ "winapi",
1525+]
1526+
1527+[[package]]
1528+name = "io-lifetimes"
1529+version = "1.0.10"
1530+source = "registry+https://github.com/rust-lang/crates.io-index"
1531+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
1532+dependencies = [
1533+ "hermit-abi 0.3.1",
1534+ "libc",
1535+ "windows-sys 0.48.0",
13711536]
13721537
13731538[[package]]
@@ -1385,17 +1550,32 @@ dependencies = [
13851550 "wasm-bindgen",
13861551]
13871552
1553+[[package]]
1554+name = "kstring"
1555+version = "2.0.0"
1556+source = "registry+https://github.com/rust-lang/crates.io-index"
1557+checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
1558+dependencies = [
1559+ "static_assertions",
1560+]
1561+
1562+[[package]]
1563+name = "lazy_static"
1564+version = "1.4.0"
1565+source = "registry+https://github.com/rust-lang/crates.io-index"
1566+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
1567+
13881568[[package]]
13891569name = "libc"
1390−version = "0.2.151"
1570+version = "0.2.147"
13911571source = "registry+https://github.com/rust-lang/crates.io-index"
1392−checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
1572+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
13931573
13941574[[package]]
13951575name = "libloading"
1396−version = "0.8.1"
1576+version = "0.8.0"
13971577source = "registry+https://github.com/rust-lang/crates.io-index"
1398−checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
1578+checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
13991579dependencies = [
14001580 "cfg-if",
14011581 "windows-sys 0.48.0",
@@ -1412,9 +1592,9 @@ dependencies = [
14121592
14131593[[package]]
14141594name = "linux-raw-sys"
1415−version = "0.4.11"
1595+version = "0.3.4"
14161596source = "registry+https://github.com/rust-lang/crates.io-index"
1417−checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
1597+checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
14181598
14191599[[package]]
14201600name = "lock_api"
@@ -1428,15 +1608,15 @@ dependencies = [
14281608
14291609[[package]]
14301610name = "log"
1431−version = "0.4.20"
1611+version = "0.4.19"
14321612source = "registry+https://github.com/rust-lang/crates.io-index"
1433−checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
1613+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
14341614
14351615[[package]]
14361616name = "lsp-types"
1437−version = "0.95.0"
1617+version = "0.94.0"
14381618source = "registry+https://github.com/rust-lang/crates.io-index"
1439−checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
1619+checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
14401620dependencies = [
14411621 "bitflags 1.3.2",
14421622 "serde",
@@ -1447,26 +1627,41 @@ dependencies = [
14471627
14481628[[package]]
14491629name = "memchr"
1450−version = "2.6.3"
1630+version = "2.5.0"
14511631source = "registry+https://github.com/rust-lang/crates.io-index"
1452−checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
1632+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
14531633
14541634[[package]]
14551635name = "memmap2"
1456−version = "0.9.0"
1636+version = "0.5.10"
14571637source = "registry+https://github.com/rust-lang/crates.io-index"
1458−checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375"
1638+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
14591639dependencies = [
14601640 "libc",
14611641]
14621642
14631643[[package]]
1464−name = "memoffset"
1465−version = "0.9.0"
1644+name = "memmap2"
1645+version = "0.7.1"
14661646source = "registry+https://github.com/rust-lang/crates.io-index"
1467−checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
1647+checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
14681648dependencies = [
1469− "autocfg",
1649+ "libc",
1650+]
1651+
1652+[[package]]
1653+name = "minimal-lexical"
1654+version = "0.2.1"
1655+source = "registry+https://github.com/rust-lang/crates.io-index"
1656+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1657+
1658+[[package]]
1659+name = "miniz_oxide"
1660+version = "0.6.2"
1661+source = "registry+https://github.com/rust-lang/crates.io-index"
1662+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
1663+dependencies = [
1664+ "adler",
14701665]
14711666
14721667[[package]]
@@ -1480,36 +1675,24 @@ dependencies = [
14801675
14811676[[package]]
14821677name = "mio"
1483−version = "0.8.9"
1678+version = "0.8.6"
14841679source = "registry+https://github.com/rust-lang/crates.io-index"
1485−checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
1680+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
14861681dependencies = [
14871682 "libc",
14881683 "log",
14891684 "wasi",
1490− "windows-sys 0.48.0",
1491−]
1492−
1493−[[package]]
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",
1685+ "windows-sys 0.45.0",
15021686]
15031687
15041688[[package]]
1505−name = "nucleo-matcher"
1506−version = "0.2.0"
1689+name = "nom"
1690+version = "7.1.3"
15071691source = "registry+https://github.com/rust-lang/crates.io-index"
1508−checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
1692+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
15091693dependencies = [
1510− "cov-mark",
15111694 "memchr",
1512− "unicode-segmentation",
1695+ "minimal-lexical",
15131696]
15141697
15151698[[package]]
@@ -1527,7 +1710,7 @@ version = "1.15.0"
15271710source = "registry+https://github.com/rust-lang/crates.io-index"
15281711checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
15291712dependencies = [
1530− "hermit-abi",
1713+ "hermit-abi 0.2.6",
15311714 "libc",
15321715]
15331716
@@ -1551,20 +1734,9 @@ dependencies = [
15511734
15521735[[package]]
15531736name = "once_cell"
1554−version = "1.19.0"
1555−source = "registry+https://github.com/rust-lang/crates.io-index"
1556−checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
1557−
1558−[[package]]
1559−name = "open"
1560−version = "5.0.1"
1737+version = "1.18.0"
15611738source = "registry+https://github.com/rust-lang/crates.io-index"
1562−checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
1563−dependencies = [
1564− "is-wsl",
1565− "libc",
1566− "pathdiff",
1567−]
1739+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
15681740
15691741[[package]]
15701742name = "parking_lot"
@@ -1589,23 +1761,17 @@ dependencies = [
15891761 "windows-sys 0.45.0",
15901762]
15911763
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−
15981764[[package]]
15991765name = "percent-encoding"
1600−version = "2.3.1"
1766+version = "2.3.0"
16011767source = "registry+https://github.com/rust-lang/crates.io-index"
1602−checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
1768+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
16031769
16041770[[package]]
16051771name = "pin-project-lite"
1606−version = "0.2.12"
1772+version = "0.2.9"
16071773source = "registry+https://github.com/rust-lang/crates.io-index"
1608−checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
1774+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
16091775
16101776[[package]]
16111777name = "pin-utils"
@@ -1615,18 +1781,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
16151781
16161782[[package]]
16171783name = "proc-macro2"
1618−version = "1.0.69"
1784+version = "1.0.63"
16191785source = "registry+https://github.com/rust-lang/crates.io-index"
1620−checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
1786+checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
16211787dependencies = [
16221788 "unicode-ident",
16231789]
16241790
16251791[[package]]
16261792name = "prodash"
1627−version = "28.0.0"
1793+version = "25.0.0"
16281794source = "registry+https://github.com/rust-lang/crates.io-index"
1629−checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
1795+checksum = "3236ce1618b6da4c7b618e0143c4d5b5dc190f75f81c49f248221382f7e9e9ae"
16301796
16311797[[package]]
16321798name = "pulldown-cmark"
@@ -1675,28 +1841,6 @@ dependencies = [
16751841 "getrandom",
16761842]
16771843
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−
17001844[[package]]
17011845name = "redox_syscall"
17021846version = "0.2.16"
@@ -1708,47 +1852,47 @@ dependencies = [
17081852
17091853[[package]]
17101854name = "redox_syscall"
1711−version = "0.4.1"
1855+version = "0.3.5"
17121856source = "registry+https://github.com/rust-lang/crates.io-index"
1713−checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
1857+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
17141858dependencies = [
17151859 "bitflags 1.3.2",
17161860]
17171861
17181862[[package]]
17191863name = "regex"
1720−version = "1.10.2"
1864+version = "1.8.4"
17211865source = "registry+https://github.com/rust-lang/crates.io-index"
1722−checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
1866+checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
17231867dependencies = [
1724− "aho-corasick",
1868+ "aho-corasick 1.0.1",
17251869 "memchr",
1726− "regex-automata",
1727− "regex-syntax",
1870+ "regex-syntax 0.7.2",
17281871]
17291872
17301873[[package]]
17311874name = "regex-automata"
1732−version = "0.4.3"
1875+version = "0.1.10"
17331876source = "registry+https://github.com/rust-lang/crates.io-index"
1734−checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
1735−dependencies = [
1736− "aho-corasick",
1737− "memchr",
1738− "regex-syntax",
1739−]
1877+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
1878+
1879+[[package]]
1880+name = "regex-syntax"
1881+version = "0.6.29"
1882+source = "registry+https://github.com/rust-lang/crates.io-index"
1883+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
17401884
17411885[[package]]
17421886name = "regex-syntax"
1743−version = "0.8.2"
1887+version = "0.7.2"
17441888source = "registry+https://github.com/rust-lang/crates.io-index"
1745−checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
1889+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
17461890
17471891[[package]]
17481892name = "ropey"
1749−version = "1.6.1"
1893+version = "1.6.0"
17501894source = "registry+https://github.com/rust-lang/crates.io-index"
1751−checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
1895+checksum = "53ce7a2c43a32e50d666e33c5a80251b31147bb4b49024bcab11fb6f20c671ed"
17521896dependencies = [
17531897 "smallvec",
17541898 "str_indices",
@@ -1762,15 +1906,16 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
17621906
17631907[[package]]
17641908name = "rustix"
1765−version = "0.38.28"
1909+version = "0.37.15"
17661910source = "registry+https://github.com/rust-lang/crates.io-index"
1767−checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
1911+checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
17681912dependencies = [
1769− "bitflags 2.4.1",
1913+ "bitflags 1.3.2",
17701914 "errno",
1915+ "io-lifetimes",
17711916 "libc",
17721917 "linux-raw-sys",
1773− "windows-sys 0.52.0",
1918+ "windows-sys 0.48.0",
17741919]
17751920
17761921[[package]]
@@ -1802,29 +1947,29 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
18021947
18031948[[package]]
18041949name = "serde"
1805−version = "1.0.193"
1950+version = "1.0.166"
18061951source = "registry+https://github.com/rust-lang/crates.io-index"
1807−checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
1952+checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
18081953dependencies = [
18091954 "serde_derive",
18101955]
18111956
18121957[[package]]
18131958name = "serde_derive"
1814−version = "1.0.193"
1959+version = "1.0.166"
18151960source = "registry+https://github.com/rust-lang/crates.io-index"
1816−checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
1961+checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
18171962dependencies = [
18181963 "proc-macro2",
18191964 "quote",
1820− "syn 2.0.38",
1965+ "syn 2.0.23",
18211966]
18221967
18231968[[package]]
18241969name = "serde_json"
1825−version = "1.0.108"
1970+version = "1.0.99"
18261971source = "registry+https://github.com/rust-lang/crates.io-index"
1827−checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
1972+checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
18281973dependencies = [
18291974 "itoa",
18301975 "ryu",
@@ -1839,7 +1984,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
18391984dependencies = [
18401985 "proc-macro2",
18411986 "quote",
1842− "syn 2.0.38",
1987+ "syn 2.0.23",
18431988]
18441989
18451990[[package]]
@@ -1859,9 +2004,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
18592004
18602005[[package]]
18612006name = "signal-hook"
1862−version = "0.3.17"
2007+version = "0.3.15"
18632008source = "registry+https://github.com/rust-lang/crates.io-index"
1864−checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
2009+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
18652010dependencies = [
18662011 "libc",
18672012 "signal-hook-registry",
@@ -1910,18 +2055,18 @@ dependencies = [
19102055
19112056[[package]]
19122057name = "slotmap"
1913−version = "1.0.7"
2058+version = "1.0.6"
19142059source = "registry+https://github.com/rust-lang/crates.io-index"
1915−checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
2060+checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
19162061dependencies = [
19172062 "version_check",
19182063]
19192064
19202065[[package]]
19212066name = "smallvec"
1922−version = "1.11.2"
2067+version = "1.10.0"
19232068source = "registry+https://github.com/rust-lang/crates.io-index"
1924−checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
2069+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
19252070
19262071[[package]]
19272072name = "smartstring"
@@ -1942,12 +2087,12 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
19422087
19432088[[package]]
19442089name = "socket2"
1945−version = "0.5.5"
2090+version = "0.4.9"
19462091source = "registry+https://github.com/rust-lang/crates.io-index"
1947−checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
2092+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
19482093dependencies = [
19492094 "libc",
1950− "windows-sys 0.48.0",
2095+ "winapi",
19512096]
19522097
19532098[[package]]
@@ -1956,6 +2101,12 @@ version = "1.1.0"
19562101source = "registry+https://github.com/rust-lang/crates.io-index"
19572102checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
19582103
2104+[[package]]
2105+name = "str-buf"
2106+version = "1.0.6"
2107+source = "registry+https://github.com/rust-lang/crates.io-index"
2108+checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
2109+
19592110[[package]]
19602111name = "str_indices"
19612112version = "0.4.1"
@@ -1975,9 +2126,9 @@ dependencies = [
19752126
19762127[[package]]
19772128name = "syn"
1978−version = "2.0.38"
2129+version = "2.0.23"
19792130source = "registry+https://github.com/rust-lang/crates.io-index"
1980−checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
2131+checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
19812132dependencies = [
19822133 "proc-macro2",
19832134 "quote",
@@ -1986,13 +2137,14 @@ dependencies = [
19862137
19872138[[package]]
19882139name = "tempfile"
1989−version = "3.8.1"
2140+version = "3.6.0"
19902141source = "registry+https://github.com/rust-lang/crates.io-index"
1991−checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
2142+checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
19922143dependencies = [
2144+ "autocfg",
19932145 "cfg-if",
1994− "fastrand",
1995− "redox_syscall 0.4.1",
2146+ "fastrand 1.9.0",
2147+ "redox_syscall 0.3.5",
19962148 "rustix",
19972149 "windows-sys 0.48.0",
19982150]
@@ -2028,22 +2180,32 @@ dependencies = [
20282180
20292181[[package]]
20302182name = "thiserror"
2031−version = "1.0.52"
2183+version = "1.0.43"
20322184source = "registry+https://github.com/rust-lang/crates.io-index"
2033−checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d"
2185+checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
20342186dependencies = [
20352187 "thiserror-impl",
20362188]
20372189
20382190[[package]]
20392191name = "thiserror-impl"
2040−version = "1.0.52"
2192+version = "1.0.43"
20412193source = "registry+https://github.com/rust-lang/crates.io-index"
2042−checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
2194+checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
20432195dependencies = [
20442196 "proc-macro2",
20452197 "quote",
2046− "syn 2.0.38",
2198+ "syn 2.0.23",
2199+]
2200+
2201+[[package]]
2202+name = "thread_local"
2203+version = "1.1.7"
2204+source = "registry+https://github.com/rust-lang/crates.io-index"
2205+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
2206+dependencies = [
2207+ "cfg-if",
2208+ "once_cell",
20472209]
20482210
20492211[[package]]
@@ -2057,9 +2219,9 @@ dependencies = [
20572219
20582220[[package]]
20592221name = "time"
2060−version = "0.3.23"
2222+version = "0.3.20"
20612223source = "registry+https://github.com/rust-lang/crates.io-index"
2062−checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
2224+checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
20632225dependencies = [
20642226 "itoa",
20652227 "libc",
@@ -2071,15 +2233,15 @@ dependencies = [
20712233
20722234[[package]]
20732235name = "time-core"
2074−version = "0.1.1"
2236+version = "0.1.0"
20752237source = "registry+https://github.com/rust-lang/crates.io-index"
2076−checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
2238+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
20772239
20782240[[package]]
20792241name = "time-macros"
2080−version = "0.2.10"
2242+version = "0.2.8"
20812243source = "registry+https://github.com/rust-lang/crates.io-index"
2082−checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
2244+checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
20832245dependencies = [
20842246 "time-core",
20852247]
@@ -2101,10 +2263,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
21012263
21022264[[package]]
21032265name = "tokio"
2104−version = "1.35.1"
2266+version = "1.29.1"
21052267source = "registry+https://github.com/rust-lang/crates.io-index"
2106−checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
2268+checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
21072269dependencies = [
2270+ "autocfg",
21082271 "backtrace",
21092272 "bytes",
21102273 "libc",
@@ -2120,13 +2283,13 @@ dependencies = [
21202283
21212284[[package]]
21222285name = "tokio-macros"
2123−version = "2.2.0"
2286+version = "2.1.0"
21242287source = "registry+https://github.com/rust-lang/crates.io-index"
2125−checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
2288+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
21262289dependencies = [
21272290 "proc-macro2",
21282291 "quote",
2129− "syn 2.0.38",
2292+ "syn 2.0.23",
21302293]
21312294
21322295[[package]]
@@ -2171,13 +2334,14 @@ dependencies = [
21712334 "serde",
21722335 "serde_spanned",
21732336 "toml_datetime",
2174− "winnow 0.4.6",
2337+ "winnow",
21752338]
21762339
21772340[[package]]
21782341name = "tree-sitter"
21792342version = "0.20.10"
2180−source = "git+https://github.com/tree-sitter/tree-sitter?rev=ab09ae20d640711174b8da8a654f6b3dec93da1a#ab09ae20d640711174b8da8a654f6b3dec93da1a"
2343+source = "registry+https://github.com/rust-lang/crates.io-index"
2344+checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d"
21812345dependencies = [
21822346 "cc",
21832347 "regex",
@@ -2218,9 +2382,13 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
22182382
22192383[[package]]
22202384name = "unicode-linebreak"
2221−version = "0.1.5"
2385+version = "0.1.4"
22222386source = "registry+https://github.com/rust-lang/crates.io-index"
2223−checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
2387+checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
2388+dependencies = [
2389+ "hashbrown 0.12.3",
2390+ "regex",
2391+]
22242392
22252393[[package]]
22262394name = "unicode-normalization"
@@ -2239,15 +2407,15 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
22392407
22402408[[package]]
22412409name = "unicode-width"
2242−version = "0.1.11"
2410+version = "0.1.10"
22432411source = "registry+https://github.com/rust-lang/crates.io-index"
2244−checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
2412+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
22452413
22462414[[package]]
22472415name = "url"
2248−version = "2.5.0"
2416+version = "2.4.0"
22492417source = "registry+https://github.com/rust-lang/crates.io-index"
2250−checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
2418+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
22512419dependencies = [
22522420 "form_urlencoded",
22532421 "idna",
@@ -2263,9 +2431,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
22632431
22642432[[package]]
22652433name = "walkdir"
2266−version = "2.4.0"
2434+version = "2.3.3"
22672435source = "registry+https://github.com/rust-lang/crates.io-index"
2268−checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
2436+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
22692437dependencies = [
22702438 "same-file",
22712439 "winapi-util",
@@ -2333,15 +2501,13 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
23332501
23342502[[package]]
23352503name = "which"
2336−version = "5.0.0"
2504+version = "4.4.0"
23372505source = "registry+https://github.com/rust-lang/crates.io-index"
2338−checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14"
2506+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
23392507dependencies = [
23402508 "either",
2341− "home",
2509+ "libc",
23422510 "once_cell",
2343− "rustix",
2344− "windows-sys 0.48.0",
23452511]
23462512
23472513[[package]]
@@ -2384,25 +2550,6 @@ dependencies = [
23842550 "windows-targets 0.48.0",
23852551]
23862552
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−
24062553[[package]]
24072554name = "windows-sys"
24082555version = "0.45.0"
@@ -2421,15 +2568,6 @@ dependencies = [
24212568 "windows-targets 0.48.0",
24222569]
24232570
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−
24332571[[package]]
24342572name = "windows-targets"
24352573version = "0.42.2"
@@ -2460,21 +2598,6 @@ dependencies = [
24602598 "windows_x86_64_msvc 0.48.0",
24612599]
24622600
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−
24782601[[package]]
24792602name = "windows_aarch64_gnullvm"
24802603version = "0.42.2"
@@ -2487,12 +2610,6 @@ version = "0.48.0"
24872610source = "registry+https://github.com/rust-lang/crates.io-index"
24882611checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
24892612
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−
24962613[[package]]
24972614name = "windows_aarch64_msvc"
24982615version = "0.42.2"
@@ -2505,12 +2622,6 @@ version = "0.48.0"
25052622source = "registry+https://github.com/rust-lang/crates.io-index"
25062623checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
25072624
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−
25142625[[package]]
25152626name = "windows_i686_gnu"
25162627version = "0.42.2"
@@ -2523,12 +2634,6 @@ version = "0.48.0"
25232634source = "registry+https://github.com/rust-lang/crates.io-index"
25242635checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
25252636
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−
25322637[[package]]
25332638name = "windows_i686_msvc"
25342639version = "0.42.2"
@@ -2541,12 +2646,6 @@ version = "0.48.0"
25412646source = "registry+https://github.com/rust-lang/crates.io-index"
25422647checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
25432648
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−
25502649[[package]]
25512650name = "windows_x86_64_gnu"
25522651version = "0.42.2"
@@ -2559,12 +2658,6 @@ version = "0.48.0"
25592658source = "registry+https://github.com/rust-lang/crates.io-index"
25602659checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
25612660
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−
25682661[[package]]
25692662name = "windows_x86_64_gnullvm"
25702663version = "0.42.2"
@@ -2577,12 +2670,6 @@ version = "0.48.0"
25772670source = "registry+https://github.com/rust-lang/crates.io-index"
25782671checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
25792672
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−
25862673[[package]]
25872674name = "windows_x86_64_msvc"
25882675version = "0.42.2"
@@ -2595,12 +2682,6 @@ version = "0.48.0"
25952682source = "registry+https://github.com/rust-lang/crates.io-index"
25962683checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
25972684
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−
26042685[[package]]
26052686name = "winnow"
26062687version = "0.4.6"
@@ -2610,18 +2691,9 @@ dependencies = [
26102691 "memchr",
26112692]
26122693
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−
26222694[[package]]
26232695name = "xtask"
2624−version = "23.10.0"
2696+version = "0.6.0"
26252697dependencies = [
26262698 "helix-core",
26272699 "helix-loader",
@@ -2629,23 +2701,3 @@ dependencies = [
26292701 "helix-view",
26302702 "toml",
26312703]
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,12 +1,10 @@
11[workspace]
2−resolver = "2"
32members = [
43 "helix-core",
54 "helix-view",
65 "helix-term",
76 "helix-tui",
87 "helix-lsp",
9− "helix-event",
108 "helix-dap",
119 "helix-loader",
1210 "helix-vcs",
@@ -34,17 +32,3 @@ inherits = "test"
3432package.helix-core.opt-level = 2
3533package.helix-tui.opt-level = 2
3634package.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+2−2
@@ -18,7 +18,7 @@
1818
1919![Screenshot](./screenshot.png)
2020
21−A [Kakoune](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.
21+A Kakoune / 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.
@@ -61,4 +61,4 @@ Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-c
6161
6262# Credits
6363
64−Thanks to [@jakenvac](https://github.com/jakenvac) for designing the logo!
64+Thanks to [@JakeHL](https://github.com/JakeHL) for designing the logo!
VERSIONnew file+1
@@ -0,0 +1 @@
1+23.05
book/book.toml−3
@@ -11,6 +11,3 @@ 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: #a2cff5;
167+ --search-mark-bg: #acff5;
168168}
169169
170170.colibri .content .header {
book/src/configuration.md+11−38
@@ -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 | `250` |
54+| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
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,9 +64,6 @@ 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`
7067
7168### `[editor.statusline]` Section
7269
@@ -92,9 +89,9 @@ The `[editor.statusline]` key takes the following sub-keys:
9289
9390| Key | Description | Default |
9491| --- | --- | --- |
95−| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
92+| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` |
9693| `center` | A list of elements aligned to the middle of the statusline | `[]` |
97−| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
94+| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "position", "file-encoding"]` |
9895| `separator` | The character used to separate elements in the statusline | `"│"` |
9996| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
10097| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
@@ -111,7 +108,6 @@ The following statusline elements can be configured:
111108| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
112109| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
113110| `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 |
115111| `total-line-numbers` | The total line numbers of the opened file |
116112| `file-type` | The type of the opened file |
117113| `diagnostics` | The number of warnings and/or errors |
@@ -168,31 +164,16 @@ All git related options are only enabled in a git repository.
168164
169165| Key | Description | Default |
170166|--|--|---------|
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`
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
179175|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
180176
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−
196177### `[editor.auto-pairs]` Section
197178
198179Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
@@ -366,11 +347,3 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
366347max-indent-retain = 0
367348wrap-indicator = "" # set wrap-indicator to "" to hide it
368349```
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+15−35
@@ -1,9 +1,8 @@
11| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
22| --- | --- | --- | --- | --- |
3−| agda | ✓ | | | |
43| astro | ✓ | | | |
54| awk | ✓ | ✓ | | `awk-language-server` |
6−| bash | ✓ | ✓ | ✓ | `bash-language-server` |
5+| bash | ✓ | | ✓ | `bash-language-server` |
76| bass | ✓ | | | `bass` |
87| beancount | ✓ | | | |
98| bibtex | ✓ | | | `texlab` |
@@ -11,13 +10,13 @@
1110| blueprint | ✓ | | | `blueprint-compiler` |
1211| c | ✓ | ✓ | ✓ | `clangd` |
1312| c-sharp | ✓ | ✓ | | `OmniSharp` |
14−| cabal | | | | `haskell-language-server-wrapper` |
13+| cabal | | | | |
1514| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
1615| capnp | ✓ | | ✓ | |
1716| clojure | ✓ | | | `clojure-lsp` |
1817| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
1918| comment | ✓ | | | |
20−| common-lisp | ✓ | | ✓ | `cl-lsp` |
19+| common-lisp | ✓ | | | `cl-lsp` |
2120| cpon | ✓ | | ✓ | |
2221| cpp | ✓ | ✓ | ✓ | `clangd` |
2322| crystal | ✓ | ✓ | | `crystalline` |
@@ -25,7 +24,6 @@
2524| cue | ✓ | | | `cuelsp` |
2625| d | ✓ | ✓ | ✓ | `serve-d` |
2726| dart | ✓ | | ✓ | `dart` |
28−| dbml | ✓ | | | |
2927| devicetree | ✓ | | | |
3028| dhall | ✓ | ✓ | | `dhall-lsp-server` |
3129| diff | ✓ | | | |
@@ -45,10 +43,7 @@
4543| fish | ✓ | ✓ | ✓ | |
4644| forth | ✓ | | | `forth-lsp` |
4745| fortran | ✓ | | ✓ | `fortls` |
48−| fsharp | ✓ | | | `fsautocomplete` |
49−| gas | ✓ | ✓ | | |
5046| gdscript | ✓ | ✓ | ✓ | |
51−| gemini | ✓ | | | |
5247| git-attributes | ✓ | | | |
5348| git-commit | ✓ | ✓ | | |
5449| git-config | ✓ | | | |
@@ -56,13 +51,12 @@
5651| git-rebase | ✓ | | | |
5752| gleam | ✓ | ✓ | | `gleam` |
5853| glsl | ✓ | ✓ | ✓ | |
59−| gn | ✓ | | | |
60−| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
54+| go | ✓ | ✓ | ✓ | `gopls` |
6155| godot-resource | ✓ | | | |
6256| gomod | ✓ | | | `gopls` |
6357| gotmpl | ✓ | | | `gopls` |
6458| gowork | ✓ | | | `gopls` |
65−| graphql | ✓ | | | `graphql-lsp` |
59+| graphql | ✓ | | | |
6660| hare | ✓ | | | |
6761| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
6862| haskell-persistent | ✓ | | | |
@@ -74,18 +68,15 @@
7468| idris | | | | `idris2-lsp` |
7569| iex | ✓ | | | |
7670| ini | ✓ | | | |
77−| janet | ✓ | | | |
78−| java | ✓ | ✓ | ✓ | `jdtls` |
71+| java | ✓ | ✓ | | `jdtls` |
7972| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
80−| jinja | ✓ | | | |
8173| jsdoc | ✓ | | | |
8274| json | ✓ | | ✓ | `vscode-json-language-server` |
83−| json5 | ✓ | | | |
8475| jsonnet | ✓ | | | `jsonnet-language-server` |
8576| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
8677| julia | ✓ | ✓ | ✓ | `julia` |
8778| just | ✓ | ✓ | ✓ | |
88−| kdl | ✓ | ✓ | ✓ | |
79+| kdl | ✓ | | | |
8980| kotlin | ✓ | | | `kotlin-language-server` |
9081| latex | ✓ | ✓ | | `texlab` |
9182| lean | ✓ | | | `lean` |
@@ -93,8 +84,6 @@
9384| llvm | ✓ | ✓ | ✓ | |
9485| llvm-mir | ✓ | ✓ | ✓ | |
9586| llvm-mir-yaml | ✓ | | ✓ | |
96−| log | ✓ | | | |
97−| lpf | ✓ | | | |
9887| lua | ✓ | ✓ | ✓ | `lua-language-server` |
9988| make | ✓ | | | |
10089| markdoc | ✓ | | | `markdoc-ls` |
@@ -109,8 +98,7 @@
10998| nickel | ✓ | | ✓ | `nls` |
11099| nim | ✓ | ✓ | ✓ | `nimlangserver` |
111100| nix | ✓ | | | `nil` |
112−| nu | ✓ | | | `nu` |
113−| nunjucks | ✓ | | | |
101+| nu | ✓ | | | |
114102| ocaml | ✓ | | ✓ | `ocamllsp` |
115103| ocaml-interface | ✓ | | | `ocamllsp` |
116104| odin | ✓ | | ✓ | `ols` |
@@ -123,17 +111,16 @@
123111| perl | ✓ | ✓ | ✓ | `perlnavigator` |
124112| php | ✓ | ✓ | ✓ | `intelephense` |
125113| po | ✓ | ✓ | | |
126−| pod | ✓ | | | |
127114| ponylang | ✓ | ✓ | ✓ | |
128115| prisma | ✓ | | | `prisma-language-server` |
129116| prolog | | | | `swipl` |
130−| protobuf | ✓ | | ✓ | `bufls`, `pb` |
117+| protobuf | ✓ | | ✓ | |
131118| prql | ✓ | | | |
132−| purescript | ✓ | ✓ | | `purescript-language-server` |
119+| purescript | ✓ | | | `purescript-language-server` |
133120| python | ✓ | ✓ | ✓ | `pylsp` |
134121| qml | ✓ | | ✓ | `qmlls` |
135122| r | ✓ | | | `R` |
136−| racket | ✓ | | ✓ | `racket` |
123+| racket | ✓ | | | `racket` |
137124| regex | ✓ | | | |
138125| rego | ✓ | | | `regols` |
139126| rescript | ✓ | ✓ | | `rescript-language-server` |
@@ -145,36 +132,30 @@
145132| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
146133| sage | ✓ | ✓ | | |
147134| scala | ✓ | | ✓ | `metals` |
148−| scheme | ✓ | | ✓ | |
135+| scheme | ✓ | | | |
149136| scss | ✓ | | | `vscode-css-language-server` |
150137| slint | ✓ | | ✓ | `slint-lsp` |
151−| smali | ✓ | | ✓ | |
152138| smithy | ✓ | | | `cs` |
153139| sml | ✓ | | | |
154140| solidity | ✓ | | | `solc` |
155141| sql | ✓ | | | |
156142| sshclientconfig | ✓ | | | |
157143| starlark | ✓ | ✓ | | |
158−| strace | ✓ | | | |
159−| svelte | ✓ | | ✓ | `svelteserver` |
144+| svelte | ✓ | | | `svelteserver` |
160145| sway | ✓ | ✓ | ✓ | `forc` |
161146| swift | ✓ | | | `sourcekit-lsp` |
162147| t32 | ✓ | | | |
163148| tablegen | ✓ | ✓ | ✓ | |
164149| task | ✓ | | | |
165−| templ | ✓ | | | `templ` |
166150| tfvars | ✓ | | ✓ | `terraform-ls` |
167−| todotxt | ✓ | | | |
168151| toml | ✓ | | | `taplo` |
169152| tsq | ✓ | | | |
170153| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
171154| twig | ✓ | | | |
172155| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
173−| typst | ✓ | | | `typst-lsp` |
174156| ungrammar | ✓ | | | |
175−| unison | ✓ | | | |
176157| uxntal | ✓ | | | |
177−| v | ✓ | ✓ | ✓ | `v-analyzer` |
158+| v | ✓ | ✓ | ✓ | `v` |
178159| vala | ✓ | | | `vala-language-server` |
179160| verilog | ✓ | ✓ | | `svlangserver` |
180161| vhdl | ✓ | | | `vhdl_ls` |
@@ -185,9 +166,8 @@
185166| webc | ✓ | | | |
186167| wgsl | ✓ | | | `wgsl_analyzer` |
187168| wit | ✓ | | ✓ | |
188−| wren | ✓ | ✓ | ✓ | |
189169| xit | ✓ | | | |
190170| xml | ✓ | | ✓ | |
191−| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
171+| yaml | ✓ | | ✓ | `yaml-language-server` |
192172| yuck | ✓ | | | |
193173| zig | ✓ | ✓ | ✓ | `zls` |
book/src/generated/typable-cmd.md+1−4
@@ -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-16 for number of spaces.) |
20+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 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,7 +55,6 @@
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. |
5958| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
6059| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
6160| `:debug-eval` | Evaluate expression in current debug context. |
@@ -84,5 +83,3 @@
8483| `:run-shell-command`, `:sh` | Run a shell command |
8584| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
8685| `: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,7 +36,6 @@ 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).
4039
4140> 💡 In Helix, the first matching query takes precedence when evaluating
4241> queries, which is different from other editors such as Neovim where the last
@@ -52,4 +51,3 @@ below.
5251 grammars.
5352- If a parser is causing a segfault, or you want to remove it, make sure to
5453 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+43−319
@@ -1,308 +1,76 @@
11# Adding indent queries
22
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.
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).
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. See [Capture types](#capture-types).
13+the total indent level by 1.
1414
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.
15+## Scopes
2316
24−## Indent queries
25−
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.
30−
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.
33−
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−}
46−```
47−
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.
50−
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−```
62−
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.
67−
68−### Capture types
69−
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.
94−- `@extend.prevent-once`:
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−```
208−
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−```
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):
23121
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.
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.
23825
239−To help, we need to signal an end to the extension. We can do this with
240−`@extend.prevent-once`.
26+- `tail`:
27+This scope applies to everything except for the first line of the
28+captured node.
24129
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:
24233```scm
243−(parenthesized_expression) @indent
244−
245−]
246− (function_definition)
247− (class_definition)
248−] @indent @extend
249−
250−(return_statement) @extend.prevent-once
34+(assignment_expression
35+ right: (_) @indent
36+ (#set! "scope" "all"))
25137```
25238
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.
39+## Capture types
25740
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.
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.
26145
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−```
46+- `@outdent` (default scope `all`):
47+Decrease the indent level by 1. The same rules as for `@indent` apply.
27548
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.
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.
28054
281−```scm
282−((block_sequence_item) @item @indent.always @extend
283− (#not-one-line? @item))
55+- `@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).
28461
285−((block_mapping_pair
286− key: (_) @key
287− value: (_) @val
288− (#not-same-line? @key @val)
289− ) @indent.always @extend
290−)
291−```
29262
29363## Predicates
29464
29565In some cases, an S-expression cannot express exactly what pattern should be matched.
29666For that, tree-sitter allows for predicates to appear anywhere within a pattern,
29767similar to how `#set!` declarations work:
298−
29968```scm
30069(some_kind
30170 (child_kind) @indent
30271 (#predicate? arg1 arg2 ...)
30372)
30473```
305−
30674The number of arguments depends on the predicate that's used.
30775Each argument is either a capture (`@name`) or a string (`"some string"`).
30876The following predicates are supported by tree-sitter:
@@ -323,47 +91,3 @@ argument (a string).
32391
32492- `#same-line?`/`#not-same-line?`:
32593The 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+6−41
@@ -9,11 +9,9 @@
99 - [Arch Linux extra](#arch-linux-extra)
1010 - [NixOS](#nixos)
1111 - [Flatpak](#flatpak)
12− - [Snap](#snap)
1312 - [AppImage](#appimage)
1413- [macOS](#macos)
1514 - [Homebrew Core](#homebrew-core)
16− - [MacPorts](#macports)
1715- [Windows](#windows)
1816 - [Winget](#winget)
1917 - [Scoop](#scoop)
@@ -65,7 +63,10 @@ sudo apt install helix
6563
6664### Fedora/RHEL
6765
66+Enable the `COPR` repository for Helix:
67+
6868```sh
69+sudo dnf copr enable varlad/helix
6970sudo dnf install helix
7071```
7172
@@ -103,16 +104,6 @@ flatpak install flathub com.helix_editor.Helix
103104flatpak run com.helix_editor.Helix
104105```
105106
106−### Snap
107−
108−Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
109−
110−```sh
111−snap install --classic helix
112−```
113−
114−This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
115−
116107### AppImage
117108
118109Install Helix using the Linux [AppImage](https://appimage.org/) format.
@@ -131,12 +122,6 @@ chmod +x helix-*.AppImage # change permission for executable mode
131122brew install helix
132123```
133124
134−### MacPorts
135−
136−```sh
137−port install helix
138−```
139−
140125## Windows
141126
142127Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
@@ -228,7 +213,7 @@ Or, create a symbolic link:
228213ln -Ts $PWD/runtime ~/.config/helix/runtime
229214```
230215
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.
216+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.
232217
233218#### Windows
234219
@@ -261,32 +246,12 @@ following order:
2612461. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
262247 developing and testing helix only).
2632482. `runtime/` subdirectory of OS-dependent helix user config directory.
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.
249+3. `$HELIX_RUNTIME`.
250+4. `runtime/` subdirectory of path to Helix executable.
268251
269252This order also sets the priority for selecting which file will be used if multiple runtime
270253directories have files with the same name.
271254
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−
290255### Validating the installation
291256
292257To make sure everything is set up as expected you should run the Helix health
book/src/keymap.md+15−16
@@ -25,7 +25,7 @@
2525
2626## Normal mode
2727
28−Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
28+Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
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` | Yank selections to clipboard | `yank_to_clipboard` |
294+| `y` | Join and yank selections to clipboard | `yank_joined_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,20 +411,19 @@ 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−| `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 |
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 |
428427
429428## Prompt
430429
book/src/languages.md+1−3
@@ -66,10 +66,8 @@ 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)
7069| `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` |
7170| `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.
7371
7472### File-type detection and the `file-types` key
7573
@@ -129,7 +127,7 @@ These are the available options for a language server.
129127| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
130128
131129A `format` sub-table within `config` can be used to pass extra formatting options to
132−[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
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).
133131For example, with typescript:
134132
135133```toml
book/src/themes.md+1−9
@@ -70,7 +70,6 @@ over it and is merged into the default palette.
7070
7171| Color Name |
7272| --- |
73−| `default` |
7473| `black` |
7574| `red` |
7675| `green` |
@@ -155,7 +154,6 @@ We use a similar set of scopes as
155154
156155- `type` - Types
157156 - `builtin` - Primitive types provided by the language (`int`, `usize`)
158− - `parameter` - Generic type parameters (`T`)
159157 - `enum`
160158 - `variant`
161159- `constructor`
@@ -246,12 +244,9 @@ We use a similar set of scopes as
246244
247245- `diff` - version control changes
248246 - `plus` - additions
249− - `gutter` - gutter indicator
250247 - `minus` - deletions
251− - `gutter` - gutter indicator
252248 - `delta` - modifications
253249 - `moved` - renamed or moved files/changes
254− - `gutter` - gutter indicator
255250
256251#### Interface
257252
@@ -296,14 +291,11 @@ These scopes are used for theming the editor interface:
296291| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
297292| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
298293| `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 |
302294| `ui.popup` | Documentation popups (e.g. Space + k) |
303295| `ui.popup.info` | Prompt for multiple key options |
304296| `ui.window` | Borderlines separating splits |
305297| `ui.help` | Description box for commands |
306−| `ui.text` | Default text style, command prompts, popup text, etc. |
298+| `ui.text` | Command prompts, popup text, etc. |
307299| `ui.text.focus` | The currently selected line in the picker |
308300| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
309301| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
book/src/usage.md+5−21
@@ -37,35 +37,19 @@ 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−### 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:
40+### Special registers
4441
4542| Register character | Contains |
4643| --- | --- |
4744| `/` | Last search |
4845| `:` | Last executed command |
4946| `"` | Last yanked text |
50−| `@` | Last recorded macro |
47+| `_` | Black hole |
5148
52−### Special registers
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.
5351
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.
52+The black hole register is a no-op register, meaning that no data will be read or written to it.
6953
7054## Surround
7155
book/theme/index.hbs+15−18
@@ -1,11 +1,11 @@
11<!DOCTYPE HTML>
2−<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
2+<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
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,6 +34,8 @@
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">
3739 {{#if copy_fonts}}
3840 <link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
3941 {{/if}}
@@ -53,7 +55,7 @@
5355 <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
5456 {{/if}}
5557 </head>
56− <body class="sidebar-visible no-js">
58+ <body>
5759 <div id="body-container">
5860 <!-- Provide site root to javascript -->
5961 <script>
@@ -83,29 +85,24 @@
8385 try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
8486 if (theme === null || theme === undefined) { theme = default_theme; }
8587 var html = document.querySelector('html');
88+ html.classList.remove('no-js')
8689 html.classList.remove('{{ default_theme }}')
8790 html.classList.add(theme);
88− var body = document.querySelector('body');
89− body.classList.remove('no-js')
90− body.classList.add('js');
91+ html.classList.add('js');
9192 </script>
9293
93− <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
94−
9594 <!-- Hide / unhide sidebar before it is displayed -->
9695 <script>
97− var body = document.querySelector('body');
96+ var html = document.querySelector('html');
9897 var sidebar = null;
99− var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
10098 if (document.body.clientWidth >= 1080) {
10199 try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
102100 sidebar = sidebar || 'visible';
103101 } else {
104102 sidebar = 'hidden';
105103 }
106− sidebar_toggle.checked = sidebar === 'visible';
107− body.classList.remove('sidebar-visible');
108− body.classList.add("sidebar-" + sidebar);
104+ html.classList.remove('sidebar-visible');
105+ html.classList.add("sidebar-" + sidebar);
109106 </script>
110107
111108 <nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -144,9 +141,9 @@
144141 <div id="menu-bar-hover-placeholder"></div>
145142 <div id="menu-bar" class="menu-bar sticky">
146143 <div class="left-buttons">
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">
144+ <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
148145 <i class="fa fa-bars"></i>
149− </label>
146+ </button>
150147 <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">
151148 <i class="fa fa-paint-brush"></i>
152149 </button>
@@ -223,7 +220,7 @@
223220 {{/previous}}
224221
225222 {{#next}}
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">
223+ <a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
227224 <i class="fa fa-angle-right"></i>
228225 </a>
229226 {{/next}}
@@ -241,7 +238,7 @@
241238 {{/previous}}
242239
243240 {{#next}}
244− <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
241+ <a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
245242 <i class="fa fa-angle-right"></i>
246243 </a>
247244 {{/next}}
contrib/Helix.appdata.xml−15
@@ -5,7 +5,6 @@
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>
98
109 <description>
1110 <p>
@@ -18,17 +17,6 @@
1817 <li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
1918 </ul>
2019 </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>
3220
3321 <launchable type="desktop-id">Helix.desktop</launchable>
3422
@@ -48,9 +36,6 @@
4836 <content_rating type="oars-1.1" />
4937
5038 <releases>
51− <release version="23.10" date="2023-10-24">
52− <url>https://helix-editor.com/news/release-23-10-highlights/</url>
53− </release>
5439 <release version="23.05" date="2023-05-18">
5540 <url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
5641 </release>
contrib/Helix.desktop+1−3
@@ -1,7 +1,6 @@
11[Desktop Entry]
22Name=Helix
33GenericName=Text Editor
4−GenericName[ar]=مُحَرِّرُ نُصُوص
54GenericName[de]=Texteditor
65GenericName[fr]=Éditeur de texte
76GenericName[ru]=Текстовый редактор
@@ -10,7 +9,7 @@ GenericName[tr]=Metin Düzenleyici
109Comment=Edit text files
1110Comment[af]=Redigeer tekslêers
1211Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
13−Comment[ar]=مُحَرِّرُ مِلَفَّاتٍ نَصِّيَّة
12+Comment[ar]=حرّر ملفات نصية
1413Comment[az]=Mətn fayllarını redaktə edin
1514Comment[be]=Рэдагаваньне тэкставых файлаў
1615Comment[bg]=Редактиране на текстови файлове
@@ -80,7 +79,6 @@ Exec=hx %F
8079Terminal=true
8180Type=Application
8281Keywords=Text;editor;
83−Keywords[ar]=نص;نصوص;محرر;
8482Keywords[fr]=Texte;éditeur;
8583Keywords[ru]=текст;текстовый редактор;
8684Keywords[sr]=Текст;едитор;
docs/CONTRIBUTING.md+3−10
@@ -29,15 +29,9 @@ files, run
2929cargo xtask docgen
3030```
3131
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).
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).
4135
4236# Testing
4337
@@ -64,5 +58,4 @@ The current MSRV and future changes to the MSRV are listed in the [Firefox docum
6458[architecture.md]: ./architecture.md
6559[docs]: https://docs.helix-editor.com/
6660[xtask]: https://github.com/matklad/cargo-xtask
67−[mdbook]: https://rust-lang.github.io/mdBook/guide/installation.html
6861[helpers.rs]: ../helix-term/tests/test/helpers.rs
docs/releases.md+5−12
@@ -1,23 +1,16 @@
11## Checklist
22
33Helix releases are versioned in the Calendar Versioning scheme:
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.
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.
76
87* Merge the changelog PR
98* 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)
109* Tag and push
1110 * `git tag -s -m "<tag>" -a <tag> && git push`
1211 * Make sure to switch to master and pull first
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`
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`
2114* Wait for the Release CI to finish
2215 * It will automatically turn the git tag into a GitHub release when it uploads artifacts
2316* Edit the new release
@@ -64,4 +57,4 @@ versions for convenience:
6457> release. For the full log, check out the git log.
6558
6659Typically, small changes like dependencies or documentation updates, refactors,
67−or meta changes like GitHub Actions work are left out.
60+or meta changes like GitHub Actions work are left out.
flake.lock+237−21
@@ -1,22 +1,126 @@
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": {
421 "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+ ],
550 "nixpkgs": [
51+ "nci",
652 "nixpkgs"
53+ ],
54+ "nixpkgsV1": "nixpkgsV1",
55+ "poetry2nix": [
56+ "nci"
57+ ],
58+ "pre-commit-hooks": [
59+ "nci"
60+ ],
61+ "pruned-racket-catalog": [
62+ "nci"
763 ]
864 },
965 "locked": {
10− "lastModified": 1701025348,
11− "narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
12− "owner": "ipetkov",
13− "repo": "crane",
14− "rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
66+ "lastModified": 1683212002,
67+ "narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=",
68+ "owner": "nix-community",
69+ "repo": "dream2nix",
70+ "rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096",
1571 "type": "github"
1672 },
1773 "original": {
18− "owner": "ipetkov",
19− "repo": "crane",
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",
119+ "type": "github"
120+ },
121+ "original": {
122+ "owner": "edolstra",
123+ "repo": "flake-compat",
20124 "type": "github"
21125 }
22126 },
@@ -25,11 +129,11 @@
25129 "systems": "systems"
26130 },
27131 "locked": {
28− "lastModified": 1694529238,
29− "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
132+ "lastModified": 1681202837,
133+ "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
30134 "owner": "numtide",
31135 "repo": "flake-utils",
32− "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
136+ "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
33137 "type": "github"
34138 },
35139 "original": {
@@ -38,13 +142,55 @@
38142 "type": "github"
39143 }
40144 },
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+ },
41187 "nixpkgs": {
42188 "locked": {
43− "lastModified": 1700794826,
44− "narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
189+ "lastModified": 1683408522,
190+ "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
45191 "owner": "nixos",
46192 "repo": "nixpkgs",
47− "rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
193+ "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
48194 "type": "github"
49195 },
50196 "original": {
@@ -54,29 +200,99 @@
54200 "type": "github"
55201 }
56202 },
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+ },
57275 "root": {
58276 "inputs": {
59− "crane": "crane",
60− "flake-utils": "flake-utils",
277+ "nci": "nci",
61278 "nixpkgs": "nixpkgs",
279+ "parts": "parts_2",
62280 "rust-overlay": "rust-overlay"
63281 }
64282 },
65283 "rust-overlay": {
66284 "inputs": {
67− "flake-utils": [
68− "flake-utils"
69− ],
285+ "flake-utils": "flake-utils",
70286 "nixpkgs": [
71287 "nixpkgs"
72288 ]
73289 },
74290 "locked": {
75− "lastModified": 1701137803,
76− "narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
291+ "lastModified": 1683771545,
292+ "narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=",
77293 "owner": "oxalica",
78294 "repo": "rust-overlay",
79− "rev": "9dd940c967502f844eacea52a61e9596268d4f70",
295+ "rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed",
80296 "type": "github"
81297 },
82298 "original": {
flake.nix+143−170
@@ -3,193 +3,166 @@
33
44 inputs = {
55 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6− flake-utils.url = "github:numtide/flake-utils";
76 rust-overlay = {
87 url = "github:oxalica/rust-overlay";
9− inputs = {
10− nixpkgs.follows = "nixpkgs";
11− flake-utils.follows = "flake-utils";
12− };
8+ inputs.nixpkgs.follows = "nixpkgs";
139 };
14− crane = {
15− url = "github:ipetkov/crane";
10+ nci = {
11+ url = "github:yusdacra/nix-cargo-integration";
1612 inputs.nixpkgs.follows = "nixpkgs";
13+ inputs.rust-overlay.follows = "rust-overlay";
1714 };
15+ parts.url = "github:hercules-ci/flake-parts";
1816 };
1917
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)];
18+ outputs = inp: let
19+ mkRootPath = rel:
20+ builtins.path {
21+ path = "${toString ./.}/${rel}";
22+ name = rel;
3223 };
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;
68− in
69− lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
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;
7056 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 {};
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;
64+ };
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+ '';
107+ in
108+ helix-wrapped
109+ // {
110+ override = makeOverridableHelix old;
91111 passthru =
92− (old.passthru or {})
112+ helix-wrapped.passthru
93113 // {
94− unwrapped = old;
114+ wrapper = old: makeOverridableHelix old config;
95115 };
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;
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";
111136 };
137+ disable-tests = {checkPhase = ":";};
138+ set-stdenv.override = _: {inherit stdenv;};
139+ set-filtered-src.override = _: {src = filteredSource;};
140+ };
112141 };
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− };
152142
153− checks = {
154− # Build the crate itself
155− inherit (self.packages.${system}) helix;
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;
156148
157− clippy = craneLibMSRV.cargoClippy (commonArgs
158− // {
159− inherit cargoArtifacts;
160− cargoClippyExtraArgs = "--all-targets -- --deny warnings";
161− });
162−
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− };
149+ overlayAttrs = {
150+ inherit (config.packages) helix;
151+ };
175152
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;
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+ });
193166 };
194167 };
195168
grammars.nix+21−24
@@ -5,7 +5,6 @@
55 runCommand,
66 yj,
77 includeGrammarIf ? _: true,
8− grammarOverlays ? [],
98 ...
109}: let
1110 # HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
@@ -49,22 +48,22 @@
4948 then sourceGitHub
5049 else sourceGit;
5150 in
52− stdenv.mkDerivation {
51+ stdenv.mkDerivation rec {
5352 # see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
5453
5554 pname = "helix-tree-sitter-${grammar.name}";
5655 version = grammar.source.rev;
5756
58− src = source;
59− sourceRoot = if builtins.hasAttr "subpath" grammar.source then
60− "source/${grammar.source.subpath}"
61− else
62− "source";
57+ src =
58+ if builtins.hasAttr "subpath" grammar.source
59+ then "${source}/${grammar.source.subpath}"
60+ else source;
6361
62+ dontUnpack = true;
6463 dontConfigure = true;
6564
6665 FLAGS = [
67− "-Isrc"
66+ "-I${src}/src"
6867 "-g"
6968 "-O3"
7069 "-fPIC"
@@ -77,13 +76,13 @@
7776 buildPhase = ''
7877 runHook preBuild
7978
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
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
8483 fi
8584
86− $CC -c src/parser.c -o parser.o $FLAGS
85+ $CC -c "$src/src/parser.c" -o parser.o $FLAGS
8786 $CXX -shared -o $NAME.so *.o
8887
8988 ls -al
@@ -106,17 +105,15 @@
106105 '';
107106 };
108107 grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
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);
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;
120117in
121118 runCommand "consolidated-helix-grammars" {} ''
122119 mkdir -p $out
helix-core/Cargo.toml+16−20
@@ -1,38 +1,37 @@
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"
37description = "Helix editor core editing primitives"
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
411include = ["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
1312
1413[features]
1514unicode-lines = ["ropey/unicode_lines"]
1615integration = []
1716
1817[dependencies]
19−helix-loader = { path = "../helix-loader" }
18+helix-loader = { version = "0.6", path = "../helix-loader" }
2019
21−ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
22−smallvec = "1.11"
20+ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
21+smallvec = "1.10"
2322smartstring = "1.0.1"
2423unicode-segmentation = "1.10"
2524unicode-width = "0.1"
2625unicode-general-category = "0.6"
2726# slab = "0.4.2"
2827slotmap = "1.0"
29−tree-sitter.workspace = true
30−once_cell = "1.19"
28+tree-sitter = "0.20"
29+once_cell = "1.18"
3130arc-swap = "1"
3231regex = "1"
33−bitflags = "2.4"
34−ahash = "0.8.6"
35−hashbrown = { version = "0.14.3", features = ["raw"] }
32+bitflags = "2.3"
33+ahash = "0.8.3"
34+hashbrown = { version = "0.14.0", features = ["raw"] }
3635dunce = "1.0"
3736
3837log = "0.4"
@@ -49,9 +48,6 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
4948etcetera = "0.8"
5049textwrap = "0.16.0"
5150
52−nucleo.workspace = true
53−parking_lot = "0.12"
54−
5551[dev-dependencies]
5652quickcheck = { version = "1", default-features = false }
57−indoc = "2.0.4"
53+indoc = "2.0.2"
helix-core/src/diagnostic.rs−4
@@ -39,10 +39,6 @@ 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,
4642 pub line: usize,
4743 pub message: String,
4844 pub severity: Option<Severity>,
helix-core/src/fuzzy.rsdeleted−43
@@ -1,43 +0,0 @@
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) | Self::MASK_OWNED,
484+ len: i32::try_from(len).unwrap() as u32,
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("".into())),
76− inversion: Transaction::from(ChangeSet::new("".into())),
75+ transaction: Transaction::from(ChangeSet::new(&Rope::new())),
76+ inversion: Transaction::from(ChangeSet::new(&Rope::new())),
7777 timestamp: Instant::now(),
7878 }],
7979 current: 0,
helix-core/src/indent.rs+127−586
@@ -1,14 +1,13 @@
1−use std::{borrow::Cow, collections::HashMap};
1+use std::collections::HashMap;
22
33use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
44
55use crate::{
66 chars::{char_is_line_ending, char_is_whitespace},
7− find_first_non_whitespace_char,
8− graphemes::{grapheme_width, tab_width_at},
9− syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
7+ graphemes::tab_width_at,
8+ syntax::{LanguageConfiguration, RopeProvider, Syntax},
109 tree_sitter::Node,
11− Position, Rope, RopeGraphemes, RopeSlice,
10+ Rope, RopeSlice,
1211};
1312
1413/// Enum representing indentation style.
@@ -22,7 +21,7 @@ pub enum IndentStyle {
2221
2322// 16 spaces
2423const INDENTS: &str = " ";
25−pub const MAX_INDENT: u8 = 16;
24+const MAX_INDENT: u8 = 16;
2625
2726impl IndentStyle {
2827 /// Creates an `IndentStyle` from an indentation string.
@@ -197,56 +196,6 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
197196 len / indent_width
198197}
199198
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−
250199/// Computes for node and all ancestors whether they are the first node on their line.
251200/// The first entry in the return value represents the root node, the last one the node itself
252201fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
@@ -288,140 +237,68 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
288237/// This is usually constructed in one of 2 ways:
289238/// - Successively add indent captures to get the (added) indent from a single line
290239/// - Successively add the indent results for each line
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> {
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.)
296244 indent: usize,
297− indent_always: usize,
298245 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>>,
303246}
304−
305−impl<'a> Indentation<'a> {
247+impl Indentation {
306248 /// Add some other [Indentation] to this.
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;
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;
317255 }
318− self.indent += added.indent;
319− self.indent_always += added.indent_always;
320− self.outdent += added.outdent;
321− self.outdent_always += added.outdent_always;
322256 }
323−
324257 /// Add an indent capture to this indent.
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>) {
258+ /// All the captures that are added in this way should be on the same line.
259+ fn add_capture(&mut self, added: IndentCaptureType) {
329260 match added {
330261 IndentCaptureType::Indent => {
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;
262+ self.indent = 1;
340263 }
341264 IndentCaptureType::Outdent => {
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− }
265+ self.outdent = 1;
354266 }
355267 }
356268 }
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− ))
269+ fn as_string(&self, indent_style: &IndentStyle) -> String {
270+ let indent_level = if self.indent >= self.outdent {
271+ self.indent - self.outdent
380272 } else {
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− )
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)
393277 }
394278}
395279
396280/// An indent definition which corresponds to a capture from the indent query
397−#[derive(Debug)]
398−struct IndentCapture<'a> {
399− capture_type: IndentCaptureType<'a>,
281+struct IndentCapture {
282+ capture_type: IndentCaptureType,
400283 scope: IndentScope,
401284}
402−#[derive(Debug, Clone, PartialEq)]
403−enum IndentCaptureType<'a> {
285+#[derive(Clone, Copy)]
286+enum IndentCaptureType {
404287 Indent,
405− IndentAlways,
406288 Outdent,
407− OutdentAlways,
408− /// Alignment given as a string of whitespace
409− Align(RopeSlice<'a>),
410289}
411−
412−impl<'a> IndentCaptureType<'a> {
290+impl IndentCaptureType {
413291 fn default_scope(&self) -> IndentScope {
414292 match self {
415− IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
416− IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
417− IndentCaptureType::Align(_) => IndentScope::All,
293+ IndentCaptureType::Indent => IndentScope::Tail,
294+ IndentCaptureType::Outdent => IndentScope::All,
418295 }
419296 }
420297}
421298/// This defines which part of a node an [IndentCapture] applies to.
422299/// Each [IndentCaptureType] has a default scope, but the scope can be changed
423300/// with `#set!` property declarations.
424−#[derive(Debug, Clone, Copy)]
301+#[derive(Clone, Copy)]
425302enum IndentScope {
426303 /// The indent applies to the whole node
427304 All,
@@ -431,7 +308,6 @@ enum IndentScope {
431308
432309/// A capture from the indent query which does not define an indent but extends
433310/// the range of a node. This is used before the indent is calculated.
434−#[derive(Debug)]
435311enum ExtendCapture {
436312 Extend,
437313 PreventOnce,
@@ -440,41 +316,24 @@ enum ExtendCapture {
440316/// The result of running a tree-sitter indent query. This stores for
441317/// each node (identified by its ID) the relevant captures (already filtered
442318/// by predicates).
443−#[derive(Debug)]
444−struct IndentQueryResult<'a> {
445− indent_captures: HashMap<usize, Vec<IndentCapture<'a>>>,
319+struct IndentQueryResult {
320+ indent_captures: HashMap<usize, Vec<IndentCapture>>,
446321 extend_captures: HashMap<usize, Vec<ExtendCapture>>,
447322}
448323
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>(
324+fn query_indents(
467325 query: &Query,
468326 syntax: &Syntax,
469327 cursor: &mut QueryCursor,
470− text: RopeSlice<'a>,
328+ text: RopeSlice,
471329 range: std::ops::Range<usize>,
472− new_line_byte_pos: Option<usize>,
473−) -> IndentQueryResult<'a> {
330+ // Position of the (optional) newly inserted line break.
331+ // Given as (line, byte_pos)
332+ new_line_break: Option<(usize, usize)>,
333+) -> IndentQueryResult {
474334 let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
475335 let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
476336 cursor.set_byte_range(range);
477−
478337 // Iterate over all captures from the query
479338 for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
480339 // Skip matches where not all custom predicates are fulfilled
@@ -501,13 +360,21 @@ fn query_indents<'a>(
501360 Some(QueryPredicateArg::Capture(capt1)),
502361 Some(QueryPredicateArg::Capture(capt2))
503362 ) => {
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+ };
504373 let n1 = m.nodes_for_capture_index(*capt1).next();
505374 let n2 = m.nodes_for_capture_index(*capt2).next();
506375 match (n1, n2) {
507376 (Some(n1), Some(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;
377+ let same_line = get_line_num(n1)==get_line_num(n2);
511378 same_line==(pred.operator.as_ref()=="same-line?")
512379 }
513380 _ => true,
@@ -518,23 +385,6 @@ fn query_indents<'a>(
518385 }
519386 }
520387 }
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− },
538388 _ => {
539389 panic!(
540390 "Invalid indent query: Unknown predicate (\"{}\")",
@@ -545,28 +395,11 @@ fn query_indents<'a>(
545395 }) {
546396 continue;
547397 }
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;
553398 for capture in m.captures {
554399 let capture_name = query.capture_names()[capture.index as usize].as_str();
555400 let capture_type = match capture_name {
556401 "indent" => IndentCaptureType::Indent,
557− "indent.always" => IndentCaptureType::IndentAlways,
558402 "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− }
570403 "extend" => {
571404 extend_captures
572405 .entry(capture.node.id())
@@ -616,40 +449,17 @@ fn query_indents<'a>(
616449 }
617450 }
618451 }
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− }
638452 indent_captures
639− .entry(node_id)
453+ .entry(capture.node.id())
454+ // Most entries only need to contain a single IndentCapture
640455 .or_insert_with(|| Vec::with_capacity(1))
641− .push(capture);
456+ .push(indent_capture);
642457 }
643458 }
644−
645− let result = IndentQueryResult {
459+ IndentQueryResult {
646460 indent_captures,
647461 extend_captures,
648− };
649−
650− log::trace!("indent result = {:?}", result);
651−
652− result
462+ }
653463}
654464
655465/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
@@ -719,27 +529,61 @@ fn extend_nodes<'a>(
719529 }
720530}
721531
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.
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+/// ```
725568#[allow(clippy::too_many_arguments)]
726−fn init_indent_query<'a, 'b>(
569+pub fn treesitter_indent_for_pos(
727570 query: &Query,
728− syntax: &'a Syntax,
729− text: RopeSlice<'b>,
571+ syntax: &Syntax,
572+ indent_style: &IndentStyle,
730573 tab_width: usize,
731574 indent_width: usize,
575+ text: RopeSlice,
732576 line: usize,
733− byte_pos: usize,
734− new_line_byte_pos: Option<usize>,
735−) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
577+ pos: usize,
578+ new_line: bool,
579+) -> Option<String> {
580+ let byte_pos = text.char_to_byte(pos);
736581 // The innermost tree-sitter node which is considered for the indent
737582 // computation. It may change if some predeceding node is extended
738583 let mut node = syntax
739584 .tree()
740585 .root_node()
741586 .descendant_for_byte_range(byte_pos, byte_pos)?;
742−
743587 let (query_result, deepest_preceding) = {
744588 // The query range should intersect with all nodes directly preceding
745589 // the position of the indent query in case one of them is extended.
@@ -770,12 +614,13 @@ fn init_indent_query<'a, 'b>(
770614 &mut cursor,
771615 text,
772616 query_range,
773− new_line_byte_pos,
617+ new_line.then_some((line, byte_pos)),
774618 );
775619 ts_parser.cursors.push(cursor);
776620 (query_result, deepest_preceding)
777621 })
778622 };
623+ let indent_captures = query_result.indent_captures;
779624 let extend_captures = query_result.extend_captures;
780625
781626 // Check for extend captures, potentially changing the node that the indent calculation starts with
@@ -790,68 +635,6 @@ fn init_indent_query<'a, 'b>(
790635 indent_width,
791636 );
792637 }
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− )?;
855638 let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
856639
857640 let mut result = Indentation::default();
@@ -859,16 +642,12 @@ pub fn treesitter_indent_for_pos<'a>(
859642 // even if there are multiple "indent" nodes on the same line
860643 let mut indent_for_line = Indentation::default();
861644 let mut indent_for_line_below = Indentation::default();
862−
863645 loop {
864646 // This can safely be unwrapped because `first_in_line` contains
865647 // one entry for each ancestor of the node (which is what we iterate over)
866648 let is_first = *first_in_line.last().unwrap();
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()) {
649+ // Apply all indent definitions for this node
650+ if let Some(definitions) = indent_captures.get(&node.id()) {
872651 for definition in definitions {
873652 match definition.scope {
874653 IndentScope::All => {
@@ -886,22 +665,28 @@ pub fn treesitter_indent_for_pos<'a>(
886665 }
887666
888667 if let Some(parent) = node.parent() {
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−
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+ };
892679 if node_line != parent_line {
893− // Don't add indent for the line below the line of the query
894680 if node_line < line + (new_line as usize) {
895− result.add_line(indent_for_line_below);
681+ // Don't add indent for the line below the line of the query
682+ result.add_line(&indent_for_line_below);
896683 }
897−
898684 if node_line == parent_line + 1 {
899685 indent_for_line_below = indent_for_line;
900686 } else {
901− result.add_line(indent_for_line);
687+ result.add_line(&indent_for_line);
902688 indent_for_line_below = Indentation::default();
903689 }
904−
905690 indent_for_line = Indentation::default();
906691 }
907692
@@ -913,13 +698,13 @@ pub fn treesitter_indent_for_pos<'a>(
913698 if (node.start_position().row < line)
914699 || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
915700 {
916− result.add_line(indent_for_line_below);
701+ result.add_line(&indent_for_line_below);
917702 }
918− result.add_line(indent_for_line);
703+ result.add_line(&indent_for_line);
919704 break;
920705 }
921706 }
922− Some(result)
707+ Some(result.as_string(indent_style))
923708}
924709
925710/// Returns the indentation for a new line.
@@ -928,7 +713,6 @@ pub fn treesitter_indent_for_pos<'a>(
928713pub fn indent_for_newline(
929714 language_config: Option<&LanguageConfiguration>,
930715 syntax: Option<&Syntax>,
931− indent_heuristic: &IndentationHeuristic,
932716 indent_style: &IndentStyle,
933717 tab_width: usize,
934718 text: RopeSlice,
@@ -937,18 +721,14 @@ pub fn indent_for_newline(
937721 current_line: usize,
938722) -> String {
939723 let indent_width = indent_style.indent_width(tab_width);
940− if let (
941− IndentationHeuristic::TreeSitter | IndentationHeuristic::Hybrid,
942− Some(query),
943− Some(syntax),
944− ) = (
945− indent_heuristic,
724+ if let (Some(query), Some(syntax)) = (
946725 language_config.and_then(|config| config.indent_query()),
947726 syntax,
948727 ) {
949728 if let Some(indent) = treesitter_indent_for_pos(
950729 query,
951730 syntax,
731+ indent_style,
952732 tab_width,
953733 indent_width,
954734 text,
@@ -956,57 +736,9 @@ pub fn indent_for_newline(
956736 line_before_end_pos,
957737 true,
958738 ) {
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);
739+ return indent;
1007740 };
1008741 }
1009− // Fallback in case we either don't have indent queries or they failed for some reason
1010742 let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
1011743 indent_style.as_str().repeat(indent_level)
1012744}
@@ -1078,195 +810,4 @@ mod test {
1078810 2
1079811 );
1080812 }
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− }
1272813}
helix-core/src/lib.rs+1−3
@@ -7,7 +7,6 @@ pub mod config;
77pub mod diagnostic;
88pub mod diff;
99pub mod doc_formatter;
10−pub mod fuzzy;
1110pub mod graphemes;
1211pub mod history;
1312pub mod increment;
@@ -19,6 +18,7 @@ pub mod movement;
1918pub mod object;
2019pub mod path;
2120mod position;
21+pub mod register;
2222pub mod search;
2323pub mod selection;
2424pub mod shellwords;
@@ -41,9 +41,7 @@ pub use helix_loader::find_workspace;
4141pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
4242 line.chars().position(|ch| !ch.is_whitespace())
4343}
44−mod rope_reader;
4544
46−pub use rope_reader::RopeReader;
4745pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
4846
4947// pub use tendril::StrTendril as Tendril;
helix-core/src/match_brackets.rs+2−6
@@ -106,16 +106,12 @@ 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 {
110− continue;
111− };
109+ let Some(open) = as_close_pair(doc, &close) else { continue; };
112110 if find_pair_end(doc, Some(node), open, Backward).is_some() {
113111 return doc.try_byte_to_char(close.start_byte()).ok();
114112 }
115113 }
116− let Some(parent) = node.parent() else {
117− break;
118− };
114+ let Some(parent) = node.parent() else { break; };
119115 node = parent;
120116 }
121117 let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
helix-core/src/movement.rs+1−80
@@ -16,7 +16,7 @@ use crate::{
1616 syntax::LanguageConfiguration,
1717 text_annotations::TextAnnotations,
1818 textobject::TextObject,
19− visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
19+ visual_offset_from_block, Range, RopeSlice,
2020};
2121
2222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -556,85 +556,6 @@ 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−
638559#[cfg(test)]
639560mod test {
640561 use ropey::Rope;
helix-core/src/path.rs+7−5
@@ -85,21 +85,23 @@ 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) -> PathBuf {
88+pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
8989 let path = expand_tilde(path);
9090 let path = if path.is_relative() {
91− helix_loader::current_working_dir().join(path)
91+ std::env::current_dir().map(|current_dir| current_dir.join(path))?
9292 } else {
9393 path
9494 };
9595
96− get_normalized_path(path.as_path())
96+ Ok(get_normalized_path(path.as_path()))
9797}
9898
9999pub fn get_relative_path(path: &Path) -> PathBuf {
100100 let path = PathBuf::from(path);
101101 let path = if path.is_absolute() {
102− let cwdir = get_normalized_path(&helix_loader::current_working_dir());
102+ let cwdir = std::env::current_dir()
103+ .map(|path| get_normalized_path(&path))
104+ .expect("couldn't determine current directory");
103105 get_normalized_path(&path)
104106 .strip_prefix(cwdir)
105107 .map(PathBuf::from)
@@ -140,7 +142,7 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
140142/// ```
141143///
142144pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
143− let cwd = helix_loader::current_working_dir();
145+ let cwd = std::env::current_dir().unwrap_or_default();
144146 let path = path
145147 .as_ref()
146148 .strip_prefix(cwd)
helix-core/src/register.rsnew file+89
@@ -0,0 +1,89 @@
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/rope_reader.rsdeleted−37
@@ -1,37 +0,0 @@
1−use std::io;
2−
3−use ropey::iter::Chunks;
4−use ropey::RopeSlice;
5−
6−pub struct RopeReader<'a> {
7− current_chunk: &'a [u8],
8− chunks: Chunks<'a>,
9−}
10−
11−impl<'a> RopeReader<'a> {
12− pub fn new(rope: RopeSlice<'a>) -> RopeReader<'a> {
13− RopeReader {
14− current_chunk: &[],
15− chunks: rope.chunks(),
16− }
17− }
18−}
19−
20−impl io::Read for RopeReader<'_> {
21− fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
22− let buf_len = buf.len();
23− loop {
24− let read_bytes = self.current_chunk.read(buf)?;
25− buf = &mut buf[read_bytes..];
26− if buf.is_empty() {
27− return Ok(buf_len);
28− }
29−
30− if let Some(next_chunk) = self.chunks.next() {
31− self.current_chunk = next_chunk.as_bytes();
32− } else {
33− return Ok(buf_len - buf.len());
34− }
35− }
36− }
37−}
helix-core/src/selection.rs+2−10
@@ -630,19 +630,11 @@ impl Selection {
630630 self.transform(|range| Range::point(range.cursor(text)))
631631 }
632632
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− {
633+ pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
638634 self.ranges.iter().map(move |range| range.fragment(text))
639635 }
640636
641− pub fn slices<'a>(
642− &'a self,
643− text: RopeSlice<'a>,
644− ) -> impl DoubleEndedIterator<Item = RopeSlice<'a>> + ExactSizeIterator<Item = RopeSlice<'a>> + 'a
645− {
637+ pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a {
646638 self.ranges.iter().map(move |range| range.slice(text))
647639 }
648640
helix-core/src/syntax.rs+80−88
@@ -4,7 +4,7 @@ use crate::{
44 diagnostic::Severity,
55 regex::Regex,
66 transaction::{ChangeSet, Operation},
7− RopeSlice, Tendril,
7+ Rope, RopeSlice, Tendril,
88};
99
1010use ahash::RandomState;
@@ -101,8 +101,7 @@ 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− #[serde(default)]
105− pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
104+ pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
106105 pub comment_token: Option<String>,
107106 pub text_width: Option<usize>,
108107 pub soft_wrap: Option<SoftWrap>,
@@ -155,8 +154,6 @@ pub struct LanguageConfiguration {
155154 /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
156155 /// Falling back to the current working directory if none are configured.
157156 pub workspace_lsp_roots: Option<Vec<PathBuf>>,
158− #[serde(default)]
159− pub persistent_diagnostic_sources: Vec<String>,
160157}
161158
162159#[derive(Debug, PartialEq, Eq, Hash)]
@@ -214,7 +211,10 @@ impl<'de> Deserialize<'de> for FileType {
214211 {
215212 match map.next_entry::<String, String>()? {
216213 Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
217− suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
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))
218218 })),
219219 Some((key, _value)) => Err(serde::de::Error::custom(format!(
220220 "unknown key in `file-types` list: {}",
@@ -444,22 +444,6 @@ 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−
463447/// Configuration for auto pairs
464448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465449#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
@@ -834,10 +818,7 @@ impl Loader {
834818 // TODO: content_regex handling conflict resolution
835819 }
836820
837− pub fn language_config_for_shebang(
838− &self,
839− source: RopeSlice,
840− ) -> Option<Arc<LanguageConfiguration>> {
821+ pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
841822 let line = Cow::from(source.line(0));
842823 static SHEBANG_REGEX: Lazy<Regex> =
843824 Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap());
@@ -947,7 +928,7 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
947928
948929impl Syntax {
949930 pub fn new(
950− source: RopeSlice,
931+ source: &Rope,
951932 config: Arc<HighlightConfiguration>,
952933 loader: Arc<Loader>,
953934 ) -> Option<Self> {
@@ -978,7 +959,7 @@ impl Syntax {
978959 let res = syntax.update(source, source, &ChangeSet::new(source));
979960
980961 if res.is_err() {
981− log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
962+ log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
982963 return None;
983964 }
984965 Some(syntax)
@@ -986,8 +967,8 @@ impl Syntax {
986967
987968 pub fn update(
988969 &mut self,
989− old_source: RopeSlice,
990− source: RopeSlice,
970+ old_source: &Rope,
971+ source: &Rope,
991972 changeset: &ChangeSet,
992973 ) -> Result<(), Error> {
993974 let mut queue = VecDeque::new();
@@ -1154,38 +1135,12 @@ impl Syntax {
11541135 layer.tree().root_node(),
11551136 RopeProvider(source_slice),
11561137 );
1157− let mut combined_injections = vec![
1158− (None, Vec::new(), IncludedChildren::default());
1159− layer.config.combined_injections_patterns.len()
1160− ];
11611138 let mut injections = Vec::new();
1162− let mut last_injection_end = 0;
11631139 for mat in matches {
11641140 let (injection_capture, content_node, included_children) = layer
11651141 .config
11661142 .injection_for_match(&layer.config.injections_query, &mat, source_slice);
11671143
1168− // in case this is a combined injection save it for more processing later
1169− if let Some(combined_injection_idx) = layer
1170− .config
1171− .combined_injections_patterns
1172− .iter()
1173− .position(|&pattern| pattern == mat.pattern_index)
1174− {
1175− let entry = &mut combined_injections[combined_injection_idx];
1176− if injection_capture.is_some() {
1177− entry.0 = injection_capture;
1178− }
1179− if let Some(content_node) = content_node {
1180− if content_node.start_byte() >= last_injection_end {
1181− entry.1.push(content_node);
1182− last_injection_end = content_node.end_byte();
1183− }
1184− }
1185− entry.2 = included_children;
1186− continue;
1187− }
1188−
11891144 // Explicitly remove this match so that none of its other captures will remain
11901145 // in the stream of captures.
11911146 mat.remove();
@@ -1200,23 +1155,49 @@ impl Syntax {
12001155 intersect_ranges(&layer.ranges, &[content_node], included_children);
12011156
12021157 if !ranges.is_empty() {
1203− if content_node.start_byte() < last_injection_end {
1204− continue;
1205− }
1206− last_injection_end = content_node.end_byte();
12071158 injections.push((config, ranges));
12081159 }
12091160 }
12101161 }
12111162 }
12121163
1213− for (lang_name, content_nodes, included_children) in combined_injections {
1214− if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
1215− if let Some(config) = (injection_callback)(&lang_name) {
1216− let ranges =
1217− intersect_ranges(&layer.ranges, &content_nodes, included_children);
1218− if !ranges.is_empty() {
1219− injections.push((config, ranges));
1164+ // Process combined injections.
1165+ if let Some(combined_injections_query) = &layer.config.combined_injections_query {
1166+ let mut injections_by_pattern_index =
1167+ vec![
1168+ (None, Vec::new(), IncludedChildren::default());
1169+ combined_injections_query.pattern_count()
1170+ ];
1171+ let matches = cursor.matches(
1172+ combined_injections_query,
1173+ layer.tree().root_node(),
1174+ RopeProvider(source_slice),
1175+ );
1176+ for mat in matches {
1177+ let entry = &mut injections_by_pattern_index[mat.pattern_index];
1178+ let (injection_capture, content_node, included_children) = layer
1179+ .config
1180+ .injection_for_match(combined_injections_query, &mat, source_slice);
1181+ if injection_capture.is_some() {
1182+ entry.0 = injection_capture;
1183+ }
1184+ if let Some(content_node) = content_node {
1185+ entry.1.push(content_node);
1186+ }
1187+ entry.2 = included_children;
1188+ }
1189+ for (lang_name, content_nodes, included_children) in injections_by_pattern_index
1190+ {
1191+ if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
1192+ if let Some(config) = (injection_callback)(&lang_name) {
1193+ let ranges = intersect_ranges(
1194+ &layer.ranges,
1195+ &content_nodes,
1196+ included_children,
1197+ );
1198+ if !ranges.is_empty() {
1199+ injections.push((config, ranges));
1200+ }
12201201 }
12211202 }
12221203 }
@@ -1406,7 +1387,7 @@ impl LanguageLayer {
14061387 self.tree.as_ref().unwrap()
14071388 }
14081389
1409− fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error> {
1390+ fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
14101391 parser
14111392 .set_included_ranges(&self.ranges)
14121393 .map_err(|_| Error::InvalidRanges)?;
@@ -1437,7 +1418,7 @@ impl LanguageLayer {
14371418}
14381419
14391420pub(crate) fn generate_edits(
1440− old_text: RopeSlice,
1421+ old_text: &Rope,
14411422 changeset: &ChangeSet,
14421423) -> Vec<tree_sitter::InputEdit> {
14431424 use Operation::*;
@@ -1453,7 +1434,7 @@ pub(crate) fn generate_edits(
14531434
14541435 // TODO; this is a lot easier with Change instead of Operation.
14551436
1456− fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
1437+ fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
14571438 let byte = text.char_to_byte(pos); // <- attempted to index past end
14581439 let line = text.char_to_line(pos);
14591440 let line_start_byte = text.line_to_byte(line);
@@ -1579,7 +1560,7 @@ pub struct HighlightConfiguration {
15791560 pub language: Grammar,
15801561 pub query: Query,
15811562 injections_query: Query,
1582− combined_injections_patterns: Vec<usize>,
1563+ combined_injections_query: Option<Query>,
15831564 highlights_pattern_index: usize,
15841565 highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
15851566 non_local_variable_patterns: Vec<bool>,
@@ -1630,7 +1611,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
16301611}
16311612
16321613pub struct RopeProvider<'a>(pub RopeSlice<'a>);
1633−impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
1614+impl<'a> TextProvider<'a> for RopeProvider<'a> {
16341615 type I = ChunksBytes<'a>;
16351616
16361617 fn text(&mut self, node: Node) -> Self::I {
@@ -1644,7 +1625,7 @@ impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
16441625struct HighlightIterLayer<'a> {
16451626 _tree: Option<Tree>,
16461627 cursor: QueryCursor,
1647− captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
1628+ captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
16481629 config: &'a HighlightConfiguration,
16491630 highlight_end_stack: Vec<usize>,
16501631 scope_stack: Vec<LocalScope<'a>>,
@@ -1695,15 +1676,26 @@ impl HighlightConfiguration {
16951676 }
16961677 }
16971678
1698− let injections_query = Query::new(language, injection_query)?;
1699− let combined_injections_patterns = (0..injections_query.pattern_count())
1700− .filter(|&i| {
1701− injections_query
1702− .property_settings(i)
1703− .iter()
1704− .any(|s| &*s.key == "injection.combined")
1705− })
1706− .collect();
1679+ let mut injections_query = Query::new(language, injection_query)?;
1680+
1681+ // Construct a separate query just for dealing with the 'combined injections'.
1682+ // Disable the combined injection patterns in the main query.
1683+ let mut combined_injections_query = Query::new(language, injection_query)?;
1684+ let mut has_combined_queries = false;
1685+ for pattern_index in 0..injections_query.pattern_count() {
1686+ let settings = injections_query.property_settings(pattern_index);
1687+ if settings.iter().any(|s| &*s.key == "injection.combined") {
1688+ has_combined_queries = true;
1689+ injections_query.disable_pattern(pattern_index);
1690+ } else {
1691+ combined_injections_query.disable_pattern(pattern_index);
1692+ }
1693+ }
1694+ let combined_injections_query = if has_combined_queries {
1695+ Some(combined_injections_query)
1696+ } else {
1697+ None
1698+ };
17071699
17081700 // Find all of the highlighting patterns that are disabled for nodes that
17091701 // have been identified as local variables.
@@ -1752,7 +1744,7 @@ impl HighlightConfiguration {
17521744 language,
17531745 query,
17541746 injections_query,
1755− combined_injections_patterns,
1747+ combined_injections_query,
17561748 highlights_pattern_index,
17571749 highlight_indices,
17581750 non_local_variable_patterns,
@@ -2548,7 +2540,7 @@ mod test {
25482540 let mut cursor = QueryCursor::new();
25492541
25502542 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2551− let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
2543+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
25522544
25532545 let root = syntax.tree().root_node();
25542546 let mut test = |capture, range| {
@@ -2622,7 +2614,7 @@ mod test {
26222614 fn main() {}
26232615 ",
26242616 );
2625− let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
2617+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
26262618 let tree = syntax.tree();
26272619 let root = tree.root_node();
26282620 assert_eq!(root.kind(), "source_file");
@@ -2649,7 +2641,7 @@ mod test {
26492641 &doc,
26502642 vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
26512643 );
2652− let edits = generate_edits(doc.slice(..), transaction.changes());
2644+ let edits = generate_edits(&doc, transaction.changes());
26532645 // transaction.apply(&mut state);
26542646
26552647 assert_eq!(
@@ -2678,7 +2670,7 @@ mod test {
26782670 let mut doc = Rope::from("fn test() {}");
26792671 let transaction =
26802672 Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
2681− let edits = generate_edits(doc.slice(..), transaction.changes());
2673+ let edits = generate_edits(&doc, transaction.changes());
26822674 transaction.apply(&mut doc);
26832675
26842676 assert_eq!(doc, "fn test(a: u32) {}");
@@ -2712,7 +2704,7 @@ mod test {
27122704 let language = get_language(language_name).unwrap();
27132705
27142706 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2715− let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
2707+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap();
27162708
27172709 let root = syntax
27182710 .tree()
helix-core/src/transaction.rs+21−84
@@ -1,7 +1,6 @@
1−use ropey::RopeSlice;
21use smallvec::SmallVec;
32
4−use crate::{chars::char_is_word, Range, Rope, Selection, Tendril};
3+use crate::{Range, Rope, Selection, Tendril};
54use std::{borrow::Cow, iter::once};
65
76/// (from, to, replacement)
@@ -23,30 +22,6 @@ pub enum Operation {
2322pub enum Assoc {
2423 Before,
2524 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− }
5025}
5126
5227#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -67,7 +42,7 @@ impl ChangeSet {
6742 }
6843
6944 #[must_use]
70− pub fn new(doc: RopeSlice) -> Self {
45+ pub fn new(doc: &Rope) -> Self {
7146 let len = doc.len_chars();
7247 Self {
7348 changes: Vec::new(),
@@ -413,10 +388,7 @@ impl ChangeSet {
413388 }
414389
415390 let Some((i, change)) = iter.next() else {
416− map!(
417− |pos, _| (old_pos == pos).then_some(new_pos),
418− self.changes.len()
419− );
391+ map!(|pos, _| (old_pos == pos).then_some(new_pos), self.changes.len());
420392 break;
421393 };
422394
@@ -439,6 +411,8 @@ impl ChangeSet {
439411 map!(|pos, _| (old_end > pos).then_some(new_pos), i);
440412 }
441413 Insert(s) => {
414+ let ins = s.chars().count();
415+
442416 // a subsequent delete means a replace, consume it
443417 if let Some((_, Delete(len))) = iter.peek() {
444418 iter.next();
@@ -446,13 +420,13 @@ impl ChangeSet {
446420 old_end = old_pos + len;
447421 // in range of replaced text
448422 map!(
449− |pos, assoc: Assoc| (old_end > pos).then(|| {
423+ |pos, assoc| (old_end > pos).then(|| {
450424 // at point or tracking before
451− if pos == old_pos && assoc.stay_at_gaps() {
425+ if pos == old_pos || assoc == Assoc::Before {
452426 new_pos
453427 } else {
454428 // place to end of insert
455− new_pos + assoc.insert_offset(s)
429+ new_pos + ins
456430 }
457431 }),
458432 i
@@ -460,15 +434,20 @@ impl ChangeSet {
460434 } else {
461435 // at insert point
462436 map!(
463− |pos, assoc: Assoc| (old_pos == pos).then(|| {
437+ |pos, assoc| (old_pos == pos).then(|| {
464438 // return position before inserted text
465− new_pos + assoc.insert_offset(s)
439+ if assoc == Assoc::Before {
440+ new_pos
441+ } else {
442+ // after text
443+ new_pos + ins
444+ }
466445 }),
467446 i
468447 );
469448 }
470449
471− new_pos += s.chars().count();
450+ new_pos += ins;
472451 }
473452 }
474453 old_pos = old_end;
@@ -506,7 +485,7 @@ impl Transaction {
506485 /// Create a new, empty transaction.
507486 pub fn new(doc: &Rope) -> Self {
508487 Self {
509− changes: ChangeSet::new(doc.slice(..)),
488+ changes: ChangeSet::new(doc),
510489 selection: None,
511490 }
512491 }
@@ -901,48 +880,6 @@ mod test {
901880 let mut positions = [4, 2];
902881 cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
903882 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);
946883 }
947884
948885 #[test]
@@ -1009,9 +946,9 @@ mod test {
1009946 #[test]
1010947 fn combine_with_empty() {
1011948 let empty = Rope::from("");
1012− let a = ChangeSet::new(empty.slice(..));
949+ let a = ChangeSet::new(&empty);
1013950
1014− let mut b = ChangeSet::new(empty.slice(..));
951+ let mut b = ChangeSet::new(&empty);
1015952 b.insert("a".into());
1016953
1017954 let changes = a.compose(b);
@@ -1025,9 +962,9 @@ mod test {
1025962 const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
1026963
1027964 let empty = Rope::from("");
1028− let a = ChangeSet::new(empty.slice(..));
965+ let a = ChangeSet::new(&empty);
1029966
1030− let mut b = ChangeSet::new(empty.slice(..));
967+ let mut b = ChangeSet::new(&empty);
1031968 b.insert(TEST_CASE.into());
1032969
1033970 let changes = a.compose(b);
helix-core/src/wrap.rs+1−3
@@ -1,9 +1,7 @@
11use smartstring::{LazyCompact, SmartString};
2−use textwrap::{Options, WordSplitter::NoHyphenation};
32
43/// Given a slice of text, return the text re-wrapped to fit it
54/// within the given width.
65pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
7− let options = Options::new(text_width).word_splitter(NoHyphenation);
8− textwrap::refill(text, options).into()
6+ textwrap::refill(text, text_width).into()
97}
helix-core/tests/data/indent/cpp.cppdeleted−48
@@ -1,48 +0,0 @@
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.rsnew file+1
@@ -0,0 +1 @@
1+../../../src/indent.rs
helix-core/tests/data/indent/languages.toml−13
@@ -11,16 +11,3 @@ 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+25−153
@@ -1,122 +1,20 @@
11use helix_core::{
22 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
3− syntax::{Configuration, Loader},
3+ syntax::Loader,
44 Syntax,
55};
6−use ropey::Rope;
7−use std::{ops::Range, path::PathBuf, process::Command};
6+use std::path::PathBuf;
87
98#[test]
109fn test_treesitter_indent_rust() {
11− standard_treesitter_test("rust.rs", "source.rust");
10+ test_treesitter_indent("rust.rs", "source.rust");
1211}
13−
1412#[test]
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− }
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");
12018}
12119
12220#[test]
@@ -152,41 +50,20 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
15250 assert_eq!(indent_level, 2)
15351}
15452
155−fn indent_tests_dir() -> PathBuf {
53+fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
15654 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
15755 test_dir.push("tests/data/indent");
158− test_dir
159−}
16056
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();
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();
17760 let doc = ropey::Rope::from_reader(test_file).unwrap();
178− test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
179−}
18061
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());
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);
19067
19168 // set runtime path so we can find the queries
19269 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -194,35 +71,30 @@ fn test_treesitter_indent(
19471 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
19572
19673 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);
19874 let highlight_config = language_config.highlight_config(&[]).unwrap();
199− let text = doc.slice(..);
200− let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
75+ let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)).unwrap();
20176 let indent_query = language_config.indent_query().unwrap();
77+ let text = doc.slice(..);
20278
20379 for i in 0..doc.len_lines() {
20480 let line = text.line(i);
205− if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
206− continue;
207− }
20881 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
209− let tab_width: usize = 4;
82+ let tab_and_indent_width: usize = 4;
21083 let suggested_indent = treesitter_indent_for_pos(
21184 indent_query,
21285 &syntax,
213− tab_width,
214− indent_style.indent_width(tab_width),
86+ &IndentStyle::Spaces(tab_and_indent_width as u8),
87+ tab_and_indent_width,
88+ tab_and_indent_width,
21589 text,
21690 i,
21791 text.line_to_char(i) + pos,
21892 false,
21993 )
220− .unwrap()
221− .to_string(&indent_style, tab_width);
94+ .unwrap();
22295 assert!(
22396 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
224− "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
225− test_name,
97+ "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
22698 i+1,
22799 line.slice(..line.len_chars()-1),
228100 suggested_indent,
helix-dap/Cargo.toml+9−11
@@ -1,27 +1,25 @@
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"
37description = "DAP client implementation for Helix project"
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
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
1211
1312# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1413
1514[dependencies]
16−helix-core = { path = "../helix-core" }
17−
15+helix-core = { version = "0.6", path = "../helix-core" }
1816anyhow = "1.0"
1917log = "0.4"
2018serde = { version = "1.0", features = ["derive"] }
2119serde_json = "1.0"
2220thiserror = "1.0"
2321tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
24−which = "5.0.0"
22+which = "4.4"
2523
2624[dev-dependencies]
2725fern = "0.6"
helix-event/Cargo.tomldeleted−16
@@ -1,16 +0,0 @@
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.rsdeleted−8
@@ -1,8 +0,0 @@
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.rsdeleted−49
@@ -1,49 +0,0 @@
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+12−14
@@ -1,14 +1,13 @@
11[package]
22name = "helix-loader"
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
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"
1211
1312[[bin]]
1413name = "hx-loader"
@@ -19,18 +18,17 @@ anyhow = "1"
1918serde = { version = "1.0", features = ["derive"] }
2019toml = "0.7"
2120etcetera = "0.8"
22−tree-sitter.workspace = true
23−once_cell = "1.19"
21+tree-sitter = "0.20"
22+once_cell = "1.18"
2423log = "0.4"
25−which = "5.0.0"
24+which = "4.4"
2625
2726# TODO: these two should be on !wasm32 only
2827
2928# cloning/compiling tree-sitter grammars
3029cc = { version = "1" }
3130threadpool = { version = "1.0" }
32−tempfile = "3.8.1"
33−dunce = "1.0.4"
31+tempfile = "3.6.0"
3432
3533[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
3634libloading = "0.8"
helix-loader/build.rs+6−20
@@ -2,17 +2,7 @@ use std::borrow::Cow;
22use std::path::Path;
33use std::process::Command;
44
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−}
5+const VERSION: &str = include_str!("../VERSION");
166
177fn main() {
188 let git_hash = Command::new("git")
@@ -22,10 +12,9 @@ fn main() {
2212 .filter(|output| output.status.success())
2313 .and_then(|x| String::from_utf8(x.stdout).ok());
2414
25− let calver = get_calver();
2615 let version: Cow<_> = match &git_hash {
27− Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(),
28− None => calver.into(),
16+ Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
17+ None => VERSION.into(),
2918 };
3019
3120 println!(
@@ -33,6 +22,7 @@ fn main() {
3322 std::env::var("TARGET").unwrap()
3423 );
3524
25+ println!("cargo:rerun-if-changed=../VERSION");
3626 println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
3727
3828 if git_hash.is_none() {
@@ -50,9 +40,7 @@ fn main() {
5040 .ok()
5141 .filter(|output| output.status.success())
5242 .and_then(|x| String::from_utf8(x.stdout).ok())
53− else {
54− return;
55− };
43+ else{ return; };
5644 // If heads starts pointing at something else (different branch)
5745 // we need to return
5846 let head = Path::new(&git_dir).join("HEAD");
@@ -67,9 +55,7 @@ fn main() {
6755 .ok()
6856 .filter(|output| output.status.success())
6957 .and_then(|x| String::from_utf8(x.stdout).ok())
70− else {
71− return;
72− };
58+ else{ return; };
7359 let head_ref = Path::new(&git_dir).join(head_ref);
7460 if head_ref.exists() {
7561 println!("cargo:rerun-if-changed={}", head_ref.display());
helix-loader/src/lib.rs+3−52
@@ -3,12 +3,9 @@ pub mod grammar;
33
44use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
55use std::path::{Path, PathBuf};
6−use std::sync::RwLock;
76
87pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
98
10−static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
11−
129static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
1310 once_cell::sync::Lazy::new(prioritize_runtime_dirs);
1411
@@ -16,31 +13,6 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe
1613
1714static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
1815
19−// Get the current working directory.
20−// This information is managed internally as the call to std::env::current_dir
21−// might fail if the cwd has been deleted.
22−pub fn current_working_dir() -> PathBuf {
23− if let Some(path) = &*CWD.read().unwrap() {
24− return path.clone();
25− }
26−
27− let path = std::env::current_dir()
28− .and_then(dunce::canonicalize)
29− .expect("Couldn't determine current working directory");
30− let mut cwd = CWD.write().unwrap();
31− *cwd = Some(path.clone());
32−
33− path
34−}
35−
36−pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
37− let path = dunce::canonicalize(path)?;
38− std::env::set_current_dir(&path)?;
39− let mut cwd = CWD.write().unwrap();
40− *cwd = Some(path);
41− Ok(())
42−}
43−
4416pub fn initialize_config_file(specified_file: Option<PathBuf>) {
4517 let config_file = specified_file.unwrap_or_else(default_config_file);
4618 ensure_parent_dir(&config_file);
@@ -60,8 +32,7 @@ pub fn initialize_log_file(specified_file: Option<PathBuf>) {
6032/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
6133/// 2. subdirectory of user config directory (always included)
6234/// 3. `HELIX_RUNTIME` (if environment variable is set)
63−/// 4. `HELIX_DEFAULT_RUNTIME` (if environment variable is set *at build time*)
64−/// 5. subdirectory of path to helix executable (always included)
35+/// 4. subdirectory of path to helix executable (always included)
6536///
6637/// Postcondition: returns at least two paths (they might not exist).
6738fn prioritize_runtime_dirs() -> Vec<PathBuf> {
@@ -82,14 +53,6 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
8253 rt_dirs.push(dir.into());
8354 }
8455
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−
9356 // fallback to location of the executable being run
9457 // canonicalize the path in case the executable is symlinked
9558 let exe_rt_dir = std::env::current_exe()
@@ -254,7 +217,7 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
254217/// If no workspace was found returns (CWD, true).
255218/// Otherwise (workspace, false) is returned
256219pub fn find_workspace() -> (PathBuf, bool) {
257− let current_dir = current_working_dir();
220+ let current_dir = std::env::current_dir().expect("unable to determine current directory");
258221 for ancestor in current_dir.ancestors() {
259222 if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
260223 return (ancestor.to_owned(), false);
@@ -280,21 +243,9 @@ fn ensure_parent_dir(path: &Path) {
280243mod merge_toml_tests {
281244 use std::str;
282245
283− use super::{current_working_dir, merge_toml_values, set_current_working_dir};
246+ use super::merge_toml_values;
284247 use toml::Value;
285248
286− #[test]
287− fn current_dir_is_set() {
288− let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
289− let cwd = current_working_dir();
290− assert_ne!(cwd, new_path);
291−
292− set_current_working_dir(&new_path).expect("Couldn't set new path");
293−
294− let cwd = current_working_dir();
295− assert_eq!(cwd, new_path);
296− }
297−
298249 #[test]
299250 fn language_toml_map_merges() {
300251 const USER: &str = r#"
helix-lsp/Cargo.toml+13−15
@@ -1,32 +1,30 @@
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"
37description = "LSP client implementation for Helix project"
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
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
1211
1312# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1413
1514[dependencies]
16−helix-core = { path = "../helix-core" }
17−helix-loader = { path = "../helix-loader" }
18−helix-parsec = { path = "../helix-parsec" }
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" }
1918
2019anyhow = "1.0"
2120futures-executor = "0.3"
2221futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
23−globset = "0.4.14"
2422log = "0.4"
25−lsp-types = { version = "0.95" }
23+lsp-types = { version = "0.94" }
2624serde = { version = "1.0", features = ["derive"] }
2725serde_json = "1.0"
2826thiserror = "1.0"
29−tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27+tokio = { version = "1.29", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
3028tokio-stream = "0.1.14"
31−which = "5.0.0"
29+which = "4.4"
3230parking_lot = "0.12.1"
helix-lsp/src/client.rs+11−123
@@ -7,9 +7,8 @@ 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, CodeActionCapabilityResolveSupport,
11− DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
12− WorkspaceFoldersChangeEvent,
10+ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
11+ PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
1312};
1413use lsp_types as lsp;
1514use parking_lot::Mutex;
@@ -401,22 +400,12 @@ impl Client {
401400 &self,
402401 params: R::Params,
403402 ) -> 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>>
415403 where
416404 R::Params: serde::Serialize,
417405 {
418406 let server_tx = self.server_tx.clone();
419407 let id = self.next_request_id();
408+ let timeout_secs = self.req_timeout;
420409
421410 async move {
422411 use std::time::Duration;
@@ -554,15 +543,6 @@ impl Client {
554543 normalizes_line_endings: Some(false),
555544 change_annotation_support: None,
556545 }),
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− }),
566546 ..Default::default()
567547 }),
568548 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -629,12 +609,6 @@ impl Client {
629609 .collect(),
630610 },
631611 }),
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− }),
638612 ..Default::default()
639613 }),
640614 publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
@@ -667,7 +641,6 @@ impl Client {
667641 version: Some(String::from(VERSION_AND_GIT_HASH)),
668642 }),
669643 locale: None, // TODO
670− work_done_progress_params: lsp::WorkDoneProgressParams::default(),
671644 };
672645
673646 self.request::<lsp::request::Initialize>(params).await
@@ -716,65 +689,6 @@ impl Client {
716689 })
717690 }
718691
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−
778692 // -------------------------------------------------------------------------------------------
779693 // Text document
780694 // -------------------------------------------------------------------------------------------
@@ -970,19 +884,20 @@ impl Client {
970884 ) -> Option<impl Future<Output = Result<()>>> {
971885 let capabilities = self.capabilities.get().unwrap();
972886
973− let include_text = match &capabilities.text_document_sync.as_ref()? {
974− lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
975− save: options,
887+ let include_text = match &capabilities.text_document_sync {
888+ Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
889+ save: Some(options),
976890 ..
977− }) => match options.as_ref()? {
891+ })) => match options {
978892 lsp::TextDocumentSyncSaveOptions::Supported(true) => false,
979893 lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions {
980894 include_text,
981895 }) => include_text.unwrap_or(false),
982− lsp::TextDocumentSyncSaveOptions::Supported(false) => return None,
896+ // Supported(false)
897+ _ => return None,
983898 },
984− // see: https://github.com/microsoft/language-server-protocol/issues/288
985− lsp::TextDocumentSyncCapability::Kind(..) => false,
899+ // unsupported
900+ _ => return None,
986901 };
987902
988903 Some(self.notify::<lsp::notification::DidSaveTextDocument>(
@@ -1039,24 +954,6 @@ impl Client {
1039954 Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
1040955 }
1041956
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−
1060957 pub fn text_document_signature_help(
1061958 &self,
1062959 text_document: lsp::TextDocumentIdentifier,
@@ -1531,13 +1428,4 @@ impl Client {
15311428
15321429 Some(self.call::<lsp::request::ExecuteCommand>(params))
15331430 }
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− }
15431431}
helix-lsp/src/file_event.rsdeleted−193
@@ -1,193 +0,0 @@
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+30−45
@@ -1,5 +1,4 @@
11mod client;
2−pub mod file_event;
32pub mod jsonrpc;
43pub mod snippet;
54mod transport;
@@ -435,7 +434,7 @@ pub mod util {
435434 }
436435
437436 let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
438− let Some(tabstops) = tabstops else {
437+ let Some(tabstops) = tabstops else{
439438 // no tabstop normal mapping
440439 mapped_selection.push(range);
441440 continue;
@@ -445,36 +444,36 @@ pub mod util {
445444 // the tabstop closest to the range simply replaces `head` while anchor remains in place
446445 // the remaining tabstops receive their own single-width cursor
447446 if range.head < range.anchor {
448− let last_idx = tabstops.len() - 1;
449− let last_tabstop = tabstop_anchor + tabstops[last_idx].0;
447+ let first_tabstop = tabstop_anchor + tabstops[0].1;
450448
451449 // if selection is forward but was moved to the right it is
452450 // contained entirely in the replacement text, just do a point
453451 // selection (fallback below)
454− if range.anchor > last_tabstop {
455− let range = Range::new(range.anchor, last_tabstop);
452+ if range.anchor >= first_tabstop {
453+ let range = Range::new(range.anchor, first_tabstop);
456454 mapped_selection.push(range);
457− let rem_tabstops = tabstops[..last_idx]
455+ let rem_tabstops = tabstops[1..]
458456 .iter()
459− .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
457+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
460458 mapped_selection.extend(rem_tabstops);
461459 continue;
462460 }
463461 } else {
464− let first_tabstop = tabstop_anchor + tabstops[0].0;
462+ let last_idx = tabstops.len() - 1;
463+ let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
465464
466465 // if selection is forward but was moved to the right it is
467466 // contained entirely in the replacement text, just do a point
468467 // selection (fallback below)
469− if range.anchor < first_tabstop {
468+ if range.anchor <= last_tabstop {
470469 // we can't properly compute the the next grapheme
471470 // here because the transaction hasn't been applied yet
472471 // that is not a problem because the range gets grapheme aligned anyway
473472 // tough so just adding one will always cause head to be grapheme
474473 // aligned correctly when applied to the document
475− let range = Range::new(range.anchor, first_tabstop + 1);
474+ let range = Range::new(range.anchor, last_tabstop + 1);
476475 mapped_selection.push(range);
477− let rem_tabstops = tabstops[1..]
476+ let rem_tabstops = tabstops[..last_idx]
478477 .iter()
479478 .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
480479 mapped_selection.extend(rem_tabstops);
@@ -548,7 +547,6 @@ pub enum MethodCall {
548547 WorkspaceFolders,
549548 WorkspaceConfiguration(lsp::ConfigurationParams),
550549 RegisterCapability(lsp::RegistrationParams),
551− UnregisterCapability(lsp::UnregistrationParams),
552550}
553551
554552impl MethodCall {
@@ -572,10 +570,6 @@ impl MethodCall {
572570 let params: lsp::RegistrationParams = params.parse()?;
573571 Self::RegisterCapability(params)
574572 }
575− lsp::request::UnregisterCapability::METHOD => {
576− let params: lsp::UnregistrationParams = params.parse()?;
577− Self::UnregisterCapability(params)
578− }
579573 _ => {
580574 return Err(Error::Unhandled);
581575 }
@@ -635,7 +629,6 @@ pub struct Registry {
635629 syn_loader: Arc<helix_core::syntax::Loader>,
636630 counter: usize,
637631 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
638− pub file_event_handler: file_event::Handler,
639632}
640633
641634impl Registry {
@@ -645,7 +638,6 @@ impl Registry {
645638 syn_loader,
646639 counter: 0,
647640 incoming: SelectAll::new(),
648− file_event_handler: file_event::Handler::new(),
649641 }
650642 }
651643
@@ -658,7 +650,6 @@ impl Registry {
658650 }
659651
660652 pub fn remove_by_id(&mut self, id: usize) {
661− self.file_event_handler.remove_client(id);
662653 self.inner.retain(|_, language_servers| {
663654 language_servers.retain(|ls| id != ls.id());
664655 !language_servers.is_empty()
@@ -724,7 +715,6 @@ impl Registry {
724715 .unwrap();
725716
726717 for old_client in old_clients {
727− self.file_event_handler.remove_client(old_client.id());
728718 tokio::spawn(async move {
729719 let _ = old_client.force_shutdown().await;
730720 });
@@ -741,7 +731,6 @@ impl Registry {
741731 pub fn stop(&mut self, name: &str) {
742732 if let Some(clients) = self.inner.remove(name) {
743733 for client in clients {
744− self.file_event_handler.remove_client(client.id());
745734 tokio::spawn(async move {
746735 let _ = client.force_shutdown().await;
747736 });
@@ -749,40 +738,36 @@ impl Registry {
749738 }
750739 }
751740
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],
741+ pub fn get(
742+ &mut self,
743+ language_config: &LanguageConfiguration,
744+ doc_path: Option<&std::path::PathBuf>,
745+ root_dirs: &[PathBuf],
757746 enable_snippets: bool,
758− ) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
759− language_config.language_servers.iter().map(
760− move |LanguageServerFeatures { name, .. }| {
747+ ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
748+ language_config
749+ .language_servers
750+ .iter()
751+ .map(|LanguageServerFeatures { name, .. }| {
761752 if let Some(clients) = self.inner.get(name) {
762753 if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
763754 client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
764755 }) {
765− return (name.to_owned(), Ok(client.clone()));
756+ return Ok((name.to_owned(), client.clone()));
766757 }
767758 }
768− match self.start_client(
759+ let client = self.start_client(
769760 name.clone(),
770761 language_config,
771762 doc_path,
772763 root_dirs,
773764 enable_snippets,
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− )
765+ )?;
766+ let clients = self.inner.entry(name.clone()).or_default();
767+ clients.push(client.clone());
768+ Ok((name.clone(), client))
769+ })
770+ .collect()
786771 }
787772
788773 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
@@ -946,7 +931,7 @@ pub fn find_lsp_workspace(
946931 let mut file = if file.is_absolute() {
947932 file.to_path_buf()
948933 } else {
949− let current_dir = helix_loader::current_working_dir();
934+ let current_dir = std::env::current_dir().expect("unable to determine current directory");
950935 current_dir.join(file)
951936 };
952937 file = path::get_normalized_path(&file);
helix-parsec/Cargo.toml+7−8
@@ -1,14 +1,13 @@
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"
37description = "Parser combinators for Helix"
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
411include = ["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
1312
1413[dependencies]
helix-term/Cargo.toml+25−33
@@ -1,16 +1,16 @@
11[package]
22name = "helix-term"
3+version = "0.6.0"
34description = "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"
411include = ["src/**/*", "README.md"]
512default-run = "hx"
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
13+rust-version = "1.65"
1414
1515[features]
1616default = ["git"]
@@ -23,22 +23,21 @@ name = "hx"
2323path = "src/main.rs"
2424
2525[dependencies]
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" }
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" }
3332
3433anyhow = "1"
35−once_cell = "1.19"
34+once_cell = "1.18"
3635
37−which = "5.0.0"
36+which = "4.4"
3837
3938tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
4039tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
41−crossterm = { version = "0.27", features = ["event-stream"] }
40+crossterm = { version = "0.26", features = ["event-stream"] }
4241signal-hook = "0.3"
4342tokio-stream = "0.1"
4443futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -50,17 +49,13 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
5049log = "0.4"
5150
5251# File picker
53−nucleo.workspace = true
52+fuzzy-matcher = "0.3"
5453ignore = "0.4"
5554# markdown doc rendering
5655pulldown-cmark = { version = "0.9", default-features = false }
5756# file type detection
5857content_inspector = "0.2.4"
5958
60−# opening URLs
61−open = "5.0.1"
62−url = "2.5.0"
63−
6459# config
6560toml = "0.7"
6661
@@ -68,20 +63,17 @@ serde_json = "1.0"
6863serde = { version = "1.0", features = ["derive"] }
6964
7065# ripgrep for global search
71−grep-regex = "0.1.12"
72−grep-searcher = "0.1.13"
66+grep-regex = "0.1.11"
67+grep-searcher = "0.1.11"
7368
7469[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
7570signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
76−libc = "0.2.151"
77−
78−[target.'cfg(target_os = "macos")'.dependencies]
79−crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
71+libc = "0.2.147"
8072
8173[build-dependencies]
82−helix-loader = { path = "../helix-loader" }
74+helix-loader = { version = "0.6", path = "../helix-loader" }
8375
8476[dev-dependencies]
85−smallvec = "1.11"
86−indoc = "2.0.4"
87−tempfile = "3.8.1"
77+smallvec = "1.10"
78+indoc = "2.0.2"
79+tempfile = "3.6.0"
helix-term/src/application.rs+95−175
@@ -1,16 +1,11 @@
11use arc_swap::{access::Map, ArcSwap};
22use futures_util::Stream;
33use helix_core::{
4− chars::char_is_word,
54 diagnostic::{DiagnosticTag, NumberOrString},
65 path::get_relative_path,
76 pos_at_coords, syntax, Selection,
87};
9−use helix_lsp::{
10− lsp::{self, notification::Notification},
11− util::lsp_pos_to_pos,
12− LspProgressMap,
13−};
8+use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
149use helix_view::{
1510 align_view,
1611 document::DocumentSavedEventResult,
@@ -34,9 +29,12 @@ use crate::{
3429};
3530
3631use log::{debug, error, warn};
37−#[cfg(not(feature = "integration"))]
38−use std::io::stdout;
39−use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
32+use std::{
33+ collections::btree_map::Entry,
34+ io::{stdin, stdout},
35+ path::Path,
36+ sync::Arc,
37+};
4038
4139use anyhow::{Context, Error};
4240
@@ -161,21 +159,17 @@ impl Application {
161159 let path = helix_loader::runtime_file(Path::new("tutor"));
162160 editor.open(&path, Action::VerticalSplit)?;
163161 // Unset path to prevent accidentally saving to the original tutor file.
164− doc_mut!(editor).set_path(None);
162+ doc_mut!(editor).set_path(None)?;
165163 } else if !args.files.is_empty() {
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);
164+ let first = &args.files[0].0; // we know it's not empty
165+ if first.is_dir() {
166+ std::env::set_current_dir(first).context("set current dir")?;
167+ editor.new_file(Action::VerticalSplit);
168+ let picker = ui::file_picker(".".into(), &config.load().editor);
171169 compositor.push(Box::new(overlaid(picker)));
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;
170+ } else {
171+ let nr_of_files = args.files.len();
172+ for (i, (file, pos)) in args.files.into_iter().enumerate() {
179173 if file.is_dir() {
180174 return Err(anyhow::anyhow!(
181175 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
@@ -187,7 +181,7 @@ impl Application {
187181 // option. If neither of those two arguments are passed
188182 // in, just load the files normally.
189183 let action = match args.split {
190− _ if nr_of_files == 1 => Action::VerticalSplit,
184+ _ if i == 0 => Action::VerticalSplit,
191185 Some(Layout::Vertical) => Action::VerticalSplit,
192186 Some(Layout::Horizontal) => Action::HorizontalSplit,
193187 None => Action::Load,
@@ -214,11 +208,14 @@ impl Application {
214208 // does not affect views without pos since it is at the top
215209 let (view, doc) = current!(editor);
216210 align_view(doc, view, Align::Center);
217− } else {
218− editor.new_file(Action::VerticalSplit);
219211 }
220212 } else if stdin().is_tty() || cfg!(feature = "integration") {
221213 editor.new_file(Action::VerticalSplit);
214+ } else if cfg!(target_os = "macos") {
215+ // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
216+ // This doesn't currently work on macOS because of the following issue:
217+ // https://github.com/crossterm-rs/crossterm/issues/500
218+ anyhow::bail!("Piping into helix-term is currently not supported on macOS");
222219 } else {
223220 editor
224221 .new_file_from_stdin(Action::VerticalSplit)
@@ -258,19 +255,22 @@ impl Application {
258255 }
259256
260257 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−
266258 let mut cx = crate::compositor::Context {
267259 editor: &mut self.editor,
268260 jobs: &mut self.jobs,
269261 scroll: None,
270262 };
271263
272− helix_event::start_frame();
264+ // Acquire mutable access to the redraw_handle lock
265+ // to ensure that there are no tasks running that want to block rendering
266+ drop(cx.editor.redraw_handle.1.write().await);
273267 cx.editor.needs_redraw = false;
268+ {
269+ // exhaust any leftover redraw notifications
270+ let notify = cx.editor.redraw_handle.0.notified();
271+ tokio::pin!(notify);
272+ notify.enable();
273+ }
274274
275275 let area = self
276276 .terminal
@@ -292,7 +292,7 @@ impl Application {
292292
293293 pub async fn event_loop<S>(&mut self, input_stream: &mut S)
294294 where
295− S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
295+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
296296 {
297297 self.render().await;
298298
@@ -305,7 +305,7 @@ impl Application {
305305
306306 pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
307307 where
308− S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
308+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
309309 {
310310 loop {
311311 if self.editor.should_close() {
@@ -558,7 +558,16 @@ impl Application {
558558 let bytes = doc_save_event.text.len_bytes();
559559
560560 if doc.path() != Some(&doc_save_event.path) {
561− doc.set_path(Some(&doc_save_event.path));
561+ if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
562+ log::error!(
563+ "error setting path for doc '{:?}': {}",
564+ doc.path(),
565+ err.to_string(),
566+ );
567+
568+ self.editor.set_error(err.to_string());
569+ return;
570+ }
562571
563572 let loader = self.editor.syn_loader.clone();
564573
@@ -594,7 +603,7 @@ impl Application {
594603 EditorEvent::LanguageServerMessage((id, call)) => {
595604 self.handle_language_server_message(call, id).await;
596605 // limit render calls for fast language server messages
597− helix_event::request_redraw();
606+ self.editor.redraw_handle.0.notify_one();
598607 }
599608 EditorEvent::DebuggerEvent(payload) => {
600609 let needs_render = self.editor.handle_debugger_message(payload).await;
@@ -602,9 +611,6 @@ impl Application {
602611 self.render().await;
603612 }
604613 }
605− EditorEvent::Redraw => {
606− self.render().await;
607− }
608614 EditorEvent::IdleTimer => {
609615 self.editor.clear_idle_timer();
610616 self.handle_idle_timeout().await;
@@ -619,7 +625,10 @@ impl Application {
619625 false
620626 }
621627
622− pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
628+ pub async fn handle_terminal_events(
629+ &mut self,
630+ event: Result<CrosstermEvent, crossterm::ErrorKind>,
631+ ) {
623632 let mut cx = crate::compositor::Context {
624633 editor: &mut self.editor,
625634 jobs: &mut self.jobs,
@@ -718,7 +727,7 @@ impl Application {
718727 ));
719728 }
720729 }
721− Notification::PublishDiagnostics(mut params) => {
730+ Notification::PublishDiagnostics(params) => {
722731 let path = match params.uri.to_file_path() {
723732 Ok(path) => path,
724733 Err(_) => {
@@ -732,69 +741,31 @@ impl Application {
732741 return;
733742 }
734743 let offset_encoding = language_server.offset_encoding();
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− }
744+ let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
745+ if let Some(version) = params.version {
746+ if version != doc.version() {
747+ log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
748+ return false;
744749 }
745− true
746− });
750+ }
751+
752+ true
753+ });
747754
748755 if let Some(doc) = doc {
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− }
756+ let lang_conf = doc.language_config();
757+ let text = doc.text();
782758
783− let unchaged_diag_sources = &unchaged_diag_sources_;
784− let diagnostics =
785− params.diagnostics.iter().filter_map(move |diagnostic| {
759+ let diagnostics = params
760+ .diagnostics
761+ .iter()
762+ .filter_map(|diagnostic| {
786763 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
787764 use lsp::DiagnosticSeverity;
788765
789− if diagnostic.source.as_ref().map_or(false, |source| {
790− unchaged_diag_sources.contains(source)
791− }) {
792− return None;
793− }
794−
795766 // TODO: convert inside server
796767 let start = if let Some(start) = lsp_pos_to_pos(
797− &text,
768+ text,
798769 diagnostic.range.start,
799770 offset_encoding,
800771 ) {
@@ -805,13 +776,14 @@ impl Application {
805776 };
806777
807778 let end = if let Some(end) =
808− lsp_pos_to_pos(&text, diagnostic.range.end, offset_encoding)
779+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
809780 {
810781 end
811782 } else {
812783 log::warn!("lsp position out of bounds - {:?}", diagnostic);
813784 return None;
814785 };
786+
815787 let severity =
816788 diagnostic.severity.map(|severity| match severity {
817789 DiagnosticSeverity::ERROR => Error,
@@ -824,7 +796,7 @@ impl Application {
824796 ),
825797 });
826798
827− if let Some(lang_conf) = &lang_conf {
799+ if let Some(lang_conf) = lang_conf {
828800 if let Some(severity) = severity {
829801 if severity < lang_conf.diagnostic_severity {
830802 return None;
@@ -863,17 +835,8 @@ impl Application {
863835 Vec::new()
864836 };
865837
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−
872838 Some(Diagnostic {
873839 range: Range { start, end },
874− ends_at_word,
875− starts_at_word,
876− zero_width: start == end,
877840 line: diagnostic.range.start.line as usize,
878841 message: diagnostic.message.clone(),
879842 severity,
@@ -883,33 +846,38 @@ impl Application {
883846 data: diagnostic.data.clone(),
884847 language_server_id: server_id,
885848 })
886− });
849+ })
850+ .collect();
887851
888− doc.replace_diagnostics(diagnostics, unchaged_diag_sources, server_id);
852+ doc.replace_diagnostics(diagnostics, server_id);
889853 }
890854
891− let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
855+ let mut diagnostics = params
856+ .diagnostics
857+ .into_iter()
858+ .map(|d| (d, server_id))
859+ .collect();
892860
893861 // Insert the original lsp::Diagnostics here because we may have no open document
894862 // for diagnosic message and so we can't calculate the exact position.
895863 // When using them later in the diagnostics picker, we calculate them on-demand.
896− let diagnostics = match self.editor.diagnostics.entry(params.uri) {
864+ match self.editor.diagnostics.entry(params.uri) {
897865 Entry::Occupied(o) => {
898866 let current_diagnostics = o.into_mut();
899867 // there may entries of other language servers, which is why we can't overwrite the whole entry
900868 current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
901− current_diagnostics.extend(diagnostics);
902− current_diagnostics
869+ current_diagnostics.append(&mut diagnostics);
903870 // Sort diagnostics first by severity and then by line numbers.
871+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
872+ current_diagnostics
873+ .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
874+ }
875+ Entry::Vacant(v) => {
876+ diagnostics
877+ .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
878+ v.insert(diagnostics);
904879 }
905− Entry::Vacant(v) => v.insert(diagnostics.collect()),
906880 };
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− });
913881 }
914882 Notification::ShowMessage(params) => {
915883 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -1117,65 +1085,17 @@ impl Application {
11171085 .collect();
11181086 Ok(json!(result))
11191087 }
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− }
1088+ Ok(MethodCall::RegisterCapability(_params)) => {
1089+ log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
1090+ // Language Servers based on the `vscode-languageserver-node` library often send
1091+ // client/registerCapability even though we do not enable dynamic registration
1092+ // for any capabilities. We should send a MethodNotFound JSONRPC error in this
1093+ // case but that rejects the registration promise in the server which causes an
1094+ // exit. So we work around this by ignoring the request and sending back an OK
1095+ // response.
11601096
11611097 Ok(serde_json::Value::Null)
11621098 }
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− }
11791099 };
11801100
11811101 tokio::spawn(language_server!().reply(id, reply));
@@ -1201,7 +1121,7 @@ impl Application {
12011121
12021122 pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
12031123 where
1204− S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
1124+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
12051125 {
12061126 self.claim_term().await?;
12071127
helix-term/src/args.rs−29
@@ -17,14 +17,12 @@ 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>,
2120}
2221
2322impl Args {
2423 pub fn parse_args() -> Result<Args> {
2524 let mut args = Args::default();
2625 let mut argv = std::env::args().peekable();
27− let mut line_number = 0;
2826
2927 argv.next(); // skip the program, we don't care about that
3028
@@ -61,20 +59,6 @@ impl Args {
6159 Some(path) => args.log_file = Some(path.into()),
6260 None => anyhow::bail!("--log must specify a path to write"),
6361 },
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− },
7862 arg if arg.starts_with("--") => {
7963 anyhow::bail!("unexpected double dash argument: {}", arg)
8064 }
@@ -89,13 +73,6 @@ impl Args {
8973 }
9074 }
9175 }
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− }
9976 arg => args.files.push(parse_file(arg)),
10077 }
10178 }
@@ -105,12 +82,6 @@ impl Args {
10582 args.files.push(parse_file(&arg));
10683 }
10784
108− if let Some(file) = args.files.first_mut() {
109− if line_number != 0 {
110− file.1.row = line_number;
111− }
112− }
113−
11485 Ok(args)
11586 }
11687}
helix-term/src/commands.rs+356−617
@@ -29,9 +29,10 @@ use helix_core::{
2929 tree_sitter::Node,
3030 unicode::width::UnicodeWidthChar,
3131 visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
32− RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
32+ RopeSlice, Selection, SmallVec, Tendril, Transaction,
3333};
3434use helix_view::{
35+ clipboard::ClipboardType,
3536 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
3637 editor::{Action, CompleteAction},
3738 info::Info,
@@ -43,6 +44,7 @@ use helix_view::{
4344};
4445
4546use anyhow::{anyhow, bail, ensure, Context as _};
47+use fuzzy_matcher::FuzzyMatcher;
4648use insert::*;
4749use movement::Movement;
4850
@@ -59,14 +61,9 @@ use crate::{
5961};
6062
6163use crate::job::{self, Jobs};
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−};
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
65+use std::{collections::HashMap, fmt, future::Future};
66+use std::{collections::HashSet, num::NonZeroUsize};
7067
7168use std::{
7269 borrow::Cow,
@@ -75,11 +72,11 @@ use std::{
7572
7673use once_cell::sync::Lazy;
7774use serde::de::{self, Deserialize, Deserializer};
78−use url::Url;
7975
8076use grep_regex::RegexMatcherBuilder;
8177use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
8278use ignore::{DirEntry, WalkBuilder, WalkState};
79+use tokio_stream::wrappers::UnboundedReceiverStream;
8380
8481pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
8582
@@ -251,8 +248,6 @@ impl MappableCommand {
251248 move_prev_long_word_start, "Move to start of previous long word",
252249 move_next_long_word_end, "Move to end of next long word",
253250 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",
256251 extend_next_word_start, "Extend to start of next word",
257252 extend_prev_word_start, "Extend to start of previous word",
258253 extend_next_word_end, "Extend to end of next word",
@@ -261,8 +256,6 @@ impl MappableCommand {
261256 extend_prev_long_word_start, "Extend to start of previous long word",
262257 extend_next_long_word_end, "Extend to end of next long word",
263258 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",
266259 find_till_char, "Move till next occurrence of char",
267260 find_next_char, "Move to next occurrence of char",
268261 extend_till_char, "Extend till next occurrence of char",
@@ -337,7 +330,7 @@ impl MappableCommand {
337330 goto_implementation, "Goto implementation",
338331 goto_file_start, "Goto line number <n> else file start",
339332 goto_file_end, "Goto file end",
340− goto_file, "Goto files/URLs in selection",
333+ goto_file, "Goto files in selection",
341334 goto_file_hsplit, "Goto files in selection (hsplit)",
342335 goto_file_vsplit, "Goto files in selection (vsplit)",
343336 goto_reference, "Goto references",
@@ -369,7 +362,6 @@ impl MappableCommand {
369362 extend_to_line_end, "Extend to line end",
370363 extend_to_line_end_newline, "Extend to line end",
371364 signature_help, "Show signature help",
372− smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
373365 insert_tab, "Insert tab char",
374366 insert_newline, "Insert newline char",
375367 delete_char_backward, "Delete previous char",
@@ -384,8 +376,6 @@ impl MappableCommand {
384376 later, "Move forward in history",
385377 commit_undo_checkpoint, "Commit changes to new checkpoint",
386378 yank, "Yank selection",
387− yank_to_clipboard, "Yank selections to clipboard",
388− yank_to_primary_clipboard, "Yank selections to primary clipboard",
389379 yank_joined, "Join and yank selections",
390380 yank_joined_to_clipboard, "Join and yank selections to clipboard",
391381 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -990,7 +980,6 @@ fn align_selections(cx: &mut Context) {
990980
991981 let transaction = Transaction::change(doc.text(), changes.into_iter());
992982 doc.apply(&transaction, view.id);
993− exit_select_mode(cx);
994983}
995984
996985fn goto_window(cx: &mut Context, align: Align) {
@@ -1165,10 +1154,6 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11651154 let (view, doc) = current_ref!(cx.editor);
11661155 let text = doc.text();
11671156 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();
11721157 let mut paths: Vec<_> = selections
11731158 .iter()
11741159 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1196,80 +1181,16 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11961181 .to_string(),
11971182 );
11981183 }
1199−
12001184 for sel in paths {
12011185 let p = sel.trim();
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) {
1186+ if !p.is_empty() {
1187+ if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
12501188 cx.editor.set_error(format!("Open file failed: {:?}", e));
12511189 }
12521190 }
12531191 }
12541192}
12551193
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−
12731194fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
12741195where
12751196 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1318,65 +1239,6 @@ fn extend_next_long_word_end(cx: &mut Context) {
13181239 extend_word_impl(cx, movement::move_next_long_word_end)
13191240}
13201241
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−
13801242fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
13811243 // TODO: count is reset to 1 before next key so we move it into the closure here.
13821244 // Would be nice to carry over.
@@ -1390,9 +1252,13 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo
13901252 KeyEvent {
13911253 code: KeyCode::Enter,
13921254 ..
1393− } => {
1394− find_char_line_ending(cx, count, direction, inclusive, extend);
1395− return;
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()
13961262 }
13971263
13981264 KeyEvent {
@@ -1594,7 +1460,6 @@ where
15941460 });
15951461
15961462 doc.apply(&transaction, view.id);
1597− exit_select_mode(cx);
15981463}
15991464
16001465fn switch_case(cx: &mut Context) {
@@ -1836,8 +1701,8 @@ fn select_regex(cx: &mut Context) {
18361701 "select:".into(),
18371702 Some(reg),
18381703 ui::completers::none,
1839− move |cx, regex, event| {
1840− let (view, doc) = current!(cx.editor);
1704+ move |editor, regex, event| {
1705+ let (view, doc) = current!(editor);
18411706 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
18421707 return;
18431708 }
@@ -1858,8 +1723,8 @@ fn split_selection(cx: &mut Context) {
18581723 "split:".into(),
18591724 Some(reg),
18601725 ui::completers::none,
1861− move |cx, regex, event| {
1862− let (view, doc) = current!(cx.editor);
1726+ move |editor, regex, event| {
1727+ let (view, doc) = current!(editor);
18631728 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
18641729 return;
18651730 }
@@ -1982,11 +1847,11 @@ fn search_impl(
19821847
19831848fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
19841849 let mut items = reg
1985− .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1986− .map_or(Vec::new(), |reg| reg.take(200).collect());
1850+ .and_then(|reg| cx.editor.registers.get(reg))
1851+ .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
19871852 items.sort_unstable();
19881853 items.dedup();
1989− items.into_iter().map(|value| value.to_string()).collect()
1854+ items.into_iter().cloned().collect()
19901855}
19911856
19921857fn search(cx: &mut Context) {
@@ -2023,14 +1888,12 @@ fn searcher(cx: &mut Context, direction: Direction) {
20231888 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
20241889 .collect()
20251890 },
2026− move |cx, regex, event| {
2027− if event == PromptEvent::Validate {
2028− cx.editor.registers.last_search_register = reg;
2029− } else if event != PromptEvent::Update {
1891+ move |editor, regex, event| {
1892+ if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
20301893 return;
20311894 }
20321895 search_impl(
2033− cx.editor,
1896+ editor,
20341897 &contents,
20351898 &regex,
20361899 Movement::Move,
@@ -2045,13 +1908,11 @@ fn searcher(cx: &mut Context, direction: Direction) {
20451908
20461909fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
20471910 let count = cx.count();
2048− let register = cx
2049− .register
2050− .unwrap_or(cx.editor.registers.last_search_register);
20511911 let config = cx.editor.config();
20521912 let scrolloff = config.scrolloff;
2053− if let Some(query) = cx.editor.registers.first(register, cx.editor) {
2054− let doc = doc!(cx.editor);
1913+ let (_, doc) = current!(cx.editor);
1914+ let registers = &cx.editor.registers;
1915+ if let Some(query) = registers.read('/').and_then(|query| query.last()) {
20551916 let contents = doc.text().slice(..).to_string();
20561917 let search_config = &config.search;
20571918 let case_insensitive = if search_config.smart_case {
@@ -2060,7 +1921,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
20601921 false
20611922 };
20621923 let wrap_around = search_config.wrap_around;
2063− if let Ok(regex) = RegexBuilder::new(&query)
1924+ if let Ok(regex) = RegexBuilder::new(query)
20641925 .case_insensitive(case_insensitive)
20651926 .multi_line(true)
20661927 .build()
@@ -2100,7 +1961,6 @@ fn extend_search_prev(cx: &mut Context) {
21001961}
21011962
21021963fn search_selection(cx: &mut Context) {
2103− let register = cx.register.unwrap_or('/');
21041964 let (view, doc) = current!(cx.editor);
21051965 let contents = doc.text().slice(..);
21061966
@@ -2113,24 +1973,13 @@ fn search_selection(cx: &mut Context) {
21131973 .collect::<Vec<_>>()
21141974 .join("|");
21151975
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− }
1976+ let msg = format!("register '{}' set to '{}'", '/', &regex);
1977+ cx.editor.registers.push('/', regex);
1978+ cx.editor.set_status(msg);
21241979}
21251980
21261981fn make_search_word_bounded(cx: &mut Context) {
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) {
1982+ let regex = match cx.editor.registers.last('/') {
21341983 Some(regex) => regex,
21351984 None => return,
21361985 };
@@ -2148,19 +1997,14 @@ fn make_search_word_bounded(cx: &mut Context) {
21481997 if !start_anchored {
21491998 new_regex.push_str("\\b");
21501999 }
2151− new_regex.push_str(&regex);
2000+ new_regex.push_str(regex);
21522001 if !end_anchored {
21532002 new_regex.push_str("\\b");
21542003 }
21552004
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− }
2005+ let msg = format!("register '{}' set to '{}'", '/', &new_regex);
2006+ cx.editor.registers.push('/', new_regex);
2007+ cx.editor.set_status(msg);
21642008}
21652009
21662010fn global_search(cx: &mut Context) {
@@ -2199,11 +2043,13 @@ fn global_search(cx: &mut Context) {
21992043 }
22002044 }
22012045
2046+ let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
22022047 let config = cx.editor.config();
22032048 let smart_case = config.search.smart_case;
22042049 let file_picker_config = config.file_picker.clone();
22052050
22062051 let reg = cx.register.unwrap_or('/');
2052+
22072053 let completions = search_completions(cx, Some(reg));
22082054 ui::regex_prompt(
22092055 cx,
@@ -2216,67 +2062,43 @@ fn global_search(cx: &mut Context) {
22162062 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
22172063 .collect()
22182064 },
2219− move |cx, regex, event| {
2065+ move |_editor, regex, event| {
22202066 if event != PromptEvent::Validate {
22212067 return;
22222068 }
2223− cx.editor.registers.last_search_register = reg;
2224−
2225− let current_path = doc_mut!(cx.editor).path().cloned();
2226− let documents: Vec<_> = cx
2227− .editor
2228− .documents()
2229− .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
2230− .collect();
22312069
22322070 if let Ok(matcher) = RegexMatcherBuilder::new()
22332071 .case_smart(smart_case)
22342072 .build(regex.as_str())
22352073 {
2236− let search_root = helix_loader::current_working_dir();
2237− if !search_root.exists() {
2238− cx.editor
2239− .set_error("Current working directory does not exist");
2240− return;
2241− }
2242−
2243− let (picker, injector) = Picker::stream(current_path);
2074+ let searcher = SearcherBuilder::new()
2075+ .binary_detection(BinaryDetection::quit(b'\x00'))
2076+ .build();
22442077
2078+ let search_root = std::env::current_dir()
2079+ .expect("Global search error: Failed to get current dir");
22452080 let dedup_symlinks = file_picker_config.deduplicate_links;
22462081 let absolute_root = search_root
22472082 .canonicalize()
22482083 .unwrap_or_else(|_| search_root.clone());
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(|| {
2084+
2085+ WalkBuilder::new(search_root)
2086+ .hidden(file_picker_config.hidden)
2087+ .parents(file_picker_config.parents)
2088+ .ignore(file_picker_config.ignore)
2089+ .follow_links(file_picker_config.follow_symlinks)
2090+ .git_ignore(file_picker_config.git_ignore)
2091+ .git_global(file_picker_config.git_global)
2092+ .git_exclude(file_picker_config.git_exclude)
2093+ .max_depth(file_picker_config.max_depth)
2094+ .filter_entry(move |entry| {
2095+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2096+ })
2097+ .build_parallel()
2098+ .run(|| {
22762099 let mut searcher = searcher.clone();
22772100 let matcher = matcher.clone();
2278− let injector = injector_.clone();
2279− let documents = &documents;
2101+ let all_matches_sx = all_matches_sx.clone();
22802102 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
22812103 let entry = match entry {
22822104 Ok(entry) => entry,
@@ -2289,39 +2111,17 @@ fn global_search(cx: &mut Context) {
22892111 _ => return WalkState::Continue,
22902112 };
22912113
2292− let mut stop = false;
2293− let sink = sinks::UTF8(|line_num, _| {
2294− stop = injector
2295− .push(FileResult::new(entry.path(), line_num as usize - 1))
2296− .is_err();
2297−
2298− Ok(!stop)
2299− });
2300− let doc = documents.iter().find(|&(doc_path, _)| {
2301− doc_path
2302− .as_ref()
2303− .map_or(false, |doc_path| doc_path == entry.path())
2304− });
2305−
2306− let result = if let Some((_, doc)) = doc {
2307− // there is already a buffer for this file
2308− // search the buffer instead of the file because it's faster
2309− // and captures new edits without requiring a save
2310− if searcher.multi_line_with_matcher(&matcher) {
2311− // in this case a continous buffer is required
2312− // convert the rope to a string
2313− let text = doc.to_string();
2314− searcher.search_slice(&matcher, text.as_bytes(), sink)
2315− } else {
2316− searcher.search_reader(
2317− &matcher,
2318− RopeReader::new(doc.slice(..)),
2319− sink,
2320− )
2321− }
2322− } else {
2323− searcher.search_path(&matcher, entry.path(), sink)
2324− };
2114+ let result = searcher.search_path(
2115+ &matcher,
2116+ entry.path(),
2117+ sinks::UTF8(|line_num, _| {
2118+ all_matches_sx
2119+ .send(FileResult::new(entry.path(), line_num as usize - 1))
2120+ .unwrap();
2121+
2122+ Ok(true)
2123+ }),
2124+ );
23252125
23262126 if let Err(err) = result {
23272127 log::error!(
@@ -2330,66 +2130,65 @@ fn global_search(cx: &mut Context) {
23302130 err
23312131 );
23322132 }
2333− if stop {
2334− WalkState::Quit
2335− } else {
2336− WalkState::Continue
2337− }
2133+ WalkState::Continue
23382134 })
23392135 });
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− })
23872136 } else {
23882137 // Otherwise do nothing
23892138 // log::warn!("Global Search Invalid Pattern")
23902139 }
23912140 },
23922141 );
2142+
2143+ let current_path = doc_mut!(cx.editor).path().cloned();
2144+
2145+ let show_picker = async move {
2146+ let all_matches: Vec<FileResult> =
2147+ UnboundedReceiverStream::new(all_matches_rx).collect().await;
2148+ let call: job::Callback = Callback::EditorCompositor(Box::new(
2149+ move |editor: &mut Editor, compositor: &mut Compositor| {
2150+ if all_matches.is_empty() {
2151+ editor.set_status("No matches found");
2152+ return;
2153+ }
2154+
2155+ let picker = Picker::new(
2156+ all_matches,
2157+ current_path,
2158+ move |cx, FileResult { path, line_num }, action| {
2159+ match cx.editor.open(path, action) {
2160+ Ok(_) => {}
2161+ Err(e) => {
2162+ cx.editor.set_error(format!(
2163+ "Failed to open file '{}': {}",
2164+ path.display(),
2165+ e
2166+ ));
2167+ return;
2168+ }
2169+ }
2170+
2171+ let line_num = *line_num;
2172+ let (view, doc) = current!(cx.editor);
2173+ let text = doc.text();
2174+ if line_num >= text.len_lines() {
2175+ cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2176+ return;
2177+ }
2178+ let start = text.line_to_char(line_num);
2179+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2180+
2181+ doc.set_selection(view.id, Selection::single(start, end));
2182+ align_view(doc, view, Align::Center);
2183+ }).with_preview(|_editor, FileResult { path, line_num }| {
2184+ Some((path.clone().into(), Some((*line_num, *line_num))))
2185+ });
2186+ compositor.push(Box::new(overlaid(picker)));
2187+ },
2188+ ));
2189+ Ok(call)
2190+ };
2191+ cx.jobs.callback(show_picker);
23932192}
23942193
23952194enum Extend {
@@ -2512,35 +2311,17 @@ enum Operation {
25122311 Change,
25132312}
25142313
2515−fn selection_is_linewise(selection: &Selection, text: &Rope) -> bool {
2516− selection.ranges().iter().all(|range| {
2517− let text = text.slice(..);
2518− if range.slice(text).len_lines() < 2 {
2519− return false;
2520− }
2521− // If the start of the selection is at the start of a line and the end at the end of a line.
2522− let (start_line, end_line) = range.line_range(text);
2523− let start = text.line_to_char(start_line);
2524− let end = text.line_to_char((end_line + 1).min(text.len_lines()));
2525− start == range.from() && end == range.to()
2526− })
2527−}
2528−
25292314fn delete_selection_impl(cx: &mut Context, op: Operation) {
25302315 let (view, doc) = current!(cx.editor);
25312316
25322317 let selection = doc.selection(view.id);
2533− let only_whole_lines = selection_is_linewise(selection, doc.text());
25342318
25352319 if cx.register != Some('_') {
25362320 // first yank the selection
25372321 let text = doc.text().slice(..);
25382322 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
25392323 let reg_name = cx.register.unwrap_or('"');
2540− if let Err(err) = cx.editor.registers.write(reg_name, values) {
2541− cx.editor.set_error(err.to_string());
2542− return;
2543− }
2324+ cx.editor.registers.write(reg_name, values);
25442325 };
25452326
25462327 // then delete
@@ -2554,11 +2335,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
25542335 exit_select_mode(cx);
25552336 }
25562337 Operation::Change => {
2557− if only_whole_lines {
2558− open_above(cx);
2559− } else {
2560− enter_insert_mode(cx);
2561− }
2338+ enter_insert_mode(cx);
25622339 }
25632340 }
25642341}
@@ -2680,10 +2457,6 @@ fn insert_mode(cx: &mut Context) {
26802457 .transform(|range| Range::new(range.to(), range.from()));
26812458
26822459 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();
26872460}
26882461
26892462// inserts at the end of each selection
@@ -2720,10 +2493,6 @@ fn append_mode(cx: &mut Context) {
27202493
27212494fn file_picker(cx: &mut Context) {
27222495 let root = find_workspace().0;
2723− if !root.exists() {
2724− cx.editor.set_error("Workspace directory does not exist");
2725− return;
2726− }
27272496 let picker = ui::file_picker(root, &cx.editor.config());
27282497 cx.push_layer(Box::new(overlaid(picker)));
27292498}
@@ -2744,14 +2513,8 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
27442513 let picker = ui::file_picker(path, &cx.editor.config());
27452514 cx.push_layer(Box::new(overlaid(picker)));
27462515}
2747−
27482516fn file_picker_in_current_directory(cx: &mut Context) {
2749− let cwd = helix_loader::current_working_dir();
2750− if !cwd.exists() {
2751− cx.editor
2752− .set_error("Current working directory does not exist");
2753− return;
2754− }
2517+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
27552518 let picker = ui::file_picker(cwd, &cx.editor.config());
27562519 cx.push_layer(Box::new(overlaid(picker)));
27572520}
@@ -2901,11 +2664,9 @@ fn jumplist_picker(cx: &mut Context) {
29012664 |cx, meta, action| {
29022665 cx.editor.switch(meta.id, action);
29032666 let config = cx.editor.config();
2904− let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
2667+ let (view, doc) = current!(cx.editor);
29052668 doc.set_selection(view.id, meta.selection.clone());
2906− if action.align_view(view, doc.id()) {
2907− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2908− }
2669+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
29092670 },
29102671 )
29112672 .with_preview(|editor, meta| {
@@ -3053,7 +2814,6 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
30532814 let indent = indent::indent_for_newline(
30542815 language_config,
30552816 syntax,
3056− &doc.config.load().indent_heuristic,
30572817 &doc.indent_style,
30582818 tab_width,
30592819 text,
@@ -3158,7 +2918,6 @@ fn open(cx: &mut Context, open: Open) {
31582918 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
31592919 Open::Above => range.from(),
31602920 });
3161−
31622921 let new_line = match open {
31632922 // adjust position to the end of the line (next line - 1)
31642923 Open::Below => cursor_line + 1,
@@ -3166,15 +2925,13 @@ fn open(cx: &mut Context, open: Open) {
31662925 Open::Above => cursor_line,
31672926 };
31682927
3169− let line_num = new_line.saturating_sub(1);
3170−
31712928 // Index to insert newlines after, as well as the char width
31722929 // to use to compensate for those inserted newlines.
31732930 let (line_end_index, line_end_offset_width) = if new_line == 0 {
31742931 (0, 0)
31752932 } else {
31762933 (
3177− line_end_char_index(&text, line_num),
2934+ line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
31782935 doc.line_ending.len_chars(),
31792936 )
31802937 };
@@ -3182,15 +2939,13 @@ fn open(cx: &mut Context, open: Open) {
31822939 let indent = indent::indent_for_newline(
31832940 doc.language_config(),
31842941 doc.syntax(),
3185− &doc.config.load().indent_heuristic,
31862942 &doc.indent_style,
31872943 doc.tab_width(),
31882944 text,
3189− line_num,
2945+ new_line.saturating_sub(1),
31902946 line_end_index,
31912947 cursor_line,
31922948 );
3193−
31942949 let indent_len = indent.len();
31952950 let mut text = String::with_capacity(1 + indent_len);
31962951 text.push_str(doc.line_ending.as_str());
@@ -3616,7 +3371,6 @@ pub mod insert {
36163371 }
36173372
36183373 use helix_core::auto_pairs;
3619− use helix_view::editor::SmartTabConfig;
36203374
36213375 pub fn insert_char(cx: &mut Context, c: char) {
36223376 let (view, doc) = current_ref!(cx.editor);
@@ -3642,31 +3396,6 @@ pub mod insert {
36423396 }
36433397 }
36443398
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−
36703399 pub fn insert_tab(cx: &mut Context) {
36713400 let (view, doc) = current!(cx.editor);
36723401 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3722,7 +3451,6 @@ pub mod insert {
37223451 let indent = indent::indent_for_newline(
37233452 doc.language_config(),
37243453 doc.syntax(),
3725− &doc.config.load().indent_heuristic,
37263454 &doc.indent_style,
37273455 doc.tab_width(),
37283456 text,
@@ -3963,22 +3691,7 @@ fn commit_undo_checkpoint(cx: &mut Context) {
39633691// Yank / Paste
39643692
39653693fn yank(cx: &mut Context) {
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);
3694+ let (view, doc) = current!(cx.editor);
39823695 let text = doc.text().slice(..);
39833696
39843697 let values: Vec<String> = doc
@@ -3986,15 +3699,19 @@ fn yank_impl(editor: &mut Editor, register: char) {
39863699 .fragments(text)
39873700 .map(Cow::into_owned)
39883701 .collect();
3989− let selections = values.len();
39903702
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− }
3703+ let msg = format!(
3704+ "yanked {} selection(s) to register {}",
3705+ values.len(),
3706+ cx.register.unwrap_or('"')
3707+ );
3708+
3709+ cx.editor
3710+ .registers
3711+ .write(cx.register.unwrap_or('"'), values);
3712+
3713+ cx.editor.set_status(msg);
3714+ exit_select_mode(cx);
39983715}
39993716
40003717fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
@@ -4002,7 +3719,6 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
40023719 let text = doc.text().slice(..);
40033720
40043721 let selection = doc.selection(view.id);
4005− let selections = selection.len();
40063722 let joined = selection
40073723 .fragments(text)
40083724 .fold(String::new(), |mut acc, fragment| {
@@ -4013,52 +3729,104 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
40133729 acc
40143730 });
40153731
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− }
3732+ let msg = format!(
3733+ "joined and yanked {} selection(s) to register {}",
3734+ selection.len(),
3735+ register,
3736+ );
3737+
3738+ editor.registers.write(register, vec![joined]);
3739+ editor.set_status(msg);
40233740}
40243741
40253742fn yank_joined(cx: &mut Context) {
4026− let separator = doc!(cx.editor).line_ending.as_str();
4027− yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
3743+ let line_ending = doc!(cx.editor).line_ending;
3744+ let register = cx.register.unwrap_or('"');
3745+ yank_joined_impl(cx.editor, line_ending.as_str(), register);
40283746 exit_select_mode(cx);
40293747}
40303748
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);
3749+fn yank_joined_to_clipboard_impl(
3750+ editor: &mut Editor,
3751+ separator: &str,
3752+ clipboard_type: ClipboardType,
3753+) -> anyhow::Result<()> {
3754+ let (view, doc) = current!(editor);
3755+ let text = doc.text().slice(..);
3756+
3757+ let values: Vec<String> = doc
3758+ .selection(view.id)
3759+ .fragments(text)
3760+ .map(Cow::into_owned)
3761+ .collect();
3762+
3763+ let clipboard_text = match clipboard_type {
3764+ ClipboardType::Clipboard => "system clipboard",
3765+ ClipboardType::Selection => "primary clipboard",
3766+ };
3767+
3768+ let msg = format!(
3769+ "joined and yanked {} selection(s) to {}",
3770+ values.len(),
3771+ clipboard_text,
3772+ );
3773+
3774+ let joined = values.join(separator);
3775+
3776+ editor
3777+ .clipboard_provider
3778+ .set_contents(joined, clipboard_type)
3779+ .context("Couldn't set system clipboard content")?;
3780+
3781+ editor.set_status(msg);
3782+
3783+ Ok(())
40353784}
40363785
4037−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3786+fn yank_joined_to_clipboard(cx: &mut Context) {
40383787 let line_ending = doc!(cx.editor).line_ending;
4039− yank_joined_impl(cx.editor, line_ending.as_str(), '*');
3788+ let _ =
3789+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
40403790 exit_select_mode(cx);
40413791}
40423792
4043−fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
3793+fn yank_main_selection_to_clipboard_impl(
3794+ editor: &mut Editor,
3795+ clipboard_type: ClipboardType,
3796+) -> anyhow::Result<()> {
40443797 let (view, doc) = current!(editor);
40453798 let text = doc.text().slice(..);
40463799
4047− let selection = doc.selection(view.id).primary().fragment(text).to_string();
3800+ let message_text = match clipboard_type {
3801+ ClipboardType::Clipboard => "yanked main selection to system clipboard",
3802+ ClipboardType::Selection => "yanked main selection to primary clipboard",
3803+ };
3804+
3805+ let value = doc.selection(view.id).primary().fragment(text);
40483806
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()),
3807+ if let Err(e) = editor
3808+ .clipboard_provider
3809+ .set_contents(value.into_owned(), clipboard_type)
3810+ {
3811+ bail!("Couldn't set system clipboard content: {}", e);
40523812 }
3813+
3814+ editor.set_status(message_text);
3815+ Ok(())
40533816}
40543817
40553818fn yank_main_selection_to_clipboard(cx: &mut Context) {
4056− yank_primary_selection_impl(cx.editor, '+');
4057− exit_select_mode(cx);
3819+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
3820+}
3821+
3822+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3823+ let line_ending = doc!(cx.editor).line_ending;
3824+ let _ =
3825+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
40583826}
40593827
40603828fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4061− yank_primary_selection_impl(cx.editor, '*');
3829+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
40623830 exit_select_mode(cx);
40633831}
40643832
@@ -4156,99 +3924,146 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
41563924 };
41573925 let (view, doc) = current!(cx.editor);
41583926 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
4159− exit_select_mode(cx);
3927+}
3928+
3929+fn paste_clipboard_impl(
3930+ editor: &mut Editor,
3931+ action: Paste,
3932+ clipboard_type: ClipboardType,
3933+ count: usize,
3934+) -> anyhow::Result<()> {
3935+ let (view, doc) = current!(editor);
3936+ match editor.clipboard_provider.get_contents(clipboard_type) {
3937+ Ok(contents) => {
3938+ paste_impl(&[contents], doc, view, action, count, editor.mode);
3939+ Ok(())
3940+ }
3941+ Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3942+ }
41603943}
41613944
41623945fn paste_clipboard_after(cx: &mut Context) {
4163− paste(cx.editor, '+', Paste::After, cx.count());
4164− exit_select_mode(cx);
3946+ let _ = paste_clipboard_impl(
3947+ cx.editor,
3948+ Paste::After,
3949+ ClipboardType::Clipboard,
3950+ cx.count(),
3951+ );
41653952}
41663953
41673954fn paste_clipboard_before(cx: &mut Context) {
4168− paste(cx.editor, '+', Paste::Before, cx.count());
4169− exit_select_mode(cx);
3955+ let _ = paste_clipboard_impl(
3956+ cx.editor,
3957+ Paste::Before,
3958+ ClipboardType::Clipboard,
3959+ cx.count(),
3960+ );
41703961}
41713962
41723963fn paste_primary_clipboard_after(cx: &mut Context) {
4173− paste(cx.editor, '*', Paste::After, cx.count());
4174− exit_select_mode(cx);
3964+ let _ = paste_clipboard_impl(
3965+ cx.editor,
3966+ Paste::After,
3967+ ClipboardType::Selection,
3968+ cx.count(),
3969+ );
41753970}
41763971
41773972fn paste_primary_clipboard_before(cx: &mut Context) {
4178− paste(cx.editor, '*', Paste::Before, cx.count());
4179− exit_select_mode(cx);
3973+ let _ = paste_clipboard_impl(
3974+ cx.editor,
3975+ Paste::Before,
3976+ ClipboardType::Selection,
3977+ cx.count(),
3978+ );
41803979}
41813980
41823981fn replace_with_yanked(cx: &mut Context) {
4183− replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
4184− exit_select_mode(cx);
3982+ let count = cx.count();
3983+ let reg_name = cx.register.unwrap_or('"');
3984+ let (view, doc) = current!(cx.editor);
3985+ let registers = &mut cx.editor.registers;
3986+
3987+ if let Some(values) = registers.read(reg_name) {
3988+ if !values.is_empty() {
3989+ let repeat = std::iter::repeat(
3990+ values
3991+ .last()
3992+ .map(|value| Tendril::from(&value.repeat(count)))
3993+ .unwrap(),
3994+ );
3995+ let mut values = values
3996+ .iter()
3997+ .map(|value| Tendril::from(&value.repeat(count)))
3998+ .chain(repeat);
3999+ let selection = doc.selection(view.id);
4000+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4001+ if !range.is_empty() {
4002+ (range.from(), range.to(), Some(values.next().unwrap()))
4003+ } else {
4004+ (range.from(), range.to(), None)
4005+ }
4006+ });
4007+
4008+ doc.apply(&transaction, view.id);
4009+ exit_select_mode(cx);
4010+ }
4011+ }
41854012}
41864013
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();
4014+fn replace_selections_with_clipboard_impl(
4015+ cx: &mut Context,
4016+ clipboard_type: ClipboardType,
4017+) -> anyhow::Result<()> {
4018+ let count = cx.count();
4019+ let (view, doc) = current!(cx.editor);
41924020
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)
4021+ match cx.editor.clipboard_provider.get_contents(clipboard_type) {
4022+ Ok(contents) => {
4023+ let selection = doc.selection(view.id);
4024+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4025+ (
4026+ range.from(),
4027+ range.to(),
4028+ Some(contents.repeat(count).as_str().into()),
4029+ )
4030+ });
4031+
4032+ doc.apply(&transaction, view.id);
4033+ doc.append_changes_to_history(view);
42104034 }
4211− });
4035+ Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
4036+ }
42124037
4213− doc.apply(&transaction, view.id);
4038+ exit_select_mode(cx);
4039+ Ok(())
42144040}
42154041
42164042fn replace_selections_with_clipboard(cx: &mut Context) {
4217− replace_with_yanked_impl(cx.editor, '+', cx.count());
4218− exit_select_mode(cx);
4043+ let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
42194044}
42204045
42214046fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4222− replace_with_yanked_impl(cx.editor, '*', cx.count());
4223− exit_select_mode(cx);
4047+ let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
42244048}
42254049
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();
4050+fn paste(cx: &mut Context, pos: Paste) {
4051+ let count = cx.count();
4052+ let reg_name = cx.register.unwrap_or('"');
4053+ let (view, doc) = current!(cx.editor);
4054+ let registers = &mut cx.editor.registers;
42294055
4230− let (view, doc) = current!(editor);
4231− paste_impl(&values, doc, view, pos, count, editor.mode);
4056+ if let Some(values) = registers.read(reg_name) {
4057+ paste_impl(values, doc, view, pos, count, cx.editor.mode);
4058+ }
42324059}
42334060
42344061fn paste_after(cx: &mut Context) {
4235− paste(
4236− cx.editor,
4237− cx.register.unwrap_or('"'),
4238− Paste::After,
4239− cx.count(),
4240− );
4241− exit_select_mode(cx);
4062+ paste(cx, Paste::After)
42424063}
42434064
42444065fn paste_before(cx: &mut Context) {
4245− paste(
4246− cx.editor,
4247− cx.register.unwrap_or('"'),
4248− Paste::Before,
4249− cx.count(),
4250− );
4251− exit_select_mode(cx);
4066+ paste(cx, Paste::Before)
42524067}
42534068
42544069fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4287,7 +4102,6 @@ fn indent(cx: &mut Context) {
42874102 }),
42884103 );
42894104 doc.apply(&transaction, view.id);
4290− exit_select_mode(cx);
42914105}
42924106
42934107fn unindent(cx: &mut Context) {
@@ -4327,7 +4141,6 @@ fn unindent(cx: &mut Context) {
43274141 let transaction = Transaction::change(doc.text(), changes.into_iter());
43284142
43294143 doc.apply(&transaction, view.id);
4330− exit_select_mode(cx);
43314144}
43324145
43334146fn format_selections(cx: &mut Context) {
@@ -4357,7 +4170,7 @@ fn format_selections(cx: &mut Context) {
43574170 })
43584171 else {
43594172 cx.editor
4360− .set_error("No configured language server supports range formatting");
4173+ .set_error("No configured language server does not support range formatting");
43614174 return;
43624175 };
43634176
@@ -4394,9 +4207,10 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
43944207 use movement::skip_while;
43954208 let (view, doc) = current!(cx.editor);
43964209 let text = doc.text();
4397− let slice = text.slice(..);
4210+ let slice = doc.text().slice(..);
43984211
43994212 let mut changes = Vec::new();
4213+ let fragment = Tendril::from(" ");
44004214
44014215 for selection in doc.selection(view.id) {
44024216 let (start, mut end) = selection.line_range(slice);
@@ -4412,13 +4226,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44124226 let mut end = text.line_to_char(line + 1);
44134227 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
44144228
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));
4229+ // need to skip from start, not end
4230+ let change = (start, end, Some(fragment.clone()));
4231+ changes.push(change);
44224232 }
44234233 }
44244234
@@ -4430,6 +4240,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44304240 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
44314241 changes.dedup();
44324242
4243+ // TODO: joining multiple empty lines should be replaced by a single space.
4244+ // need to merge change ranges that touch
4245+
44334246 // select inserted spaces
44344247 let transaction = if select_space {
44354248 let ranges: SmallVec<_> = changes
@@ -4441,9 +4254,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44414254 })
44424255 .collect();
44434256 let selection = Selection::new(ranges, 0);
4444− Transaction::change(text, changes.into_iter()).with_selection(selection)
4257+ Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
44454258 } else {
4446− Transaction::change(text, changes.into_iter())
4259+ Transaction::change(doc.text(), changes.into_iter())
44474260 };
44484261
44494262 doc.apply(&transaction, view.id);
@@ -4457,8 +4270,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
44574270 if remove { "remove:" } else { "keep:" }.into(),
44584271 Some(reg),
44594272 ui::completers::none,
4460− move |cx, regex, event| {
4461− let (view, doc) = current!(cx.editor);
4273+ move |editor, regex, event| {
4274+ let (view, doc) = current!(editor);
44624275 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
44634276 return;
44644277 }
@@ -4819,49 +4632,6 @@ fn select_prev_sibling(cx: &mut Context) {
48194632 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
48204633}
48214634
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−
48654635fn match_brackets(cx: &mut Context) {
48664636 let (view, doc) = current!(cx.editor);
48674637 let is_select = cx.editor.mode == Mode::Select;
@@ -4975,19 +4745,17 @@ fn transpose_view(cx: &mut Context) {
49754745 cx.editor.transpose_view()
49764746}
49774747
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);
4748+// split helper, clear it later
4749+fn split(cx: &mut Context, action: Action) {
4750+ let (view, doc) = current!(cx.editor);
49834751 let id = doc.id();
49844752 let selection = doc.selection(view.id).clone();
49854753 let offset = view.offset;
49864754
4987− editor.switch(id, action);
4755+ cx.editor.switch(id, action);
49884756
49894757 // match the selection in the previous view
4990− let (view, doc) = current!(editor);
4758+ let (view, doc) = current!(cx.editor);
49914759 doc.set_selection(view.id, selection);
49924760 // match the view scroll offset (switch doesn't handle this fully
49934761 // since the selection is only matched after the split)
@@ -4995,7 +4763,7 @@ fn split(editor: &mut Editor, action: Action) {
49954763}
49964764
49974765fn hsplit(cx: &mut Context) {
4998− split(cx.editor, Action::HorizontalSplit);
4766+ split(cx, Action::HorizontalSplit);
49994767}
50004768
50014769fn hsplit_new(cx: &mut Context) {
@@ -5003,7 +4771,7 @@ fn hsplit_new(cx: &mut Context) {
50034771}
50044772
50054773fn vsplit(cx: &mut Context) {
5006− split(cx.editor, Action::VerticalSplit);
4774+ split(cx, Action::VerticalSplit);
50074775}
50084776
50094777fn vsplit_new(cx: &mut Context) {
@@ -5052,12 +4820,7 @@ fn insert_register(cx: &mut Context) {
50524820 if let Some(ch) = event.char() {
50534821 cx.editor.autoinfo = None;
50544822 cx.register = Some(ch);
5055− paste(
5056− cx.editor,
5057− cx.register.unwrap_or('"'),
5058− Paste::Cursor,
5059− cx.count(),
5060− );
4823+ paste(cx, Paste::Cursor);
50614824 }
50624825 })
50634826}
@@ -5352,18 +5115,11 @@ fn surround_replace(cx: &mut Context) {
53525115 }
53535116 };
53545117
5355− let selection = selection.clone();
5356− let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect();
5357− doc.set_selection(
5358− view.id,
5359− Selection::new(ranges, selection.primary_index() * 2),
5360− );
5361−
53625118 cx.on_next_key(move |cx, event| {
53635119 let (view, doc) = current!(cx.editor);
53645120 let to = match event.char() {
53655121 Some(to) => to,
5366− None => return doc.set_selection(view.id, selection),
5122+ None => return,
53675123 };
53685124 let (open, close) = surround::get_pair(to);
53695125 let transaction = Transaction::change(
@@ -5374,7 +5130,6 @@ fn surround_replace(cx: &mut Context) {
53745130 (pos, pos + 1, Some(t))
53755131 }),
53765132 );
5377− doc.set_selection(view.id, selection);
53785133 doc.apply(&transaction, view.id);
53795134 exit_select_mode(cx);
53805135 });
@@ -5609,18 +5364,12 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
56095364 };
56105365
56115366 // These `usize`s cannot underflow because selection ranges cannot overlap.
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");
5367+ // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5368+ // arithmetic to assert this.
5369+ let anchor = (to as isize + offset - deleted_len as isize) as usize;
56175370 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
56185371 ranges.push(new_range);
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");
5372+ offset = offset + output_len as isize - deleted_len as isize;
56245373
56255374 changes.push((from, to, Some(output)));
56265375 }
@@ -5759,7 +5508,6 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
57595508 let transaction = Transaction::change(doc.text(), changes.into_iter());
57605509 let transaction = transaction.with_selection(new_selection);
57615510 doc.apply(&transaction, view.id);
5762− exit_select_mode(cx);
57635511 }
57645512}
57655513
@@ -5778,12 +5526,9 @@ fn record_macro(cx: &mut Context) {
57785526 }
57795527 })
57805528 .collect::<String>();
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− }
5529+ cx.editor.registers.write(reg, vec![s]);
5530+ cx.editor
5531+ .set_status(format!("Recorded to register [{}]", reg));
57875532 } else {
57885533 let reg = cx.register.take().unwrap_or('@');
57895534 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5803,14 +5548,8 @@ fn replay_macro(cx: &mut Context) {
58035548 return;
58045549 }
58055550
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) {
5551+ let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
5552+ match helix_view::input::parse_macro(keys_str) {
58145553 Ok(keys) => keys,
58155554 Err(err) => {
58165555 cx.editor.set_error(format!("Invalid macro: {}", err));
helix-term/src/commands/dap.rs+5−14
@@ -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, graphics::Margin};
11+use helix_view::editor::Breakpoint;
1212
1313use serde_json::{to_value, Value};
1414use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -217,7 +217,7 @@ pub fn dap_start_impl(
217217 }
218218 }
219219
220− args.insert("cwd", to_value(helix_loader::current_working_dir())?);
220+ args.insert("cwd", to_value(std::env::current_dir().unwrap())?);
221221
222222 let args = to_value(args).unwrap();
223223
@@ -339,12 +339,8 @@ fn debug_parameter_prompt(
339339 .to_owned();
340340
341341 let completer = match field_type {
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− },
342+ "filename" => ui::completers::filename,
343+ "directory" => ui::completers::directory,
348344 _ => ui::completers::none,
349345 };
350346
@@ -581,12 +577,7 @@ pub fn dap_variables(cx: &mut Context) {
581577 }
582578
583579 let contents = Text::from(tui::text::Text::from(variables));
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);
580+ let popup = Popup::new("dap-variables", contents);
590581 cx.replace_or_push_layer("dap-variables", popup);
591582}
592583
helix-term/src/commands/lsp.rs+62−60
@@ -23,7 +23,6 @@ use helix_core::{
2323use helix_view::{
2424 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
2525 editor::Action,
26− graphics::Margin,
2726 theme::Style,
2827 Document, View,
2928};
@@ -50,7 +49,7 @@ use std::{
5049/// If there is no configured language server that supports the feature, this displays a status message.
5150/// Using this macro in a context where the editor automatically queries the LSP
5251/// (instead of when the user explicitly does so via a keybind like `gd`)
53−/// will spam the "No configured language server supports \<feature>" status message confusingly.
52+/// will spam the "No configured language server supports <feature>" status message confusingly.
5453#[macro_export]
5554macro_rules! language_server_with_feature {
5655 ($editor:expr, $doc:expr, $feature:expr) => {{
@@ -196,6 +195,7 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
196195 (path.into(), line)
197196}
198197
198+// TODO: share with symbol picker(symbol.location)
199199fn jump_to_location(
200200 editor: &mut Editor,
201201 location: &lsp::Location,
@@ -213,16 +213,15 @@ fn jump_to_location(
213213 return;
214214 }
215215 };
216−
217− let doc = match editor.open(&path, action) {
218− Ok(id) => doc_mut!(editor, &id),
216+ match editor.open(&path, action) {
217+ Ok(_) => (),
219218 Err(err) => {
220219 let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
221220 editor.set_error(err);
222221 return;
223222 }
224− };
225− let view = view_mut!(editor);
223+ }
224+ let (view, doc) = current!(editor);
226225 // TODO: convert inside server
227226 let new_range =
228227 if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
@@ -234,22 +233,45 @@ fn jump_to_location(
234233 // we flip the range so that the cursor sits on the start of the symbol
235234 // (for example start of the function).
236235 doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
237− if action.align_view(view, doc.id()) {
238− align_view(doc, view, Align::Center);
239− }
236+ align_view(doc, view, Align::Center);
240237}
241238
242239type SymbolPicker = Picker<SymbolInformationItem>;
243240
244241fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
245242 // TODO: drop current_path comparison and instead use workspace: bool flag?
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− );
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+ }
253275 })
254276 .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
255277 .truncate_start(false)
@@ -264,7 +286,7 @@ enum DiagnosticsFormat {
264286fn diag_picker(
265287 cx: &Context,
266288 diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
267− _current_path: Option<lsp::Url>,
289+ current_path: Option<lsp::Url>,
268290 format: DiagnosticsFormat,
269291) -> Picker<PickerDiagnostic> {
270292 // TODO: drop current_path comparison and instead use workspace: bool flag?
@@ -302,12 +324,22 @@ fn diag_picker(
302324 offset_encoding,
303325 },
304326 action| {
305− jump_to_location(
306− cx.editor,
307− &lsp::Location::new(url.clone(), diag.range),
308− *offset_encoding,
309− action,
310− )
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+ }
311343 },
312344 )
313345 .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
@@ -698,8 +730,7 @@ pub fn code_action(cx: &mut Context) {
698730
699731 // always present here
700732 let action = action.unwrap();
701− let Some(language_server) = editor.language_server_by_id(action.language_server_id)
702− else {
733+ let Some(language_server) = editor.language_server_by_id(action.language_server_id) else {
703734 editor.set_error("Language Server disappeared");
704735 return;
705736 };
@@ -712,25 +743,7 @@ pub fn code_action(cx: &mut Context) {
712743 }
713744 lsp::CodeActionOrCommand::CodeAction(code_action) => {
714745 log::debug!("code action: {:?}", code_action);
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 {
746+ if let Some(ref workspace_edit) = code_action.edit {
734747 log::debug!("edit: {:?}", workspace_edit);
735748 let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
736749 }
@@ -745,16 +758,7 @@ pub fn code_action(cx: &mut Context) {
745758 });
746759 picker.move_down(); // pre-select the first item
747760
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−
761+ let popup = Popup::new("code-action", picker).with_scrollbar(false);
758762 compositor.replace_or_push("code-action", popup);
759763 };
760764
@@ -1029,7 +1033,7 @@ fn goto_impl(
10291033 locations: Vec<lsp::Location>,
10301034 offset_encoding: OffsetEncoding,
10311035) {
1032− let cwdir = helix_loader::current_working_dir();
1036+ let cwdir = std::env::current_dir().unwrap_or_default();
10331037
10341038 match locations.as_slice() {
10351039 [location] => {
@@ -1169,8 +1173,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11691173 // Do not show the message if signature help was invoked
11701174 // automatically on backspace, trigger characters, etc.
11711175 if invoked == SignatureHelpInvoked::Manual {
1172− cx.editor
1173− .set_error("No configured language server supports signature-help");
1176+ cx.editor.set_error("No configured language server supports signature-help");
11741177 }
11751178 return;
11761179 };
@@ -1395,8 +1398,7 @@ pub fn rename_symbol(cx: &mut Context) {
13951398 .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
13961399 .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
13971400 else {
1398− cx.editor
1399− .set_error("No configured language server supports symbol renaming");
1401+ cx.editor.set_error("No configured language server supports symbol renaming");
14001402 return;
14011403 };
14021404
helix-term/src/commands/typed.rs+706−915
@@ -5,10 +5,7 @@ use crate::job::Job;
55
66use super::*;
77
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};
8+use helix_core::{encoding, shellwords::Shellwords};
129use helix_view::document::DEFAULT_LANGUAGE_NAME;
1310use helix_view::editor::{Action, CloseError, ConfigEvent};
1411use serde_json::Value;
@@ -332,16 +329,12 @@ fn write_impl(
332329 path: Option<&Cow<str>>,
333330 force: bool,
334331) -> anyhow::Result<()> {
335− let config = cx.editor.config();
332+ let editor_auto_fmt = cx.editor.config().auto_format;
336333 let jobs = &mut cx.jobs;
337334 let (view, doc) = current!(cx.editor);
338335 let path = path.map(AsRef::as_ref);
339336
340− if config.insert_final_newline {
341− insert_final_newline(doc, view);
342− }
343−
344− let fmt = if config.auto_format {
337+ let fmt = if editor_auto_fmt {
345338 doc.auto_format().map(|fmt| {
346339 let callback = make_format_callback(
347340 doc.id(),
@@ -365,16 +358,6 @@ fn write_impl(
365358 Ok(())
366359}
367360
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−
378361fn write(
379362 cx: &mut compositor::Context,
380363 args: &[Cow<str>],
@@ -477,7 +460,8 @@ fn set_indent_style(
477460 cx.editor.set_status(match style {
478461 Tabs => "tabs".to_owned(),
479462 Spaces(1) => "1 space".to_owned(),
480− Spaces(n) => format!("{} spaces", n),
463+ Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
464+ _ => unreachable!(), // Shouldn't happen.
481465 });
482466 return Ok(());
483467 }
@@ -489,7 +473,7 @@ fn set_indent_style(
489473 Some(arg) => arg
490474 .parse::<u8>()
491475 .ok()
492− .filter(|n| (1..=MAX_INDENT).contains(n))
476+ .filter(|n| (1..=8).contains(n))
493477 .map(Spaces),
494478 _ => None,
495479 };
@@ -574,6 +558,7 @@ fn set_line_ending(
574558
575559 Ok(())
576560}
561+
577562fn earlier(
578563 cx: &mut compositor::Context,
579564 args: &[Cow<str>],
@@ -672,10 +657,11 @@ pub fn write_all_impl(
672657 write_scratch: bool,
673658) -> anyhow::Result<()> {
674659 let mut errors: Vec<&'static str> = Vec::new();
675− let config = cx.editor.config();
660+ let auto_format = cx.editor.config().auto_format;
676661 let jobs = &mut cx.jobs;
677662 let current_view = view!(cx.editor);
678663
664+ // save all documents
679665 let saves: Vec<_> = cx
680666 .editor
681667 .documents
@@ -686,7 +672,7 @@ pub fn write_all_impl(
686672 }
687673 if doc.path().is_none() {
688674 if write_scratch {
689− errors.push("cannot write a buffer without a filename");
675+ errors.push("cannot write a buffer without a filename\n");
690676 }
691677 return None;
692678 }
@@ -706,35 +692,32 @@ pub fn write_all_impl(
706692 current_view.id
707693 };
708694
709− Some((doc.id(), target_view))
710− })
711− .collect();
712−
713− for (doc_id, target_view) in saves {
714− let doc = doc_mut!(cx.editor, &doc_id);
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+ };
715709
716− if config.insert_final_newline {
717− insert_final_newline(doc, view_mut!(cx.editor, target_view));
718− }
710+ if fmt.is_none() {
711+ return Some(doc.id());
712+ }
719713
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 {
732714 None
733− };
715+ })
716+ .collect();
734717
735− if fmt.is_none() {
736− cx.editor.save::<PathBuf>(doc_id, None, force)?;
737− }
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)?;
738721 }
739722
740723 if !errors.is_empty() && !force {
@@ -921,8 +904,7 @@ fn yank_main_selection_to_clipboard(
921904 return Ok(());
922905 }
923906
924− yank_primary_selection_impl(cx.editor, '+');
925− Ok(())
907+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
926908}
927909
928910fn yank_joined(
@@ -956,8 +938,7 @@ fn yank_joined_to_clipboard(
956938 let doc = doc!(cx.editor);
957939 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
958940 let separator = args.first().unwrap_or(&default_sep);
959− yank_joined_impl(cx.editor, separator, '+');
960− Ok(())
941+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
961942}
962943
963944fn yank_main_selection_to_primary_clipboard(
@@ -969,8 +950,7 @@ fn yank_main_selection_to_primary_clipboard(
969950 return Ok(());
970951 }
971952
972− yank_primary_selection_impl(cx.editor, '*');
973− Ok(())
953+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
974954}
975955
976956fn yank_joined_to_primary_clipboard(
@@ -985,8 +965,7 @@ fn yank_joined_to_primary_clipboard(
985965 let doc = doc!(cx.editor);
986966 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
987967 let separator = args.first().unwrap_or(&default_sep);
988− yank_joined_impl(cx.editor, separator, '*');
989− Ok(())
968+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
990969}
991970
992971fn paste_clipboard_after(
@@ -998,8 +977,7 @@ fn paste_clipboard_after(
998977 return Ok(());
999978 }
1000979
1001− paste(cx.editor, '+', Paste::After, 1);
1002− Ok(())
980+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
1003981}
1004982
1005983fn paste_clipboard_before(
@@ -1011,8 +989,7 @@ fn paste_clipboard_before(
1011989 return Ok(());
1012990 }
1013991
1014− paste(cx.editor, '+', Paste::Before, 1);
1015− Ok(())
992+ paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard, 1)
1016993}
1017994
1018995fn paste_primary_clipboard_after(
@@ -1024,8 +1001,7 @@ fn paste_primary_clipboard_after(
10241001 return Ok(());
10251002 }
10261003
1027− paste(cx.editor, '*', Paste::After, 1);
1028− Ok(())
1004+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
10291005}
10301006
10311007fn paste_primary_clipboard_before(
@@ -1037,8 +1013,30 @@ fn paste_primary_clipboard_before(
10371013 return Ok(());
10381014 }
10391015
1040− paste(cx.editor, '*', Paste::Before, 1);
1041− Ok(())
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+ }
10421040}
10431041
10441042fn replace_selections_with_clipboard(
@@ -1050,8 +1048,7 @@ fn replace_selections_with_clipboard(
10501048 return Ok(());
10511049 }
10521050
1053− replace_with_yanked_impl(cx.editor, '+', 1);
1054− Ok(())
1051+ replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
10551052}
10561053
10571054fn replace_selections_with_primary_clipboard(
@@ -1063,8 +1060,7 @@ fn replace_selections_with_primary_clipboard(
10631060 return Ok(());
10641061 }
10651062
1066− replace_with_yanked_impl(cx.editor, '*', 1);
1067− Ok(())
1063+ replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
10681064}
10691065
10701066fn show_clipboard_provider(
@@ -1077,7 +1073,7 @@ fn show_clipboard_provider(
10771073 }
10781074
10791075 cx.editor
1080− .set_status(cx.editor.registers.clipboard_provider_name().to_string());
1076+ .set_status(cx.editor.clipboard_provider.name().to_string());
10811077 Ok(())
10821078}
10831079
@@ -1097,11 +1093,14 @@ fn change_current_directory(
10971093 .as_ref(),
10981094 );
10991095
1100− helix_loader::set_current_working_dir(dir)?;
1096+ if let Err(e) = std::env::set_current_dir(dir) {
1097+ bail!("Couldn't change the current working directory: {}", e);
1098+ }
11011099
1100+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
11021101 cx.editor.set_status(format!(
11031102 "Current working directory is now {}",
1104− helix_loader::current_working_dir().display()
1103+ cwd.display()
11051104 ));
11061105 Ok(())
11071106}
@@ -1115,14 +1114,9 @@ fn show_current_directory(
11151114 return Ok(());
11161115 }
11171116
1118− let cwd = helix_loader::current_working_dir();
1119− let message = format!("Current working directory is {}", cwd.display());
1120−
1121− if cwd.exists() {
1122− cx.editor.set_status(message);
1123− } else {
1124− cx.editor.set_error(format!("{} (deleted)", message));
1125− }
1117+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
1118+ cx.editor
1119+ .set_status(format!("Current working directory is {}", cwd.display()));
11261120 Ok(())
11271121}
11281122
@@ -1282,17 +1276,12 @@ fn reload(
12821276 }
12831277
12841278 let scrolloff = cx.editor.config().scrolloff;
1279+ let redraw_handle = cx.editor.redraw_handle.clone();
12851280 let (view, doc) = current!(cx.editor);
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(())
1281+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1282+ .map(|_| {
1283+ view.ensure_cursor_in_view(doc, scrolloff);
1284+ })
12961285}
12971286
12981287fn reload_all(
@@ -1331,13 +1320,8 @@ fn reload_all(
13311320 // Ensure that the view is synced with the document's history.
13321321 view.sync_changes(doc);
13331322
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− }
1323+ let redraw_handle = cx.editor.redraw_handle.clone();
1324+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
13411325
13421326 for view_id in view_ids {
13431327 let view = view_mut!(cx.editor, view_id);
@@ -1386,8 +1370,9 @@ fn lsp_workspace_command(
13861370 .map(|options| (ls.id(), options))
13871371 })
13881372 else {
1389− cx.editor
1390− .set_status("No active language servers for this document support workspace commands");
1373+ cx.editor.set_status(
1374+ "No active language servers for this document support workspace commands",
1375+ );
13911376 return Ok(());
13921377 };
13931378
@@ -1543,84 +1528,6 @@ fn tree_sitter_scopes(
15431528 Ok(())
15441529}
15451530
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−
16241531fn vsplit(
16251532 cx: &mut compositor::Context,
16261533 args: &[Cow<str>],
@@ -1630,8 +1537,10 @@ fn vsplit(
16301537 return Ok(());
16311538 }
16321539
1540+ let id = view!(cx.editor).doc;
1541+
16331542 if args.is_empty() {
1634− split(cx.editor, Action::VerticalSplit);
1543+ cx.editor.switch(id, Action::VerticalSplit);
16351544 } else {
16361545 for arg in args {
16371546 cx.editor
@@ -1651,8 +1560,10 @@ fn hsplit(
16511560 return Ok(());
16521561 }
16531562
1563+ let id = view!(cx.editor).doc;
1564+
16541565 if args.is_empty() {
1655− split(cx.editor, Action::HorizontalSplit);
1566+ cx.editor.switch(id, Action::HorizontalSplit);
16561567 } else {
16571568 for arg in args {
16581569 cx.editor
@@ -1767,7 +1678,7 @@ fn tutor(
17671678 let path = helix_loader::runtime_file(Path::new("tutor"));
17681679 cx.editor.open(&path, Action::Replace)?;
17691680 // Unset path to prevent accidentally saving to the original tutor file.
1770− doc_mut!(cx.editor).set_path(None);
1681+ doc_mut!(cx.editor).set_path(None)?;
17711682 Ok(())
17721683}
17731684
@@ -1944,29 +1855,14 @@ fn toggle_option(
19441855 .to_string(),
19451856 )
19461857 }
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(_) => {
1858+ Value::Null | Value::Object(_) | Value::Array(_) | Value::Number(_) => {
19631859 anyhow::bail!("Configuration {key} does not support toggle yet")
19641860 }
19651861 };
19661862
19671863 let status = format!("'{key}' is now set to {value}");
19681864 let config = serde_json::from_value(config)
1969− .map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?;
1865+ .map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?;
19701866
19711867 cx.editor
19721868 .config_events
@@ -2375,721 +2271,606 @@ fn clear_register(
23752271 format!("Invalid register {}", args[0])
23762272 );
23772273 let register = args[0].chars().next().unwrap_or_default();
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(());
2395− }
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(());
2274+ match cx.editor.registers.remove(register) {
2275+ Some(_) => cx
2276+ .editor
2277+ .set_status(format!("Register {} cleared", register)),
2278+ None => cx
2279+ .editor
2280+ .set_error(format!("Register {} not found", register)),
24182281 }
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−
24822282 Ok(())
24832283}
24842284
24852285pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
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−];
2286+ TypableCommand {
2287+ name: "quit",
2288+ aliases: &["q"],
2289+ doc: "Close the current view.",
2290+ fun: quit,
2291+ signature: CommandSignature::none(),
2292+ },
2293+ TypableCommand {
2294+ name: "quit!",
2295+ aliases: &["q!"],
2296+ doc: "Force close the current view, ignoring unsaved changes.",
2297+ fun: force_quit,
2298+ signature: CommandSignature::none(),
2299+ },
2300+ TypableCommand {
2301+ name: "open",
2302+ aliases: &["o"],
2303+ doc: "Open a file from disk into the current view.",
2304+ fun: open,
2305+ signature: CommandSignature::all(completers::filename),
2306+ },
2307+ TypableCommand {
2308+ name: "buffer-close",
2309+ aliases: &["bc", "bclose"],
2310+ doc: "Close the current buffer.",
2311+ fun: buffer_close,
2312+ signature: CommandSignature::all(completers::buffer),
2313+ },
2314+ TypableCommand {
2315+ name: "buffer-close!",
2316+ aliases: &["bc!", "bclose!"],
2317+ doc: "Close the current buffer forcefully, ignoring unsaved changes.",
2318+ fun: force_buffer_close,
2319+ signature: CommandSignature::all(completers::buffer)
2320+ },
2321+ TypableCommand {
2322+ name: "buffer-close-others",
2323+ aliases: &["bco", "bcloseother"],
2324+ doc: "Close all buffers but the currently focused one.",
2325+ fun: buffer_close_others,
2326+ signature: CommandSignature::none(),
2327+ },
2328+ TypableCommand {
2329+ name: "buffer-close-others!",
2330+ aliases: &["bco!", "bcloseother!"],
2331+ doc: "Force close all buffers but the currently focused one.",
2332+ fun: force_buffer_close_others,
2333+ signature: CommandSignature::none(),
2334+ },
2335+ TypableCommand {
2336+ name: "buffer-close-all",
2337+ aliases: &["bca", "bcloseall"],
2338+ doc: "Close all buffers without quitting.",
2339+ fun: buffer_close_all,
2340+ signature: CommandSignature::none(),
2341+ },
2342+ TypableCommand {
2343+ name: "buffer-close-all!",
2344+ aliases: &["bca!", "bcloseall!"],
2345+ doc: "Force close all buffers ignoring unsaved changes without quitting.",
2346+ fun: force_buffer_close_all,
2347+ signature: CommandSignature::none(),
2348+ },
2349+ TypableCommand {
2350+ name: "buffer-next",
2351+ aliases: &["bn", "bnext"],
2352+ doc: "Goto next buffer.",
2353+ fun: buffer_next,
2354+ signature: CommandSignature::none(),
2355+ },
2356+ TypableCommand {
2357+ name: "buffer-previous",
2358+ aliases: &["bp", "bprev"],
2359+ doc: "Goto previous buffer.",
2360+ fun: buffer_previous,
2361+ signature: CommandSignature::none(),
2362+ },
2363+ TypableCommand {
2364+ name: "write",
2365+ aliases: &["w"],
2366+ doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2367+ fun: write,
2368+ signature: CommandSignature::positional(&[completers::filename]),
2369+ },
2370+ TypableCommand {
2371+ name: "write!",
2372+ aliases: &["w!"],
2373+ doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
2374+ fun: force_write,
2375+ signature: CommandSignature::positional(&[completers::filename]),
2376+ },
2377+ TypableCommand {
2378+ name: "write-buffer-close",
2379+ aliases: &["wbc"],
2380+ doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
2381+ fun: write_buffer_close,
2382+ signature: CommandSignature::positional(&[completers::filename]),
2383+ },
2384+ TypableCommand {
2385+ name: "write-buffer-close!",
2386+ aliases: &["wbc!"],
2387+ doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
2388+ fun: force_write_buffer_close,
2389+ signature: CommandSignature::positional(&[completers::filename]),
2390+ },
2391+ TypableCommand {
2392+ name: "new",
2393+ aliases: &["n"],
2394+ doc: "Create a new scratch buffer.",
2395+ fun: new_file,
2396+ // TODO: This seems to complete with a filename, but doesn't use that filename to
2397+ // set the path of the newly created buffer.
2398+ signature: CommandSignature::positional(&[completers::filename]),
2399+ },
2400+ TypableCommand {
2401+ name: "format",
2402+ aliases: &["fmt"],
2403+ doc: "Format the file using the LSP formatter.",
2404+ fun: format,
2405+ signature: CommandSignature::none(),
2406+ },
2407+ TypableCommand {
2408+ name: "indent-style",
2409+ aliases: &[],
2410+ doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
2411+ fun: set_indent_style,
2412+ signature: CommandSignature::none(),
2413+ },
2414+ TypableCommand {
2415+ name: "line-ending",
2416+ aliases: &[],
2417+ #[cfg(not(feature = "unicode-lines"))]
2418+ doc: "Set the document's default line ending. Options: crlf, lf.",
2419+ #[cfg(feature = "unicode-lines")]
2420+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2421+ fun: set_line_ending,
2422+ signature: CommandSignature::none(),
2423+ },
2424+ TypableCommand {
2425+ name: "earlier",
2426+ aliases: &["ear"],
2427+ doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2428+ fun: earlier,
2429+ signature: CommandSignature::none(),
2430+ },
2431+ TypableCommand {
2432+ name: "later",
2433+ aliases: &["lat"],
2434+ doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2435+ fun: later,
2436+ signature: CommandSignature::none(),
2437+ },
2438+ TypableCommand {
2439+ name: "write-quit",
2440+ aliases: &["wq", "x"],
2441+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2442+ fun: write_quit,
2443+ signature: CommandSignature::positional(&[completers::filename]),
2444+ },
2445+ TypableCommand {
2446+ name: "write-quit!",
2447+ aliases: &["wq!", "x!"],
2448+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2449+ fun: force_write_quit,
2450+ signature: CommandSignature::positional(&[completers::filename]),
2451+ },
2452+ TypableCommand {
2453+ name: "write-all",
2454+ aliases: &["wa"],
2455+ doc: "Write changes from all buffers to disk.",
2456+ fun: write_all,
2457+ signature: CommandSignature::none(),
2458+ },
2459+ TypableCommand {
2460+ name: "write-all!",
2461+ aliases: &["wa!"],
2462+ doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
2463+ fun: force_write_all,
2464+ signature: CommandSignature::none(),
2465+ },
2466+ TypableCommand {
2467+ name: "write-quit-all",
2468+ aliases: &["wqa", "xa"],
2469+ doc: "Write changes from all buffers to disk and close all views.",
2470+ fun: write_all_quit,
2471+ signature: CommandSignature::none(),
2472+ },
2473+ TypableCommand {
2474+ name: "write-quit-all!",
2475+ aliases: &["wqa!", "xa!"],
2476+ doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
2477+ fun: force_write_all_quit,
2478+ signature: CommandSignature::none(),
2479+ },
2480+ TypableCommand {
2481+ name: "quit-all",
2482+ aliases: &["qa"],
2483+ doc: "Close all views.",
2484+ fun: quit_all,
2485+ signature: CommandSignature::none(),
2486+ },
2487+ TypableCommand {
2488+ name: "quit-all!",
2489+ aliases: &["qa!"],
2490+ doc: "Force close all views ignoring unsaved changes.",
2491+ fun: force_quit_all,
2492+ signature: CommandSignature::none(),
2493+ },
2494+ TypableCommand {
2495+ name: "cquit",
2496+ aliases: &["cq"],
2497+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2498+ fun: cquit,
2499+ signature: CommandSignature::none(),
2500+ },
2501+ TypableCommand {
2502+ name: "cquit!",
2503+ aliases: &["cq!"],
2504+ doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
2505+ fun: force_cquit,
2506+ signature: CommandSignature::none(),
2507+ },
2508+ TypableCommand {
2509+ name: "theme",
2510+ aliases: &[],
2511+ doc: "Change the editor theme (show current theme if no name specified).",
2512+ fun: theme,
2513+ signature: CommandSignature::positional(&[completers::theme]),
2514+ },
2515+ TypableCommand {
2516+ name: "yank-join",
2517+ aliases: &[],
2518+ doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
2519+ fun: yank_joined,
2520+ signature: CommandSignature::none(),
2521+ },
2522+ TypableCommand {
2523+ name: "clipboard-yank",
2524+ aliases: &[],
2525+ doc: "Yank main selection into system clipboard.",
2526+ fun: yank_main_selection_to_clipboard,
2527+ signature: CommandSignature::none(),
2528+ },
2529+ TypableCommand {
2530+ name: "clipboard-yank-join",
2531+ aliases: &[],
2532+ 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.
2533+ fun: yank_joined_to_clipboard,
2534+ signature: CommandSignature::none(),
2535+ },
2536+ TypableCommand {
2537+ name: "primary-clipboard-yank",
2538+ aliases: &[],
2539+ doc: "Yank main selection into system primary clipboard.",
2540+ fun: yank_main_selection_to_primary_clipboard,
2541+ signature: CommandSignature::none(),
2542+ },
2543+ TypableCommand {
2544+ name: "primary-clipboard-yank-join",
2545+ aliases: &[],
2546+ 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.
2547+ fun: yank_joined_to_primary_clipboard,
2548+ signature: CommandSignature::none(),
2549+ },
2550+ TypableCommand {
2551+ name: "clipboard-paste-after",
2552+ aliases: &[],
2553+ doc: "Paste system clipboard after selections.",
2554+ fun: paste_clipboard_after,
2555+ signature: CommandSignature::none(),
2556+ },
2557+ TypableCommand {
2558+ name: "clipboard-paste-before",
2559+ aliases: &[],
2560+ doc: "Paste system clipboard before selections.",
2561+ fun: paste_clipboard_before,
2562+ signature: CommandSignature::none(),
2563+ },
2564+ TypableCommand {
2565+ name: "clipboard-paste-replace",
2566+ aliases: &[],
2567+ doc: "Replace selections with content of system clipboard.",
2568+ fun: replace_selections_with_clipboard,
2569+ signature: CommandSignature::none(),
2570+ },
2571+ TypableCommand {
2572+ name: "primary-clipboard-paste-after",
2573+ aliases: &[],
2574+ doc: "Paste primary clipboard after selections.",
2575+ fun: paste_primary_clipboard_after,
2576+ signature: CommandSignature::none(),
2577+ },
2578+ TypableCommand {
2579+ name: "primary-clipboard-paste-before",
2580+ aliases: &[],
2581+ doc: "Paste primary clipboard before selections.",
2582+ fun: paste_primary_clipboard_before,
2583+ signature: CommandSignature::none(),
2584+ },
2585+ TypableCommand {
2586+ name: "primary-clipboard-paste-replace",
2587+ aliases: &[],
2588+ doc: "Replace selections with content of system primary clipboard.",
2589+ fun: replace_selections_with_primary_clipboard,
2590+ signature: CommandSignature::none(),
2591+ },
2592+ TypableCommand {
2593+ name: "show-clipboard-provider",
2594+ aliases: &[],
2595+ doc: "Show clipboard provider name in status bar.",
2596+ fun: show_clipboard_provider,
2597+ signature: CommandSignature::none(),
2598+ },
2599+ TypableCommand {
2600+ name: "change-current-directory",
2601+ aliases: &["cd"],
2602+ doc: "Change the current working directory.",
2603+ fun: change_current_directory,
2604+ signature: CommandSignature::positional(&[completers::directory]),
2605+ },
2606+ TypableCommand {
2607+ name: "show-directory",
2608+ aliases: &["pwd"],
2609+ doc: "Show the current working directory.",
2610+ fun: show_current_directory,
2611+ signature: CommandSignature::none(),
2612+ },
2613+ TypableCommand {
2614+ name: "encoding",
2615+ aliases: &[],
2616+ doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
2617+ fun: set_encoding,
2618+ signature: CommandSignature::none(),
2619+ },
2620+ TypableCommand {
2621+ name: "character-info",
2622+ aliases: &["char"],
2623+ doc: "Get info about the character under the primary cursor.",
2624+ fun: get_character_info,
2625+ signature: CommandSignature::none(),
2626+ },
2627+ TypableCommand {
2628+ name: "reload",
2629+ aliases: &["rl"],
2630+ doc: "Discard changes and reload from the source file.",
2631+ fun: reload,
2632+ signature: CommandSignature::none(),
2633+ },
2634+ TypableCommand {
2635+ name: "reload-all",
2636+ aliases: &["rla"],
2637+ doc: "Discard changes and reload all documents from the source files.",
2638+ fun: reload_all,
2639+ signature: CommandSignature::none(),
2640+ },
2641+ TypableCommand {
2642+ name: "update",
2643+ aliases: &["u"],
2644+ doc: "Write changes only if the file has been modified.",
2645+ fun: update,
2646+ signature: CommandSignature::none(),
2647+ },
2648+ TypableCommand {
2649+ name: "lsp-workspace-command",
2650+ aliases: &[],
2651+ doc: "Open workspace command picker",
2652+ fun: lsp_workspace_command,
2653+ signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
2654+ },
2655+ TypableCommand {
2656+ name: "lsp-restart",
2657+ aliases: &[],
2658+ doc: "Restarts the language servers used by the current doc",
2659+ fun: lsp_restart,
2660+ signature: CommandSignature::none(),
2661+ },
2662+ TypableCommand {
2663+ name: "lsp-stop",
2664+ aliases: &[],
2665+ doc: "Stops the language servers that are used by the current doc",
2666+ fun: lsp_stop,
2667+ signature: CommandSignature::none(),
2668+ },
2669+ TypableCommand {
2670+ name: "tree-sitter-scopes",
2671+ aliases: &[],
2672+ doc: "Display tree sitter scopes, primarily for theming and development.",
2673+ fun: tree_sitter_scopes,
2674+ signature: CommandSignature::none(),
2675+ },
2676+ TypableCommand {
2677+ name: "debug-start",
2678+ aliases: &["dbg"],
2679+ doc: "Start a debug session from a given template with given parameters.",
2680+ fun: debug_start,
2681+ signature: CommandSignature::none(),
2682+ },
2683+ TypableCommand {
2684+ name: "debug-remote",
2685+ aliases: &["dbg-tcp"],
2686+ doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
2687+ fun: debug_remote,
2688+ signature: CommandSignature::none(),
2689+ },
2690+ TypableCommand {
2691+ name: "debug-eval",
2692+ aliases: &[],
2693+ doc: "Evaluate expression in current debug context.",
2694+ fun: debug_eval,
2695+ signature: CommandSignature::none(),
2696+ },
2697+ TypableCommand {
2698+ name: "vsplit",
2699+ aliases: &["vs"],
2700+ doc: "Open the file in a vertical split.",
2701+ fun: vsplit,
2702+ signature: CommandSignature::all(completers::filename)
2703+ },
2704+ TypableCommand {
2705+ name: "vsplit-new",
2706+ aliases: &["vnew"],
2707+ doc: "Open a scratch buffer in a vertical split.",
2708+ fun: vsplit_new,
2709+ signature: CommandSignature::none(),
2710+ },
2711+ TypableCommand {
2712+ name: "hsplit",
2713+ aliases: &["hs", "sp"],
2714+ doc: "Open the file in a horizontal split.",
2715+ fun: hsplit,
2716+ signature: CommandSignature::all(completers::filename)
2717+ },
2718+ TypableCommand {
2719+ name: "hsplit-new",
2720+ aliases: &["hnew"],
2721+ doc: "Open a scratch buffer in a horizontal split.",
2722+ fun: hsplit_new,
2723+ signature: CommandSignature::none(),
2724+ },
2725+ TypableCommand {
2726+ name: "tutor",
2727+ aliases: &[],
2728+ doc: "Open the tutorial.",
2729+ fun: tutor,
2730+ signature: CommandSignature::none(),
2731+ },
2732+ TypableCommand {
2733+ name: "goto",
2734+ aliases: &["g"],
2735+ doc: "Goto line number.",
2736+ fun: goto_line_number,
2737+ signature: CommandSignature::none(),
2738+ },
2739+ TypableCommand {
2740+ name: "set-language",
2741+ aliases: &["lang"],
2742+ doc: "Set the language of current buffer (show current language if no value specified).",
2743+ fun: language,
2744+ signature: CommandSignature::positional(&[completers::language]),
2745+ },
2746+ TypableCommand {
2747+ name: "set-option",
2748+ aliases: &["set"],
2749+ doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
2750+ fun: set_option,
2751+ // TODO: Add support for completion of the options value(s), when appropriate.
2752+ signature: CommandSignature::positional(&[completers::setting]),
2753+ },
2754+ TypableCommand {
2755+ name: "toggle-option",
2756+ aliases: &["toggle"],
2757+ doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
2758+ fun: toggle_option,
2759+ signature: CommandSignature::positional(&[completers::setting]),
2760+ },
2761+ TypableCommand {
2762+ name: "get-option",
2763+ aliases: &["get"],
2764+ doc: "Get the current value of a config option.",
2765+ fun: get_option,
2766+ signature: CommandSignature::positional(&[completers::setting]),
2767+ },
2768+ TypableCommand {
2769+ name: "sort",
2770+ aliases: &[],
2771+ doc: "Sort ranges in selection.",
2772+ fun: sort,
2773+ signature: CommandSignature::none(),
2774+ },
2775+ TypableCommand {
2776+ name: "rsort",
2777+ aliases: &[],
2778+ doc: "Sort ranges in selection in reverse order.",
2779+ fun: sort_reverse,
2780+ signature: CommandSignature::none(),
2781+ },
2782+ TypableCommand {
2783+ name: "reflow",
2784+ aliases: &[],
2785+ doc: "Hard-wrap the current selection of lines to a given width.",
2786+ fun: reflow,
2787+ signature: CommandSignature::none(),
2788+ },
2789+ TypableCommand {
2790+ name: "tree-sitter-subtree",
2791+ aliases: &["ts-subtree"],
2792+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
2793+ fun: tree_sitter_subtree,
2794+ signature: CommandSignature::none(),
2795+ },
2796+ TypableCommand {
2797+ name: "config-reload",
2798+ aliases: &[],
2799+ doc: "Refresh user config.",
2800+ fun: refresh_config,
2801+ signature: CommandSignature::none(),
2802+ },
2803+ TypableCommand {
2804+ name: "config-open",
2805+ aliases: &[],
2806+ doc: "Open the user config.toml file.",
2807+ fun: open_config,
2808+ signature: CommandSignature::none(),
2809+ },
2810+ TypableCommand {
2811+ name: "config-open-workspace",
2812+ aliases: &[],
2813+ doc: "Open the workspace config.toml file.",
2814+ fun: open_workspace_config,
2815+ signature: CommandSignature::none(),
2816+ },
2817+ TypableCommand {
2818+ name: "log-open",
2819+ aliases: &[],
2820+ doc: "Open the helix log file.",
2821+ fun: open_log,
2822+ signature: CommandSignature::none(),
2823+ },
2824+ TypableCommand {
2825+ name: "insert-output",
2826+ aliases: &[],
2827+ doc: "Run shell command, inserting output before each selection.",
2828+ fun: insert_output,
2829+ signature: CommandSignature::none(),
2830+ },
2831+ TypableCommand {
2832+ name: "append-output",
2833+ aliases: &[],
2834+ doc: "Run shell command, appending output after each selection.",
2835+ fun: append_output,
2836+ signature: CommandSignature::none(),
2837+ },
2838+ TypableCommand {
2839+ name: "pipe",
2840+ aliases: &[],
2841+ doc: "Pipe each selection to the shell command.",
2842+ fun: pipe,
2843+ signature: CommandSignature::none(),
2844+ },
2845+ TypableCommand {
2846+ name: "pipe-to",
2847+ aliases: &[],
2848+ doc: "Pipe each selection to the shell command, ignoring output.",
2849+ fun: pipe_to,
2850+ signature: CommandSignature::none(),
2851+ },
2852+ TypableCommand {
2853+ name: "run-shell-command",
2854+ aliases: &["sh"],
2855+ doc: "Run a shell command",
2856+ fun: run_shell_command,
2857+ signature: CommandSignature::all(completers::filename)
2858+ },
2859+ TypableCommand {
2860+ name: "reset-diff-change",
2861+ aliases: &["diffget", "diffg"],
2862+ doc: "Reset the diff change at the cursor position.",
2863+ fun: reset_diff_change,
2864+ signature: CommandSignature::none(),
2865+ },
2866+ TypableCommand {
2867+ name: "clear-register",
2868+ aliases: &[],
2869+ doc: "Clear given register. If no argument is provided, clear all registers.",
2870+ fun: clear_register,
2871+ signature: CommandSignature::none(),
2872+ },
2873+ ];
30932874
30942875pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
30952876 Lazy::new(|| {
@@ -3108,18 +2889,28 @@ pub(super) fn command_mode(cx: &mut Context) {
31082889 ":".into(),
31092890 Some(':'),
31102891 |editor: &Editor, input: &str| {
2892+ static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2893+ Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2894+
31112895 let shellwords = Shellwords::from(input);
31122896 let words = shellwords.words();
31132897
31142898 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
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()
2899+ // If the command has not been finished yet, complete commands.
2900+ let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2901+ .iter()
2902+ .filter_map(|command| {
2903+ FUZZY_MATCHER
2904+ .fuzzy_match(command.name, input)
2905+ .map(|score| (command.name, score))
2906+ })
2907+ .collect();
2908+
2909+ matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
2910+ matches
2911+ .into_iter()
2912+ .map(|(name, _)| (0.., name.into()))
2913+ .collect()
31232914 } else {
31242915 // Otherwise, use the command's completer and the last shellword
31252916 // as completion input.
helix-term/src/compositor.rs−12
@@ -16,7 +16,6 @@ pub enum EventResult {
1616}
1717
1818use crate::job::Jobs;
19−use crate::ui::picker;
2019use helix_view::Editor;
2120
2221pub use helix_view::input::Event;
@@ -80,7 +79,6 @@ pub struct Compositor {
8079 area: Rect,
8180
8281 pub(crate) last_picker: Option<Box<dyn Component>>,
83− pub(crate) full_redraw: bool,
8482}
8583
8684impl Compositor {
@@ -89,7 +87,6 @@ impl Compositor {
8987 layers: Vec::new(),
9088 area,
9189 last_picker: None,
92− full_redraw: false,
9390 }
9491 }
9592
@@ -103,11 +100,6 @@ impl Compositor {
103100
104101 /// Add a layer to be rendered in front of all existing layers.
105102 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− }
111103 let size = self.size();
112104 // trigger required_size on init
113105 layer.required_size((size.width, size.height));
@@ -208,10 +200,6 @@ impl Compositor {
208200 .find(|component| component.id() == Some(id))
209201 .and_then(|component| component.as_any_mut().downcast_mut())
210202 }
211−
212− pub fn need_full_redraw(&mut self) {
213− self.full_redraw = true;
214− }
215203}
216204
217205// View casting, taken straight from Cursive
helix-term/src/config.rs−1
@@ -109,7 +109,6 @@ impl Config {
109109 )?,
110110 }
111111 }
112−
113112 // these are just two io errors return the one for the global config
114113 (Err(err), Err(_)) => return Err(err),
115114 };
helix-term/src/health.rs+16−44
@@ -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<&str>| match cmd {
185− Some(cmd) => match which::which(cmd) {
184+ let check_binary = |cmd: Option<String>| 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,15 +192,17 @@ pub fn languages_all() -> std::io::Result<()> {
192192 for lang in &syn_loader_conf.language {
193193 column(&lang.language_id, Color::Reset);
194194
195− let mut cmds = lang.language_servers.iter().filter_map(|ls| {
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| {
196198 syn_loader_conf
197199 .language_server
198200 .get(&ls.name)
199− .map(|config| config.command.as_str())
201+ .map(|config| config.command.clone())
200202 });
201− check_binary(cmds.next());
203+ check_binary(lsp);
202204
203− let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
205+ let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
204206 check_binary(dap);
205207
206208 for ts_feat in TsFeature::all() {
@@ -211,12 +213,6 @@ pub fn languages_all() -> std::io::Result<()> {
211213 }
212214
213215 writeln!(stdout)?;
214−
215− for cmd in cmds {
216− column("", Color::Reset);
217− check_binary(Some(cmd));
218− writeln!(stdout)?;
219− }
220216 }
221217
222218 Ok(())
@@ -272,12 +268,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
272268 }
273269 };
274270
275− probe_protocols(
271+ // TODO multiple language servers
272+ probe_protocol(
276273 "language server",
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()),
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+ }),
281280 )?;
282281
283282 probe_protocol(
@@ -292,33 +291,6 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
292291 Ok(())
293292}
294293
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−
322294/// Display diagnostics about LSP and DAP.
323295fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::Result<()> {
324296 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,
477+ "space" => { ""
478+ "s" => { ""
479+ "v" => vsplit,
480+ "c" => hsplit,
481+ },
481482 },
482− },
483− })
483+ })
484484 };
485485 let mut merged_keyamp = default();
486486 merge_keys(&mut merged_keyamp, keymap.clone());
helix-term/src/keymap/default.rs+2−8
@@ -88,8 +88,6 @@ 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,
9391
9492 "%" => select_all,
9593 "x" => extend_line_below,
@@ -267,7 +265,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
267265 "C-v" | "v" => vsplit_new,
268266 },
269267 },
270− "y" => yank_to_clipboard,
268+ "y" => yank_joined_to_clipboard,
271269 "Y" => yank_main_selection_to_clipboard,
272270 "p" => paste_clipboard_after,
273271 "P" => paste_clipboard_before,
@@ -338,9 +336,6 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
338336 "B" => extend_prev_long_word_start,
339337 "E" => extend_next_long_word_end,
340338
341− "A-e" => extend_parent_node_end,
342− "A-b" => extend_parent_node_start,
343−
344339 "n" => extend_search_next,
345340 "N" => extend_search_prev,
346341
@@ -373,8 +368,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
373368 "C-h" | "backspace" | "S-backspace" => delete_char_backward,
374369 "C-d" | "del" => delete_char_forward,
375370 "C-j" | "ret" => insert_newline,
376− "tab" => smart_tab,
377− "S-tab" => insert_tab,
371+ "tab" => insert_tab,
378372
379373 "up" => move_visual_line_up,
380374 "down" => move_visual_line_down,
helix-term/src/main.rs+1−17
@@ -66,8 +66,6 @@ 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
7169",
7270 env!("CARGO_PKG_NAME"),
7371 VERSION_AND_GIT_HASH,
@@ -76,7 +74,7 @@ FLAGS:
7674 helix_loader::default_log_file().display(),
7775 );
7876
79− let mut args = Args::parse_args().context("could not parse arguments")?;
77+ let args = Args::parse_args().context("could not parse arguments")?;
8078
8179 helix_loader::initialize_config_file(args.config_file.clone());
8280 helix_loader::initialize_log_file(args.log_file.clone());
@@ -116,20 +114,6 @@ FLAGS:
116114
117115 setup_logging(args.verbosity).context("failed to initialize logging")?;
118116
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−
133117 let config = match Config::load_default() {
134118 Ok(config) => config,
135119 Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
helix-term/src/ui/completion.rs+4−32
@@ -2,7 +2,6 @@ use crate::compositor::{Component, Context, Event, EventResult};
22use helix_view::{
33 document::SavePoint,
44 editor::CompleteAction,
5− graphics::Margin,
65 theme::{Modifier, Style},
76 ViewId,
87};
@@ -145,9 +144,7 @@ impl Completion {
145144 }
146145 };
147146
148− let Some(range) =
149− util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
150− else {
147+ let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
151148 return Transaction::new(doc.text());
152149 };
153150
@@ -295,8 +292,6 @@ impl Completion {
295292 };
296293 // if more text was entered, remove it
297294 doc.restore(view, &savepoint, true);
298− // save an undo checkpoint before the completion
299− doc.append_changes_to_history(view);
300295 let transaction = item_to_transaction(
301296 doc,
302297 view.id,
@@ -327,18 +322,9 @@ impl Completion {
327322 }
328323 };
329324 });
330−
331− let margin = if editor.menu_border() {
332− Margin::vertical(1)
333− } else {
334− Margin::none()
335− };
336−
337325 let popup = Popup::new(Self::ID, menu)
338326 .with_scrollbar(false)
339− .ignore_escape_key(true)
340− .margin(margin);
341−
327+ .ignore_escape_key(true);
342328 let mut completion = Self {
343329 popup,
344330 start_offset,
@@ -425,18 +411,10 @@ impl Completion {
425411 _ => return false,
426412 };
427413
428− let Some(language_server) = cx
429− .editor
430− .language_server_by_id(current_item.language_server_id)
431− else {
432− return false;
433− };
414+ let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
434415
435416 // This method should not block the compositor so we handle the response asynchronously.
436− let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
437− else {
438− return false;
439− };
417+ let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
440418
441419 cx.callback(
442420 future,
@@ -579,12 +557,6 @@ impl Component for Completion {
579557 // clear area
580558 let background = cx.editor.theme.get("ui.popup");
581559 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−
588560 markdown_doc.render(doc_area, surface, cx);
589561 }
590562}
helix-term/src/ui/document.rs+24−60
@@ -97,8 +97,7 @@ pub fn render_document(
9797 doc: &Document,
9898 offset: ViewPosition,
9999 doc_annotations: &TextAnnotations,
100− syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
101− overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
100+ highlight_iter: impl Iterator<Item = HighlightEvent>,
102101 theme: &Theme,
103102 line_decoration: &mut [Box<dyn LineDecoration + '_>],
104103 translated_positions: &mut [TranslatedPosition],
@@ -110,8 +109,7 @@ pub fn render_document(
110109 offset,
111110 &doc.text_format(viewport.width, Some(theme)),
112111 doc_annotations,
113− syntax_highlight_iter,
114− overlay_highlight_iter,
112+ highlight_iter,
115113 theme,
116114 line_decoration,
117115 translated_positions,
@@ -159,8 +157,7 @@ pub fn render_text<'t>(
159157 offset: ViewPosition,
160158 text_fmt: &TextFormat,
161159 text_annotations: &TextAnnotations,
162− syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
163− overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
160+ highlight_iter: impl Iterator<Item = HighlightEvent>,
164161 theme: &Theme,
165162 line_decorations: &mut [Box<dyn LineDecoration + '_>],
166163 translated_positions: &mut [TranslatedPosition],
@@ -181,16 +178,10 @@ pub fn render_text<'t>(
181178
182179 let (mut formatter, mut first_visible_char_idx) =
183180 DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
184− let mut syntax_styles = StyleIter {
181+ let mut styles = StyleIter {
185182 text_style: renderer.text_style,
186183 active_highlights: Vec::with_capacity(64),
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,
184+ highlight_iter,
194185 theme,
195186 };
196187
@@ -202,10 +193,7 @@ pub fn render_text<'t>(
202193 };
203194 let mut is_in_indent_area = true;
204195 let mut last_line_indent_level = 0;
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
196+ let mut style_span = styles
209197 .next()
210198 .unwrap_or_else(|| (Style::default(), usize::MAX));
211199
@@ -233,16 +221,9 @@ pub fn render_text<'t>(
233221
234222 // skip any graphemes on visual lines before the block start
235223 if pos.row < row_off {
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
224+ if char_pos >= style_span.1 {
225+ style_span = if let Some(style_span) = styles.next() {
226+ style_span
246227 } else {
247228 break;
248229 }
@@ -279,15 +260,8 @@ pub fn render_text<'t>(
279260 }
280261
281262 // acquire the correct grapheme style
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));
263+ if char_pos >= style_span.1 {
264+ style_span = styles.next().unwrap_or((Style::default(), usize::MAX));
291265 }
292266 char_pos += grapheme.doc_chars();
293267
@@ -301,25 +275,22 @@ pub fn render_text<'t>(
301275 pos,
302276 );
303277
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())
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))
311282 } else {
312− (syntax_style_span.0, overlay_style_span.0)
313− };
283+ style
284+ }
285+ } else {
286+ style_span.0
287+ };
314288
315− let is_virtual = grapheme.is_virtual();
289+ let virt = grapheme.is_virtual();
316290 renderer.draw_grapheme(
317291 grapheme.grapheme,
318− GraphemeStyle {
319− syntax_style,
320− overlay_style,
321− },
322− is_virtual,
292+ grapheme_style,
293+ virt,
323294 &mut last_line_indent_level,
324295 &mut is_in_indent_area,
325296 pos,
@@ -351,11 +322,6 @@ pub struct TextRenderer<'a> {
351322 pub viewport: Rect,
352323}
353324
354−pub struct GraphemeStyle {
355− syntax_style: Style,
356− overlay_style: Style,
357−}
358−
359325impl<'a> TextRenderer<'a> {
360326 pub fn new(
361327 surface: &'a mut Surface,
@@ -429,7 +395,7 @@ impl<'a> TextRenderer<'a> {
429395 pub fn draw_grapheme(
430396 &mut self,
431397 grapheme: Grapheme,
432− grapheme_style: GraphemeStyle,
398+ mut style: Style,
433399 is_virtual: bool,
434400 last_indent_level: &mut usize,
435401 is_in_indent_area: &mut bool,
@@ -439,11 +405,9 @@ impl<'a> TextRenderer<'a> {
439405 let is_whitespace = grapheme.is_whitespace();
440406
441407 // TODO is it correct to apply the whitespace style to all unicode white spaces?
442− let mut style = grapheme_style.syntax_style;
443408 if is_whitespace {
444409 style = style.patch(self.whitespace_style);
445410 }
446− style = style.patch(grapheme_style.overlay_style);
447411
448412 let width = grapheme.width();
449413 let space = if is_virtual { " " } else { &self.space };
helix-term/src/ui/editor.rs+54−102
@@ -43,8 +43,6 @@ 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,
4846}
4947
5048#[derive(Debug, Clone)]
@@ -73,7 +71,6 @@ impl EditorView {
7371 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
7472 completion: None,
7573 spinners: ProgressSpinners::default(),
76− terminal_focused: true,
7774 }
7875 }
7976
@@ -124,20 +121,16 @@ impl EditorView {
124121 line_decorations.push(Box::new(line_decoration));
125122 }
126123
127− let syntax_highlights =
124+ let mut highlights =
128125 Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
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(
126+ let overlay_highlights = Self::overlay_syntax_highlights(
133127 doc,
134128 view.offset.anchor,
135129 inner.height,
136130 &text_annotations,
137131 );
138− if !overlay_syntax_highlights.is_empty() {
139− overlay_highlights =
140− Box::new(syntax::merge(overlay_highlights, overlay_syntax_highlights));
132+ if !overlay_highlights.is_empty() {
133+ highlights = Box::new(syntax::merge(highlights, overlay_highlights));
141134 }
142135
143136 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
@@ -146,41 +139,39 @@ impl EditorView {
146139 if diagnostic.is_empty() {
147140 continue;
148141 }
149− overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic));
142+ highlights = Box::new(syntax::merge(highlights, diagnostic));
150143 }
151144
152− if is_focused {
145+ let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
153146 let highlights = syntax::merge(
154− overlay_highlights,
147+ highlights,
155148 Self::doc_selection_highlights(
156149 editor.mode(),
157150 doc,
158151 view,
159152 theme,
160153 &config.cursor_shape,
161− self.terminal_focused,
162154 ),
163155 );
164156 let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
165157 if focused_view_elements.is_empty() {
166− overlay_highlights = Box::new(highlights)
158+ Box::new(highlights)
167159 } else {
168− overlay_highlights = Box::new(syntax::merge(highlights, focused_view_elements))
160+ Box::new(syntax::merge(highlights, focused_view_elements))
169161 }
170− }
162+ } else {
163+ Box::new(highlights)
164+ };
171165
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− }
166+ Self::render_gutter(
167+ editor,
168+ doc,
169+ view,
170+ view.area,
171+ theme,
172+ is_focused,
173+ &mut line_decorations,
174+ );
184175
185176 if is_focused {
186177 let cursor = doc
@@ -200,8 +191,7 @@ impl EditorView {
200191 doc,
201192 view.offset,
202193 &text_annotations,
203− syntax_highlights,
204− overlay_highlights,
194+ highlights,
205195 theme,
206196 &mut line_decorations,
207197 &mut translated_positions,
@@ -261,39 +251,27 @@ impl EditorView {
261251 .for_each(|area| surface.set_style(area, ruler_theme))
262252 }
263253
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(
254+ pub fn overlay_syntax_highlights(
280255 doc: &Document,
281256 anchor: usize,
282257 height: u16,
283− ) -> Box<dyn Iterator<Item = HighlightEvent>> {
258+ text_annotations: &TextAnnotations,
259+ ) -> Vec<(usize, std::ops::Range<usize>)> {
284260 let text = doc.text().slice(..);
285261 let row = text.char_to_line(anchor.min(text.len_chars()));
286262
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− )
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)
297275 }
298276
299277 /// Get syntax highlights for a document in a view represented by the first line
@@ -308,7 +286,16 @@ impl EditorView {
308286 let text = doc.text().slice(..);
309287 let row = text.char_to_line(anchor.min(text.len_chars()));
310288
311− let range = Self::viewport_byte_range(text, row, height);
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+ };
312299
313300 match doc.syntax() {
314301 Some(syntax) => {
@@ -341,20 +328,6 @@ impl EditorView {
341328 }
342329 }
343330
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−
358331 /// Get highlight spans for document diagnostics
359332 pub fn doc_diagnostics_highlights(
360333 doc: &Document,
@@ -386,7 +359,7 @@ impl EditorView {
386359 let mut warning_vec = Vec::new();
387360 let mut error_vec = Vec::new();
388361
389− for diagnostic in doc.shown_diagnostics() {
362+ for diagnostic in doc.diagnostics() {
390363 // Separate diagnostics into different Vecs by severity.
391364 let (vec, scope) = match diagnostic.severity {
392365 Some(Severity::Info) => (&mut info_vec, info),
@@ -421,7 +394,6 @@ impl EditorView {
421394 view: &View,
422395 theme: &Theme,
423396 cursor_shape_config: &CursorShapeConfig,
424− is_terminal_focused: bool,
425397 ) -> Vec<(usize, std::ops::Range<usize>)> {
426398 let text = doc.text().slice(..);
427399 let selection = doc.selection(view.id);
@@ -469,7 +441,7 @@ impl EditorView {
469441
470442 // Special-case: cursor at end of the rope.
471443 if range.head == range.anchor && range.head == text.len_chars() {
472− if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
444+ if !selection_is_primary || cursor_is_block {
473445 // Bar and underline cursors are drawn by the terminal
474446 // BUG: If the editor area loses focus while having a bar or
475447 // underline cursor (eg. when a regex prompt has focus) then
@@ -492,17 +464,13 @@ impl EditorView {
492464 cursor_start
493465 };
494466 spans.push((selection_scope, range.anchor..selection_end));
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) {
467+ if !selection_is_primary || cursor_is_block {
498468 spans.push((cursor_scope, cursor_start..range.head));
499469 }
500470 } else {
501471 // Reverse case.
502472 let cursor_end = next_grapheme_boundary(text, range.head);
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) {
473+ if !selection_is_primary || cursor_is_block {
506474 spans.push((cursor_scope, range.head..cursor_end));
507475 }
508476 // non block cursors look like they exclude the cursor
@@ -1099,7 +1067,6 @@ impl EditorView {
10991067 let editor = &mut cxt.editor;
11001068
11011069 if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
1102− let prev_view_id = view!(editor).id;
11031070 let doc = doc_mut!(editor, &view!(editor, view_id).doc);
11041071
11051072 if modifiers == KeyModifiers::ALT {
@@ -1109,10 +1076,6 @@ impl EditorView {
11091076 doc.set_selection(view_id, Selection::point(pos));
11101077 }
11111078
1112− if view_id != prev_view_id {
1113− self.clear_completion(editor);
1114− }
1115−
11161079 editor.focus(view_id);
11171080 editor.ensure_cursor_in_view(view_id);
11181081
@@ -1406,17 +1369,13 @@ impl Component for EditorView {
14061369
14071370 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
14081371 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1409− Event::FocusGained => {
1410− self.terminal_focused = true;
1411− EventResult::Consumed(None)
1412− }
1372+ Event::FocusGained => EventResult::Ignored(None),
14131373 Event::FocusLost => {
14141374 if context.editor.config().auto_save {
14151375 if let Err(e) = commands::typed::write_all_impl(context, false, false) {
14161376 context.editor.set_error(format!("{}", e));
14171377 }
14181378 }
1419− self.terminal_focused = false;
14201379 EventResult::Consumed(None)
14211380 }
14221381 }
@@ -1526,15 +1485,8 @@ impl Component for EditorView {
15261485
15271486 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
15281487 match editor.cursor() {
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− }
1488+ // All block cursors are drawn manually
1489+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
15381490 cursor => cursor,
15391491 }
15401492 }
helix-term/src/ui/fuzzy_match.rsnew file+239
@@ -0,0 +1,239 @@
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.rsnew file+47
@@ -0,0 +1,47 @@
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+3−5
@@ -62,7 +62,7 @@ impl Component for SignatureHelp {
6262 });
6363
6464 let sig_text = crate::ui::markdown::highlighted_code_block(
65− &self.signature,
65+ self.signature.clone(),
6666 &self.language,
6767 Some(&cx.editor.theme),
6868 Arc::clone(&self.config_loader),
@@ -92,9 +92,7 @@ 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
96− .clip_top(sig_text_area.height + 2)
97− .clip_bottom(u16::from(cx.editor.popup_border()));
95+ let sig_doc_area = area.clip_top(sig_text_area.height + 2);
9896 let sig_doc_para = Paragraph::new(sig_doc)
9997 .wrap(Wrap { trim: false })
10098 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
@@ -111,7 +109,7 @@ impl Component for SignatureHelp {
111109 let max_text_width = (viewport.0 - PADDING).min(120);
112110
113111 let signature_text = crate::ui::markdown::highlighted_code_block(
114− &self.signature,
112+ self.signature.clone(),
115113 &self.language,
116114 None,
117115 Arc::clone(&self.config_loader),
helix-term/src/ui/markdown.rs+12−19
@@ -10,15 +10,14 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
1010
1111use helix_core::{
1212 syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
13− RopeSlice,
13+ Rope,
1414};
1515use helix_view::{
1616 graphics::{Margin, Rect, Style},
17− theme::Modifier,
1817 Theme,
1918};
2019
21−fn styled_multiline_text<'a>(text: &str, style: Style) -> Text<'a> {
20+fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
2221 let spans: Vec<_> = text
2322 .lines()
2423 .map(|line| Span::styled(line.to_string(), style))
@@ -28,7 +27,7 @@ fn styled_multiline_text<'a>(text: &str, style: Style) -> Text<'a> {
2827}
2928
3029pub fn highlighted_code_block<'a>(
31− text: &str,
30+ text: String,
3231 language: &str,
3332 theme: Option<&Theme>,
3433 config_loader: Arc<syntax::Loader>,
@@ -46,13 +45,13 @@ pub fn highlighted_code_block<'a>(
4645 None => return styled_multiline_text(text, code_style),
4746 };
4847
49− let ropeslice = RopeSlice::from(text);
48+ let rope = Rope::from(text.as_ref());
5049 let syntax = config_loader
5150 .language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
5251 language.into(),
5352 ))
5453 .and_then(|config| config.highlight_config(theme.scopes()))
55− .and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
54+ .and_then(|config| Syntax::new(&rope, config, Arc::clone(&config_loader)));
5655
5756 let syntax = match syntax {
5857 Some(s) => s,
@@ -60,7 +59,7 @@ pub fn highlighted_code_block<'a>(
6059 };
6160
6261 let highlight_iter = syntax
63− .highlight_iter(ropeslice, None, None)
62+ .highlight_iter(rope.slice(..), None, None)
6463 .map(|e| e.unwrap());
6564 let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
6665 if let Some(spans) = additional_highlight_spans {
@@ -184,9 +183,7 @@ impl Markdown {
184183 // Transform text in `<code>` blocks into `Event::Code`
185184 let mut in_code = false;
186185 let parser = parser.filter_map(|event| match event {
187− Event::Html(tag)
188− if tag.starts_with("<code") && matches!(tag.chars().nth(5), Some(' ' | '>')) =>
189− {
186+ Event::Html(tag) if *tag == *"<code>" => {
190187 in_code = true;
191188 None
192189 }
@@ -270,7 +267,7 @@ impl Markdown {
270267 CodeBlockKind::Indented => "",
271268 };
272269 let tui_text = highlighted_code_block(
273− &text,
270+ text.to_string(),
274271 language,
275272 theme,
276273 Arc::clone(&self.config_loader),
@@ -278,21 +275,17 @@ impl Markdown {
278275 );
279276 lines.extend(tui_text.lines.into_iter());
280277 } else {
281− let style = match tags.last() {
282− Some(Tag::Heading(level, ..)) => match level {
278+ let style = if let Some(Tag::Heading(level, ..)) = tags.last() {
279+ match level {
283280 HeadingLevel::H1 => heading_styles[0],
284281 HeadingLevel::H2 => heading_styles[1],
285282 HeadingLevel::H3 => heading_styles[2],
286283 HeadingLevel::H4 => heading_styles[3],
287284 HeadingLevel::H5 => heading_styles[4],
288285 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)
294286 }
295− _ => text_style,
287+ } else {
288+ text_style
296289 };
297290 spans.push(Span::styled(text, style));
298291 }
helix-term/src/ui/menu.rs+40−73
@@ -1,29 +1,22 @@
1−use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
1+use std::{borrow::Cow, path::PathBuf};
22
33use crate::{
44 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
55 ctrl, key, shift,
66};
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−};
7+use tui::{buffer::Buffer as Surface, widgets::Table};
148
159pub use tui::widgets::{Cell, Row};
1610
17−use helix_view::{
18− editor::SmartTabConfig,
19− graphics::{Margin, Rect},
20− Editor,
21−};
11+use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12+use fuzzy_matcher::FuzzyMatcher;
13+
14+use helix_view::{graphics::Rect, Editor};
2215use tui::layout::Constraint;
2316
24−pub trait Item: Sync + Send + 'static {
17+pub trait Item {
2518 /// Additional editor state that is used for label calculation.
26− type Data: Sync + Send + 'static;
19+ type Data;
2720
2821 fn format(&self, data: &Self::Data) -> Row;
2922
@@ -58,8 +51,9 @@ pub struct Menu<T: Item> {
5851
5952 cursor: Option<usize>,
6053
54+ matcher: Box<Matcher>,
6155 /// (index, score)
62− matches: Vec<(u32, u32)>,
56+ matches: Vec<(usize, i64)>,
6357
6458 widths: Vec<Constraint>,
6559
@@ -81,10 +75,11 @@ impl<T: Item> Menu<T> {
8175 editor_data: <T as Item>::Data,
8276 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
8377 ) -> Self {
84− let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
78+ let matches = (0..options.len()).map(|i| (i, 0)).collect();
8579 Self {
8680 options,
8781 editor_data,
82+ matcher: Box::new(Matcher::default().ignore_case()),
8883 matches,
8984 cursor: None,
9085 widths: Vec::new(),
@@ -99,19 +94,20 @@ impl<T: Item> Menu<T> {
9994 pub fn score(&mut self, pattern: &str) {
10095 // reuse the matches allocation
10196 self.matches.clear();
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));
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);
115111
116112 // reset cursor position
117113 self.cursor = None;
@@ -205,7 +201,7 @@ impl<T: Item> Menu<T> {
205201 self.cursor.and_then(|cursor| {
206202 self.matches
207203 .get(cursor)
208− .map(|(index, _score)| &self.options[*index as usize])
204+ .map(|(index, _score)| &self.options[*index])
209205 })
210206 }
211207
@@ -213,7 +209,7 @@ impl<T: Item> Menu<T> {
213209 self.cursor.and_then(|cursor| {
214210 self.matches
215211 .get(cursor)
216− .map(|(index, _score)| &mut self.options[*index as usize])
212+ .map(|(index, _score)| &mut self.options[*index])
217213 })
218214 }
219215
@@ -251,21 +247,6 @@ impl<T: Item + 'static> Component for Menu<T> {
251247 compositor.pop();
252248 }));
253249
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−
269250 match event {
270251 // esc or ctrl-c aborts the completion and closes the menu
271252 key!(Esc) | ctrl!('c') => {
@@ -329,15 +310,6 @@ impl<T: Item + 'static> Component for Menu<T> {
329310 let selected = theme.get("ui.menu.selected");
330311 surface.clear_with(area, style);
331312
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−
341313 let scroll = self.scroll;
342314
343315 let options: Vec<_> = self
@@ -345,7 +317,7 @@ impl<T: Item + 'static> Component for Menu<T> {
345317 .iter()
346318 .map(|(index, _score)| {
347319 // (index, self.options.get(*index).unwrap()) // get_unchecked
348− &self.options[*index as usize] // get_unchecked
320+ &self.options[*index] // get_unchecked
349321 })
350322 .collect();
351323
@@ -378,19 +350,15 @@ impl<T: Item + 'static> Component for Menu<T> {
378350 false,
379351 );
380352
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− }
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);
394362 }
395363
396364 let fits = len <= win_height;
@@ -405,13 +373,12 @@ impl<T: Item + 'static> Component for Menu<T> {
405373 for i in 0..win_height {
406374 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
407375
408− let half_block = if render_borders { "▌" } else { "▐" };
376+ cell.set_symbol("▐"); // right half block
409377
410378 if scroll_line <= i && i < scroll_line + scroll_height {
411379 // Draw scroll thumb
412− cell.set_symbol(half_block);
413380 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
414− } else if !render_borders {
381+ } else {
415382 // Draw scroll track
416383 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
417384 }
helix-term/src/ui/mod.rs+150−91
@@ -1,12 +1,13 @@
11mod completion;
22mod document;
33pub(crate) mod editor;
4+mod fuzzy_match;
45mod info;
56pub mod lsp;
67mod markdown;
78pub mod menu;
89pub mod overlay;
9−pub mod picker;
10+mod picker;
1011pub mod popup;
1112mod prompt;
1213mod spinner;
@@ -63,7 +64,7 @@ pub fn regex_prompt(
6364 prompt: std::borrow::Cow<'static, str>,
6465 history_register: Option<char>,
6566 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
66− fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
67+ fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
6768) {
6869 let (view, doc) = current!(cx.editor);
6970 let doc_id = view.doc;
@@ -110,7 +111,7 @@ pub fn regex_prompt(
110111 view.jumps.push((doc_id, snapshot.clone()));
111112 }
112113
113− fun(cx, regex, event);
114+ fun(cx.editor, regex, event);
114115
115116 let (view, doc) = current!(cx.editor);
116117 view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -141,14 +142,16 @@ pub fn regex_prompt(
141142 };
142143
143144 cx.jobs.callback(callback);
145+ } else {
146+ // Update
147+ // TODO: mark command line as error
144148 }
145149 }
146150 }
147151 }
148152 }
149153 },
150− )
151− .with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader));
154+ );
152155 // Calculate initial completion
153156 prompt.recalculate_completion(cx.editor);
154157 // prompt
@@ -173,13 +176,9 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
173176 .git_ignore(config.file_picker.git_ignore)
174177 .git_global(config.file_picker.git_global)
175178 .git_exclude(config.file_picker.git_exclude)
176− .sort_by_file_name(|name1, name2| name1.cmp(name2))
177179 .max_depth(config.file_picker.max_depth)
178180 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
179181
180− walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
181− walk_builder.add_custom_ignore_filename(".helix/ignore");
182−
183182 // We want to exclude files that the editor can't handle yet
184183 let mut type_builder = TypesBuilder::new();
185184 type_builder
@@ -193,16 +192,32 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
193192 .build()
194193 .expect("failed to build excluded_types");
195194 walk_builder.types(excluded_types);
196− let mut files = walk_builder.build().filter_map(|entry| {
195+
196+ // We want files along with their modification date for sorting
197+ let files = walk_builder.build().filter_map(|entry| {
197198 let entry = entry.ok()?;
198− if !entry.file_type()?.is_file() {
199− return None;
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
200204 }
201− Some(entry.into_path())
202205 });
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+
203218 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
204219
205− let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
220+ Picker::new(files, root, move |cx, path: &PathBuf, action| {
206221 if let Err(e) = cx.editor.open(path, action) {
207222 let err = if let Some(err) = e.source() {
208223 format!("{}", err)
@@ -212,41 +227,20 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
212227 cx.editor.set_error(err);
213228 }
214229 })
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
230+ .with_preview(|_editor, path| Some((path.clone().into(), None)))
239231}
240232
241233pub mod completers {
242234 use crate::ui::prompt::Completion;
243− use helix_core::fuzzy::fuzzy_match;
235+ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
236+ use fuzzy_matcher::FuzzyMatcher;
244237 use helix_core::syntax::LanguageServerFeature;
245238 use helix_view::document::SCRATCH_BUFFER_NAME;
246239 use helix_view::theme;
247240 use helix_view::{editor::Config, Editor};
248241 use once_cell::sync::Lazy;
249242 use std::borrow::Cow;
243+ use std::cmp::Reverse;
250244
251245 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
252246
@@ -255,16 +249,31 @@ pub mod completers {
255249 }
256250
257251 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
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− });
263−
264− fuzzy_match(input, names, true)
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
265267 .into_iter()
266− .map(|(name, _)| ((0..), name))
267− .collect()
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();
275+
276+ names
268277 }
269278
270279 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@@ -277,10 +286,26 @@ pub mod completers {
277286 names.sort();
278287 names.dedup();
279288
280− fuzzy_match(input, names, false)
289+ let mut names: Vec<_> = names
281290 .into_iter()
282− .map(|(name, _)| ((0..), name.into()))
283− .collect()
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
284309 }
285310
286311 /// Recursive function to get all keys from this value and add them to vec
@@ -307,22 +332,22 @@ pub mod completers {
307332 keys
308333 });
309334
310− fuzzy_match(input, &*KEYS, false)
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
311344 .into_iter()
312345 .map(|(name, _)| ((0..), name.into()))
313346 .collect()
314347 }
315348
316349 pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> {
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| {
350+ filename_impl(editor, input, |entry| {
326351 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
327352
328353 if is_dir {
@@ -334,6 +359,8 @@ pub mod completers {
334359 }
335360
336361 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
362+ let matcher = Matcher::default();
363+
337364 let text: String = "text".into();
338365
339366 let language_ids = editor
@@ -342,13 +369,27 @@ pub mod completers {
342369 .map(|config| &config.language_id)
343370 .chain(std::iter::once(&text));
344371
345− fuzzy_match(input, language_ids, false)
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
346385 .into_iter()
347− .map(|(name, _)| ((0..), name.to_owned().into()))
386+ .map(|(language, _score)| ((0..), language.clone().into()))
348387 .collect()
349388 }
350389
351390 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
391+ let matcher = Matcher::default();
392+
352393 let Some(options) = doc!(editor)
353394 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
354395 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@@ -356,22 +397,28 @@ pub mod completers {
356397 return vec![];
357398 };
358399
359− fuzzy_match(input, &options.commands, false)
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
360415 .into_iter()
361− .map(|(name, _)| ((0..), name.to_owned().into()))
416+ .map(|(command, _score)| ((0..), command.clone().into()))
362417 .collect()
363418 }
364419
365420 pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
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| {
421+ filename_impl(editor, input, |entry| {
375422 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
376423
377424 if is_dir {
@@ -394,12 +441,7 @@ pub mod completers {
394441 }
395442
396443 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
397− fn filename_impl<F>(
398− _editor: &Editor,
399− input: &str,
400− git_ignore: bool,
401− filter_fn: F,
402− ) -> Vec<Completion>
444+ fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion>
403445 where
404446 F: Fn(&ignore::DirEntry) -> FileMatch,
405447 {
@@ -430,7 +472,7 @@ pub mod completers {
430472 match path.parent() {
431473 Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
432474 // Path::new("h")'s parent is Some("")...
433− _ => helix_loader::current_working_dir(),
475+ _ => std::env::current_dir().expect("couldn't determine current directory"),
434476 }
435477 };
436478
@@ -439,10 +481,9 @@ pub mod completers {
439481
440482 let end = input.len()..;
441483
442− let files = WalkBuilder::new(&dir)
484+ let mut files: Vec<_> = WalkBuilder::new(&dir)
443485 .hidden(false)
444486 .follow_links(false) // We're scanning over depth 1
445− .git_ignore(git_ignore)
446487 .max_depth(Some(1))
447488 .build()
448489 .filter_map(|file| {
@@ -471,25 +512,43 @@ pub mod completers {
471512 path.push("");
472513 }
473514
474− let path = path.into_os_string().into_string().ok()?;
475− Some(Cow::from(path))
515+ let path = path.to_str()?.to_owned();
516+ Some((end.clone(), Cow::from(path)))
476517 })
477518 }) // TODO: unwrap or skip
478− .filter(|path| !path.is_empty());
519+ .filter(|(_, path)| !path.is_empty()) // TODO
520+ .collect();
479521
480522 // if empty, return a list of dirs and files in current dir
481523 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+
482536 let range = (input.len().saturating_sub(file_name.len()))..;
483− fuzzy_match(&file_name, files, true)
537+
538+ matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
539+ (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
540+ });
541+
542+ files = matches
484543 .into_iter()
485− .map(|(name, _)| (range.clone(), name))
486− .collect()
544+ .map(|(file, _)| (range.clone(), file))
545+ .collect();
487546
488547 // TODO: complete to longest common match
489548 } else {
490− let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
491549 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
492− files
493550 }
551+
552+ files
494553 }
495554}
helix-term/src/ui/picker.rs+325−362
@@ -7,12 +7,11 @@ use crate::{
77 ui::{
88 self,
99 document::{render_document, LineDecoration, LinePos, TextRenderer},
10+ fuzzy_match::FuzzyQuery,
1011 EditorView,
1112 },
1213};
1314use futures_util::{future::BoxFuture, FutureExt};
14−use nucleo::pattern::CaseMatching;
15−use nucleo::{Config, Nucleo, Utf32String};
1615use tui::{
1716 buffer::Buffer as Surface,
1817 layout::Constraint,
@@ -20,23 +19,16 @@ use tui::{
2019 widgets::{Block, BorderType, Borders, Cell, Table},
2120};
2221
22+use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2323use tui::widgets::Widget;
2424
25−use std::{
26− collections::HashMap,
27− io::Read,
28− path::PathBuf,
29− sync::{
30− atomic::{self, AtomicBool},
31− Arc,
32− },
33−};
25+use std::cmp::{self, Ordering};
26+use std::{collections::HashMap, io::Read, path::PathBuf};
3427
3528use crate::ui::{Prompt, PromptEvent};
3629use helix_core::{
37− char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
38− text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39− Syntax,
30+ movement::Direction, text_annotations::TextAnnotations,
31+ unicode::segmentation::UnicodeSegmentation, Position, Syntax,
4032};
4133use helix_view::{
4234 editor::Action,
@@ -46,7 +38,6 @@ use helix_view::{
4638 Document, DocumentId, Editor,
4739};
4840
49−pub const ID: &str = "picker";
5041use super::{menu::Item, overlay::Overlay};
5142
5243pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
@@ -60,12 +51,12 @@ pub enum PathOrId {
6051}
6152
6253impl PathOrId {
63− fn get_canonicalized(self) -> Self {
54+ fn get_canonicalized(self) -> std::io::Result<Self> {
6455 use PathOrId::*;
65− match self {
66− Path(path) => Path(helix_core::path::get_canonicalized_path(&path)),
56+ Ok(match self {
57+ Path(path) => Path(helix_core::path::get_canonicalized_path(&path)?),
6758 Id(id) => Id(id),
68− }
59+ })
6960 }
7061}
7162
@@ -112,9 +103,9 @@ impl Preview<'_, '_> {
112103 /// Alternate text to show for the preview.
113104 fn placeholder(&self) -> &str {
114105 match *self {
115− Self::EditorDocument(_) => "<Invalid file location>",
106+ Self::EditorDocument(_) => "<File preview>",
116107 Self::Cached(preview) => match preview {
117− CachedPreview::Document(_) => "<Invalid file location>",
108+ CachedPreview::Document(_) => "<File preview>",
118109 CachedPreview::Binary => "<Binary file>",
119110 CachedPreview::LargeFile => "<File too large to preview>",
120111 CachedPreview::NotFound => "<File not found>",
@@ -123,71 +114,20 @@ impl Preview<'_, '_> {
123114 }
124115}
125116
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−
179117pub struct Picker<T: Item> {
180− editor_data: Arc<T::Data>,
181− shutdown: Arc<AtomicBool>,
182− matcher: Nucleo<T>,
118+ options: Vec<T>,
119+ editor_data: T::Data,
120+ // filter: String,
121+ matcher: Box<Matcher>,
122+ matches: Vec<PickerMatch>,
183123
184124 /// Current height of the completions box
185125 completion_height: u16,
186126
187− cursor: u32,
127+ cursor: usize,
128+ // pattern: String,
188129 prompt: Prompt,
189− previous_pattern: String,
190−
130+ previous_pattern: (String, FuzzyQuery),
191131 /// Whether to show the preview panel (default true)
192132 show_preview: bool,
193133 /// Constraints for tabular formatting
@@ -204,59 +144,10 @@ pub struct Picker<T: Item> {
204144}
205145
206146impl<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−
222147 pub fn new(
223148 options: Vec<T>,
224149 editor_data: T::Data,
225150 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,
260151 ) -> Self {
261152 let prompt = Prompt::new(
262153 "".into(),
@@ -265,13 +156,14 @@ impl<T: Item + 'static> Picker<T> {
265156 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
266157 );
267158
268− Self {
269− matcher,
159+ let mut picker = Self {
160+ options,
270161 editor_data,
271− shutdown,
162+ matcher: Box::default(),
163+ matches: Vec::new(),
272164 cursor: 0,
273165 prompt,
274− previous_pattern: String::new(),
166+ previous_pattern: (String::new(), FuzzyQuery::default()),
275167 truncate_start: true,
276168 show_preview: true,
277169 callback_fn: Box::new(callback_fn),
@@ -280,15 +172,24 @@ impl<T: Item + 'static> Picker<T> {
280172 preview_cache: HashMap::new(),
281173 read_buffer: Vec::with_capacity(1024),
282174 file_fn: None,
283− }
284− }
175+ };
285176
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− }
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+ }));
191+
192+ picker
292193 }
293194
294195 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -301,25 +202,122 @@ impl<T: Item + 'static> Picker<T> {
301202 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
302203 ) -> Self {
303204 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());
307205 self
308206 }
309207
310208 pub fn set_options(&mut self, new_options: Vec<T>) {
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);
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+ }
316231 }
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;
317245 }
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();
318316 }
319317
320318 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
321− pub fn move_by(&mut self, amount: u32, direction: Direction) {
322− let len = self.matcher.snapshot().matched_item_count();
319+ pub fn move_by(&mut self, amount: usize, direction: Direction) {
320+ let len = self.matches.len();
323321
324322 if len == 0 {
325323 // No results, can't move.
@@ -338,12 +336,12 @@ impl<T: Item + 'static> Picker<T> {
338336
339337 /// Move the cursor down by exactly one page. After the last page comes the first page.
340338 pub fn page_up(&mut self) {
341− self.move_by(self.completion_height as u32, Direction::Backward);
339+ self.move_by(self.completion_height as usize, Direction::Backward);
342340 }
343341
344342 /// Move the cursor up by exactly one page. After the first page comes the last page.
345343 pub fn page_down(&mut self) {
346− self.move_by(self.completion_height as u32, Direction::Forward);
344+ self.move_by(self.completion_height as usize, Direction::Forward);
347345 }
348346
349347 /// Move the cursor to the first entry
@@ -353,18 +351,13 @@ impl<T: Item + 'static> Picker<T> {
353351
354352 /// Move the cursor to the last entry
355353 pub fn to_end(&mut self) {
356− self.cursor = self
357− .matcher
358− .snapshot()
359− .matched_item_count()
360− .saturating_sub(1);
354+ self.cursor = self.matches.len().saturating_sub(1);
361355 }
362356
363357 pub fn selection(&self) -> Option<&T> {
364− self.matcher
365− .snapshot()
366− .get_matched_item(self.cursor)
367− .map(|item| item.data)
358+ self.matches
359+ .get(self.cursor)
360+ .map(|pmatch| &self.options[pmatch.index])
368361 }
369362
370363 pub fn toggle_preview(&mut self) {
@@ -373,17 +366,8 @@ impl<T: Item + 'static> Picker<T> {
373366
374367 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
375368 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
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− }
369+ // TODO: recalculate only if pattern changed
370+ self.score();
387371 }
388372 EventResult::Consumed(None)
389373 }
@@ -391,7 +375,7 @@ impl<T: Item + 'static> Picker<T> {
391375 fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
392376 self.selection()
393377 .and_then(|current| (self.file_fn.as_ref()?)(editor, current))
394− .map(|(path_or_id, line)| (path_or_id.get_canonicalized(), line))
378+ .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
395379 }
396380
397381 /// Get (cached) preview for a given path. If a document corresponding
@@ -427,9 +411,12 @@ impl<T: Item + 'static> Picker<T> {
427411 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
428412 CachedPreview::LargeFile
429413 }
430− _ => Document::open(path, None, None, editor.config.clone())
431− .map(|doc| CachedPreview::Document(Box::new(doc)))
432− .unwrap_or(CachedPreview::NotFound),
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+ }
433420 },
434421 )
435422 .unwrap_or(CachedPreview::NotFound);
@@ -445,7 +432,7 @@ impl<T: Item + 'static> Picker<T> {
445432
446433 fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
447434 let Some((current_file, _)) = self.current_file(cx.editor) else {
448− return EventResult::Consumed(None);
435+ return EventResult::Consumed(None)
449436 };
450437
451438 // Try to find a document in the cache
@@ -466,24 +453,17 @@ impl<T: Item + 'static> Picker<T> {
466453 let text = doc.text().clone();
467454 let loader = cx.editor.syn_loader.clone();
468455 let job = tokio::task::spawn_blocking(move || {
469− let syntax = language_config.highlight_config(&loader.scopes()).and_then(
470− |highlight_config| Syntax::new(text.slice(..), highlight_config, loader),
471− );
456+ let syntax = language_config
457+ .highlight_config(&loader.scopes())
458+ .and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
472459 let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
473460 let Some(syntax) = syntax else {
474461 log::info!("highlighting picker item failed");
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),
462+ return
482463 };
483− let Some(picker) = picker
484− else {
464+ let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
485465 log::info!("picker closed before syntax highlighting finished");
486− return;
466+ return
487467 };
488468 // Try to find a document in the cache
489469 let doc = match current_file {
@@ -512,14 +492,6 @@ impl<T: Item + 'static> Picker<T> {
512492 }
513493
514494 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−
523495 let text_style = cx.editor.theme.get("ui.text");
524496 let selected = cx.editor.theme.get("ui.text.focus");
525497 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -540,15 +512,8 @@ impl<T: Item + 'static> Picker<T> {
540512 // -- Render the input bar:
541513
542514 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);
545515
546− let count = format!(
547− "{}{}/{}",
548− if status.running { "(running) " } else { "" },
549− snapshot.matched_item_count(),
550− snapshot.item_count(),
551− );
516+ let count = format!("{}/{}", self.matches.len(), self.options.len());
552517 surface.set_stringn(
553518 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
554519 area.y,
@@ -557,6 +522,8 @@ impl<T: Item + 'static> Picker<T> {
557522 text_style,
558523 );
559524
525+ self.prompt.render(area, surface, cx);
526+
560527 // -- Separator
561528 let sep_style = cx.editor.theme.get("ui.background.separator");
562529 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -569,89 +536,106 @@ impl<T: Item + 'static> Picker<T> {
569536 // -- Render the contents:
570537 // subtract area of prompt from top
571538 let inner = inner.clip_top(2);
572− let rows = inner.height as u32;
573− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
539+
540+ let rows = inner.height;
541+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
574542 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− }
584543
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− };
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+ });
606559
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)
624− } else {
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))
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+ };
588+
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)
630613 }
631− current_span = String::new();
632− current_style = style;
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);
622+ } else {
623+ span_list.push((String::from(grapheme), style))
633624 }
634− current_span.push_str(grapheme);
635− grapheme_idx += 1;
636625 }
637− width += span.width();
638− }
639626
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));
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);
645633
646− // spacer
647− if grapheme_idx == next_highlight_idx {
648− next_highlight_idx = indices.next().unwrap_or(u32::MAX);
634+ cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
649635 }
650− grapheme_idx += 1;
651− }
652636
653− row
654− });
637+ row
638+ });
655639
656640 let table = Table::new(options)
657641 .style(text_style)
@@ -667,7 +651,7 @@ impl<T: Item + 'static> Picker<T> {
667651 surface,
668652 &mut TableState {
669653 offset: 0,
670− selected: Some(cursor as usize),
654+ selected: Some(cursor),
671655 },
672656 self.truncate_start,
673657 );
@@ -693,14 +677,8 @@ impl<T: Item + 'static> Picker<T> {
693677 if let Some((path, range)) = self.current_file(cx.editor) {
694678 let preview = self.get_preview(path, cx.editor);
695679 let doc = match preview.document() {
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− _ => {
680+ Some(doc) => doc,
681+ None => {
704682 let alt_text = preview.placeholder();
705683 let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
706684 let y = inner.y + inner.height / 2;
@@ -709,47 +687,32 @@ impl<T: Item + 'static> Picker<T> {
709687 }
710688 };
711689
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− }
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);
738698
739− let syntax_highlights = EditorView::doc_syntax_highlights(
699+ let offset = ViewPosition {
700+ anchor: doc.text().line_to_char(first_line),
701+ horizontal_offset: 0,
702+ vertical_offset: 0,
703+ };
704+
705+ let mut highlights = EditorView::doc_syntax_highlights(
740706 doc,
741707 offset.anchor,
742708 area.height,
743709 &cx.editor.theme,
744710 );
745−
746− let mut overlay_highlights =
747− EditorView::empty_highlight_iter(doc, offset.anchor, area.height);
748711 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
749712 if spans.is_empty() {
750713 continue;
751714 }
752− overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
715+ highlights = Box::new(helix_core::syntax::merge(highlights, spans));
753716 }
754717 let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
755718
@@ -780,8 +743,7 @@ impl<T: Item + 'static> Picker<T> {
780743 offset,
781744 // TODO: compute text annotations asynchronously here (like inlay hints)
782745 &TextAnnotations::default(),
783− syntax_highlights,
784− overlay_highlights,
746+ highlights,
785747 &cx.editor.theme,
786748 &mut decorations,
787749 &mut [],
@@ -790,7 +752,7 @@ impl<T: Item + 'static> Picker<T> {
790752 }
791753}
792754
793−impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
755+impl<T: Item + 'static> Component for Picker<T> {
794756 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
795757 // +---------+ +---------+
796758 // |prompt | |preview |
@@ -799,8 +761,7 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
799761 // | | | |
800762 // +---------+ +---------+
801763
802− let render_preview =
803− self.show_preview && self.file_fn.is_some() && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
764+ let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
804765
805766 let picker_width = if render_preview {
806767 area.width / 2
@@ -830,28 +791,11 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
830791 _ => return EventResult::Ignored(None),
831792 };
832793
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− };
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+ })));
855799
856800 // So that idle timeout retriggers
857801 ctx.editor.reset_idle_timer();
@@ -875,7 +819,9 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
875819 key!(End) => {
876820 self.to_end();
877821 }
878− key!(Esc) | ctrl!('c') => return close_fn(self),
822+ key!(Esc) | ctrl!('c') => {
823+ return close_fn;
824+ }
879825 alt!(Enter) => {
880826 if let Some(option) = self.selection() {
881827 (self.callback_fn)(ctx, option, Action::Load);
@@ -885,19 +831,19 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
885831 if let Some(option) = self.selection() {
886832 (self.callback_fn)(ctx, option, Action::Replace);
887833 }
888− return close_fn(self);
834+ return close_fn;
889835 }
890836 ctrl!('s') => {
891837 if let Some(option) = self.selection() {
892838 (self.callback_fn)(ctx, option, Action::HorizontalSplit);
893839 }
894− return close_fn(self);
840+ return close_fn;
895841 }
896842 ctrl!('v') => {
897843 if let Some(option) = self.selection() {
898844 (self.callback_fn)(ctx, option, Action::VerticalSplit);
899845 }
900− return close_fn(self);
846+ return close_fn;
901847 }
902848 ctrl!('t') => {
903849 self.toggle_preview();
@@ -925,15 +871,30 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
925871 self.completion_height = height.saturating_sub(4);
926872 Some((width, height))
927873 }
874+}
928875
929− fn id(&self) -> Option<&'static str> {
930− Some(ID)
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)
931886 }
932887}
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)
888+
889+impl PartialOrd for PickerMatch {
890+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
891+ Some(self.cmp(other))
892+ }
893+}
894+
895+impl Ord for PickerMatch {
896+ fn cmp(&self, other: &Self) -> Ordering {
897+ self.key().cmp(&other.key())
937898 }
938899}
939900
@@ -946,13 +907,15 @@ pub type DynQueryCallback<T> =
946907
947908/// A picker that updates its contents via a callback whenever the
948909/// query string changes. Useful for live grep, workspace symbols, etc.
949−pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
910+pub struct DynamicPicker<T: ui::menu::Item + Send> {
950911 file_picker: Picker<T>,
951912 query_callback: DynQueryCallback<T>,
952913 query: String,
953914}
954915
955−impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
916+impl<T: ui::menu::Item + Send> DynamicPicker<T> {
917+ pub const ID: &'static str = "dynamic-picker";
918+
956919 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
957920 Self {
958921 file_picker,
@@ -962,7 +925,7 @@ impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
962925 }
963926}
964927
965−impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
928+impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
966929 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
967930 self.file_picker.render(area, surface, cx);
968931 }
@@ -984,7 +947,7 @@ impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
984947 let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
985948 // Wrapping of pickers in overlay is done outside the picker code,
986949 // so this is fragile and will break if wrapped in some other widget.
987− let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
950+ let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
988951 Some(overlay) => &mut overlay.content.file_picker,
989952 None => return,
990953 };
@@ -1005,6 +968,6 @@ impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
1005968 }
1006969
1007970 fn id(&self) -> Option<&'static str> {
1008− Some(ID)
971+ Some(Self::ID)
1009972 }
1010973}
helix-term/src/ui/popup.rs+7−27
@@ -3,10 +3,7 @@ use crate::{
33 compositor::{Callback, Component, Context, Event, EventResult},
44 ctrl, key,
55};
6−use tui::{
7− buffer::Buffer as Surface,
8− widgets::{Block, Borders, Widget},
9−};
6+use tui::buffer::Buffer as Surface;
107
118use helix_core::Position;
129use helix_view::{
@@ -255,29 +252,13 @@ impl<T: Component> Component for Popup<T> {
255252 let background = cx.editor.theme.get("ui.popup");
256253 surface.clear_with(area, background);
257254
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−
255+ let inner = area.inner(&self.margin);
275256 self.contents.render(inner, surface, cx);
276257
277258 // render scrollbar if contents do not fit
278259 if self.has_scrollbar {
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);
260+ let win_height = inner.height as usize;
261+ let len = self.child_size.1 as usize;
281262 let fits = len <= win_height;
282263 let scroll = self.scroll;
283264 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -293,15 +274,14 @@ impl<T: Component> Component for Popup<T> {
293274
294275 let mut cell;
295276 for i in 0..win_height {
296− cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
277+ cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
297278
298− let half_block = if render_borders { "▌" } else { "▐" };
279+ cell.set_symbol("▐"); // right half block
299280
300281 if scroll_line <= i && i < scroll_line + scroll_height {
301282 // Draw scroll thumb
302− cell.set_symbol(half_block);
303283 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
304− } else if !render_borders {
284+ } else {
305285 // Draw scroll track
306286 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
307287 }
helix-term/src/ui/prompt.rs+48−55
@@ -1,9 +1,7 @@
11use crate::compositor::{Component, Compositor, Context, Event, EventResult};
22use crate::{alt, ctrl, key, shift, ui};
3−use helix_core::syntax;
43use helix_view::input::KeyEvent;
54use helix_view::keyboard::KeyCode;
6−use std::sync::Arc;
75use std::{borrow::Cow, ops::RangeFrom};
86use tui::buffer::Buffer as Surface;
97use tui::widgets::{Block, Borders, Widget};
@@ -34,7 +32,6 @@ pub struct Prompt {
3432 callback_fn: CallbackFn,
3533 pub doc_fn: DocFn,
3634 next_char_handler: Option<PromptCharHandler>,
37− language: Option<(&'static str, Arc<syntax::Loader>)>,
3835}
3936
4037#[derive(Clone, Copy, PartialEq, Eq)]
@@ -86,7 +83,6 @@ impl Prompt {
8683 callback_fn: Box::new(callback_fn),
8784 doc_fn: Box::new(|_| None),
8885 next_char_handler: None,
89− language: None,
9086 }
9187 }
9288
@@ -98,11 +94,6 @@ impl Prompt {
9894 self
9995 }
10096
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−
10697 pub fn line(&self) -> &String {
10798 &self.line
10899 }
@@ -306,8 +297,8 @@ impl Prompt {
306297 direction: CompletionDirection,
307298 ) {
308299 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
309− let mut values = match cx.editor.registers.read(register, cx.editor) {
310− Some(values) if values.len() > 0 => values.rev(),
300+ let values = match cx.editor.registers.read(register) {
301+ Some(values) if !values.is_empty() => values,
311302 _ => return,
312303 };
313304
@@ -315,16 +306,13 @@ impl Prompt {
315306
316307 let index = match direction {
317308 CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
318− CompletionDirection::Backward => self
319− .history_pos
320− .unwrap_or_else(|| values.len())
321− .saturating_sub(1),
309+ CompletionDirection::Backward => {
310+ self.history_pos.unwrap_or(values.len()).saturating_sub(1)
311+ }
322312 }
323313 .min(end);
324314
325− self.line = values.nth(index).unwrap().to_string();
326− // Appease the borrow checker.
327− drop(values);
315+ self.line = values[index].clone();
328316
329317 self.history_pos = Some(index);
330318
@@ -368,7 +356,6 @@ impl Prompt {
368356 let completion_color = theme.get("ui.menu");
369357 let selected_color = theme.get("ui.menu.selected");
370358 let suggestion_color = theme.get("ui.text.inactive");
371− let background = theme.get("ui.background");
372359 // completion
373360
374361 let max_len = self
@@ -464,32 +451,33 @@ impl Prompt {
464451 }
465452
466453 let line = area.height - 1;
467− surface.clear_with(area.clip_top(line), background);
468454 // render buffer text
469455 surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
470456
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
457+ let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() {
458+ // latest value in the register list
459+ match self
475460 .history_register
476− .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
461+ .and_then(|reg| cx.editor.registers.last(reg))
462+ .map(|entry| entry.into())
477463 {
478− surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
464+ Some(value) => (value, true),
465+ None => (Cow::from(""), false),
479466 }
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);
490467 } else {
491− surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
492− }
468+ (self.line.as_str().into(), false)
469+ };
470+
471+ surface.set_string(
472+ area.x + self.prompt.len() as u16,
473+ area.y + line,
474+ &input,
475+ if is_suggestion {
476+ suggestion_color
477+ } else {
478+ prompt_color
479+ },
480+ );
493481 }
494482}
495483
@@ -570,29 +558,25 @@ impl Component for Prompt {
570558 } else {
571559 let last_item = self
572560 .history_register
573− .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
574− .map(|entry| entry.to_string())
575− .unwrap_or_else(|| String::from(""));
561+ .and_then(|reg| cx.editor.registers.last(reg).cloned())
562+ .map(|entry| entry.into())
563+ .unwrap_or_else(|| Cow::from(""));
576564
577565 // handle executing with last command in history if nothing entered
578− let input = if self.line.is_empty() {
579− &last_item
566+ let input: Cow<str> = if self.line.is_empty() {
567+ last_item
580568 } else {
581569 if last_item != self.line {
582570 // store in history
583571 if let Some(register) = self.history_register {
584− if let Err(err) =
585− cx.editor.registers.push(register, self.line.clone())
586− {
587− cx.editor.set_error(err.to_string());
588− }
572+ cx.editor.registers.push(register, self.line.clone());
589573 };
590574 }
591575
592− &self.line
576+ self.line.as_str().into()
593577 };
594578
595− (self.callback_fn)(cx, input, PromptEvent::Validate);
579+ (self.callback_fn)(cx, &input, PromptEvent::Validate);
596580
597581 return close_fn;
598582 }
@@ -624,16 +608,25 @@ impl Component for Prompt {
624608 self.completion = cx
625609 .editor
626610 .registers
627− .iter_preview()
628− .map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
611+ .inner()
612+ .iter()
613+ .map(|(ch, reg)| {
614+ let content = reg
615+ .read()
616+ .get(0)
617+ .and_then(|s| s.lines().next().to_owned())
618+ .unwrap_or_default();
619+ (0.., format!("{} {}", ch, &content).into())
620+ })
629621 .collect();
630622 self.next_char_handler = Some(Box::new(|prompt, c, context| {
631623 prompt.insert_str(
632− &context
624+ context
633625 .editor
634626 .registers
635− .first(c, context.editor)
636− .unwrap_or_default(),
627+ .read(c)
628+ .and_then(|r| r.first())
629+ .map_or("", |r| r.as_str()),
637630 context.editor,
638631 );
639632 }));
helix-term/src/ui/statusline.rs−14
@@ -145,7 +145,6 @@ where
145145 helix_view::editor::StatusLineElement::FileModificationIndicator => {
146146 render_file_modification_indicator
147147 }
148− helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator,
149148 helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
150149 helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
151150 helix_view::editor::StatusLineElement::FileType => render_file_type,
@@ -443,19 +442,6 @@ where
443442 write(context, title, None);
444443}
445444
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−
459445fn render_file_base_name<F>(context: &mut RenderContext, write: F)
460446where
461447 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
helix-term/tests/integration.rs−1
@@ -18,7 +18,6 @@ mod test {
1818 mod auto_indent;
1919 mod auto_pairs;
2020 mod commands;
21− mod languages;
2221 mod movement;
2322 mod prompt;
2423 mod splits;
helix-term/tests/test/commands.rs−47
@@ -2,7 +2,6 @@ use helix_term::application::Application;
22
33use super::*;
44
5−mod movement;
65mod write;
76
87#[tokio::test(flavor = "multi_thread")]
@@ -480,49 +479,3 @@ fn bar() {#(\n|)#\
480479
481480 Ok(())
482481}
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.rsdeleted−452
@@ -1,452 +0,0 @@
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+3−122
@@ -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(), &platform_line(&RANGE.end().to_string()))?;
96+ helpers::assert_file_has_content(file.as_file_mut(), &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!(platform_line(&RANGE.end().to_string()), file_content);
212+ assert_eq!(RANGE.end().to_string(), file_content);
213213
214214 Ok(())
215215}
@@ -424,132 +424,13 @@ 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−
538427async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
539428 let mut file = tempfile::NamedTempFile::new()?;
540429
541430 file.as_file_mut().write_all(&file_content)?;
542431
543432 helpers::test_key_sequence(
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()?,
433+ &mut helpers::AppBuilder::new().build()?,
553434 Some(&format!(":o {}<ret>:x<ret>", file.path().to_string_lossy())),
554435 None,
555436 true,
helix-term/tests/test/helpers.rs+1−9
@@ -320,14 +320,6 @@ 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−
331323 let mut app = Application::new(self.args, self.config, self.syn_conf)?;
332324
333325 if let Some((text, selection)) = self.input {
@@ -358,7 +350,7 @@ pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result
358350
359351 let mut file_content = String::new();
360352 file.read_to_string(&mut file_content)?;
361− assert_eq!(file_content, content);
353+ assert_eq!(content, file_content);
362354
363355 Ok(())
364356}
helix-term/tests/test/languages/go.rsdeleted−41
@@ -1,41 +0,0 @@
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.rsdeleted−4
@@ -1,4 +0,0 @@
1−use super::*;
2−
3−mod go;
4−mod yaml;
helix-term/tests/test/languages/yaml.rsdeleted−819
@@ -1,819 +0,0 @@
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,42 +513,3 @@ 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(), &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"))?;
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")?;
6868
6969 Ok(())
7070}
helix-tui/Cargo.toml+15−15
@@ -1,28 +1,28 @@
11[package]
22name = "helix-tui"
3−description = """A library to build rich terminal user interfaces or dashboards"""
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"
413include = ["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
1314
1415[features]
1516default = ["crossterm"]
1617
1718[dependencies]
18−helix-view = { path = "../helix-view", features = ["term"] }
19−helix-core = { path = "../helix-core" }
20−
21−bitflags = "2.4"
19+bitflags = "2.3"
2220cassowary = "0.3"
2321unicode-segmentation = "1.10"
24−crossterm = { version = "0.27", optional = true }
22+crossterm = { version = "0.26", optional = true }
2523termini = "1.0"
2624serde = { version = "1", "optional" = true, features = ["derive"]}
27−once_cell = "1.19"
25+once_cell = "1.18"
2826log = "~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+33−51
@@ -32,21 +32,10 @@ fn vte_version() -> Option<usize> {
3232}
3333
3434/// Describes terminal capabilities like extended underline, truecolor, etc.
35−#[derive(Clone, Debug)]
35+#[derive(Copy, Clone, Debug, Default)]
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− }
5039}
5140
5241impl Capabilities {
@@ -65,10 +54,6 @@ impl Capabilities {
6554 || t.extended_cap("Su").is_some()
6655 || vte_version() >= Some(5102)
6756 || 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(),
7257 },
7358 }
7459 }
@@ -169,8 +154,7 @@ where
169154
170155 fn restore(&mut self, config: Config) -> io::Result<()> {
171156 // reset cursor shape
172− self.buffer
173− .write_all(self.capabilities.reset_cursor_command.as_bytes())?;
157+ write!(self.buffer, "\x1B[0 q")?;
174158 if config.enable_mouse_capture {
175159 execute!(self.buffer, DisableMouseCapture)?;
176160 }
@@ -217,7 +201,7 @@ where
217201 for (x, y, cell) in content {
218202 // Move the cursor if the previous location was not (x - 1, y)
219203 if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
220− queue!(self.buffer, MoveTo(x, y))?;
204+ map_error(queue!(self.buffer, MoveTo(x, y)))?;
221205 }
222206 last_pos = Some((x, y));
223207 if cell.modifier != modifier {
@@ -230,12 +214,12 @@ where
230214 }
231215 if cell.fg != fg {
232216 let color = CColor::from(cell.fg);
233− queue!(self.buffer, SetForegroundColor(color))?;
217+ map_error(queue!(self.buffer, SetForegroundColor(color)))?;
234218 fg = cell.fg;
235219 }
236220 if cell.bg != bg {
237221 let color = CColor::from(cell.bg);
238− queue!(self.buffer, SetBackgroundColor(color))?;
222+ map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
239223 bg = cell.bg;
240224 }
241225
@@ -243,7 +227,7 @@ where
243227 if self.capabilities.has_extended_underlines {
244228 if cell.underline_color != underline_color {
245229 let color = CColor::from(cell.underline_color);
246− queue!(self.buffer, SetUnderlineColor(color))?;
230+ map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
247231 underline_color = cell.underline_color;
248232 }
249233 } else {
@@ -255,24 +239,24 @@ where
255239
256240 if new_underline_style != underline_style {
257241 let attr = CAttribute::from(new_underline_style);
258− queue!(self.buffer, SetAttribute(attr))?;
242+ map_error(queue!(self.buffer, SetAttribute(attr)))?;
259243 underline_style = new_underline_style;
260244 }
261245
262− queue!(self.buffer, Print(&cell.symbol))?;
246+ map_error(queue!(self.buffer, Print(&cell.symbol)))?;
263247 }
264248
265− queue!(
249+ map_error(queue!(
266250 self.buffer,
267251 SetUnderlineColor(CColor::Reset),
268252 SetForegroundColor(CColor::Reset),
269253 SetBackgroundColor(CColor::Reset),
270254 SetAttribute(CAttribute::Reset)
271− )
255+ ))
272256 }
273257
274258 fn hide_cursor(&mut self) -> io::Result<()> {
275− execute!(self.buffer, Hide)
259+ map_error(execute!(self.buffer, Hide))
276260 }
277261
278262 fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
@@ -282,7 +266,7 @@ where
282266 CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
283267 CursorKind::Hidden => unreachable!(),
284268 };
285− execute!(self.buffer, Show, shape)
269+ map_error(execute!(self.buffer, Show, shape))
286270 }
287271
288272 fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -291,11 +275,11 @@ where
291275 }
292276
293277 fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
294− execute!(self.buffer, MoveTo(x, y))
278+ map_error(execute!(self.buffer, MoveTo(x, y)))
295279 }
296280
297281 fn clear(&mut self) -> io::Result<()> {
298− execute!(self.buffer, Clear(ClearType::All))
282+ map_error(execute!(self.buffer, Clear(ClearType::All)))
299283 }
300284
301285 fn size(&self) -> io::Result<Rect> {
@@ -310,6 +294,10 @@ where
310294 }
311295}
312296
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+
313301#[derive(Debug)]
314302struct ModifierDiff {
315303 pub from: Modifier,
@@ -324,54 +312,48 @@ impl ModifierDiff {
324312 //use crossterm::Attribute;
325313 let removed = self.from - self.to;
326314 if removed.contains(Modifier::REVERSED) {
327− queue!(w, SetAttribute(CAttribute::NoReverse))?;
315+ map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
328316 }
329317 if removed.contains(Modifier::BOLD) {
330− queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
318+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
331319 if self.to.contains(Modifier::DIM) {
332− queue!(w, SetAttribute(CAttribute::Dim))?;
320+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
333321 }
334322 }
335323 if removed.contains(Modifier::ITALIC) {
336− queue!(w, SetAttribute(CAttribute::NoItalic))?;
324+ map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
337325 }
338326 if removed.contains(Modifier::DIM) {
339− queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
327+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
340328 }
341329 if removed.contains(Modifier::CROSSED_OUT) {
342− queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
330+ map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
343331 }
344332 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
345− queue!(w, SetAttribute(CAttribute::NoBlink))?;
346− }
347− if removed.contains(Modifier::HIDDEN) {
348− queue!(w, SetAttribute(CAttribute::NoHidden))?;
333+ map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
349334 }
350335
351336 let added = self.to - self.from;
352337 if added.contains(Modifier::REVERSED) {
353− queue!(w, SetAttribute(CAttribute::Reverse))?;
338+ map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
354339 }
355340 if added.contains(Modifier::BOLD) {
356− queue!(w, SetAttribute(CAttribute::Bold))?;
341+ map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
357342 }
358343 if added.contains(Modifier::ITALIC) {
359− queue!(w, SetAttribute(CAttribute::Italic))?;
344+ map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
360345 }
361346 if added.contains(Modifier::DIM) {
362− queue!(w, SetAttribute(CAttribute::Dim))?;
347+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
363348 }
364349 if added.contains(Modifier::CROSSED_OUT) {
365− queue!(w, SetAttribute(CAttribute::CrossedOut))?;
350+ map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
366351 }
367352 if added.contains(Modifier::SLOW_BLINK) {
368− queue!(w, SetAttribute(CAttribute::SlowBlink))?;
353+ map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
369354 }
370355 if added.contains(Modifier::RAPID_BLINK) {
371− queue!(w, SetAttribute(CAttribute::RapidBlink))?;
372− }
373− if added.contains(Modifier::HIDDEN) {
374− queue!(w, SetAttribute(CAttribute::Hidden))?;
356+ map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
375357 }
376358
377359 Ok(())
@@ -425,7 +407,7 @@ impl Command for SetUnderlineColor {
425407 }
426408
427409 #[cfg(windows)]
428− fn execute_winapi(&self) -> io::Result<()> {
410+ fn execute_winapi(&self) -> crossterm::Result<()> {
429411 Err(std::io::Error::new(
430412 std::io::ErrorKind::Other,
431413 "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,6 +65,20 @@ 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+
6882impl<B> Terminal<B>
6983where
7084 B: Backend,
helix-vcs/Cargo.toml+10−12
@@ -1,25 +1,23 @@
11[package]
22name = "helix-vcs"
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
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"
1110
1211# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1312
1413[dependencies]
15−helix-core = { path = "../helix-core" }
16−helix-event = { path = "../helix-event" }
14+helix-core = { version = "0.6", path = "../helix-core" }
1715
1816tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
1917parking_lot = "0.12"
2018arc-swap = { version = "1.6.0" }
2119
22−gix = { version = "0.57.0", default-features = false , optional = true }
20+gix = { version = "0.48.0", default-features = false , optional = true }
2321imara-diff = "0.1.5"
2422anyhow = "1"
2523
@@ -29,4 +27,4 @@ log = "0.4"
2927git = ["gix"]
3028
3129[dev-dependencies]
32−tempfile = "3.8"
30+tempfile = "3.6"
helix-vcs/src/diff.rs+19−6
@@ -2,10 +2,10 @@ use std::ops::Range;
22use std::sync::Arc;
33
44use helix_core::Rope;
5−use helix_event::RenderLockGuard;
65use imara_diff::Algorithm;
76use parking_lot::{Mutex, MutexGuard};
87use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8+use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
99use tokio::task::JoinHandle;
1010use tokio::time::Instant;
1111
@@ -14,9 +14,11 @@ use crate::diff::worker::DiffWorker;
1414mod line_cache;
1515mod worker;
1616
17+type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18+
1719/// A rendering lock passed to the differ the prevents redraws from occurring
1820struct RenderLock {
19− pub lock: RenderLockGuard,
21+ pub lock: OwnedRwLockReadGuard<()>,
2022 pub timeout: Option<Instant>,
2123}
2224
@@ -36,22 +38,28 @@ struct DiffInner {
3638#[derive(Clone, Debug)]
3739pub struct DiffHandle {
3840 channel: UnboundedSender<Event>,
41+ render_lock: Arc<RwLock<()>>,
3942 diff: Arc<Mutex<DiffInner>>,
4043 inverted: bool,
4144}
4245
4346impl DiffHandle {
44− pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
45− DiffHandle::new_with_handle(diff_base, doc).0
47+ pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
48+ DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
4649 }
4750
48− fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) {
51+ fn new_with_handle(
52+ diff_base: Rope,
53+ doc: Rope,
54+ redraw_handle: RedrawHandle,
55+ ) -> (DiffHandle, JoinHandle<()>) {
4956 let (sender, receiver) = unbounded_channel();
5057 let diff: Arc<Mutex<DiffInner>> = Arc::default();
5158 let worker = DiffWorker {
5259 channel: receiver,
5360 diff: diff.clone(),
5461 new_hunks: Vec::default(),
62+ redraw_notify: redraw_handle.0,
5563 diff_finished_notify: Arc::default(),
5664 };
5765 let handle = tokio::spawn(worker.run(diff_base, doc));
@@ -59,6 +67,7 @@ impl DiffHandle {
5967 channel: sender,
6068 diff,
6169 inverted: false,
70+ render_lock: redraw_handle.1,
6271 };
6372 (differ, handle)
6473 }
@@ -78,7 +87,11 @@ impl DiffHandle {
7887 /// This function is only intended to be called from within the rendering loop
7988 /// if called from elsewhere it may fail to acquire the render lock and panic
8089 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
81− let lock = helix_event::lock_frame();
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();
8295 let timeout = if block {
8396 None
8497 } else {
helix-vcs/src/diff/worker.rs+9−3
@@ -23,6 +23,7 @@ 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>,
2627 pub diff_finished_notify: Arc<Notify>,
2728}
2829
@@ -31,7 +32,11 @@ impl DiffWorker {
3132 let mut accumulator = EventAccumulator::new();
3233 accumulator.handle_event(event).await;
3334 accumulator
34− .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
35+ .accumulate_debounced_events(
36+ &mut self.channel,
37+ self.redraw_notify.clone(),
38+ self.diff_finished_notify.clone(),
39+ )
3540 .await;
3641 (accumulator.doc, accumulator.diff_base)
3742 }
@@ -132,6 +137,7 @@ impl<'a> EventAccumulator {
132137 async fn accumulate_debounced_events(
133138 &mut self,
134139 channel: &mut UnboundedReceiver<Event>,
140+ redraw_notify: Arc<Notify>,
135141 diff_finished_notify: Arc<Notify>,
136142 ) {
137143 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@@ -158,7 +164,7 @@ impl<'a> EventAccumulator {
158164 None => {
159165 tokio::spawn(async move {
160166 diff_finished_notify.notified().await;
161− helix_event::request_redraw();
167+ redraw_notify.notify_one();
162168 });
163169 }
164170 // diff is performed inside the rendering loop
@@ -184,7 +190,7 @@ impl<'a> EventAccumulator {
184190 // and wait until the diff occurs to trigger an async redraw
185191 log::info!("Diff computation timed out, update of diffs might appear delayed");
186192 diff_finished_notify.notified().await;
187− helix_event::request_redraw()
193+ redraw_notify.notify_one();
188194 });
189195 }
190196 // a blocking diff is performed inside the rendering loop
helix-vcs/src/diff/worker/test.rs+5−1
@@ -5,7 +5,11 @@ use crate::diff::{DiffHandle, Hunk};
55
66impl DiffHandle {
77 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8− DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
8+ DiffHandle::new_with_handle(
9+ Rope::from_str(diff_base),
10+ Rope::from_str(doc),
11+ Default::default(),
12+ )
913 }
1014 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
1115 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::EntryKind;
6+use gix::objs::tree::EntryMode;
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, &mut Vec::new())?
129+ .lookup_entry_by_path(rel_path)?
130130 .context("file is untracked")?;
131− match tree_entry.mode().kind() {
131+ match tree_entry.mode() {
132132 // not a file, everything is new, do not show diff
133− mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
133+ mode @ (EntryMode::Tree | EntryMode::Commit | EntryMode::Link) => {
134134 bail!("entry at {} is not a file but a {mode:?}", file.display())
135135 }
136136 // found a file
137− EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
137+ EntryMode::Blob | EntryMode::BlobExecutable => Ok(tree_entry.object_id()),
138138 }
139139}
helix-view/Cargo.toml+18−22
@@ -1,34 +1,31 @@
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"
37description = "UI abstractions for use in backends"
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
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
1211
1312[features]
1413default = []
1514term = ["crossterm"]
1615
1716[dependencies]
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"
17+bitflags = "2.3"
2618anyhow = "1"
27−crossterm = { version = "0.27", optional = true }
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" }
2825
2926# Conversion traits
30−once_cell = "1.19"
31−url = "2.5.0"
27+once_cell = "1.18"
28+url = "2"
3229
3330arc-swap = { version = "1.6.0" }
3431
@@ -45,16 +42,15 @@ serde_json = "1.0"
4542toml = "0.7"
4643log = "~0.4"
4744
48−which = "5.0.0"
45+which = "4.4"
4946parking_lot = "0.12.1"
5047
5148
5249[target.'cfg(windows)'.dependencies]
53−clipboard-win = { version = "5.0", features = ["std"] }
50+clipboard-win = { version = "4.5", features = ["std"] }
5451
5552[target.'cfg(unix)'.dependencies]
5653libc = "0.2"
57−rustix = { version = "0.38", features = ["fs"] }
5854
5955[dev-dependencies]
6056helix-tui = { path = "../helix-tui" }
helix-view/src/clipboard.rs+2−7
@@ -73,14 +73,9 @@ 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, env_var_is_set};
76+ use crate::env::binary_exists;
7777
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") {
78+ if binary_exists("pbcopy") && binary_exists("pbpaste") {
8479 command_provider! {
8580 paste => "pbpaste";
8681 copy => "pbcopy";
helix-view/src/document.rs+46−122
@@ -33,7 +33,7 @@ use helix_core::{
3333 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
3434};
3535
36−use crate::editor::Config;
36+use crate::editor::{Config, RedrawHandle};
3737use crate::{DocumentId, Editor, Theme, View, ViewId};
3838
3939/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -185,8 +185,6 @@ 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,
190188}
191189
192190/// Inlay hints for a single `(Document, View)` combo.
@@ -644,7 +642,7 @@ impl Document {
644642 ) -> Self {
645643 let (encoding, has_bom) = encoding_with_bom_info.unwrap_or((encoding::UTF_8, false));
646644 let line_ending = config.load().default_line_ending.into();
647− let changes = ChangeSet::new(text.slice(..));
645+ let changes = ChangeSet::new(&text);
648646 let old_state = None;
649647
650648 Self {
@@ -675,7 +673,6 @@ impl Document {
675673 config,
676674 version_control_head: None,
677675 focused_at: std::time::Instant::now(),
678− readonly: false,
679676 }
680677 }
681678
@@ -708,7 +705,7 @@ impl Document {
708705 let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
709706
710707 // set the path and try detecting the language
711− doc.set_path(Some(path));
708+ doc.set_path(Some(path))?;
712709 if let Some(loader) = config_loader {
713710 doc.detect_language(loader);
714711 }
@@ -733,16 +730,16 @@ impl Document {
733730 // We can't use anyhow::Result here since the output of the future has to be
734731 // clonable to be used as shared future. So use a custom error type.
735732 pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
736− if let Some((fmt_cmd, fmt_args)) = self
733+ if let Some(formatter) = self
737734 .language_config()
738− .and_then(|c| c.formatter.as_ref())
739− .and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args)))
735+ .and_then(|c| c.formatter.clone())
736+ .filter(|formatter| which::which(&formatter.command).is_ok())
740737 {
741738 use std::process::Stdio;
742739 let text = self.text().clone();
743− let mut process = tokio::process::Command::new(&fmt_cmd);
740+ let mut process = tokio::process::Command::new(&formatter.command);
744741 process
745− .args(fmt_args)
742+ .args(&formatter.args)
746743 .stdin(Stdio::piped())
747744 .stdout(Stdio::piped())
748745 .stderr(Stdio::piped());
@@ -751,7 +748,7 @@ impl Document {
751748 let mut process = process
752749 .spawn()
753750 .map_err(|e| FormatterError::SpawningFailed {
754− command: fmt_cmd.to_string_lossy().into(),
751+ command: formatter.command.clone(),
755752 error: e.kind(),
756753 })?;
757754 {
@@ -853,7 +850,7 @@ impl Document {
853850 let text = self.text().clone();
854851
855852 let path = match path {
856− Some(path) => helix_core::path::get_canonicalized_path(&path),
853+ Some(path) => helix_core::path::get_canonicalized_path(&path)?,
857854 None => {
858855 if self.path.is_none() {
859856 bail!("Can't save with no path set!");
@@ -941,7 +938,7 @@ impl Document {
941938 ) -> Option<Arc<helix_core::syntax::LanguageConfiguration>> {
942939 config_loader
943940 .language_config_for_file_name(self.path.as_ref()?)
944− .or_else(|| config_loader.language_config_for_shebang(self.text().slice(..)))
941+ .or_else(|| config_loader.language_config_for_shebang(self.text()))
945942 }
946943
947944 /// Detect the indentation used in the file, or otherwise defaults to the language indentation
@@ -958,43 +955,12 @@ impl Document {
958955 }
959956 }
960957
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−
993958 /// Reload the document from its path.
994959 pub fn reload(
995960 &mut self,
996961 view: &mut View,
997962 provider_registry: &DiffProviderRegistry,
963+ redraw_handle: RedrawHandle,
998964 ) -> Result<(), Error> {
999965 let encoding = self.encoding;
1000966 let path = self
@@ -1003,9 +969,6 @@ impl Document {
1003969 .ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
1004970 .to_owned();
1005971
1006− // Once we have a valid path we check if its readonly status has changed
1007− self.detect_readonly();
1008−
1009972 let mut file = std::fs::File::open(&path)?;
1010973 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
1011974
@@ -1022,7 +985,7 @@ impl Document {
1022985 self.detect_indent_and_line_ending();
1023986
1024987 match provider_registry.get_diff_base(&path) {
1025− Some(diff_base) => self.set_diff_base(diff_base),
988+ Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
1026989 None => self.diff_handle = None,
1027990 }
1028991
@@ -1046,14 +1009,16 @@ impl Document {
10461009 self.encoding
10471010 }
10481011
1049− pub fn set_path(&mut self, path: Option<&Path>) {
1050− let path = path.map(helix_core::path::get_canonicalized_path);
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()?;
10511016
10521017 // if parent doesn't exist we still want to open the document
10531018 // and error out when document is saved
10541019 self.path = path;
10551020
1056− self.detect_readonly();
1021+ Ok(())
10571022 }
10581023
10591024 /// Set the programming language for the file and load associated data (e.g. highlighting)
@@ -1065,7 +1030,7 @@ impl Document {
10651030 ) {
10661031 if let (Some(language_config), Some(loader)) = (language_config, loader) {
10671032 if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
1068− self.syntax = Syntax::new(self.text.slice(..), highlight_config, loader);
1033+ self.syntax = Syntax::new(&self.text, highlight_config, loader);
10691034 }
10701035
10711036 self.language = Some(language_config);
@@ -1200,53 +1165,32 @@ impl Document {
12001165 // update tree-sitter syntax tree
12011166 if let Some(syntax) = &mut self.syntax {
12021167 // TODO: no unwrap
1203− let res = syntax.update(
1204− old_doc.slice(..),
1205− self.text.slice(..),
1206− transaction.changes(),
1207− );
1168+ let res = syntax.update(&old_doc, &self.text, transaction.changes());
12081169 if res.is_err() {
1209− log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
1170+ log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
12101171 self.syntax = None;
12111172 }
12121173 }
12131174
12141175 let changes = transaction.changes();
12151176
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− }
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 {
12391189 diagnostic.line = self.text.char_to_line(diagnostic.range.start);
1240− true
1241− });
1190+ }
12421191
1243− self.diagnostics.sort_unstable_by_key(|diagnostic| {
1244− (
1245− diagnostic.range,
1246− diagnostic.severity,
1247− diagnostic.language_server_id,
1248− )
1249− });
1192+ self.diagnostics
1193+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
12501194
12511195 // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
12521196 let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
@@ -1344,7 +1288,7 @@ impl Document {
13441288
13451289 if success {
13461290 // reset changeset to fix len
1347− self.changes = ChangeSet::new(self.text().slice(..));
1291+ self.changes = ChangeSet::new(self.text());
13481292 // Sync with changes with the jumplist selections.
13491293 view.sync_changes(self);
13501294 }
@@ -1427,7 +1371,7 @@ impl Document {
14271371 }
14281372 if success {
14291373 // reset changeset to fix len
1430− self.changes = ChangeSet::new(self.text().slice(..));
1374+ self.changes = ChangeSet::new(self.text());
14311375 // Sync with changes with the jumplist selections.
14321376 view.sync_changes(self);
14331377 }
@@ -1450,7 +1394,7 @@ impl Document {
14501394 return;
14511395 }
14521396
1453− let new_changeset = ChangeSet::new(self.text().slice(..));
1397+ let new_changeset = ChangeSet::new(self.text());
14541398 let changes = std::mem::replace(&mut self.changes, new_changeset);
14551399 // Instead of doing this messy merge we could always commit, and based on transaction
14561400 // annotations either add a new layer or compose into the previous one.
@@ -1599,13 +1543,13 @@ impl Document {
15991543 }
16001544
16011545 /// Intialize/updates the differ for this document with a new base.
1602− pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
1546+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
16031547 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
16041548 if let Some(differ) = &self.diff_handle {
16051549 differ.update_diff_base(diff_base);
16061550 return;
16071551 }
1608− self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone()))
1552+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
16091553 } else {
16101554 self.diff_handle = None;
16111555 }
@@ -1723,33 +1667,13 @@ impl Document {
17231667
17241668 pub fn replace_diagnostics(
17251669 &mut self,
1726− diagnostics: impl IntoIterator<Item = Diagnostic>,
1727− unchanged_sources: &[String],
1670+ mut diagnostics: Vec<Diagnostic>,
17281671 language_server_id: usize,
17291672 ) {
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− });
1673+ self.clear_diagnostics(language_server_id);
1674+ self.diagnostics.append(&mut diagnostics);
1675+ self.diagnostics
1676+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
17531677 }
17541678
17551679 pub fn clear_diagnostics(&mut self, language_server_id: usize) {
helix-view/src/editor.rs+77−154
@@ -1,10 +1,10 @@
11use crate::{
22 align_view,
3+ clipboard::{get_clipboard_provider, ClipboardProvider},
34 document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
45 graphics::{CursorKind, Rect},
56 info::Info,
67 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,
35+ oneshot, Notify, RwLock,
3636 },
3737 time::{sleep, Duration, Instant, Sleep},
3838};
@@ -40,11 +40,13 @@ use tokio::{
4040use anyhow::{anyhow, bail, Error};
4141
4242pub use helix_core::diagnostic::Severity;
43+pub use helix_core::register::Registers;
4344use helix_core::{
4445 auto_pairs::AutoPairs,
45− syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
46− Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
46+ syntax::{self, AutoPairConfig, SoftWrap},
47+ Change, LineEnding, NATIVE_LINE_ENDING,
4748};
49+use helix_core::{Position, Selection};
4850use helix_dap as dap;
4951use helix_lsp::lsp;
5052
@@ -243,7 +245,7 @@ pub struct Config {
243245 /// Set a global text_width
244246 pub text_width: usize,
245247 /// Time in milliseconds since last keypress before idle timers trigger.
246− /// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
248+ /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
247249 #[serde(
248250 serialize_with = "serialize_duration_millis",
249251 deserialize_with = "deserialize_duration_millis"
@@ -286,31 +288,6 @@ pub struct Config {
286288 pub workspace_lsp_roots: Vec<PathBuf>,
287289 /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
288290 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− }
314291}
315292
316293#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -427,7 +404,6 @@ impl Default for StatusLineConfig {
427404 E::Mode,
428405 E::Spinner,
429406 E::FileName,
430− E::ReadOnlyIndicator,
431407 E::FileModificationIndicator,
432408 ],
433409 center: vec![],
@@ -471,18 +447,15 @@ pub enum StatusLineElement {
471447 /// The LSP activity spinner
472448 Spinner,
473449
474− /// The file basename (the leaf of the open file's path)
450+ /// The base file name, including a dirty flag if it's unsaved
475451 FileBaseName,
476452
477− /// The relative file path
453+ /// The relative file path, including a dirty flag if it's unsaved
478454 FileName,
479455
480456 // The file modification indicator
481457 FileModificationIndicator,
482458
483− /// An indicator that shows `"[readonly]"` when a file cannot be written
484− ReadOnlyIndicator,
485−
486459 /// The file encoding
487460 FileEncoding,
488461
@@ -731,7 +704,7 @@ pub struct WhitespaceCharacters {
731704impl Default for WhitespaceCharacters {
732705 fn default() -> Self {
733706 Self {
734− space: '·', // U+00B7
707+ space: '·', // U+00B7
735708 nbsp: '⍽', // U+237D
736709 tab: '→', // U+2192
737710 newline: '⏎', // U+23CE
@@ -759,13 +732,12 @@ impl Default for IndentGuidesConfig {
759732}
760733
761734/// Line ending configuration.
762−#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
735+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
763736#[serde(rename_all = "lowercase")]
764737pub enum LineEndingConfig {
765738 /// The platform's native line ending.
766739 ///
767740 /// `crlf` on Windows, otherwise `lf`.
768− #[default]
769741 Native,
770742 /// Line feed.
771743 LF,
@@ -782,6 +754,12 @@ pub enum LineEndingConfig {
782754 Nel,
783755}
784756
757+impl Default for LineEndingConfig {
758+ fn default() -> Self {
759+ LineEndingConfig::Native
760+ }
761+}
762+
785763impl From<LineEndingConfig> for LineEnding {
786764 fn from(line_ending: LineEndingConfig) -> Self {
787765 match line_ending {
@@ -798,15 +776,6 @@ impl From<LineEndingConfig> for LineEnding {
798776 }
799777}
800778
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−
810779impl Default for Config {
811780 fn default() -> Self {
812781 Self {
@@ -827,7 +796,7 @@ impl Default for Config {
827796 auto_completion: true,
828797 auto_format: true,
829798 auto_save: false,
830− idle_timeout: Duration::from_millis(250),
799+ idle_timeout: Duration::from_millis(400),
831800 preview_completion_insert: true,
832801 completion_trigger_len: 2,
833802 auto_info: true,
@@ -852,10 +821,6 @@ impl Default for Config {
852821 completion_replace: false,
853822 workspace_lsp_roots: Vec::new(),
854823 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(),
859824 }
860825 }
861826}
@@ -910,6 +875,8 @@ pub struct Editor {
910875 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
911876 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
912877
878+ pub clipboard_provider: Box<dyn ClipboardProvider>,
879+
913880 pub syn_loader: Arc<syntax::Loader>,
914881 pub theme_loader: Arc<theme::Loader>,
915882 /// last_theme is used for theme previews. We store the current theme here,
@@ -931,13 +898,16 @@ pub struct Editor {
931898 pub auto_pairs: Option<AutoPairs>,
932899
933900 pub idle_timer: Pin<Box<Sleep>>,
934− redraw_timer: Pin<Box<Sleep>>,
935901 last_motion: Option<Motion>,
936902 pub last_completion: Option<CompleteAction>,
937903
938904 pub exit_code: i32,
939905
940906 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,
941911 pub needs_redraw: bool,
942912 /// Cached position of the cursor calculated during rendering.
943913 /// The content of `cursor_cache` is returned by `Editor::cursor` if
@@ -964,6 +934,8 @@ pub struct Editor {
964934
965935pub type Motion = Box<dyn Fn(&mut Editor)>;
966936
937+pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
938+
967939#[derive(Debug)]
968940pub enum EditorEvent {
969941 DocumentSaved(DocumentSavedEventResult),
@@ -971,7 +943,6 @@ pub enum EditorEvent {
971943 LanguageServerMessage((usize, Call)),
972944 DebuggerEvent(dap::Payload),
973945 IdleTimer,
974− Redraw,
975946}
976947
977948#[derive(Debug, Clone)]
@@ -1004,13 +975,6 @@ pub enum Action {
1004975 VerticalSplit,
1005976}
1006977
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−
1014978/// Error thrown on failed document closed
1015979pub enum CloseError {
1016980 /// Document doesn't exist
@@ -1059,32 +1023,23 @@ impl Editor {
10591023 last_theme: None,
10601024 last_selection: None,
10611025 registers: Registers::default(),
1026+ clipboard_provider: get_clipboard_provider(),
10621027 status_msg: None,
10631028 autoinfo: None,
10641029 idle_timer: Box::pin(sleep(conf.idle_timeout)),
1065− redraw_timer: Box::pin(sleep(Duration::MAX)),
10661030 last_motion: None,
10671031 last_completion: None,
10681032 config,
10691033 auto_pairs,
10701034 exit_code: 0,
10711035 config_events: unbounded_channel(),
1036+ redraw_handle: Default::default(),
10721037 needs_redraw: false,
10731038 cursor_cache: Cell::new(None),
10741039 completion_request_handle: None,
10751040 }
10761041 }
10771042
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−
10881043 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
10891044 motion(self);
10901045 self.last_motion = Some(Box::new(motion));
@@ -1208,80 +1163,71 @@ impl Editor {
12081163 }
12091164
12101165 /// Refreshes the language server for a given document
1211− pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
1166+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
12121167 self.launch_language_servers(doc_id)
12131168 }
12141169
12151170 /// Launch a language server for a given document
1216− fn launch_language_servers(&mut self, doc_id: DocumentId) {
1171+ fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
12171172 if !self.config().lsp.enable {
1218− return;
1173+ return None;
12191174 }
12201175 // if doc doesn't have a URL it's a scratch buffer, ignore it
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− };
1176+ let doc = self.documents.get_mut(&doc_id)?;
1177+ let doc_url = doc.url()?;
12271178 let (lang, path) = (doc.language.clone(), doc.path().cloned());
12281179 let config = doc.config.load();
12291180 let root_dirs = &config.workspace_lsp_roots;
12301181
1231− // store only successfully started language servers
1232− let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| {
1182+ // try to find language servers based on the language name
1183+ let language_servers = lang.as_ref().and_then(|language| {
12331184 self.language_servers
12341185 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
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− }
1186+ .map_err(|e| {
1187+ log::error!(
1188+ "Failed to initialize the language servers for `{}` {{ {} }}",
1189+ language.scope(),
1190+ e
1191+ )
12461192 })
1247− .collect::<HashMap<_, _>>()
1193+ .ok()
12481194 });
12491195
1250− if language_servers.is_empty() {
1251− return;
1252− }
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
12531200
1254− let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
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+ });
12551207
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
1208+ for (_, language_server) in doc_language_servers_not_in_registry {
1209+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1210+ }
1211+
1212+ let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1213+ doc.language_servers
12601214 .get(*name)
1261− .map_or(true, |ls| ls.id() != doc_ls.id())
1215+ .map_or(true, |doc_ls| ls.id() != doc_ls.id())
12621216 });
12631217
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− });
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+ }
12731227
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− ));
1228+ doc.language_servers = language_servers;
12821229 }
1283−
1284− doc.language_servers = language_servers;
1230+ Some(())
12851231 }
12861232
12871233 fn _refresh(&mut self) {
@@ -1464,7 +1410,7 @@ impl Editor {
14641410
14651411 // ??? possible use for integration tests
14661412 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
1467− let path = helix_core::path::get_canonicalized_path(path);
1413+ let path = helix_core::path::get_canonicalized_path(path)?;
14681414 let id = self.document_by_path(&path).map(|doc| doc.id);
14691415
14701416 let id = if let Some(id) = id {
@@ -1478,12 +1424,12 @@ impl Editor {
14781424 )?;
14791425
14801426 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1481− doc.set_diff_base(diff_base);
1427+ doc.set_diff_base(diff_base, self.redraw_handle.clone());
14821428 }
14831429 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
14841430
14851431 let id = self.new_document(doc);
1486− self.launch_language_servers(id);
1432+ let _ = self.launch_language_servers(id);
14871433
14881434 id
14891435 };
@@ -1589,18 +1535,7 @@ impl Editor {
15891535
15901536 let path = path.map(|path| path.into());
15911537 let doc = doc_mut!(self, &doc_id);
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− };
1538+ let future = doc.save(path, force)?;
16041539
16051540 use futures_util::stream;
16061541
@@ -1736,14 +1671,6 @@ impl Editor {
17361671 &self,
17371672 timeout: Option<u64>,
17381673 ) -> 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−
17471674 tokio::time::timeout(
17481675 Duration::from_millis(timeout.unwrap_or(3000)),
17491676 future::join_all(
@@ -1777,20 +1704,16 @@ impl Editor {
17771704 return EditorEvent::DebuggerEvent(event)
17781705 }
17791706
1780− _ = helix_event::redraw_requested() => {
1707+ _ = self.redraw_handle.0.notified() => {
17811708 if !self.needs_redraw{
17821709 self.needs_redraw = true;
17831710 let timeout = Instant::now() + Duration::from_millis(33);
1784− if timeout < self.idle_timer.deadline() && timeout < self.redraw_timer.deadline(){
1785− self.redraw_timer.as_mut().reset(timeout)
1711+ if timeout < self.idle_timer.deadline(){
1712+ self.idle_timer.as_mut().reset(timeout)
17861713 }
17871714 }
17881715 }
17891716
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− }
17941717 _ = &mut self.idle_timer => {
17951718 return EditorEvent::IdleTimer
17961719 }
helix-view/src/gutter.rs+5−4
@@ -9,7 +9,8 @@ use crate::{
99};
1010
1111fn count_digits(n: usize) -> usize {
12− (usize::checked_ilog10(n).unwrap_or(0) + 1) as 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()
1314}
1415
1516pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
@@ -93,9 +94,9 @@ pub fn diff<'doc>(
9394 theme: &Theme,
9495 _is_focused: bool,
9596) -> GutterFn<'doc> {
96− let added = theme.get("diff.plus.gutter");
97− let deleted = theme.get("diff.minus.gutter");
98− let modified = theme.get("diff.delta.gutter");
97+ let added = theme.get("diff.plus");
98+ let deleted = theme.get("diff.minus");
99+ let modified = theme.get("diff.delta");
99100 if let Some(diff_handle) = doc.diff_handle() {
100101 let hunks = diff_handle.load();
101102 let mut hunk_i = 0;
helix-view/src/handlers/dap.rs+3−1
@@ -369,7 +369,9 @@ impl Editor {
369369 {
370370 Ok(process) => process,
371371 Err(err) => {
372− self.set_error(format!("Error starting external terminal: {}", 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));
373375 return true;
374376 }
375377 };
helix-view/src/info.rs+11−4
@@ -1,5 +1,4 @@
1−use crate::register::Registers;
2−use helix_core::unicode::width::UnicodeWidthStr;
1+use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
32use std::fmt::Write;
43
54#[derive(Debug)]
@@ -57,8 +56,16 @@ impl Info {
5756
5857 pub fn from_registers(registers: &Registers) -> Self {
5958 let body: Vec<_> = registers
60− .iter_preview()
61− .map(|(ch, preview)| (ch.to_string(), preview))
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+ })
6269 .collect();
6370
6471 let mut infobox = Self::new("Registers", &body);
helix-view/src/input.rs+3−67
@@ -43,10 +43,6 @@ 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,
5046}
5147
5248/// Represents a mouse button.
@@ -383,7 +379,7 @@ impl std::str::FromStr for KeyEvent {
383379 function if function.len() > 1 && function.starts_with('F') => {
384380 let function: String = function.chars().skip(1).collect();
385381 let function = str::parse::<u8>(&function)?;
386− (function > 0 && function < 25)
382+ (function > 0 && function < 13)
387383 .then_some(KeyCode::F(function))
388384 .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
389385 }
@@ -462,8 +458,6 @@ impl From<crossterm::event::MouseEventKind> for MouseEventKind {
462458 crossterm::event::MouseEventKind::Moved => Self::Moved,
463459 crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
464460 crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
465− crossterm::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
466− crossterm::event::MouseEventKind::ScrollRight => Self::ScrollRight,
467461 }
468462 }
469463}
@@ -549,7 +543,7 @@ pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
549543 if c == ">" {
550544 keys_res = Err(anyhow!("Unmatched '>'"));
551545 } else if c != "<" {
552− keys.push(if c == "-" { keys::MINUS } else { c });
546+ keys.push(c);
553547 i += end_i;
554548 } else {
555549 match s.find('>').context("'>' expected") {
@@ -688,7 +682,7 @@ mod test {
688682
689683 #[test]
690684 fn parsing_nonsensical_keys_fails() {
691− assert!(str::parse::<KeyEvent>("F25").is_err());
685+ assert!(str::parse::<KeyEvent>("F13").is_err());
692686 assert!(str::parse::<KeyEvent>("F0").is_err());
693687 assert!(str::parse::<KeyEvent>("aaa").is_err());
694688 assert!(str::parse::<KeyEvent>("S-S-a").is_err());
@@ -813,64 +807,6 @@ mod test {
813807 },
814808 ])
815809 );
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− );
874810 }
875811
876812 #[test]
helix-view/src/lib.rs−1
@@ -15,7 +15,6 @@ pub mod base64;
1515pub mod info;
1616pub mod input;
1717pub mod keyboard;
18−pub mod register;
1918pub mod theme;
2019pub mod tree;
2120pub mod view;
helix-view/src/register.rsdeleted−333
@@ -1,333 +0,0 @@
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,11 +297,6 @@ impl Theme {
297297 self.highlights[index]
298298 }
299299
300− #[inline]
301− pub fn scope(&self, index: usize) -> &str {
302− &self.scopes[index]
303− }
304−
305300 pub fn name(&self) -> &str {
306301 &self.name
307302 }
@@ -364,7 +359,6 @@ impl Default for ThemePalette {
364359 fn default() -> Self {
365360 Self {
366361 palette: hashmap! {
367− "default".to_string() => Color::Reset,
368362 "black".to_string() => Color::Black,
369363 "red".to_string() => Color::Red,
370364 "green".to_string() => Color::Green,
helix-view/src/view.rs+2−8
@@ -187,17 +187,11 @@ impl View {
187187 }
188188
189189 pub fn gutter_offset(&self, doc: &Document) -> u16 {
190− let total_width = self
191− .gutters
190+ self.gutters
192191 .layout
193192 .iter()
194193 .map(|gutter| gutter.width(self, doc) as u16)
195− .sum();
196− if total_width < self.area.width {
197− total_width
198− } else {
199− 0
200− }
194+ .sum()
201195 }
202196
203197 //
languages.toml+207−569

This diff is too big to show here.

logo.svg+1−1

This diff is too big to show here.

runtime/queries/_jsx/highlights.scm+15−3

This diff is too big to show here.

runtime/queries/_jsx/indents.scm+1

This diff is too big to show here.

runtime/queries/_typescript/highlights.scm+3−6

This diff is too big to show here.

runtime/queries/_typescript/locals.scm−17

This diff is too big to show here.

runtime/queries/agda/highlights.scmdeleted−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.scmdeleted−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+1−18

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.scmdeleted−1

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/dbml/highlights.scmdeleted−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/highlights.scmdeleted−196

This diff is too big to show here.

runtime/queries/fsharp/locals.scmdeleted−25

This diff is too big to show here.

runtime/queries/gas/highlights.scmdeleted−21

This diff is too big to show here.

runtime/queries/gas/injections.scmdeleted−2

This diff is too big to show here.

runtime/queries/gas/textobjects.scmdeleted−2

This diff is too big to show here.

runtime/queries/gemini/highlights.scmdeleted−26

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/gn/highlights.scmdeleted−42

This diff is too big to show here.

runtime/queries/gn/injections.scmdeleted−15

This diff is too big to show here.

runtime/queries/gn/locals.scmdeleted−16

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/janet/highlights.scmdeleted−1

This diff is too big to show here.

runtime/queries/java/indents.scmdeleted−35

This diff is too big to show here.

runtime/queries/jinja/highlights.scmdeleted−7

This diff is too big to show here.

runtime/queries/jinja/injections.scmdeleted−4

This diff is too big to show here.

runtime/queries/json5/highlights.scmdeleted−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.scmdeleted−3

This diff is too big to show here.

runtime/queries/kdl/textobjects.scmdeleted−27

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/kotlin/locals.scmdeleted−15

This diff is too big to show here.

runtime/queries/log/highlights.scmdeleted−10

This diff is too big to show here.

runtime/queries/lpf/highlights.scmdeleted−19

This diff is too big to show here.

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

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.scmnew file+4

This diff is too big to show here.

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

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.scmnew file+13

This diff is too big to show here.

runtime/queries/nunjucks/highlights.scmdeleted−1

This diff is too big to show here.

runtime/queries/nunjucks/injections.scmdeleted−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/fold.scmdeleted−19

This diff is too big to show here.

runtime/queries/perl/highlights.scm+155−72

This diff is too big to show here.

runtime/queries/perl/indents.scm+11−25

This diff is too big to show here.

runtime/queries/perl/injections.scm+1−3

This diff is too big to show here.

runtime/queries/perl/textobjects.scm+13−10

This diff is too big to show here.

runtime/queries/pod/highlights.scmdeleted−61

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/purescript/textobjects.scmdeleted−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.scmdeleted−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+13−51

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

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+2−69

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+1−14

This diff is too big to show here.

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

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.scmdeleted−43

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/slint/injections.scmdeleted−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.scmdeleted−12

This diff is too big to show here.

runtime/queries/smali/highlights.scmdeleted−216

This diff is too big to show here.

runtime/queries/smali/indents.scmdeleted−31

This diff is too big to show here.

runtime/queries/smali/injections.scmdeleted−2

This diff is too big to show here.

runtime/queries/smali/locals.scmdeleted−42

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/strace/highlights.scmdeleted−8

This diff is too big to show here.

runtime/queries/svelte/indents.scmdeleted−18

This diff is too big to show here.

runtime/queries/svelte/injections.scm−10

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/templ/highlights.scmdeleted−99

This diff is too big to show here.

runtime/queries/templ/injections.scmdeleted−4

This diff is too big to show here.

runtime/queries/todotxt/highlights.scmdeleted−6

This diff is too big to show here.

runtime/queries/typst/highlights.scmdeleted−77

This diff is too big to show here.

runtime/queries/typst/injections.scmdeleted−6

This diff is too big to show here.

runtime/queries/unison/highlights.scmdeleted−87

This diff is too big to show here.

runtime/queries/unison/injections.scmdeleted−1

This diff is too big to show here.

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

This diff is too big to show here.

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

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.scmdeleted−82

This diff is too big to show here.

runtime/queries/wren/indents.scmdeleted−29

This diff is too big to show here.

runtime/queries/wren/injections.scmdeleted−3

This diff is too big to show here.

runtime/queries/wren/locals.scmdeleted−21

This diff is too big to show here.

runtime/queries/wren/textobjects.scmdeleted−16

This diff is too big to show here.

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

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+2−5

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+65−62

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.tomldeleted−154

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/darcula.toml+4−10

This diff is too big to show here.

runtime/themes/dark_plus.toml+1−2

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.tomldeleted−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.tomldeleted−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.tomldeleted−108

This diff is too big to show here.

runtime/themes/kaolin-light.tomldeleted−60

This diff is too big to show here.

runtime/themes/kaolin-valley-dark.tomldeleted−73

This diff is too big to show here.

runtime/themes/licenses/everforest.LICENSEdeleted−21

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi.LICENSEdeleted−5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_deuteranopia.LICENSEdeleted−5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_tinted.LICENSEdeleted−5

This diff is too big to show here.

runtime/themes/licenses/modus_vivendi_tritanopia.LICENSEdeleted−5

This diff is too big to show here.

runtime/themes/licenses/poimandres.LICENSEdeleted−21

This diff is too big to show here.

runtime/themes/licenses/serika-syntax.LICENSEdeleted−21

This diff is too big to show here.

runtime/themes/licenses/sonokai.LICENSEdeleted−21

This diff is too big to show here.

runtime/themes/licenses/starlight.LICENSEdeleted−24

This diff is too big to show here.

runtime/themes/material_darker.tomldeleted−20

This diff is too big to show here.

runtime/themes/material_deep_ocean.tomldeleted−123

This diff is too big to show here.

runtime/themes/material_oceanic.tomldeleted−21

This diff is too big to show here.

runtime/themes/material_palenight.tomldeleted−19

This diff is too big to show here.

runtime/themes/modus_operandi.tomldeleted−194

This diff is too big to show here.

runtime/themes/modus_operandi_deuteranopia.tomldeleted−46

This diff is too big to show here.

runtime/themes/modus_operandi_tinted.tomldeleted−45

This diff is too big to show here.

runtime/themes/modus_operandi_tritanopia.tomldeleted−73

This diff is too big to show here.

runtime/themes/modus_vivendi.tomldeleted−242

This diff is too big to show here.

runtime/themes/modus_vivendi_deuteranopia.tomldeleted−158

This diff is too big to show here.

runtime/themes/modus_vivendi_tinted.tomldeleted−144

This diff is too big to show here.

runtime/themes/modus_vivendi_tritanopia.tomldeleted−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.tomldeleted−91

This diff is too big to show here.

runtime/themes/new_moon.tomldeleted−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.tomldeleted−23

This diff is too big to show here.

runtime/themes/nord.toml+13−23

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+116−67

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/poimandres.tomldeleted−113

This diff is too big to show here.

runtime/themes/poimandres_storm.tomldeleted−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+29−31

This diff is too big to show here.

runtime/themes/starlight.tomldeleted−113

This diff is too big to show here.

runtime/themes/vim_dark_high_contrast.tomldeleted−72

This diff is too big to show here.

runtime/themes/yellowed.tomldeleted−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+3−157

This diff is too big to show here.

rust-toolchain.toml+2−2

This diff is too big to show here.

xtask/Cargo.toml+6−12

This diff is too big to show here.