on master help sign in
.envrc+1
since splitsnapshots
unifiedsplit
@@ -1,5 +1,6 @@
1watch_file shell.nix
1watch_file shell.nix
2watch_file flake.lock
2watch_file flake.lock
3+watch_file rust-toolchain.toml
3
4
4# try to use flakes, if it fails use normal nix (ie. shell.nix)
5# try to use flakes, if it fails use normal nix (ie. shell.nix)
5use flake || use nix
6use flake || use nix
.github/ISSUE_TEMPLATE/bug_report.yaml+10
@@ -55,6 +55,16 @@ body:
55 placeholder: wezterm 20220101-133340-7edc5b5a
55 placeholder: wezterm 20220101-133340-7edc5b5a
56 validations:
56 validations:
57 required: true
57 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
58 - type: input
68 - type: input
59 id: helix-version
69 id: helix-version
60 attributes:
70 attributes:
.github/workflows/build.yml+15−7
@@ -14,7 +14,7 @@ jobs:
14 runs-on: ubuntu-latest
14 runs-on: ubuntu-latest
15 steps:
15 steps:
16 - name: Checkout sources
16 - name: Checkout sources
17− uses: actions/checkout@v3
17+ uses: actions/checkout@v4
18 - name: Install stable toolchain
18 - name: Install stable toolchain
19 uses: helix-editor/rust-toolchain@v1
19 uses: helix-editor/rust-toolchain@v1
20 with:
20 with:
@@ -22,6 +22,8 @@ jobs:
22 override: true
22 override: true
23
23
24 - uses: Swatinem/rust-cache@v2
24 - uses: Swatinem/rust-cache@v2
25+ with:
26+ shared-key: "build"
25
27
26 - name: Run cargo check
28 - name: Run cargo check
27 run: cargo check
29 run: cargo check
@@ -34,12 +36,14 @@ jobs:
34 HELIX_LOG_LEVEL: info
36 HELIX_LOG_LEVEL: info
35 steps:
37 steps:
36 - name: Checkout sources
38 - name: Checkout sources
37− uses: actions/checkout@v3
39+ uses: actions/checkout@v4
38
40
39 - name: Install stable toolchain
41 - name: Install stable toolchain
40− uses: dtolnay/rust-toolchain@1.65
42+ uses: dtolnay/rust-toolchain@1.70
41
43
42 - uses: Swatinem/rust-cache@v2
44 - uses: Swatinem/rust-cache@v2
45+ with:
46+ shared-key: "build"
43
47
44 - name: Cache test tree-sitter grammar
48 - name: Cache test tree-sitter grammar
45 uses: actions/cache@v3
49 uses: actions/cache@v3
@@ -63,14 +67,16 @@ jobs:
63 runs-on: ubuntu-latest
67 runs-on: ubuntu-latest
64 steps:
68 steps:
65 - name: Checkout sources
69 - name: Checkout sources
66− uses: actions/checkout@v3
70+ uses: actions/checkout@v4
67
71
68 - name: Install stable toolchain
72 - name: Install stable toolchain
69− uses: dtolnay/rust-toolchain@1.65
73+ uses: dtolnay/rust-toolchain@1.70
70 with:
74 with:
71 components: rustfmt, clippy
75 components: rustfmt, clippy
72
76
73 - uses: Swatinem/rust-cache@v2
77 - uses: Swatinem/rust-cache@v2
78+ with:
79+ shared-key: "build"
74
80
75 - name: Run cargo fmt
81 - name: Run cargo fmt
76 run: cargo fmt --all --check
82 run: cargo fmt --all --check
@@ -88,12 +94,14 @@ jobs:
88 runs-on: ubuntu-latest
94 runs-on: ubuntu-latest
89 steps:
95 steps:
90 - name: Checkout sources
96 - name: Checkout sources
91− uses: actions/checkout@v3
97+ uses: actions/checkout@v4
92
98
93 - name: Install stable toolchain
99 - name: Install stable toolchain
94− uses: dtolnay/rust-toolchain@1.65
100+ uses: dtolnay/rust-toolchain@1.70
95
101
96 - uses: Swatinem/rust-cache@v2
102 - uses: Swatinem/rust-cache@v2
103+ with:
104+ shared-key: "build"
97
105
98 - name: Validate queries
106 - name: Validate queries
99 run: cargo xtask query-check
107 run: cargo xtask query-check
.github/workflows/cachix.yml+3−3
@@ -11,13 +11,13 @@ jobs:
11 runs-on: ubuntu-latest
11 runs-on: ubuntu-latest
12 steps:
12 steps:
13 - name: Checkout sources
13 - name: Checkout sources
14− uses: actions/checkout@v3
14+ uses: actions/checkout@v4
15
15
16 - name: Install nix
16 - name: Install nix
17− uses: cachix/install-nix-action@v22
17+ uses: cachix/install-nix-action@v24
18
18
19 - name: Authenticate with Cachix
19 - name: Authenticate with Cachix
20− uses: cachix/cachix-action@v12
20+ uses: cachix/cachix-action@v13
21 with:
21 with:
22 name: helix
22 name: helix
23 authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
23 authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
.github/workflows/gh-pages.yml+5−5
@@ -11,7 +11,7 @@ jobs:
11 deploy:
11 deploy:
12 runs-on: ubuntu-latest
12 runs-on: ubuntu-latest
13 steps:
13 steps:
14− - uses: actions/checkout@v3
14+ - uses: actions/checkout@v4
15
15
16 - name: Setup mdBook
16 - name: Setup mdBook
17 uses: peaceiris/actions-mdbook@v1
17 uses: peaceiris/actions-mdbook@v1
@@ -26,16 +26,16 @@ jobs:
26 OUTDIR=$(basename ${{ github.ref }})
26 OUTDIR=$(basename ${{ github.ref }})
27 echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
27 echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
28
28
29− - name: Deploy
29+ - name: Deploy stable
30 uses: peaceiris/actions-gh-pages@v3
30 uses: peaceiris/actions-gh-pages@v3
31+ if: startswith(github.ref, 'refs/tags/')
31 with:
32 with:
32 github_token: ${{ secrets.GITHUB_TOKEN }}
33 github_token: ${{ secrets.GITHUB_TOKEN }}
33 publish_dir: ./book/book
34 publish_dir: ./book/book
34− destination_dir: ./${{ env.OUTDIR }}
35
35
36− - name: Deploy stable
36+ - name: Deploy
37 uses: peaceiris/actions-gh-pages@v3
37 uses: peaceiris/actions-gh-pages@v3
38− if: startswith(github.ref, 'refs/tags/')
39 with:
38 with:
40 github_token: ${{ secrets.GITHUB_TOKEN }}
39 github_token: ${{ secrets.GITHUB_TOKEN }}
41 publish_dir: ./book/book
40 publish_dir: ./book/book
41+ destination_dir: ./${{ env.OUTDIR }}
.github/workflows/release.yml+15−15
@@ -23,7 +23,7 @@ jobs:
23 runs-on: ubuntu-latest
23 runs-on: ubuntu-latest
24 steps:
24 steps:
25 - name: Checkout sources
25 - name: Checkout sources
26− uses: actions/checkout@v3
26+ uses: actions/checkout@v4
27
27
28 - name: Install stable toolchain
28 - name: Install stable toolchain
29 uses: dtolnay/rust-toolchain@stable
29 uses: dtolnay/rust-toolchain@stable
@@ -36,7 +36,7 @@ jobs:
36 - name: Bundle grammars
36 - name: Bundle grammars
37 run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
37 run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
38
38
39− - uses: actions/upload-artifact@v3
39+ - uses: actions/upload-artifact@v4
40 with:
40 with:
41 name: grammars
41 name: grammars
42 path: grammars.tar.xz
42 path: grammars.tar.xz
@@ -70,11 +70,11 @@ jobs:
70 rust: stable
70 rust: stable
71 target: aarch64-unknown-linux-gnu
71 target: aarch64-unknown-linux-gnu
72 cross: true
72 cross: true
73− - build: riscv64-linux
73+ # - build: riscv64-linux
74− os: ubuntu-latest
74+ # os: ubuntu-latest
75− rust: stable
75+ # rust: stable
76− target: riscv64gc-unknown-linux-gnu
76+ # target: riscv64gc-unknown-linux-gnu
77− cross: true
77+ # cross: true
78 - build: x86_64-macos
78 - build: x86_64-macos
79 os: macos-latest
79 os: macos-latest
80 rust: stable
80 rust: stable
@@ -103,10 +103,10 @@ jobs:
103
103
104 steps:
104 steps:
105 - name: Checkout sources
105 - name: Checkout sources
106− uses: actions/checkout@v3
106+ uses: actions/checkout@v4
107
107
108 - name: Download grammars
108 - name: Download grammars
109− uses: actions/download-artifact@v3
109+ uses: actions/download-artifact@v4
110
110
111 - name: Move grammars under runtime
111 - name: Move grammars under runtime
112 if: "!startsWith(matrix.os, 'windows')"
112 if: "!startsWith(matrix.os, 'windows')"
@@ -160,7 +160,7 @@ jobs:
160
160
161 - name: Build AppImage
161 - name: Build AppImage
162 shell: bash
162 shell: bash
163− if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
163+ if: matrix.build == 'x86_64-linux'
164 run: |
164 run: |
165 # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
165 # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
166 sudo add-apt-repository universe
166 sudo add-apt-repository universe
@@ -220,7 +220,7 @@ jobs:
220 fi
220 fi
221 cp -r runtime dist
221 cp -r runtime dist
222
222
223− - uses: actions/upload-artifact@v3
223+ - uses: actions/upload-artifact@v4
224 with:
224 with:
225 name: bins-${{ matrix.build }}
225 name: bins-${{ matrix.build }}
226 path: dist
226 path: dist
@@ -231,9 +231,9 @@ jobs:
231 runs-on: ubuntu-latest
231 runs-on: ubuntu-latest
232 steps:
232 steps:
233 - name: Checkout sources
233 - name: Checkout sources
234− uses: actions/checkout@v3
234+ uses: actions/checkout@v4
235
235
236− - uses: actions/download-artifact@v3
236+ - uses: actions/download-artifact@v4
237
237
238 - name: Build archive
238 - name: Build archive
239 shell: bash
239 shell: bash
@@ -263,7 +263,7 @@ jobs:
263 mv bins-$platform/hx$exe $pkgname
263 mv bins-$platform/hx$exe $pkgname
264 chmod +x $pkgname/hx$exe
264 chmod +x $pkgname/hx$exe
265
265
266− if [[ "$platform" = "aarch64-linux" || "$platform" = "x86_64-linux" ]]; then
266+ if [[ "$platform" = "x86_64-linux" ]]; then
267 mv bins-$platform/helix-*.AppImage* dist/
267 mv bins-$platform/helix-*.AppImage* dist/
268 fi
268 fi
269
269
@@ -288,7 +288,7 @@ jobs:
288 overwrite: true
288 overwrite: true
289
289
290 - name: Upload binaries as artifact
290 - name: Upload binaries as artifact
291− uses: actions/upload-artifact@v3
291+ uses: actions/upload-artifact@v4
292 if: env.preview == 'true'
292 if: env.preview == 'true'
293 with:
293 with:
294 name: release
294 name: release
CHANGELOG.md+260
@@ -1,3 +1,263 @@
1+# 23.10 (2023-10-24)
2+
3+A big shout out to all the contributors! We had 118 contributors in this release.
4+
5+Breaking changes:
6+
7+- Support multiple language servers per language ([#2507](https://github.com/helix-editor/helix/pull/2507))
8+ - This is a breaking change to language configuration
9+
10+Features:
11+
12+- Support multiple language servers per language ([#2507](https://github.com/helix-editor/helix/pull/2507), [#7082](https://github.com/helix-editor/helix/pull/7082), [#7286](https://github.com/helix-editor/helix/pull/7286), [#8374](https://github.com/helix-editor/helix/pull/8374))
13+- Add a statusline element for the selected register ([#7222](https://github.com/helix-editor/helix/pull/7222))
14+- Add `%`, `#`, `.`, `*` and `+` special registers ([#6985](https://github.com/helix-editor/helix/pull/6985))
15+- Add initial support for LSP DidChangeWatchedFiles notifications ([#7665](https://github.com/helix-editor/helix/pull/7665))
16+- Search buffer contents in `global_search` ([#5652](https://github.com/helix-editor/helix/pull/5652))
17+- Add a "smart tab" command that intelligently jumps the cursor on tab ([#4443](https://github.com/helix-editor/helix/pull/4443))
18+- Add a statusline element for whether a file is read-only ([#7222](https://github.com/helix-editor/helix/pull/7222), [#7875](https://github.com/helix-editor/helix/pull/7875))
19+- Syntax highlight regex prompts ([#7738](https://github.com/helix-editor/helix/pull/7738))
20+- Allow defining alignment in indent queries ([#5355](https://github.com/helix-editor/helix/pull/5355))
21+- Show visual feedback in `surround_replace` ([#7588](https://github.com/helix-editor/helix/pull/7588))
22+- Switch to Nucleo for fuzzy matching ([#7814](https://github.com/helix-editor/helix/pull/7814), [#8148](https://github.com/helix-editor/helix/pull/8148), [#8192](https://github.com/helix-editor/helix/pull/8192), [#8194](https://github.com/helix-editor/helix/pull/8194))
23+- Insert a trailing newline on write ([#8157](https://github.com/helix-editor/helix/pull/8157))
24+- Add a `-w`/`--working-dir` CLI flag for specifying a working directory on startup ([#8223](https://github.com/helix-editor/helix/pull/8223), [#8498](https://github.com/helix-editor/helix/pull/8498), [#8520](https://github.com/helix-editor/helix/pull/8520))
25+- Accept a `+N` CLI argument to set the first file's line number ([#8521](https://github.com/helix-editor/helix/pull/8521))
26+- Accept Helix-specific ignore files in `.helix/ignore` and `~/.config/helix/ignore` ([#8099](https://github.com/helix-editor/helix/pull/8099))
27+
28+Commands:
29+
30+- `merge_selections` (`A-minus`) - merge all selections into one selection that covers all ranges ([#7053](https://github.com/helix-editor/helix/pull/7053))
31+- `move_prev_long_word_end` and `extend_prev_long_word_end` - move/extend to the end of the previous WORD ([#6905](https://github.com/helix-editor/helix/pull/6905))
32+- `reverse_selection_contents` - swaps the values of each selection so they are reversed ([#7329](https://github.com/helix-editor/helix/pull/7329))
33+- Add `:rl` and `:rla` aliases for `:reload` and `:reload-all` ([#7158](https://github.com/helix-editor/helix/pull/7158))
34+- `yank_joined` - join the selections and yank to the selected register ([#7195](https://github.com/helix-editor/helix/pull/7195))
35+- `:write-all!` (`:wa!`) - forcibly write all buffers to disk and create any necessary subdirectories ([#7577](https://github.com/helix-editor/helix/pull/7577))
36+- `:redraw` - clear re-render the UI ([#6949](https://github.com/helix-editor/helix/pull/6949))
37+- `:tree-sitter-highlight-name` - show the theme scope name of the highlight under the cursor ([#8170](https://github.com/helix-editor/helix/pull/8170))
38+
39+Usability improvements:
40+
41+- Allow cycling option values at runtime ([#4411](https://github.com/helix-editor/helix/pull/4411), [#7240](https://github.com/helix-editor/helix/pull/7240), [#7877](https://github.com/helix-editor/helix/pull/7877))
42+- Exit gracefully on termination signals ([#7236](https://github.com/helix-editor/helix/pull/7236))
43+- Add plaintext matching fallback to tree-sitter pair matching ([#4288](https://github.com/helix-editor/helix/pull/4288))
44+- Persist register selection in pending keymaps ([0e08349](https://github.com/helix-editor/helix/commit/0e08349))
45+- Propagate the count and register to command palette commands ([b394997](https://github.com/helix-editor/helix/commit/b394997))
46+- Auto indent on `insert_at_line_start` ([#5837](https://github.com/helix-editor/helix/pull/5837))
47+- Add a config option to control whether LSP completions are automatically inserted on preview ([#7189](https://github.com/helix-editor/helix/pull/7189))
48+- Add a config option for default line endings ([#5621](https://github.com/helix-editor/helix/pull/5621), [#7357](https://github.com/helix-editor/helix/pull/7357))
49+- Allow ANSI colors in themes ([#5119](https://github.com/helix-editor/helix/pull/5119))
50+- Match pairs that don't form a standalone tree-sitter node ([#7242](https://github.com/helix-editor/helix/pull/7242))
51+- Allow indent sizes of up to 16 columns ([#7429](https://github.com/helix-editor/helix/pull/7429))
52+- Improve performance of mapping positions through changes ([#7408](https://github.com/helix-editor/helix/pull/7408), [8d39a81](https://github.com/helix-editor/helix/commit/8d39a81), [#7471](https://github.com/helix-editor/helix/pull/7471))
53+- Mark buffers created from stdin as modified ([#7431](https://github.com/helix-editor/helix/pull/7431))
54+- Forcibly shut down uninitialized language servers ([#7449](https://github.com/helix-editor/helix/pull/7449))
55+- Add filename completer for shell prompts ([#7569](https://github.com/helix-editor/helix/pull/7569))
56+- Allow binding F13-F24 ([#7672](https://github.com/helix-editor/helix/pull/7672))
57+- Resolve LSP code actions ([#7677](https://github.com/helix-editor/helix/pull/7677), [#8421](https://github.com/helix-editor/helix/pull/8421))
58+- Save an undo checkpoint before accepting completions ([#7747](https://github.com/helix-editor/helix/pull/7747))
59+- Include gitignored files in debugger completions ([#7936](https://github.com/helix-editor/helix/pull/7936))
60+- Make editor remember the last search register ([#5244](https://github.com/helix-editor/helix/pull/5244))
61+- Open directories with `goto_file` ([#7909](https://github.com/helix-editor/helix/pull/7909))
62+- Use relative path to open buffer in `goto_file` (`gf`) ([#7965](https://github.com/helix-editor/helix/pull/7965))
63+- Support `default` color in themes ([#8083](https://github.com/helix-editor/helix/pull/8083), [#8114](https://github.com/helix-editor/helix/pull/8114))
64+- Toggle between relative and absolute line numbers when the terminal loses focus ([#7955](https://github.com/helix-editor/helix/pull/7955))
65+- Lower default idle-timeout to 250ms ([060e73a](https://github.com/helix-editor/helix/commit/060e73a))
66+- Allow theming diff gutters separately from other diff colors ([#8343](https://github.com/helix-editor/helix/pull/8343))
67+- Style bold/italic/strikethrough in markdown doc popups ([#8385](https://github.com/helix-editor/helix/pull/8385))
68+- Maintain the cursor position and view when splitting with `:hsplit`/`:vsplit` ([#8109](https://github.com/helix-editor/helix/pull/8109))
69+- Accept `-` in macros outside of `<`/`>` ([#8475](https://github.com/helix-editor/helix/pull/8475))
70+- Show all language servers for each language in `--health` ([#7315](https://github.com/helix-editor/helix/pull/7315))
71+- Don't break on hyphens in `:reflow` ([#8569](https://github.com/helix-editor/helix/pull/8569))
72+
73+Fixes:
74+
75+- Update diagnostics correctly on language server exit ([#7111](https://github.com/helix-editor/helix/pull/7111))
76+- Fix off-by-one in `select_references_to_symbol_under_cursor` ([#7132](https://github.com/helix-editor/helix/pull/7132))
77+- Extend selection with repeat-last-motion only if the original motion extended the selection ([#7159](https://github.com/helix-editor/helix/pull/7159))
78+- Fix undefined behavior in the diff gutter ([#7227](https://github.com/helix-editor/helix/pull/7227))
79+- Check that tab width is non-zero ([#7178](https://github.com/helix-editor/helix/pull/7178))
80+- Fix styles being overwritten in table rows with multiple cells ([#7281](https://github.com/helix-editor/helix/pull/7281))
81+- Add file for `--log` CLI arg in help text ([#7307](https://github.com/helix-editor/helix/pull/7307))
82+- Fix underflow when repeating a completion that has a negative shift position ([#7322](https://github.com/helix-editor/helix/pull/7322))
83+- Prefer longer matches in `select_next_sibling` and `select_prev_sibling` ([#7332](https://github.com/helix-editor/helix/pull/7332))
84+- Preview scratch buffers in the jumplist picker ([#7331](https://github.com/helix-editor/helix/pull/7331))
85+- Fix chunking by bytes in tree-sitter parsing ([#7417](https://github.com/helix-editor/helix/pull/7417))
86+- Discard LSP publishDiagnostic from uninitialized servers ([#7467](https://github.com/helix-editor/helix/pull/7467))
87+- Use negotiated position encoding for LSP workspace edits ([#7469](https://github.com/helix-editor/helix/pull/7469))
88+- Fix error message for unknown gutter types in config ([#7534](https://github.com/helix-editor/helix/pull/7534))
89+- Fix `:log-open` when `--log` CLI arg is specified ([#7573](https://github.com/helix-editor/helix/pull/7573), [#7585](https://github.com/helix-editor/helix/pull/7585))
90+- Fix debouncing of LSP messages to fix the last message sticking around ([#7538](https://github.com/helix-editor/helix/pull/7538), [#8023](https://github.com/helix-editor/helix/pull/8023))
91+- Fix crash when the current working directory is deleted ([#7185](https://github.com/helix-editor/helix/pull/7185))
92+- Fix piping to Helix on macOS ([#5468](https://github.com/helix-editor/helix/pull/5468))
93+- Fix crash when parsing overlapping injections ([#7621](https://github.com/helix-editor/helix/pull/7621))
94+- Clear the statusline when the prompt is visible ([#7646](https://github.com/helix-editor/helix/pull/7646))
95+- Fix range formatting error message typo ([#7823](https://github.com/helix-editor/helix/pull/7823))
96+- Skip rendering gutters when gutter width exceeds view width ([#7821](https://github.com/helix-editor/helix/pull/7821))
97+- Center the picker preview using visual lines ([#7837](https://github.com/helix-editor/helix/pull/7837))
98+- Align view correctly for background buffers opened with `A-ret` ([#7691](https://github.com/helix-editor/helix/pull/7691))
99+- Fix cursor resetting to block when quitting via a keybind ([#7931](https://github.com/helix-editor/helix/pull/7931))
100+- Remove path completions for the `:new` command ([#8010](https://github.com/helix-editor/helix/pull/8010))
101+- Use binary path resolved by `which` for formatter commands ([#8064](https://github.com/helix-editor/helix/pull/8064))
102+- Handle crossterm's `hidden` modifier ([#8120](https://github.com/helix-editor/helix/pull/8120))
103+- Clear completion when switching between windows with the mouse ([#8118](https://github.com/helix-editor/helix/pull/8118))
104+- Eagerly remove the last picker (`<space>'`) when the picker has many items ([#8127](https://github.com/helix-editor/helix/pull/8127))
105+- Fix find commands for buffers with non-LF line-endings ([#8111](https://github.com/helix-editor/helix/pull/8111))
106+- Detect the tmux clipboard provider on macOS ([#8182](https://github.com/helix-editor/helix/pull/8182))
107+- Fix syntax highlighting in dynamic picker preview pane ([#8206](https://github.com/helix-editor/helix/pull/8206))
108+- Recognize HTML code tags with attributes as code in markdown previews ([#8397](https://github.com/helix-editor/helix/pull/8397))
109+- Fix multicursor snippet placeholder directions ([#8423](https://github.com/helix-editor/helix/pull/8423))
110+- Only show diagnostic highlights when diagnostics are enabled for a language server ([#8551](https://github.com/helix-editor/helix/pull/8551))
111+
112+Themes:
113+
114+- Improve the selection color in `ferra` ([#7138](https://github.com/helix-editor/helix/pull/7138))
115+- Add `variable.other.member` theming to `spacebones_light` ([#7125](https://github.com/helix-editor/helix/pull/7125))
116+- Update `autumn` and theme the soft-wrap indicator ([#7229](https://github.com/helix-editor/helix/pull/7229))
117+- Add `gruvbox_dark_soft` ([#7139](https://github.com/helix-editor/helix/pull/7139))
118+- Add `merionette` ([#7186](https://github.com/helix-editor/helix/pull/7186))
119+- Add `zed_onedark` and `zed_onelight` ([#7250](https://github.com/helix-editor/helix/pull/7250))
120+- Use light-gray for `onedarker` inlay hint theming ([#7433](https://github.com/helix-editor/helix/pull/7433))
121+- Update the Nord theme to follow the style guidelines ([#7490](https://github.com/helix-editor/helix/pull/7490))
122+- Tune `dark_plus` inlay hint colors ([#7611](https://github.com/helix-editor/helix/pull/7611))
123+- Add `naysayer` ([#7570](https://github.com/helix-editor/helix/pull/7570))
124+- Add `kaolin-dark`, `kaolin-light` and `kaolin-valley-dark` ([#7151](https://github.com/helix-editor/helix/pull/7151))
125+- Fix selection highlighting in gruvbox variants ([#7717](https://github.com/helix-editor/helix/pull/7717))
126+- Add soft-wrap indicator to `gruvbox` ([#7736](https://github.com/helix-editor/helix/pull/7736))
127+- Add missing palette definitions in `everforest_dark` ([#7739](https://github.com/helix-editor/helix/pull/7739))
128+- Increase diagnostics clarity in `pop-dark` ([#7702](https://github.com/helix-editor/helix/pull/7702))
129+- Add `vim_dark_high_contrast` ([#7785](https://github.com/helix-editor/helix/pull/7785))
130+- Add `new_moon` ([#7834](https://github.com/helix-editor/helix/pull/7834))
131+- Add `yellowed` ([#7849](https://github.com/helix-editor/helix/pull/7849))
132+- Improve comment readability for `autumn` ([#7939](https://github.com/helix-editor/helix/pull/7939))
133+- Distinguish active bufferline buffer in `monokai` ([#7983](https://github.com/helix-editor/helix/pull/7983))
134+- Update ruler colors in `nord` ([#7995](https://github.com/helix-editor/helix/pull/7995))
135+- Update Catppuccin themes ([#8102](https://github.com/helix-editor/helix/pull/8102))
136+- Add text focus scope and diagnostics undercurls for `nord` ([#8165](https://github.com/helix-editor/helix/pull/8165))
137+- Add material theme collection ([#8211](https://github.com/helix-editor/helix/pull/8211))
138+- Improve indent line color in `dracula` ([#8266](https://github.com/helix-editor/helix/pull/8266))
139+- Clean up and refactor `papercolor` to use inheritance ([#8276](https://github.com/helix-editor/helix/pull/8276))
140+- Fix `zenburn` inlay hint color ([#8278](https://github.com/helix-editor/helix/pull/8278)a)
141+- Fix picker crash when previewing an invalid range ([e9d0bd7](https://github.com/helix-editor/helix/commit/e9d0bd7))
142+- Correctly center items in the picker preview ([13d4463](https://github.com/helix-editor/helix/commit/13d4463))
143+- Add `cyan_light` ([#8293](https://github.com/helix-editor/helix/pull/8293), [#8587](https://github.com/helix-editor/helix/pull/8587))
144+- Theme HTML tags in `onedark` ([#8409](https://github.com/helix-editor/helix/pull/8409))
145+- Refine `darcula` and `darcula-solid` themes ([#8412](https://github.com/helix-editor/helix/pull/8412))
146+- Improve `nord` highlights ([#8414](https://github.com/helix-editor/helix/pull/8414))
147+- Add `nord-night` ([#8549](https://github.com/helix-editor/helix/pull/8549))
148+
149+New languages:
150+
151+- Blueprint ([#7213](https://github.com/helix-editor/helix/pull/7213), [#8161](https://github.com/helix-editor/helix/pull/8161))
152+- Forth ([#7256](https://github.com/helix-editor/helix/pull/7256), [#7334](https://github.com/helix-editor/helix/pull/7334))
153+- t32 ([#7140](https://github.com/helix-editor/helix/pull/7140), [#7811](https://github.com/helix-editor/helix/pull/7811))
154+- WebC ([#7290](https://github.com/helix-editor/helix/pull/7290))
155+- Persistent DSL for Haskell ([#7261](https://github.com/helix-editor/helix/pull/7261))
156+- F# ([#7619](https://github.com/helix-editor/helix/pull/7619), [#8024](https://github.com/helix-editor/helix/pull/8024))
157+- Wren ([#7765](https://github.com/helix-editor/helix/pull/7765), [#7819](https://github.com/helix-editor/helix/pull/7819))
158+- Unison ([#7724](https://github.com/helix-editor/helix/pull/7724))
159+- Todo.txt ([#7835](https://github.com/helix-editor/helix/pull/7835))
160+- Jinja and Handlebars ([#7233](https://github.com/helix-editor/helix/pull/7233))
161+- Pod ([#7907](https://github.com/helix-editor/helix/pull/7907))
162+- Strace ([#7928](https://github.com/helix-editor/helix/pull/7928))
163+- Gemini ([#8070](https://github.com/helix-editor/helix/pull/8070))
164+- GNU Assembler (GAS) ([#8291](https://github.com/helix-editor/helix/pull/8291))
165+- JSON5 ([#8473](https://github.com/helix-editor/helix/pull/8473))
166+- TEMPL ([#8540](https://github.com/helix-editor/helix/pull/8540))
167+
168+Updated languages and queries:
169+
170+- Add one to the ruler numbers for git-commit ([#7072](https://github.com/helix-editor/helix/pull/7072))
171+- Recognize XAML files as XML ([#7083](https://github.com/helix-editor/helix/pull/7083))
172+- Recognize `Cargo.lock` as TOML ([#7095](https://github.com/helix-editor/helix/pull/7095))
173+- Use Rust grammar for Cairo ([c6d1430](https://github.com/helix-editor/helix/commit/c6d1430))
174+- Update tree-sitter-nickel ([#7059](https://github.com/helix-editor/helix/pull/7059), [#7551](https://github.com/helix-editor/helix/pull/7551))
175+- Tune auto-pair characters for Nickel ([#7059](https://github.com/helix-editor/helix/pull/7059))
176+- Recognize `Vagrantfile` as Ruby ([#7112](https://github.com/helix-editor/helix/pull/7112))
177+- Recognize hidden justfiles as Just ([#7088](https://github.com/helix-editor/helix/pull/7088))
178+- Update Java and TypeScript highlight queries ([#7145](https://github.com/helix-editor/helix/pull/7145))
179+- Recognize `.zimrc` as Bash ([#7146](https://github.com/helix-editor/helix/pull/7146))
180+- Recognize `.gir` as XML ([#7152](https://github.com/helix-editor/helix/pull/7152))
181+- Update tree-sitter-scala ([#7147](https://github.com/helix-editor/helix/pull/7147))
182+- Recognize make file-type as Makefile ([#7212](https://github.com/helix-editor/helix/pull/7212))
183+- Update tree-sitter-verilog ([#7262](https://github.com/helix-editor/helix/pull/7262))
184+- Update tree-sitter-cpp ([#7285](https://github.com/helix-editor/helix/pull/7285))
185+- Support core mode for delve debugger ([#7300](https://github.com/helix-editor/helix/pull/7300))
186+- Add Fortran comment injections ([#7305](https://github.com/helix-editor/helix/pull/7305))
187+- Switch Vue language server to `vue-language-server` ([#7312](https://github.com/helix-editor/helix/pull/7312))
188+- Update tree-sitter-sql ([#7387](https://github.com/helix-editor/helix/pull/7387), [#8464](https://github.com/helix-editor/helix/pull/8464))
189+- Replace the MATLAB tre-sitter grammar ([#7388](https://github.com/helix-editor/helix/pull/7388), [#7442](https://github.com/helix-editor/helix/pull/7442), [#7491](https://github.com/helix-editor/helix/pull/7491), [#7493](https://github.com/helix-editor/helix/pull/7493), [#7511](https://github.com/helix-editor/helix/pull/7511), [#7532](https://github.com/helix-editor/helix/pull/7532), [#8040](https://github.com/helix-editor/helix/pull/8040))
190+- Highlight TOML table headers ([#7441](https://github.com/helix-editor/helix/pull/7441))
191+- Recognize `cppm` file-type as C++ ([#7492](https://github.com/helix-editor/helix/pull/7492))
192+- Refactor ecma language queries into private and public queries ([#7207](https://github.com/helix-editor/helix/pull/7207))
193+- Update tree-sitter-dart ([#7576](https://github.com/helix-editor/helix/pull/7576))
194+- Add shebang for nushell files ([#7606](https://github.com/helix-editor/helix/pull/7606))
195+- Recognize systemd files as INI ([#7592](https://github.com/helix-editor/helix/pull/7592))
196+- Update TypeScript, TSX and Svelte grammars ([#6874](https://github.com/helix-editor/helix/pull/6874))
197+- Enable inlay hints in the Svelte language server ([#7622](https://github.com/helix-editor/helix/pull/7622))
198+- Recognize `Brewfile`s as Ruby ([#7629](https://github.com/helix-editor/helix/pull/7629))
199+- Add more file-types for R ([#7633](https://github.com/helix-editor/helix/pull/7633))
200+- Switch tree-sitter-perl to official upstream parser ([#7644](https://github.com/helix-editor/helix/pull/7644), [#7947](https://github.com/helix-editor/helix/pull/7947))
201+- Fix predicate typo in comment highlights ([#7732](https://github.com/helix-editor/helix/pull/7732))
202+- Update tree-sitter-prql ([#7771](https://github.com/helix-editor/helix/pull/7771))
203+- Recognize `.gitf` as JSON ([#7781](https://github.com/helix-editor/helix/pull/7781))
204+- Switch V language server to `v-analyzer` ([#7760](https://github.com/helix-editor/helix/pull/7760))
205+- Add protobuf language servers ([#7796](https://github.com/helix-editor/helix/pull/7796))
206+- Update tree-sitter-zig ([#7803](https://github.com/helix-editor/helix/pull/7803))
207+- Update tree-sitter-hare ([#7784](https://github.com/helix-editor/helix/pull/7784))
208+- Add Java indent queries ([#7844](https://github.com/helix-editor/helix/pull/7844))
209+- Update tree-sitter-scheme ([979933b](https://github.com/helix-editor/helix/commit/979933b))
210+- Recognize `scm` as Scheme instead of TSQ ([5707151](https://github.com/helix-editor/helix/commit/5707151))
211+- Update tree-sitter-git-commit ([#7831](https://github.com/helix-editor/helix/pull/7831))
212+- Update JavaScript, TypeScript and TSX grammars ([#7852](https://github.com/helix-editor/helix/pull/7852))
213+- Update tree-sitter-nu ([#7873](https://github.com/helix-editor/helix/pull/7873))
214+- Fix YAML indentation ([#6768](https://github.com/helix-editor/helix/pull/6768))
215+- Add `csharp-ls`, Pyright, Pylyzer and add roots for Python ([#7897](https://github.com/helix-editor/helix/pull/7897), [#8032](https://github.com/helix-editor/helix/pull/8032))
216+- Update tree-sitter-slint ([#7893](https://github.com/helix-editor/helix/pull/7893))
217+- Recognize more ZSH file-types as Bash ([#7930](https://github.com/helix-editor/helix/pull/7930))
218+- Recognize `star` extension as Starlark ([#7922](https://github.com/helix-editor/helix/pull/7922))
219+- Fix inline HTML tag highlighting in markdown ([#7960](https://github.com/helix-editor/helix/pull/7960))
220+- Update tree-sitter-robot ([#7970](https://github.com/helix-editor/helix/pull/7970))
221+- Highlight Dart 3 `sealed` and `base` keywords ([#7974](https://github.com/helix-editor/helix/pull/7974))
222+- Add configuration for `ltex-ls` to the default `languages.toml` ([#7838](https://github.com/helix-editor/helix/pull/7838))
223+- Update tree-sitter-strace ([#8087](https://github.com/helix-editor/helix/pull/8087))
224+- Update tree-sitter-gleam, enable auto-format ([#8085](https://github.com/helix-editor/helix/pull/8085))
225+- Update tree-sitter-esdl ([#8222](https://github.com/helix-editor/helix/pull/8222))
226+- Expand ignore file-types ([#8220](https://github.com/helix-editor/helix/pull/8220))
227+- Recognize feed related formats as XML ([#8232](https://github.com/helix-editor/helix/pull/8232))
228+- Improve YAML injections ([#8217](https://github.com/helix-editor/helix/pull/8217))
229+- Add shebangs for TypeScript, Julia, Java and OCaml ([95e994a](https://github.com/helix-editor/helix/commit/95e994a))
230+- Highlight abbreviations in Scheme ([ef23847](https://github.com/helix-editor/helix/commit/ef23847))
231+- Remove backtic auto-pair in OCaml ([#8260](https://github.com/helix-editor/helix/pull/8260))
232+- Recognize `flake.lock` as JSON ([#8304](https://github.com/helix-editor/helix/pull/8304))
233+- Add Python test script injection for Nix ([b4494e1](https://github.com/helix-editor/helix/commit/b4494e1))
234+- Fix Nix comment injection precedence ([37e48f4](https://github.com/helix-editor/helix/commit/37e48f4))
235+- Recognize editorconfig files as INI ([#8308](https://github.com/helix-editor/helix/pull/8308))
236+- Recognize `.babelrc` as JSON ([#8309](https://github.com/helix-editor/helix/pull/8309))
237+- Switch Purescript to its own tree-sitter parser ([#8306](https://github.com/helix-editor/helix/pull/8306), [#8338](https://github.com/helix-editor/helix/pull/8338), [#8527](https://github.com/helix-editor/helix/pull/8527))
238+- Update Unison highlights ([#8315](https://github.com/helix-editor/helix/pull/8315))
239+- Recognize `.webmanifest` as JSON ([#8342](https://github.com/helix-editor/helix/pull/8342))
240+- Recognize polkit policy files as XML ([#8369](https://github.com/helix-editor/helix/pull/8369))
241+- Recognize polkit rules files as JavaScript ([#8370](https://github.com/helix-editor/helix/pull/8370))
242+- Update Go highlight queries ([#8399](https://github.com/helix-editor/helix/pull/8399))
243+- Add shebangs for Makefiles ([#8410](https://github.com/helix-editor/helix/pull/8410))
244+- Add file-type associations from VSCode ([#8388](https://github.com/helix-editor/helix/pull/8388))
245+- Add validation to JSON/CSS language server configs ([#8433](https://github.com/helix-editor/helix/pull/8433))
246+- Add a configuration for the tailwind language server ([#8442](https://github.com/helix-editor/helix/pull/8442))
247+- Add a configuration for the ansible language server ([#7973](https://github.com/helix-editor/helix/pull/7973))
248+- Add a configuration for the GraphQL language server ([#8492](https://github.com/helix-editor/helix/pull/8492))
249+- Indent while statements in Bash ([#8528](https://github.com/helix-editor/helix/pull/8528))
250+- Update tree-sitter-haskell and queries ([#8558](https://github.com/helix-editor/helix/pull/8558))
251+
252+Packaging:
253+
254+- Add an overlay to the Nix flake ([#7078](https://github.com/helix-editor/helix/pull/7078))
255+- Check for `git` before fetching or building grammars ([#7320](https://github.com/helix-editor/helix/pull/7320))
256+- Refactor Nix flake to use Crane ([#7763](https://github.com/helix-editor/helix/pull/7763))
257+- Remove the aarch64 appimage from the release CI ([#7832](https://github.com/helix-editor/helix/pull/7832))
258+- Add desktop and icon files to Nix flake output ([#7979](https://github.com/helix-editor/helix/pull/7979))
259+- Build flake packages with the latest stable Rust ([#8133](https://github.com/helix-editor/helix/pull/8133))
260+
1# 23.05 (2023-05-18)
261# 23.05 (2023-05-18)
2
262
323.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
26323.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
Cargo.lock+538−609
@@ -19,41 +19,22 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19
19
20[[package]]
20[[package]]
21name = "ahash"
21name = "ahash"
22−version = "0.7.6"
22+version = "0.8.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"
34source = "registry+https://github.com/rust-lang/crates.io-index"
23source = "registry+https://github.com/rust-lang/crates.io-index"
35−checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
24+checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
36dependencies = [
25dependencies = [
37 "cfg-if",
26 "cfg-if",
38 "getrandom",
27 "getrandom",
39 "once_cell",
28 "once_cell",
40 "version_check",
29 "version_check",
30+ "zerocopy",
41]
31]
42
32
43[[package]]
33[[package]]
44name = "aho-corasick"
34name = "aho-corasick"
45−version = "0.7.20"
35+version = "1.1.2"
46−source = "registry+https://github.com/rust-lang/crates.io-index"
47−checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
48−dependencies = [
49− "memchr",
50−]
51−
52−[[package]]
53−name = "aho-corasick"
54−version = "1.0.2"
55source = "registry+https://github.com/rust-lang/crates.io-index"
36source = "registry+https://github.com/rust-lang/crates.io-index"
56−checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
37+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
57dependencies = [
38dependencies = [
58 "memchr",
39 "memchr",
59]
40]
@@ -81,9 +62,9 @@ dependencies = [
81
62
82[[package]]
63[[package]]
83name = "anyhow"
64name = "anyhow"
84−version = "1.0.72"
65+version = "1.0.76"
85source = "registry+https://github.com/rust-lang/crates.io-index"
66source = "registry+https://github.com/rust-lang/crates.io-index"
86−checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
67+checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
87
68
88[[package]]
69[[package]]
89name = "arc-swap"
70name = "arc-swap"
@@ -107,7 +88,7 @@ dependencies = [
107 "cc",
88 "cc",
108 "cfg-if",
89 "cfg-if",
109 "libc",
90 "libc",
110− "miniz_oxide 0.7.1",
91+ "miniz_oxide",
111 "object",
92 "object",
112 "rustc-demangle",
93 "rustc-demangle",
113]
94]
@@ -120,15 +101,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
120
101
121[[package]]
102[[package]]
122name = "bitflags"
103name = "bitflags"
123−version = "2.3.3"
104+version = "2.4.1"
124source = "registry+https://github.com/rust-lang/crates.io-index"
105source = "registry+https://github.com/rust-lang/crates.io-index"
125−checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
106+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
126
107
127[[package]]
108[[package]]
128name = "bstr"
109name = "bstr"
129−version = "1.6.0"
110+version = "1.8.0"
130source = "registry+https://github.com/rust-lang/crates.io-index"
111source = "registry+https://github.com/rust-lang/crates.io-index"
131−checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
112+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
132dependencies = [
113dependencies = [
133 "memchr",
114 "memchr",
134 "regex-automata",
115 "regex-automata",
@@ -150,12 +131,6 @@ version = "3.12.0"
150source = "registry+https://github.com/rust-lang/crates.io-index"
131source = "registry+https://github.com/rust-lang/crates.io-index"
151checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
132checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
152
133
153−[[package]]
154−name = "bytecount"
155−version = "0.6.3"
156−source = "registry+https://github.com/rust-lang/crates.io-index"
157−checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
158−
159[[package]]
134[[package]]
160name = "bytes"
135name = "bytes"
161version = "1.4.0"
136version = "1.4.0"
@@ -170,9 +145,12 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
170
145
171[[package]]
146[[package]]
172name = "cc"
147name = "cc"
173−version = "1.0.79"
148+version = "1.0.84"
174source = "registry+https://github.com/rust-lang/crates.io-index"
149source = "registry+https://github.com/rust-lang/crates.io-index"
175−checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
150+checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
151+dependencies = [
152+ "libc",
153+]
176
154
177[[package]]
155[[package]]
178name = "cfg-if"
156name = "cfg-if"
@@ -193,25 +171,23 @@ dependencies = [
193
171
194[[package]]
172[[package]]
195name = "chrono"
173name = "chrono"
196−version = "0.4.26"
174+version = "0.4.31"
197source = "registry+https://github.com/rust-lang/crates.io-index"
175source = "registry+https://github.com/rust-lang/crates.io-index"
198−checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
176+checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
199dependencies = [
177dependencies = [
200 "android-tzdata",
178 "android-tzdata",
201 "iana-time-zone",
179 "iana-time-zone",
202 "num-traits",
180 "num-traits",
203− "winapi",
181+ "windows-targets 0.48.0",
204]
182]
205
183
206[[package]]
184[[package]]
207name = "clipboard-win"
185name = "clipboard-win"
208−version = "4.5.0"
186+version = "5.0.0"
209source = "registry+https://github.com/rust-lang/crates.io-index"
187source = "registry+https://github.com/rust-lang/crates.io-index"
210−checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
188+checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc"
211dependencies = [
189dependencies = [
212 "error-code",
190 "error-code",
213− "str-buf",
214− "winapi",
215]
191]
216
192
217[[package]]
193[[package]]
@@ -245,6 +221,12 @@ version = "0.8.4"
245source = "registry+https://github.com/rust-lang/crates.io-index"
221source = "registry+https://github.com/rust-lang/crates.io-index"
246checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
222checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
247
223
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+
248[[package]]
230[[package]]
249name = "crc32fast"
231name = "crc32fast"
250version = "1.3.2"
232version = "1.3.2"
@@ -254,13 +236,56 @@ dependencies = [
254 "cfg-if",
236 "cfg-if",
255]
237]
256
238
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+
257[[package]]
282[[package]]
258name = "crossterm"
283name = "crossterm"
259−version = "0.26.1"
284+version = "0.27.0"
260source = "registry+https://github.com/rust-lang/crates.io-index"
285source = "registry+https://github.com/rust-lang/crates.io-index"
261−checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
286+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
262dependencies = [
287dependencies = [
263− "bitflags 1.3.2",
288+ "bitflags 2.4.1",
264 "crossterm_winapi",
289 "crossterm_winapi",
265 "filedescriptor",
290 "filedescriptor",
266 "futures-core",
291 "futures-core",
@@ -274,9 +299,9 @@ dependencies = [
274
299
275[[package]]
300[[package]]
276name = "crossterm_winapi"
301name = "crossterm_winapi"
277−version = "0.9.0"
302+version = "0.9.1"
278source = "registry+https://github.com/rust-lang/crates.io-index"
303source = "registry+https://github.com/rust-lang/crates.io-index"
279−checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
304+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
280dependencies = [
305dependencies = [
281 "winapi",
306 "winapi",
282]
307]
@@ -305,7 +330,7 @@ dependencies = [
305 "proc-macro2",
330 "proc-macro2",
306 "quote",
331 "quote",
307 "scratch",
332 "scratch",
308− "syn 2.0.25",
333+ "syn 2.0.38",
309]
334]
310
335
311[[package]]
336[[package]]
@@ -322,7 +347,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
322dependencies = [
347dependencies = [
323 "proc-macro2",
348 "proc-macro2",
324 "quote",
349 "quote",
325− "syn 2.0.25",
350+ "syn 2.0.38",
326]
351]
327
352
328[[package]]
353[[package]]
@@ -339,9 +364,9 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
339
364
340[[package]]
365[[package]]
341name = "encoding_rs"
366name = "encoding_rs"
342−version = "0.8.32"
367+version = "0.8.33"
343source = "registry+https://github.com/rust-lang/crates.io-index"
368source = "registry+https://github.com/rust-lang/crates.io-index"
344−checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
369+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
345dependencies = [
370dependencies = [
346 "cfg-if",
371 "cfg-if",
347]
372]
@@ -363,34 +388,19 @@ checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
363
388
364[[package]]
389[[package]]
365name = "errno"
390name = "errno"
366−version = "0.3.1"
391+version = "0.3.8"
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"
378source = "registry+https://github.com/rust-lang/crates.io-index"
392source = "registry+https://github.com/rust-lang/crates.io-index"
379−checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
393+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
380dependencies = [
394dependencies = [
381− "cc",
382 "libc",
395 "libc",
396+ "windows-sys 0.52.0",
383]
397]
384
398
385[[package]]
399[[package]]
386name = "error-code"
400name = "error-code"
387−version = "2.3.1"
401+version = "3.0.0"
388source = "registry+https://github.com/rust-lang/crates.io-index"
402source = "registry+https://github.com/rust-lang/crates.io-index"
389−checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
403+checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc"
390−dependencies = [
391− "libc",
392− "str-buf",
393−]
394
404
395[[package]]
405[[package]]
396name = "etcetera"
406name = "etcetera"
@@ -403,6 +413,15 @@ dependencies = [
403 "windows-sys 0.48.0",
413 "windows-sys 0.48.0",
404]
414]
405
415
416+[[package]]
417+name = "faster-hex"
418+version = "0.9.0"
419+source = "registry+https://github.com/rust-lang/crates.io-index"
420+checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
421+dependencies = [
422+ "serde",
423+]
424+
406[[package]]
425[[package]]
407name = "fastrand"
426name = "fastrand"
408version = "2.0.0"
427version = "2.0.0"
@@ -429,54 +448,36 @@ dependencies = [
429 "winapi",
448 "winapi",
430]
449]
431
450
432−[[package]]
433−name = "filetime"
434−version = "0.2.21"
435−source = "registry+https://github.com/rust-lang/crates.io-index"
436−checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
437−dependencies = [
438− "cfg-if",
439− "libc",
440− "redox_syscall 0.2.16",
441− "windows-sys 0.48.0",
442−]
443−
444[[package]]
451[[package]]
445name = "flate2"
452name = "flate2"
446−version = "1.0.25"
453+version = "1.0.27"
447source = "registry+https://github.com/rust-lang/crates.io-index"
454source = "registry+https://github.com/rust-lang/crates.io-index"
448−checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
455+checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
449dependencies = [
456dependencies = [
450 "crc32fast",
457 "crc32fast",
451− "miniz_oxide 0.6.2",
458+ "miniz_oxide",
452]
459]
453
460
454−[[package]]
455−name = "fnv"
456−version = "1.0.7"
457−source = "registry+https://github.com/rust-lang/crates.io-index"
458−checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
459−
460[[package]]
461[[package]]
461name = "form_urlencoded"
462name = "form_urlencoded"
462−version = "1.2.0"
463+version = "1.2.1"
463source = "registry+https://github.com/rust-lang/crates.io-index"
464source = "registry+https://github.com/rust-lang/crates.io-index"
464−checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
465+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
465dependencies = [
466dependencies = [
466 "percent-encoding",
467 "percent-encoding",
467]
468]
468
469
469[[package]]
470[[package]]
470name = "futures-core"
471name = "futures-core"
471−version = "0.3.28"
472+version = "0.3.30"
472source = "registry+https://github.com/rust-lang/crates.io-index"
473source = "registry+https://github.com/rust-lang/crates.io-index"
473−checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
474+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
474
475
475[[package]]
476[[package]]
476name = "futures-executor"
477name = "futures-executor"
477−version = "0.3.28"
478+version = "0.3.29"
478source = "registry+https://github.com/rust-lang/crates.io-index"
479source = "registry+https://github.com/rust-lang/crates.io-index"
479−checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
480+checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
480dependencies = [
481dependencies = [
481 "futures-core",
482 "futures-core",
482 "futures-task",
483 "futures-task",
@@ -485,15 +486,15 @@ dependencies = [
485
486
486[[package]]
487[[package]]
487name = "futures-task"
488name = "futures-task"
488−version = "0.3.28"
489+version = "0.3.30"
489source = "registry+https://github.com/rust-lang/crates.io-index"
490source = "registry+https://github.com/rust-lang/crates.io-index"
490−checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
491+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
491
492
492[[package]]
493[[package]]
493name = "futures-util"
494name = "futures-util"
494−version = "0.3.28"
495+version = "0.3.30"
495source = "registry+https://github.com/rust-lang/crates.io-index"
496source = "registry+https://github.com/rust-lang/crates.io-index"
496−checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
497+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
497dependencies = [
498dependencies = [
498 "futures-core",
499 "futures-core",
499 "futures-task",
500 "futures-task",
@@ -502,15 +503,6 @@ dependencies = [
502 "slab",
503 "slab",
503]
504]
504
505
505−[[package]]
506−name = "fuzzy-matcher"
507−version = "0.3.7"
508−source = "registry+https://github.com/rust-lang/crates.io-index"
509−checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
510−dependencies = [
511− "thread_local",
512−]
513−
514[[package]]
506[[package]]
515name = "getrandom"
507name = "getrandom"
516version = "0.2.9"
508version = "0.2.9"
@@ -530,15 +522,13 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
530
522
531[[package]]
523[[package]]
532name = "gix"
524name = "gix"
533−version = "0.48.0"
525+version = "0.57.0"
534source = "registry+https://github.com/rust-lang/crates.io-index"
526source = "registry+https://github.com/rust-lang/crates.io-index"
535−checksum = "c1e74cea676de7f53a79f3c0365812b11f6814b81e671b8ee4abae6ca09c7881"
527+checksum = "721c7497ab24b665ed5a1eb2b3526936aa60068e61ba260651f7e77c52feec69"
536dependencies = [
528dependencies = [
537 "gix-actor",
529 "gix-actor",
538− "gix-attributes",
539 "gix-commitgraph",
530 "gix-commitgraph",
540 "gix-config",
531 "gix-config",
541− "gix-credentials",
542 "gix-date",
532 "gix-date",
543 "gix-diff",
533 "gix-diff",
544 "gix-discover",
534 "gix-discover",
@@ -547,19 +537,16 @@ dependencies = [
547 "gix-glob",
537 "gix-glob",
548 "gix-hash",
538 "gix-hash",
549 "gix-hashtable",
539 "gix-hashtable",
550− "gix-ignore",
551− "gix-index",
552 "gix-lock",
540 "gix-lock",
553− "gix-mailmap",
541+ "gix-macros",
554− "gix-negotiate",
555 "gix-object",
542 "gix-object",
556 "gix-odb",
543 "gix-odb",
557 "gix-pack",
544 "gix-pack",
558 "gix-path",
545 "gix-path",
559− "gix-prompt",
560 "gix-ref",
546 "gix-ref",
561 "gix-refspec",
547 "gix-refspec",
562 "gix-revision",
548 "gix-revision",
549+ "gix-revwalk",
563 "gix-sec",
550 "gix-sec",
564 "gix-tempfile",
551 "gix-tempfile",
565 "gix-trace",
552 "gix-trace",
@@ -567,10 +554,8 @@ dependencies = [
567 "gix-url",
554 "gix-url",
568 "gix-utils",
555 "gix-utils",
569 "gix-validate",
556 "gix-validate",
570− "gix-worktree",
571− "log",
572 "once_cell",
557 "once_cell",
573− "signal-hook",
558+ "parking_lot",
574 "smallvec",
559 "smallvec",
575 "thiserror",
560 "thiserror",
576 "unicode-normalization",
561 "unicode-normalization",
@@ -578,81 +563,46 @@ dependencies = [
578
563
579[[package]]
564[[package]]
580name = "gix-actor"
565name = "gix-actor"
581−version = "0.23.0"
566+version = "0.29.0"
582source = "registry+https://github.com/rust-lang/crates.io-index"
567source = "registry+https://github.com/rust-lang/crates.io-index"
583−checksum = "1969b77b9ee4cc1755c841987ec6f7622aaca95e952bcafb76973ae59d1b8716"
568+checksum = "886014c4865b93ce268f1d3eddd4fd3261242c3f3ee61eb36009f913016a9059"
584dependencies = [
569dependencies = [
585 "bstr",
570 "bstr",
586 "btoi",
571 "btoi",
587 "gix-date",
572 "gix-date",
588 "itoa",
573 "itoa",
589− "nom",
590− "thiserror",
591−]
592−
593−[[package]]
594−name = "gix-attributes"
595−version = "0.14.1"
596−source = "registry+https://github.com/rust-lang/crates.io-index"
597−checksum = "e3772b0129dcd1fc73e985bbd08a1482d082097d2915cb1ee31ce8092b8e4434"
598−dependencies = [
599− "bstr",
600− "gix-glob",
601− "gix-path",
602− "gix-quote",
603− "kstring",
604− "log",
605− "smallvec",
606− "thiserror",
607− "unicode-bom",
608−]
609−
610−[[package]]
611−name = "gix-bitmap"
612−version = "0.2.5"
613−source = "registry+https://github.com/rust-lang/crates.io-index"
614−checksum = "311e2fa997be6560c564b070c5da2d56d038b645a94e1e5796d5d85a350da33c"
615−dependencies = [
616 "thiserror",
574 "thiserror",
575+ "winnow 0.5.28",
617]
576]
618
577
619[[package]]
578[[package]]
620name = "gix-chunk"
579name = "gix-chunk"
621−version = "0.4.3"
580+version = "0.4.6"
622source = "registry+https://github.com/rust-lang/crates.io-index"
581source = "registry+https://github.com/rust-lang/crates.io-index"
623−checksum = "39db5ed0fc0a2e9b1b8265993f7efdbc30379dec268f3b91b7af0c2de4672fdd"
582+checksum = "2ade37ef69870de0ed966b97c57a3a947a22ff3482a52c3b99b205f77bcb08fb"
624dependencies = [
583dependencies = [
625 "thiserror",
584 "thiserror",
626]
585]
627
586
628−[[package]]
629−name = "gix-command"
630−version = "0.2.6"
631−source = "registry+https://github.com/rust-lang/crates.io-index"
632−checksum = "bb49ab557a37b0abb2415bca2b10e541277dff0565deb5bd5e99fd95f93f51eb"
633−dependencies = [
634− "bstr",
635−]
636−
637[[package]]
587[[package]]
638name = "gix-commitgraph"
588name = "gix-commitgraph"
639−version = "0.17.1"
589+version = "0.23.0"
640source = "registry+https://github.com/rust-lang/crates.io-index"
590source = "registry+https://github.com/rust-lang/crates.io-index"
641−checksum = "ed42baa50075d41c1a0931074ce1a97c5797c7c6fe7591d9f1f2dcd448532c26"
591+checksum = "e7559ea9cefee188cd88b05afcc8e3ef7a3cb4a5c647bccf06b981e591b02b77"
642dependencies = [
592dependencies = [
643 "bstr",
593 "bstr",
644 "gix-chunk",
594 "gix-chunk",
645 "gix-features",
595 "gix-features",
646 "gix-hash",
596 "gix-hash",
647− "memmap2 0.7.1",
597+ "memmap2",
648 "thiserror",
598 "thiserror",
649]
599]
650
600
651[[package]]
601[[package]]
652name = "gix-config"
602name = "gix-config"
653−version = "0.25.1"
603+version = "0.33.0"
654source = "registry+https://github.com/rust-lang/crates.io-index"
604source = "registry+https://github.com/rust-lang/crates.io-index"
655−checksum = "817688c7005a716d9363e267913526adea402dabd947f4ba63842d10cc5132af"
605+checksum = "9c35dc7f9c00a42bbc9cfa1ca2ec0a78ad1b76ff0736d3d35dfd612962244467"
656dependencies = [
606dependencies = [
657 "bstr",
607 "bstr",
658 "gix-config-value",
608 "gix-config-value",
@@ -661,49 +611,32 @@ dependencies = [
661 "gix-path",
611 "gix-path",
662 "gix-ref",
612 "gix-ref",
663 "gix-sec",
613 "gix-sec",
664− "log",
665 "memchr",
614 "memchr",
666− "nom",
667 "once_cell",
615 "once_cell",
668 "smallvec",
616 "smallvec",
669 "thiserror",
617 "thiserror",
670 "unicode-bom",
618 "unicode-bom",
619+ "winnow 0.5.28",
671]
620]
672
621
673[[package]]
622[[package]]
674name = "gix-config-value"
623name = "gix-config-value"
675−version = "0.12.3"
624+version = "0.14.2"
676source = "registry+https://github.com/rust-lang/crates.io-index"
625source = "registry+https://github.com/rust-lang/crates.io-index"
677−checksum = "83960be5e99266bcf55dae5a24731bbd39f643bfb68f27e939d6b06836b5b87d"
626+checksum = "39f388cb2396aee82d6f460a2e7770659bdf8854e9e5478f7d2b1324a9698284"
678dependencies = [
627dependencies = [
679− "bitflags 2.3.3",
628+ "bitflags 2.4.1",
680 "bstr",
629 "bstr",
681 "gix-path",
630 "gix-path",
682 "libc",
631 "libc",
683 "thiserror",
632 "thiserror",
684]
633]
685
634
686−[[package]]
687−name = "gix-credentials"
688−version = "0.16.1"
689−source = "registry+https://github.com/rust-lang/crates.io-index"
690−checksum = "75a75565e0e6e7f80cfa4eb1b05cc448c6846ddd48dcf413a28875fbc11ee9af"
691−dependencies = [
692− "bstr",
693− "gix-command",
694− "gix-config-value",
695− "gix-path",
696− "gix-prompt",
697− "gix-sec",
698− "gix-url",
699− "thiserror",
700−]
701−
702[[package]]
635[[package]]
703name = "gix-date"
636name = "gix-date"
704−version = "0.7.0"
637+version = "0.8.2"
705source = "registry+https://github.com/rust-lang/crates.io-index"
638source = "registry+https://github.com/rust-lang/crates.io-index"
706−checksum = "8e9a04a1d2387c955ec91059d56b673000dd24f3c07cad08ed253e36381782bf"
639+checksum = "9d85f4a01e0d05c3de585e28bae514d4baf01655e3fc3f14ce6f30bf62405345"
707dependencies = [
640dependencies = [
708 "bstr",
641 "bstr",
709 "itoa",
642 "itoa",
@@ -713,21 +646,21 @@ dependencies = [
713
646
714[[package]]
647[[package]]
715name = "gix-diff"
648name = "gix-diff"
716−version = "0.32.0"
649+version = "0.39.0"
717source = "registry+https://github.com/rust-lang/crates.io-index"
650source = "registry+https://github.com/rust-lang/crates.io-index"
718−checksum = "aaf5d9b9b521b284ebe53ee69eee33341835ec70edc314f36b2100ea81396121"
651+checksum = "ac0e75f5afd2f6c47c800b6b0a000a08045739d0450d20482e8faa42543f62d1"
719dependencies = [
652dependencies = [
653+ "bstr",
720 "gix-hash",
654 "gix-hash",
721 "gix-object",
655 "gix-object",
722− "imara-diff",
723 "thiserror",
656 "thiserror",
724]
657]
725
658
726[[package]]
659[[package]]
727name = "gix-discover"
660name = "gix-discover"
728−version = "0.21.1"
661+version = "0.28.0"
729source = "registry+https://github.com/rust-lang/crates.io-index"
662source = "registry+https://github.com/rust-lang/crates.io-index"
730−checksum = "272aad20dc63dedba76615373dd8885fb5aebe4795e5b5b0aa2a24e63c82085c"
663+checksum = "f0b63ce2ad81632ac1a84ac370f85a5e580c3261580bd85ea17ff35d3a0bba18"
731dependencies = [
664dependencies = [
732 "bstr",
665 "bstr",
733 "dunce",
666 "dunce",
@@ -740,9 +673,9 @@ dependencies = [
740
673
741[[package]]
674[[package]]
742name = "gix-features"
675name = "gix-features"
743−version = "0.31.1"
676+version = "0.37.0"
744source = "registry+https://github.com/rust-lang/crates.io-index"
677source = "registry+https://github.com/rust-lang/crates.io-index"
745−checksum = "06142d8cff5d17509399b04052b64d2f9b3a311d5cff0b1a32b220f62cd0d595"
678+checksum = "befe7edea299a824504b5acc96d7a3a538125b38c42f3a8379f6912a29c90c81"
746dependencies = [
679dependencies = [
747 "crc32fast",
680 "crc32fast",
748 "flate2",
681 "flate2",
@@ -758,20 +691,20 @@ dependencies = [
758
691
759[[package]]
692[[package]]
760name = "gix-fs"
693name = "gix-fs"
761−version = "0.3.0"
694+version = "0.9.0"
762source = "registry+https://github.com/rust-lang/crates.io-index"
695source = "registry+https://github.com/rust-lang/crates.io-index"
763−checksum = "bb15956bc0256594c62a2399fcf6958a02a11724217eddfdc2b49b21b6292496"
696+checksum = "007017ce93b819ea52c0ec68306f7d72212a2d307c3d70f1548c7141c015d0a1"
764dependencies = [
697dependencies = [
765 "gix-features",
698 "gix-features",
766]
699]
767
700
768[[package]]
701[[package]]
769name = "gix-glob"
702name = "gix-glob"
770−version = "0.9.1"
703+version = "0.15.0"
771source = "registry+https://github.com/rust-lang/crates.io-index"
704source = "registry+https://github.com/rust-lang/crates.io-index"
772−checksum = "c18bdff83143d61e7d60da6183b87542a870d026b2a2d0b30170b8e9c0cd321a"
705+checksum = "61fb116c2516d3a1170e010118f639944a6389872c875a008960cdab2a44ac72"
773dependencies = [
706dependencies = [
774− "bitflags 2.3.3",
707+ "bitflags 2.4.1",
775 "bstr",
708 "bstr",
776 "gix-features",
709 "gix-features",
777 "gix-path",
710 "gix-path",
@@ -779,64 +712,30 @@ dependencies = [
779
712
780[[package]]
713[[package]]
781name = "gix-hash"
714name = "gix-hash"
782−version = "0.11.3"
715+version = "0.14.0"
783source = "registry+https://github.com/rust-lang/crates.io-index"
716source = "registry+https://github.com/rust-lang/crates.io-index"
784−checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272"
717+checksum = "c52c3170a17b6031833cd2eb4ad7bc23f2755d06e6e70f78dec21d42e8fe1b30"
785dependencies = [
718dependencies = [
786− "hex",
719+ "faster-hex",
787 "thiserror",
720 "thiserror",
788]
721]
789
722
790[[package]]
723[[package]]
791name = "gix-hashtable"
724name = "gix-hashtable"
792−version = "0.2.3"
725+version = "0.5.0"
793source = "registry+https://github.com/rust-lang/crates.io-index"
726source = "registry+https://github.com/rust-lang/crates.io-index"
794−checksum = "9e133bc56d938eaec1c675af7c681a51de9662b0ada779f45607b967a10da77a"
727+checksum = "40a838e6366a5e5b84668b6997ce0981b833136468e8ba949f424c0ef2927eba"
795dependencies = [
728dependencies = [
796 "gix-hash",
729 "gix-hash",
797− "hashbrown 0.14.0",
730+ "hashbrown 0.14.3",
798 "parking_lot",
731 "parking_lot",
799]
732]
800
733
801−[[package]]
802−name = "gix-ignore"
803−version = "0.4.1"
804−source = "registry+https://github.com/rust-lang/crates.io-index"
805−checksum = "ca801f2d0535210f77b33e2c067d565aedecacc82f1b3dbce26da1388ebc4634"
806−dependencies = [
807− "bstr",
808− "gix-glob",
809− "gix-path",
810− "unicode-bom",
811−]
812−
813−[[package]]
814−name = "gix-index"
815−version = "0.20.0"
816−source = "registry+https://github.com/rust-lang/crates.io-index"
817−checksum = "68099abdf6ee50ae3c897e8b05de96871cbe54d52a37cdf559101f911b883562"
818−dependencies = [
819− "bitflags 2.3.3",
820− "bstr",
821− "btoi",
822− "filetime",
823− "gix-bitmap",
824− "gix-features",
825− "gix-hash",
826− "gix-lock",
827− "gix-object",
828− "gix-traverse",
829− "itoa",
830− "memmap2 0.7.1",
831− "smallvec",
832− "thiserror",
833−]
834−
835[[package]]
734[[package]]
836name = "gix-lock"
735name = "gix-lock"
837−version = "7.0.0"
736+version = "12.0.0"
838source = "registry+https://github.com/rust-lang/crates.io-index"
737source = "registry+https://github.com/rust-lang/crates.io-index"
839−checksum = "328f50aad713ab606caeaf834459ef915ccdfbb9133ac6cd54616d601aa9249f"
738+checksum = "6cf112ddee94223c119a8534dad027740dc3aba3365ac5edeef8a7f6660c74db"
840dependencies = [
739dependencies = [
841 "gix-tempfile",
740 "gix-tempfile",
842 "gix-utils",
741 "gix-utils",
@@ -844,38 +743,21 @@ dependencies = [
844]
743]
845
744
846[[package]]
745[[package]]
847−name = "gix-mailmap"
746+name = "gix-macros"
848−version = "0.15.0"
849−source = "registry+https://github.com/rust-lang/crates.io-index"
850−checksum = "1787e3c37fc43b1f7c0e3be6196c6837b3ba5f869190dfeaa444b816f0a7f34b"
851−dependencies = [
852− "bstr",
853− "gix-actor",
854− "gix-date",
855− "thiserror",
856−]
857−
858−[[package]]
859−name = "gix-negotiate"
860−version = "0.4.0"
747+version = "0.1.2"
861source = "registry+https://github.com/rust-lang/crates.io-index"
748source = "registry+https://github.com/rust-lang/crates.io-index"
862−checksum = "4e7bce64d4452dd609f44d04b14b29da2e0ad2c45fcdf4ce1472a5f5f8ec21c2"
749+checksum = "fc207b64189cf71bcb17fc841ab99a5d63d0845546b611e2703ce467f659323a"
863dependencies = [
750dependencies = [
864− "bitflags 2.3.3",
751+ "proc-macro2",
865− "gix-commitgraph",
752+ "quote",
866− "gix-date",
753+ "syn 2.0.38",
867− "gix-hash",
868− "gix-object",
869− "gix-revwalk",
870− "smallvec",
871− "thiserror",
872]
754]
873
755
874[[package]]
756[[package]]
875name = "gix-object"
757name = "gix-object"
876−version = "0.32.0"
758+version = "0.40.0"
877source = "registry+https://github.com/rust-lang/crates.io-index"
759source = "registry+https://github.com/rust-lang/crates.io-index"
878−checksum = "a953f3d7ffad16734aa3ab1d05807972c80e339d1bd9dde03e0198716b99e2a6"
760+checksum = "e4c77e47ffba92127faf632a841fce23d19547e269bd8b88e68961a70eab4e93"
879dependencies = [
761dependencies = [
880 "bstr",
762 "bstr",
881 "btoi",
763 "btoi",
@@ -884,18 +766,17 @@ dependencies = [
884 "gix-features",
766 "gix-features",
885 "gix-hash",
767 "gix-hash",
886 "gix-validate",
768 "gix-validate",
887− "hex",
888 "itoa",
769 "itoa",
889− "nom",
890 "smallvec",
770 "smallvec",
891 "thiserror",
771 "thiserror",
772+ "winnow 0.5.28",
892]
773]
893
774
894[[package]]
775[[package]]
895name = "gix-odb"
776name = "gix-odb"
896−version = "0.49.1"
777+version = "0.56.0"
897source = "registry+https://github.com/rust-lang/crates.io-index"
778source = "registry+https://github.com/rust-lang/crates.io-index"
898−checksum = "f6418cff00ecc2713b58c8e04bff30dda808fbba1a080e7248b299d069894a01"
779+checksum = "a3254f2005cc7553ea78e85e816a09150c6f7a64e6b7627b8d1fdc56721bea73"
899dependencies = [
780dependencies = [
900 "arc-swap",
781 "arc-swap",
901 "gix-date",
782 "gix-date",
@@ -912,21 +793,19 @@ dependencies = [
912
793
913[[package]]
794[[package]]
914name = "gix-pack"
795name = "gix-pack"
915−version = "0.39.1"
796+version = "0.46.0"
916source = "registry+https://github.com/rust-lang/crates.io-index"
797source = "registry+https://github.com/rust-lang/crates.io-index"
917−checksum = "414935138d90043ea5898de7a93f02c2558e52652492719470e203ef26a8fd0a"
798+checksum = "02ebc8cd657eec207d82d8f876ca402361308ecd3c87a47935b0299506257b4f"
918dependencies = [
799dependencies = [
919 "clru",
800 "clru",
920 "gix-chunk",
801 "gix-chunk",
921− "gix-diff",
922 "gix-features",
802 "gix-features",
923 "gix-hash",
803 "gix-hash",
924 "gix-hashtable",
804 "gix-hashtable",
925 "gix-object",
805 "gix-object",
926 "gix-path",
806 "gix-path",
927 "gix-tempfile",
807 "gix-tempfile",
928− "gix-traverse",
929− "memmap2 0.7.1",
808+ "memmap2",
930 "parking_lot",
809 "parking_lot",
931 "smallvec",
810 "smallvec",
932 "thiserror",
811 "thiserror",
@@ -934,9 +813,9 @@ dependencies = [
934
813
935[[package]]
814[[package]]
936name = "gix-path"
815name = "gix-path"
937−version = "0.8.3"
816+version = "0.10.2"
938source = "registry+https://github.com/rust-lang/crates.io-index"
817source = "registry+https://github.com/rust-lang/crates.io-index"
939−checksum = "dfca182d2575ded2ed38280f1ebf75cd5d3790b77e0872de07854cf085821fbe"
818+checksum = "10931652a3da126990ac93bce1b1c600cc99d7c268d712b6360ed52174ce1b68"
940dependencies = [
819dependencies = [
941 "bstr",
820 "bstr",
942 "gix-trace",
821 "gix-trace",
@@ -945,24 +824,11 @@ dependencies = [
945 "thiserror",
824 "thiserror",
946]
825]
947
826
948−[[package]]
949−name = "gix-prompt"
950−version = "0.5.2"
951−source = "registry+https://github.com/rust-lang/crates.io-index"
952−checksum = "8dfd363fd89a40c1e7bff9c9c1b136cd2002480f724b0c627c1bc771cd5480ec"
953−dependencies = [
954− "gix-command",
955− "gix-config-value",
956− "parking_lot",
957− "rustix 0.37.15",
958− "thiserror",
959−]
960−
961[[package]]
827[[package]]
962name = "gix-quote"
828name = "gix-quote"
963−version = "0.4.5"
829+version = "0.4.9"
964source = "registry+https://github.com/rust-lang/crates.io-index"
830source = "registry+https://github.com/rust-lang/crates.io-index"
965−checksum = "3874de636c2526de26a3405b8024b23ef1a327bebf4845d770d00d48700b6a40"
831+checksum = "6c43530cb94a7759807e6f8d180e17ac9c65673b891645c6c433831dc0cf4342"
966dependencies = [
832dependencies = [
967 "bstr",
833 "bstr",
968 "btoi",
834 "btoi",
@@ -971,9 +837,9 @@ dependencies = [
971
837
972[[package]]
838[[package]]
973name = "gix-ref"
839name = "gix-ref"
974−version = "0.32.1"
840+version = "0.40.0"
975source = "registry+https://github.com/rust-lang/crates.io-index"
841source = "registry+https://github.com/rust-lang/crates.io-index"
976−checksum = "39453f4e5f23cddc2e6e4cca2ba20adfdbec29379e3ca829714dfe98ae068ccd"
842+checksum = "baa951b3b850d6d1be4f900768e49af20b76bf9505beac22af723d57249a2f1d"
977dependencies = [
843dependencies = [
978 "gix-actor",
844 "gix-actor",
979 "gix-date",
845 "gix-date",
@@ -985,16 +851,16 @@ dependencies = [
985 "gix-path",
851 "gix-path",
986 "gix-tempfile",
852 "gix-tempfile",
987 "gix-validate",
853 "gix-validate",
988− "memmap2 0.7.1",
854+ "memmap2",
989− "nom",
990 "thiserror",
855 "thiserror",
856+ "winnow 0.5.28",
991]
857]
992
858
993[[package]]
859[[package]]
994name = "gix-refspec"
860name = "gix-refspec"
995−version = "0.13.0"
861+version = "0.21.0"
996source = "registry+https://github.com/rust-lang/crates.io-index"
862source = "registry+https://github.com/rust-lang/crates.io-index"
997−checksum = "b8e76ff1f82fba295a121e31ab02f69642994e532c45c0c899aa393f4b740302"
863+checksum = "88c61f849d58c06e3068a0b601cf10127d2a07cdad00a725ed66cf303f76f6b3"
998dependencies = [
864dependencies = [
999 "bstr",
865 "bstr",
1000 "gix-hash",
866 "gix-hash",
@@ -1006,9 +872,9 @@ dependencies = [
1006
872
1007[[package]]
873[[package]]
1008name = "gix-revision"
874name = "gix-revision"
1009−version = "0.17.0"
875+version = "0.25.0"
1010source = "registry+https://github.com/rust-lang/crates.io-index"
876source = "registry+https://github.com/rust-lang/crates.io-index"
1011−checksum = "237428a7d3978e8572964e1e45d984027c2acc94df47e594baa6c4b0da7c9922"
877+checksum = "bcbbf91f4200c5c76802ef5f057b96f5a336827508881fe55a780be71a794d22"
1012dependencies = [
878dependencies = [
1013 "bstr",
879 "bstr",
1014 "gix-date",
880 "gix-date",
@@ -1016,14 +882,15 @@ dependencies = [
1016 "gix-hashtable",
882 "gix-hashtable",
1017 "gix-object",
883 "gix-object",
1018 "gix-revwalk",
884 "gix-revwalk",
885+ "gix-trace",
1019 "thiserror",
886 "thiserror",
1020]
887]
1021
888
1022[[package]]
889[[package]]
1023name = "gix-revwalk"
890name = "gix-revwalk"
1024−version = "0.3.0"
891+version = "0.11.0"
1025source = "registry+https://github.com/rust-lang/crates.io-index"
892source = "registry+https://github.com/rust-lang/crates.io-index"
1026−checksum = "028d50fcaf8326a8f79a359490d9ca9fb4e2b51ac9ac86503560d0bcc888d2eb"
893+checksum = "33ab66354d83f70f2730391747d0c75f94ef3c3dd40ebde2e206db9faaf7a0a7"
1027dependencies = [
894dependencies = [
1028 "gix-commitgraph",
895 "gix-commitgraph",
1029 "gix-date",
896 "gix-date",
@@ -1036,42 +903,40 @@ dependencies = [
1036
903
1037[[package]]
904[[package]]
1038name = "gix-sec"
905name = "gix-sec"
1039−version = "0.8.3"
906+version = "0.10.2"
1040source = "registry+https://github.com/rust-lang/crates.io-index"
907source = "registry+https://github.com/rust-lang/crates.io-index"
1041−checksum = "ede298863db2a0574a14070991710551e76d1f47c9783b62d4fcbca17f56371c"
908+checksum = "a9963f38a42144253ed4d571882d7db5ef644c12e6726e4b75135597cc9d0e1a"
1042dependencies = [
909dependencies = [
1043− "bitflags 2.3.3",
910+ "bitflags 2.4.1",
1044 "gix-path",
911 "gix-path",
1045 "libc",
912 "libc",
1046− "windows",
913+ "windows 0.52.0",
1047]
914]
1048
915
1049[[package]]
916[[package]]
1050name = "gix-tempfile"
917name = "gix-tempfile"
1051−version = "7.0.0"
918+version = "12.0.0"
1052source = "registry+https://github.com/rust-lang/crates.io-index"
919source = "registry+https://github.com/rust-lang/crates.io-index"
1053−checksum = "4fac8310c17406ea619af72f42ee46dac795110f68f41b4f4fa231b69889c6a2"
920+checksum = "e76a494bd530e1a1309188ff971825a24f159c76c2db0bf71fa5dfb469a2c915"
1054dependencies = [
921dependencies = [
1055 "gix-fs",
922 "gix-fs",
1056 "libc",
923 "libc",
1057 "once_cell",
924 "once_cell",
1058 "parking_lot",
925 "parking_lot",
1059− "signal-hook",
1060− "signal-hook-registry",
1061 "tempfile",
926 "tempfile",
1062]
927]
1063
928
1064[[package]]
929[[package]]
1065name = "gix-trace"
930name = "gix-trace"
1066−version = "0.1.2"
931+version = "0.1.5"
1067source = "registry+https://github.com/rust-lang/crates.io-index"
932source = "registry+https://github.com/rust-lang/crates.io-index"
1068−checksum = "103eac621617be3ebe0605c9065ca51a223279a23218aaf67d10daa6e452f663"
933+checksum = "bda62acb44dd86a40c7c3762a5403cfc1ac789ea559df54085cedf79864f809e"
1069
934
1070[[package]]
935[[package]]
1071name = "gix-traverse"
936name = "gix-traverse"
1072−version = "0.29.0"
937+version = "0.36.0"
1073source = "registry+https://github.com/rust-lang/crates.io-index"
938source = "registry+https://github.com/rust-lang/crates.io-index"
1074−checksum = "e3cdfd54598db4fae57d5ae6f52958422b2d13382d2745796bfe5c8015ffa86e"
939+checksum = "8661fab39985c9214e56d81a63ceb5886ac948cec2fba76c39494d1e0e307ea8"
1075dependencies = [
940dependencies = [
1076 "gix-commitgraph",
941 "gix-commitgraph",
1077 "gix-date",
942 "gix-date",
@@ -1085,9 +950,9 @@ dependencies = [
1085
950
1086[[package]]
951[[package]]
1087name = "gix-url"
952name = "gix-url"
1088−version = "0.20.1"
953+version = "0.26.0"
1089source = "registry+https://github.com/rust-lang/crates.io-index"
954source = "registry+https://github.com/rust-lang/crates.io-index"
1090−checksum = "beaede6dbc83f408b19adfd95bb52f1dbf01fb8862c3faf6c6243e2e67fcdfa1"
955+checksum = "10a0129c1e8b52736d7c5128300a4485dbc85863001371e2771ac1754bd89fd7"
1091dependencies = [
956dependencies = [
1092 "bstr",
957 "bstr",
1093 "gix-features",
958 "gix-features",
@@ -1099,94 +964,71 @@ dependencies = [
1099
964
1100[[package]]
965[[package]]
1101name = "gix-utils"
966name = "gix-utils"
1102−version = "0.1.4"
967+version = "0.1.7"
1103source = "registry+https://github.com/rust-lang/crates.io-index"
968source = "registry+https://github.com/rust-lang/crates.io-index"
1104−checksum = "7058c94f4164fcf5b8457d35f6d8f6e1007f9f7f938c9c7684a7e01d23c6ddde"
969+checksum = "ab9277a5e32e85d53f738096d872a4a9b76067ad471894ad31bd99c8fa2da1dc"
1105dependencies = [
970dependencies = [
1106 "fastrand",
971 "fastrand",
1107]
972]
1108
973
1109[[package]]
974[[package]]
1110name = "gix-validate"
975name = "gix-validate"
1111−version = "0.7.6"
976+version = "0.8.2"
1112−source = "registry+https://github.com/rust-lang/crates.io-index"
1113−checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13"
1114−dependencies = [
1115− "bstr",
1116− "thiserror",
1117−]
1118−
1119−[[package]]
1120−name = "gix-worktree"
1121−version = "0.21.1"
1122source = "registry+https://github.com/rust-lang/crates.io-index"
977source = "registry+https://github.com/rust-lang/crates.io-index"
1123−checksum = "c1363b9aa66b9e14412ac04e1f759827203f491729d92172535a8ce6cde02efa"
978+checksum = "f805ebbdbaa4bfd98e2ee43e6d14099b8a4d9141f5d7b8202fea4d48e44263e7"
1124dependencies = [
979dependencies = [
1125 "bstr",
980 "bstr",
1126− "filetime",
1127− "gix-attributes",
1128− "gix-features",
1129− "gix-fs",
1130− "gix-glob",
1131− "gix-hash",
1132− "gix-ignore",
1133− "gix-index",
1134− "gix-object",
1135− "gix-path",
1136− "io-close",
1137 "thiserror",
981 "thiserror",
1138]
982]
1139
983
1140[[package]]
984[[package]]
1141name = "globset"
985name = "globset"
1142−version = "0.4.11"
986+version = "0.4.14"
1143source = "registry+https://github.com/rust-lang/crates.io-index"
987source = "registry+https://github.com/rust-lang/crates.io-index"
1144−checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
988+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
1145dependencies = [
989dependencies = [
1146− "aho-corasick 1.0.2",
990+ "aho-corasick",
1147 "bstr",
991 "bstr",
1148− "fnv",
1149 "log",
992 "log",
1150− "regex",
993+ "regex-automata",
994+ "regex-syntax",
1151]
995]
1152
996
1153[[package]]
997[[package]]
1154name = "grep-matcher"
998name = "grep-matcher"
1155−version = "0.1.6"
999+version = "0.1.7"
1156source = "registry+https://github.com/rust-lang/crates.io-index"
1000source = "registry+https://github.com/rust-lang/crates.io-index"
1157−checksum = "3902ca28f26945fe35cad349d776f163981d777fee382ccd6ef451126f51b319"
1001+checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02"
1158dependencies = [
1002dependencies = [
1159 "memchr",
1003 "memchr",
1160]
1004]
1161
1005
1162[[package]]
1006[[package]]
1163name = "grep-regex"
1007name = "grep-regex"
1164−version = "0.1.11"
1008+version = "0.1.12"
1165source = "registry+https://github.com/rust-lang/crates.io-index"
1009source = "registry+https://github.com/rust-lang/crates.io-index"
1166−checksum = "997598b41d53a37a2e3fc5300d5c11d825368c054420a9c65125b8fe1078463f"
1010+checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d"
1167dependencies = [
1011dependencies = [
1168− "aho-corasick 0.7.20",
1169 "bstr",
1012 "bstr",
1170 "grep-matcher",
1013 "grep-matcher",
1171 "log",
1014 "log",
1172− "regex",
1015+ "regex-automata",
1173− "regex-syntax 0.6.29",
1016+ "regex-syntax",
1174− "thread_local",
1175]
1017]
1176
1018
1177[[package]]
1019[[package]]
1178name = "grep-searcher"
1020name = "grep-searcher"
1179−version = "0.1.11"
1021+version = "0.1.13"
1180source = "registry+https://github.com/rust-lang/crates.io-index"
1022source = "registry+https://github.com/rust-lang/crates.io-index"
1181−checksum = "5601c4b9f480f0c9ebb40b1f6cbf447b8a50c5369223937a6c5214368c58779f"
1023+checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54"
1182dependencies = [
1024dependencies = [
1183 "bstr",
1025 "bstr",
1184− "bytecount",
1185 "encoding_rs",
1026 "encoding_rs",
1186 "encoding_rs_io",
1027 "encoding_rs_io",
1187 "grep-matcher",
1028 "grep-matcher",
1188 "log",
1029 "log",
1030+ "memchr",
1189− "memmap2 0.5.10",
1031+ "memmap2",
1190]
1032]
1191
1033
1192[[package]]
1034[[package]]
@@ -1194,37 +1036,36 @@ name = "hashbrown"
1194version = "0.12.3"
1036version = "0.12.3"
1195source = "registry+https://github.com/rust-lang/crates.io-index"
1037source = "registry+https://github.com/rust-lang/crates.io-index"
1196checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
1038checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
1197−dependencies = [
1198− "ahash 0.7.6",
1199−]
1200
1039
1201[[package]]
1040[[package]]
1202name = "hashbrown"
1041name = "hashbrown"
1203−version = "0.14.0"
1042+version = "0.14.3"
1204source = "registry+https://github.com/rust-lang/crates.io-index"
1043source = "registry+https://github.com/rust-lang/crates.io-index"
1205−checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
1044+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
1206dependencies = [
1045dependencies = [
1207− "ahash 0.8.3",
1046+ "ahash",
1208 "allocator-api2",
1047 "allocator-api2",
1209]
1048]
1210
1049
1211[[package]]
1050[[package]]
1212name = "helix-core"
1051name = "helix-core"
1213−version = "0.6.0"
1052+version = "23.10.0"
1214dependencies = [
1053dependencies = [
1215− "ahash 0.8.3",
1054+ "ahash",
1216 "arc-swap",
1055 "arc-swap",
1217− "bitflags 2.3.3",
1056+ "bitflags 2.4.1",
1218 "chrono",
1057 "chrono",
1219 "dunce",
1058 "dunce",
1220 "encoding_rs",
1059 "encoding_rs",
1221 "etcetera",
1060 "etcetera",
1222− "hashbrown 0.14.0",
1061+ "hashbrown 0.14.3",
1223 "helix-loader",
1062 "helix-loader",
1224 "imara-diff",
1063 "imara-diff",
1225 "indoc",
1064 "indoc",
1226 "log",
1065 "log",
1066+ "nucleo",
1227 "once_cell",
1067 "once_cell",
1068+ "parking_lot",
1228 "quickcheck",
1069 "quickcheck",
1229 "regex",
1070 "regex",
1230 "ropey",
1071 "ropey",
@@ -1243,7 +1084,7 @@ dependencies = [
1243
1084
1244[[package]]
1085[[package]]
1245name = "helix-dap"
1086name = "helix-dap"
1246−version = "0.6.0"
1087+version = "23.10.0"
1247dependencies = [
1088dependencies = [
1248 "anyhow",
1089 "anyhow",
1249 "fern",
1090 "fern",
@@ -1256,9 +1097,17 @@ dependencies = [
1256 "which",
1097 "which",
1257]
1098]
1258
1099
1100+[[package]]
1101+name = "helix-event"
1102+version = "23.10.0"
1103+dependencies = [
1104+ "parking_lot",
1105+ "tokio",
1106+]
1107+
1259[[package]]
1108[[package]]
1260name = "helix-loader"
1109name = "helix-loader"
1261−version = "0.6.0"
1110+version = "23.10.0"
1262dependencies = [
1111dependencies = [
1263 "anyhow",
1112 "anyhow",
1264 "cc",
1113 "cc",
@@ -1277,7 +1126,7 @@ dependencies = [
1277
1126
1278[[package]]
1127[[package]]
1279name = "helix-lsp"
1128name = "helix-lsp"
1280−version = "0.6.0"
1129+version = "23.10.0"
1281dependencies = [
1130dependencies = [
1282 "anyhow",
1131 "anyhow",
1283 "futures-executor",
1132 "futures-executor",
@@ -1299,11 +1148,11 @@ dependencies = [
1299
1148
1300[[package]]
1149[[package]]
1301name = "helix-parsec"
1150name = "helix-parsec"
1302−version = "0.6.0"
1151+version = "23.10.0"
1303
1152
1304[[package]]
1153[[package]]
1305name = "helix-term"
1154name = "helix-term"
1306−version = "0.6.0"
1155+version = "23.10.0"
1307dependencies = [
1156dependencies = [
1308 "anyhow",
1157 "anyhow",
1309 "arc-swap",
1158 "arc-swap",
@@ -1312,11 +1161,11 @@ dependencies = [
1312 "crossterm",
1161 "crossterm",
1313 "fern",
1162 "fern",
1314 "futures-util",
1163 "futures-util",
1315− "fuzzy-matcher",
1316 "grep-regex",
1164 "grep-regex",
1317 "grep-searcher",
1165 "grep-searcher",
1318 "helix-core",
1166 "helix-core",
1319 "helix-dap",
1167 "helix-dap",
1168+ "helix-event",
1320 "helix-loader",
1169 "helix-loader",
1321 "helix-lsp",
1170 "helix-lsp",
1322 "helix-tui",
1171 "helix-tui",
@@ -1326,7 +1175,9 @@ dependencies = [
1326 "indoc",
1175 "indoc",
1327 "libc",
1176 "libc",
1328 "log",
1177 "log",
1178+ "nucleo",
1329 "once_cell",
1179 "once_cell",
1180+ "open",
1330 "pulldown-cmark",
1181 "pulldown-cmark",
1331 "serde",
1182 "serde",
1332 "serde_json",
1183 "serde_json",
@@ -1337,14 +1188,15 @@ dependencies = [
1337 "tokio",
1188 "tokio",
1338 "tokio-stream",
1189 "tokio-stream",
1339 "toml",
1190 "toml",
1191+ "url",
1340 "which",
1192 "which",
1341]
1193]
1342
1194
1343[[package]]
1195[[package]]
1344name = "helix-tui"
1196name = "helix-tui"
1345−version = "0.6.0"
1197+version = "23.10.0"
1346dependencies = [
1198dependencies = [
1347− "bitflags 2.3.3",
1199+ "bitflags 2.4.1",
1348 "cassowary",
1200 "cassowary",
1349 "crossterm",
1201 "crossterm",
1350 "helix-core",
1202 "helix-core",
@@ -1358,12 +1210,13 @@ dependencies = [
1358
1210
1359[[package]]
1211[[package]]
1360name = "helix-vcs"
1212name = "helix-vcs"
1361−version = "0.6.0"
1213+version = "23.10.0"
1362dependencies = [
1214dependencies = [
1363 "anyhow",
1215 "anyhow",
1364 "arc-swap",
1216 "arc-swap",
1365 "gix",
1217 "gix",
1366 "helix-core",
1218 "helix-core",
1219+ "helix-event",
1367 "imara-diff",
1220 "imara-diff",
1368 "log",
1221 "log",
1369 "parking_lot",
1222 "parking_lot",
@@ -1373,17 +1226,18 @@ dependencies = [
1373
1226
1374[[package]]
1227[[package]]
1375name = "helix-view"
1228name = "helix-view"
1376−version = "0.6.0"
1229+version = "23.10.0"
1377dependencies = [
1230dependencies = [
1378 "anyhow",
1231 "anyhow",
1379 "arc-swap",
1232 "arc-swap",
1380− "bitflags 2.3.3",
1233+ "bitflags 2.4.1",
1381 "chardetng",
1234 "chardetng",
1382 "clipboard-win",
1235 "clipboard-win",
1383 "crossterm",
1236 "crossterm",
1384 "futures-util",
1237 "futures-util",
1385 "helix-core",
1238 "helix-core",
1386 "helix-dap",
1239 "helix-dap",
1240+ "helix-event",
1387 "helix-loader",
1241 "helix-loader",
1388 "helix-lsp",
1242 "helix-lsp",
1389 "helix-tui",
1243 "helix-tui",
@@ -1392,6 +1246,7 @@ dependencies = [
1392 "log",
1246 "log",
1393 "once_cell",
1247 "once_cell",
1394 "parking_lot",
1248 "parking_lot",
1249+ "rustix",
1395 "serde",
1250 "serde",
1396 "serde_json",
1251 "serde_json",
1397 "slotmap",
1252 "slotmap",
@@ -1411,25 +1266,13 @@ dependencies = [
1411 "libc",
1266 "libc",
1412]
1267]
1413
1268
1414−[[package]]
1415−name = "hermit-abi"
1416−version = "0.3.1"
1417−source = "registry+https://github.com/rust-lang/crates.io-index"
1418−checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
1419−
1420−[[package]]
1421−name = "hex"
1422−version = "0.4.3"
1423−source = "registry+https://github.com/rust-lang/crates.io-index"
1424−checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1425−
1426[[package]]
1269[[package]]
1427name = "home"
1270name = "home"
1428−version = "0.5.4"
1271+version = "0.5.5"
1429source = "registry+https://github.com/rust-lang/crates.io-index"
1272source = "registry+https://github.com/rust-lang/crates.io-index"
1430−checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408"
1273+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
1431dependencies = [
1274dependencies = [
1432− "winapi",
1275+ "windows-sys 0.48.0",
1433]
1276]
1434
1277
1435[[package]]
1278[[package]]
@@ -1443,7 +1286,7 @@ dependencies = [
1443 "iana-time-zone-haiku",
1286 "iana-time-zone-haiku",
1444 "js-sys",
1287 "js-sys",
1445 "wasm-bindgen",
1288 "wasm-bindgen",
1446− "windows",
1289+ "windows 0.48.0",
1447]
1290]
1448
1291
1449[[package]]
1292[[package]]
@@ -1458,9 +1301,9 @@ dependencies = [
1458
1301
1459[[package]]
1302[[package]]
1460name = "idna"
1303name = "idna"
1461−version = "0.4.0"
1304+version = "0.5.0"
1462source = "registry+https://github.com/rust-lang/crates.io-index"
1305source = "registry+https://github.com/rust-lang/crates.io-index"
1463−checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
1306+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
1464dependencies = [
1307dependencies = [
1465 "unicode-bidi",
1308 "unicode-bidi",
1466 "unicode-normalization",
1309 "unicode-normalization",
@@ -1468,17 +1311,16 @@ dependencies = [
1468
1311
1469[[package]]
1312[[package]]
1470name = "ignore"
1313name = "ignore"
1471−version = "0.4.20"
1314+version = "0.4.21"
1472source = "registry+https://github.com/rust-lang/crates.io-index"
1315source = "registry+https://github.com/rust-lang/crates.io-index"
1473−checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
1316+checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
1474dependencies = [
1317dependencies = [
1318+ "crossbeam-deque",
1475 "globset",
1319 "globset",
1476− "lazy_static",
1477 "log",
1320 "log",
1478 "memchr",
1321 "memchr",
1479− "regex",
1322+ "regex-automata",
1480 "same-file",
1323 "same-file",
1481− "thread_local",
1482 "walkdir",
1324 "walkdir",
1483 "winapi-util",
1325 "winapi-util",
1484]
1326]
@@ -1489,7 +1331,7 @@ version = "0.1.5"
1489source = "registry+https://github.com/rust-lang/crates.io-index"
1331source = "registry+https://github.com/rust-lang/crates.io-index"
1490checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
1332checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
1491dependencies = [
1333dependencies = [
1492− "ahash 0.8.3",
1334+ "ahash",
1493 "hashbrown 0.12.3",
1335 "hashbrown 0.12.3",
1494]
1336]
1495
1337
@@ -1500,34 +1342,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1500checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
1342checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
1501dependencies = [
1343dependencies = [
1502 "equivalent",
1344 "equivalent",
1503− "hashbrown 0.14.0",
1345+ "hashbrown 0.14.3",
1504]
1346]
1505
1347
1506[[package]]
1348[[package]]
1507name = "indoc"
1349name = "indoc"
1508−version = "2.0.3"
1350+version = "2.0.4"
1509source = "registry+https://github.com/rust-lang/crates.io-index"
1351source = "registry+https://github.com/rust-lang/crates.io-index"
1510−checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
1352+checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
1511
1353
1512[[package]]
1354[[package]]
1513−name = "io-close"
1355+name = "is-docker"
1514−version = "0.3.7"
1356+version = "0.2.0"
1515source = "registry+https://github.com/rust-lang/crates.io-index"
1357source = "registry+https://github.com/rust-lang/crates.io-index"
1516−checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
1358+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
1517dependencies = [
1359dependencies = [
1518− "libc",
1360+ "once_cell",
1519− "winapi",
1520]
1361]
1521
1362
1522[[package]]
1363[[package]]
1523−name = "io-lifetimes"
1364+name = "is-wsl"
1524−version = "1.0.10"
1365+version = "0.4.0"
1525source = "registry+https://github.com/rust-lang/crates.io-index"
1366source = "registry+https://github.com/rust-lang/crates.io-index"
1526−checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
1367+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
1527dependencies = [
1368dependencies = [
1528− "hermit-abi 0.3.1",
1369+ "is-docker",
1529− "libc",
1370+ "once_cell",
1530− "windows-sys 0.48.0",
1531]
1371]
1532
1372
1533[[package]]
1373[[package]]
@@ -1545,32 +1385,17 @@ dependencies = [
1545 "wasm-bindgen",
1385 "wasm-bindgen",
1546]
1386]
1547
1387
1548−[[package]]
1549−name = "kstring"
1550−version = "2.0.0"
1551−source = "registry+https://github.com/rust-lang/crates.io-index"
1552−checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
1553−dependencies = [
1554− "static_assertions",
1555−]
1556−
1557−[[package]]
1558−name = "lazy_static"
1559−version = "1.4.0"
1560−source = "registry+https://github.com/rust-lang/crates.io-index"
1561−checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
1562−
1563[[package]]
1388[[package]]
1564name = "libc"
1389name = "libc"
1565−version = "0.2.147"
1390+version = "0.2.151"
1566source = "registry+https://github.com/rust-lang/crates.io-index"
1391source = "registry+https://github.com/rust-lang/crates.io-index"
1567−checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
1392+checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
1568
1393
1569[[package]]
1394[[package]]
1570name = "libloading"
1395name = "libloading"
1571−version = "0.8.0"
1396+version = "0.8.1"
1572source = "registry+https://github.com/rust-lang/crates.io-index"
1397source = "registry+https://github.com/rust-lang/crates.io-index"
1573−checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
1398+checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
1574dependencies = [
1399dependencies = [
1575 "cfg-if",
1400 "cfg-if",
1576 "windows-sys 0.48.0",
1401 "windows-sys 0.48.0",
@@ -1587,15 +1412,9 @@ dependencies = [
1587
1412
1588[[package]]
1413[[package]]
1589name = "linux-raw-sys"
1414name = "linux-raw-sys"
1590−version = "0.3.4"
1415+version = "0.4.11"
1591−source = "registry+https://github.com/rust-lang/crates.io-index"
1592−checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
1593−
1594−[[package]]
1595−name = "linux-raw-sys"
1596−version = "0.4.3"
1597source = "registry+https://github.com/rust-lang/crates.io-index"
1416source = "registry+https://github.com/rust-lang/crates.io-index"
1598−checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
1417+checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
1599
1418
1600[[package]]
1419[[package]]
1601name = "lock_api"
1420name = "lock_api"
@@ -1609,15 +1428,15 @@ dependencies = [
1609
1428
1610[[package]]
1429[[package]]
1611name = "log"
1430name = "log"
1612−version = "0.4.19"
1431+version = "0.4.20"
1613source = "registry+https://github.com/rust-lang/crates.io-index"
1432source = "registry+https://github.com/rust-lang/crates.io-index"
1614−checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
1433+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
1615
1434
1616[[package]]
1435[[package]]
1617name = "lsp-types"
1436name = "lsp-types"
1618−version = "0.94.0"
1437+version = "0.95.0"
1619source = "registry+https://github.com/rust-lang/crates.io-index"
1438source = "registry+https://github.com/rust-lang/crates.io-index"
1620−checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
1439+checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
1621dependencies = [
1440dependencies = [
1622 "bitflags 1.3.2",
1441 "bitflags 1.3.2",
1623 "serde",
1442 "serde",
@@ -1628,41 +1447,26 @@ dependencies = [
1628
1447
1629[[package]]
1448[[package]]
1630name = "memchr"
1449name = "memchr"
1631−version = "2.5.0"
1450+version = "2.6.3"
1632−source = "registry+https://github.com/rust-lang/crates.io-index"
1633−checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
1634−
1635−[[package]]
1636−name = "memmap2"
1637−version = "0.5.10"
1638source = "registry+https://github.com/rust-lang/crates.io-index"
1451source = "registry+https://github.com/rust-lang/crates.io-index"
1639−checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
1452+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
1640−dependencies = [
1641− "libc",
1642−]
1643
1453
1644[[package]]
1454[[package]]
1645name = "memmap2"
1455name = "memmap2"
1646−version = "0.7.1"
1456+version = "0.9.0"
1647source = "registry+https://github.com/rust-lang/crates.io-index"
1457source = "registry+https://github.com/rust-lang/crates.io-index"
1648−checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
1458+checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375"
1649dependencies = [
1459dependencies = [
1650 "libc",
1460 "libc",
1651]
1461]
1652
1462
1653[[package]]
1463[[package]]
1654−name = "minimal-lexical"
1464+name = "memoffset"
1655−version = "0.2.1"
1465+version = "0.9.0"
1656−source = "registry+https://github.com/rust-lang/crates.io-index"
1657−checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1658−
1659−[[package]]
1660−name = "miniz_oxide"
1661−version = "0.6.2"
1662source = "registry+https://github.com/rust-lang/crates.io-index"
1466source = "registry+https://github.com/rust-lang/crates.io-index"
1663−checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
1467+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
1664dependencies = [
1468dependencies = [
1665− "adler",
1469+ "autocfg",
1666]
1470]
1667
1471
1668[[package]]
1472[[package]]
@@ -1676,24 +1480,36 @@ dependencies = [
1676
1480
1677[[package]]
1481[[package]]
1678name = "mio"
1482name = "mio"
1679−version = "0.8.6"
1483+version = "0.8.9"
1680source = "registry+https://github.com/rust-lang/crates.io-index"
1484source = "registry+https://github.com/rust-lang/crates.io-index"
1681−checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
1485+checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
1682dependencies = [
1486dependencies = [
1683 "libc",
1487 "libc",
1684 "log",
1488 "log",
1685 "wasi",
1489 "wasi",
1686− "windows-sys 0.45.0",
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",
1687]
1502]
1688
1503
1689[[package]]
1504[[package]]
1690−name = "nom"
1505+name = "nucleo-matcher"
1691−version = "7.1.3"
1506+version = "0.2.0"
1692source = "registry+https://github.com/rust-lang/crates.io-index"
1507source = "registry+https://github.com/rust-lang/crates.io-index"
1693−checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1508+checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
1694dependencies = [
1509dependencies = [
1510+ "cov-mark",
1695 "memchr",
1511 "memchr",
1696− "minimal-lexical",
1512+ "unicode-segmentation",
1697]
1513]
1698
1514
1699[[package]]
1515[[package]]
@@ -1711,7 +1527,7 @@ version = "1.15.0"
1711source = "registry+https://github.com/rust-lang/crates.io-index"
1527source = "registry+https://github.com/rust-lang/crates.io-index"
1712checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
1528checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
1713dependencies = [
1529dependencies = [
1714− "hermit-abi 0.2.6",
1530+ "hermit-abi",
1715 "libc",
1531 "libc",
1716]
1532]
1717
1533
@@ -1735,9 +1551,20 @@ dependencies = [
1735
1551
1736[[package]]
1552[[package]]
1737name = "once_cell"
1553name = "once_cell"
1738−version = "1.18.0"
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"
1739source = "registry+https://github.com/rust-lang/crates.io-index"
1561source = "registry+https://github.com/rust-lang/crates.io-index"
1740−checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
1562+checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
1563+dependencies = [
1564+ "is-wsl",
1565+ "libc",
1566+ "pathdiff",
1567+]
1741
1568
1742[[package]]
1569[[package]]
1743name = "parking_lot"
1570name = "parking_lot"
@@ -1762,17 +1589,23 @@ dependencies = [
1762 "windows-sys 0.45.0",
1589 "windows-sys 0.45.0",
1763]
1590]
1764
1591
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+
1765[[package]]
1598[[package]]
1766name = "percent-encoding"
1599name = "percent-encoding"
1767−version = "2.3.0"
1600+version = "2.3.1"
1768source = "registry+https://github.com/rust-lang/crates.io-index"
1601source = "registry+https://github.com/rust-lang/crates.io-index"
1769−checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
1602+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
1770
1603
1771[[package]]
1604[[package]]
1772name = "pin-project-lite"
1605name = "pin-project-lite"
1773−version = "0.2.9"
1606+version = "0.2.12"
1774source = "registry+https://github.com/rust-lang/crates.io-index"
1607source = "registry+https://github.com/rust-lang/crates.io-index"
1775−checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
1608+checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
1776
1609
1777[[package]]
1610[[package]]
1778name = "pin-utils"
1611name = "pin-utils"
@@ -1782,18 +1615,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1782
1615
1783[[package]]
1616[[package]]
1784name = "proc-macro2"
1617name = "proc-macro2"
1785−version = "1.0.63"
1618+version = "1.0.69"
1786source = "registry+https://github.com/rust-lang/crates.io-index"
1619source = "registry+https://github.com/rust-lang/crates.io-index"
1787−checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
1620+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
1788dependencies = [
1621dependencies = [
1789 "unicode-ident",
1622 "unicode-ident",
1790]
1623]
1791
1624
1792[[package]]
1625[[package]]
1793name = "prodash"
1626name = "prodash"
1794−version = "25.0.0"
1627+version = "28.0.0"
1795source = "registry+https://github.com/rust-lang/crates.io-index"
1628source = "registry+https://github.com/rust-lang/crates.io-index"
1796−checksum = "3236ce1618b6da4c7b618e0143c4d5b5dc190f75f81c49f248221382f7e9e9ae"
1629+checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
1797
1630
1798[[package]]
1631[[package]]
1799name = "pulldown-cmark"
1632name = "pulldown-cmark"
@@ -1842,6 +1675,28 @@ dependencies = [
1842 "getrandom",
1675 "getrandom",
1843]
1676]
1844
1677
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+
1845[[package]]
1700[[package]]
1846name = "redox_syscall"
1701name = "redox_syscall"
1847version = "0.2.16"
1702version = "0.2.16"
@@ -1853,53 +1708,47 @@ dependencies = [
1853
1708
1854[[package]]
1709[[package]]
1855name = "redox_syscall"
1710name = "redox_syscall"
1856−version = "0.3.5"
1711+version = "0.4.1"
1857source = "registry+https://github.com/rust-lang/crates.io-index"
1712source = "registry+https://github.com/rust-lang/crates.io-index"
1858−checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
1713+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
1859dependencies = [
1714dependencies = [
1860 "bitflags 1.3.2",
1715 "bitflags 1.3.2",
1861]
1716]
1862
1717
1863[[package]]
1718[[package]]
1864name = "regex"
1719name = "regex"
1865−version = "1.9.1"
1720+version = "1.10.2"
1866source = "registry+https://github.com/rust-lang/crates.io-index"
1721source = "registry+https://github.com/rust-lang/crates.io-index"
1867−checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
1722+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
1868dependencies = [
1723dependencies = [
1869− "aho-corasick 1.0.2",
1724+ "aho-corasick",
1870 "memchr",
1725 "memchr",
1871 "regex-automata",
1726 "regex-automata",
1872− "regex-syntax 0.7.3",
1727+ "regex-syntax",
1873]
1728]
1874
1729
1875[[package]]
1730[[package]]
1876name = "regex-automata"
1731name = "regex-automata"
1877−version = "0.3.2"
1732+version = "0.4.3"
1878source = "registry+https://github.com/rust-lang/crates.io-index"
1733source = "registry+https://github.com/rust-lang/crates.io-index"
1879−checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
1734+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
1880dependencies = [
1735dependencies = [
1881− "aho-corasick 1.0.2",
1736+ "aho-corasick",
1882 "memchr",
1737 "memchr",
1883− "regex-syntax 0.7.3",
1738+ "regex-syntax",
1884]
1739]
1885
1740
1886[[package]]
1741[[package]]
1887name = "regex-syntax"
1742name = "regex-syntax"
1888−version = "0.6.29"
1743+version = "0.8.2"
1889−source = "registry+https://github.com/rust-lang/crates.io-index"
1890−checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
1891−
1892−[[package]]
1893−name = "regex-syntax"
1894−version = "0.7.3"
1895source = "registry+https://github.com/rust-lang/crates.io-index"
1744source = "registry+https://github.com/rust-lang/crates.io-index"
1896−checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
1745+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
1897
1746
1898[[package]]
1747[[package]]
1899name = "ropey"
1748name = "ropey"
1900−version = "1.6.0"
1749+version = "1.6.1"
1901source = "registry+https://github.com/rust-lang/crates.io-index"
1750source = "registry+https://github.com/rust-lang/crates.io-index"
1902−checksum = "53ce7a2c43a32e50d666e33c5a80251b31147bb4b49024bcab11fb6f20c671ed"
1751+checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
1903dependencies = [
1752dependencies = [
1904 "smallvec",
1753 "smallvec",
1905 "str_indices",
1754 "str_indices",
@@ -1913,29 +1762,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
1913
1762
1914[[package]]
1763[[package]]
1915name = "rustix"
1764name = "rustix"
1916−version = "0.37.15"
1765+version = "0.38.28"
1917source = "registry+https://github.com/rust-lang/crates.io-index"
1766source = "registry+https://github.com/rust-lang/crates.io-index"
1918−checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
1767+checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
1919dependencies = [
1768dependencies = [
1920− "bitflags 1.3.2",
1769+ "bitflags 2.4.1",
1921 "errno",
1770 "errno",
1922− "io-lifetimes",
1923 "libc",
1771 "libc",
1924− "linux-raw-sys 0.3.4",
1772+ "linux-raw-sys",
1925− "windows-sys 0.48.0",
1773+ "windows-sys 0.52.0",
1926−]
1927−
1928−[[package]]
1929−name = "rustix"
1930−version = "0.38.4"
1931−source = "registry+https://github.com/rust-lang/crates.io-index"
1932−checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
1933−dependencies = [
1934− "bitflags 2.3.3",
1935− "errno",
1936− "libc",
1937− "linux-raw-sys 0.4.3",
1938− "windows-sys 0.48.0",
1939]
1774]
1940
1775
1941[[package]]
1776[[package]]
@@ -1967,29 +1802,29 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
1967
1802
1968[[package]]
1803[[package]]
1969name = "serde"
1804name = "serde"
1970−version = "1.0.175"
1805+version = "1.0.193"
1971source = "registry+https://github.com/rust-lang/crates.io-index"
1806source = "registry+https://github.com/rust-lang/crates.io-index"
1972−checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
1807+checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
1973dependencies = [
1808dependencies = [
1974 "serde_derive",
1809 "serde_derive",
1975]
1810]
1976
1811
1977[[package]]
1812[[package]]
1978name = "serde_derive"
1813name = "serde_derive"
1979−version = "1.0.175"
1814+version = "1.0.193"
1980source = "registry+https://github.com/rust-lang/crates.io-index"
1815source = "registry+https://github.com/rust-lang/crates.io-index"
1981−checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
1816+checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
1982dependencies = [
1817dependencies = [
1983 "proc-macro2",
1818 "proc-macro2",
1984 "quote",
1819 "quote",
1985− "syn 2.0.25",
1820+ "syn 2.0.38",
1986]
1821]
1987
1822
1988[[package]]
1823[[package]]
1989name = "serde_json"
1824name = "serde_json"
1990−version = "1.0.103"
1825+version = "1.0.108"
1991source = "registry+https://github.com/rust-lang/crates.io-index"
1826source = "registry+https://github.com/rust-lang/crates.io-index"
1992−checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
1827+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
1993dependencies = [
1828dependencies = [
1994 "itoa",
1829 "itoa",
1995 "ryu",
1830 "ryu",
@@ -2004,7 +1839,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
2004dependencies = [
1839dependencies = [
2005 "proc-macro2",
1840 "proc-macro2",
2006 "quote",
1841 "quote",
2007− "syn 2.0.25",
1842+ "syn 2.0.38",
2008]
1843]
2009
1844
2010[[package]]
1845[[package]]
@@ -2075,18 +1910,18 @@ dependencies = [
2075
1910
2076[[package]]
1911[[package]]
2077name = "slotmap"
1912name = "slotmap"
2078−version = "1.0.6"
1913+version = "1.0.7"
2079source = "registry+https://github.com/rust-lang/crates.io-index"
1914source = "registry+https://github.com/rust-lang/crates.io-index"
2080−checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
1915+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
2081dependencies = [
1916dependencies = [
2082 "version_check",
1917 "version_check",
2083]
1918]
2084
1919
2085[[package]]
1920[[package]]
2086name = "smallvec"
1921name = "smallvec"
2087−version = "1.11.0"
1922+version = "1.11.2"
2088source = "registry+https://github.com/rust-lang/crates.io-index"
1923source = "registry+https://github.com/rust-lang/crates.io-index"
2089−checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
1924+checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
2090
1925
2091[[package]]
1926[[package]]
2092name = "smartstring"
1927name = "smartstring"
@@ -2107,12 +1942,12 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
2107
1942
2108[[package]]
1943[[package]]
2109name = "socket2"
1944name = "socket2"
2110−version = "0.4.9"
1945+version = "0.5.5"
2111source = "registry+https://github.com/rust-lang/crates.io-index"
1946source = "registry+https://github.com/rust-lang/crates.io-index"
2112−checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
1947+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
2113dependencies = [
1948dependencies = [
2114 "libc",
1949 "libc",
2115− "winapi",
1950+ "windows-sys 0.48.0",
2116]
1951]
2117
1952
2118[[package]]
1953[[package]]
@@ -2121,12 +1956,6 @@ version = "1.1.0"
2121source = "registry+https://github.com/rust-lang/crates.io-index"
1956source = "registry+https://github.com/rust-lang/crates.io-index"
2122checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
1957checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
2123
1958
2124−[[package]]
2125−name = "str-buf"
2126−version = "1.0.6"
2127−source = "registry+https://github.com/rust-lang/crates.io-index"
2128−checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
2129−
2130[[package]]
1959[[package]]
2131name = "str_indices"
1960name = "str_indices"
2132version = "0.4.1"
1961version = "0.4.1"
@@ -2146,9 +1975,9 @@ dependencies = [
2146
1975
2147[[package]]
1976[[package]]
2148name = "syn"
1977name = "syn"
2149−version = "2.0.25"
1978+version = "2.0.38"
2150source = "registry+https://github.com/rust-lang/crates.io-index"
1979source = "registry+https://github.com/rust-lang/crates.io-index"
2151−checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
1980+checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
2152dependencies = [
1981dependencies = [
2153 "proc-macro2",
1982 "proc-macro2",
2154 "quote",
1983 "quote",
@@ -2157,14 +1986,14 @@ dependencies = [
2157
1986
2158[[package]]
1987[[package]]
2159name = "tempfile"
1988name = "tempfile"
2160−version = "3.7.0"
1989+version = "3.8.1"
2161source = "registry+https://github.com/rust-lang/crates.io-index"
1990source = "registry+https://github.com/rust-lang/crates.io-index"
2162−checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998"
1991+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
2163dependencies = [
1992dependencies = [
2164 "cfg-if",
1993 "cfg-if",
2165 "fastrand",
1994 "fastrand",
2166− "redox_syscall 0.3.5",
1995+ "redox_syscall 0.4.1",
2167− "rustix 0.38.4",
1996+ "rustix",
2168 "windows-sys 0.48.0",
1997 "windows-sys 0.48.0",
2169]
1998]
2170
1999
@@ -2199,32 +2028,22 @@ dependencies = [
2199
2028
2200[[package]]
2029[[package]]
2201name = "thiserror"
2030name = "thiserror"
2202−version = "1.0.44"
2031+version = "1.0.52"
2203source = "registry+https://github.com/rust-lang/crates.io-index"
2032source = "registry+https://github.com/rust-lang/crates.io-index"
2204−checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
2033+checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d"
2205dependencies = [
2034dependencies = [
2206 "thiserror-impl",
2035 "thiserror-impl",
2207]
2036]
2208
2037
2209[[package]]
2038[[package]]
2210name = "thiserror-impl"
2039name = "thiserror-impl"
2211−version = "1.0.44"
2040+version = "1.0.52"
2212source = "registry+https://github.com/rust-lang/crates.io-index"
2041source = "registry+https://github.com/rust-lang/crates.io-index"
2213−checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
2042+checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
2214dependencies = [
2043dependencies = [
2215 "proc-macro2",
2044 "proc-macro2",
2216 "quote",
2045 "quote",
2217− "syn 2.0.25",
2046+ "syn 2.0.38",
2218−]
2219−
2220−[[package]]
2221−name = "thread_local"
2222−version = "1.1.7"
2223−source = "registry+https://github.com/rust-lang/crates.io-index"
2224−checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
2225−dependencies = [
2226− "cfg-if",
2227− "once_cell",
2228]
2047]
2229
2048
2230[[package]]
2049[[package]]
@@ -2238,9 +2057,9 @@ dependencies = [
2238
2057
2239[[package]]
2058[[package]]
2240name = "time"
2059name = "time"
2241−version = "0.3.20"
2060+version = "0.3.23"
2242source = "registry+https://github.com/rust-lang/crates.io-index"
2061source = "registry+https://github.com/rust-lang/crates.io-index"
2243−checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
2062+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
2244dependencies = [
2063dependencies = [
2245 "itoa",
2064 "itoa",
2246 "libc",
2065 "libc",
@@ -2252,15 +2071,15 @@ dependencies = [
2252
2071
2253[[package]]
2072[[package]]
2254name = "time-core"
2073name = "time-core"
2255−version = "0.1.0"
2074+version = "0.1.1"
2256source = "registry+https://github.com/rust-lang/crates.io-index"
2075source = "registry+https://github.com/rust-lang/crates.io-index"
2257−checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
2076+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
2258
2077
2259[[package]]
2078[[package]]
2260name = "time-macros"
2079name = "time-macros"
2261−version = "0.2.8"
2080+version = "0.2.10"
2262source = "registry+https://github.com/rust-lang/crates.io-index"
2081source = "registry+https://github.com/rust-lang/crates.io-index"
2263−checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
2082+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
2264dependencies = [
2083dependencies = [
2265 "time-core",
2084 "time-core",
2266]
2085]
@@ -2282,11 +2101,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2282
2101
2283[[package]]
2102[[package]]
2284name = "tokio"
2103name = "tokio"
2285−version = "1.29.1"
2104+version = "1.35.1"
2286source = "registry+https://github.com/rust-lang/crates.io-index"
2105source = "registry+https://github.com/rust-lang/crates.io-index"
2287−checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
2106+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
2288dependencies = [
2107dependencies = [
2289− "autocfg",
2290 "backtrace",
2108 "backtrace",
2291 "bytes",
2109 "bytes",
2292 "libc",
2110 "libc",
@@ -2302,13 +2120,13 @@ dependencies = [
2302
2120
2303[[package]]
2121[[package]]
2304name = "tokio-macros"
2122name = "tokio-macros"
2305−version = "2.1.0"
2123+version = "2.2.0"
2306source = "registry+https://github.com/rust-lang/crates.io-index"
2124source = "registry+https://github.com/rust-lang/crates.io-index"
2307−checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
2125+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
2308dependencies = [
2126dependencies = [
2309 "proc-macro2",
2127 "proc-macro2",
2310 "quote",
2128 "quote",
2311− "syn 2.0.25",
2129+ "syn 2.0.38",
2312]
2130]
2313
2131
2314[[package]]
2132[[package]]
@@ -2353,14 +2171,13 @@ dependencies = [
2353 "serde",
2171 "serde",
2354 "serde_spanned",
2172 "serde_spanned",
2355 "toml_datetime",
2173 "toml_datetime",
2356− "winnow",
2174+ "winnow 0.4.6",
2357]
2175]
2358
2176
2359[[package]]
2177[[package]]
2360name = "tree-sitter"
2178name = "tree-sitter"
2361version = "0.20.10"
2179version = "0.20.10"
2362−source = "registry+https://github.com/rust-lang/crates.io-index"
2180+source = "git+https://github.com/tree-sitter/tree-sitter?rev=ab09ae20d640711174b8da8a654f6b3dec93da1a#ab09ae20d640711174b8da8a654f6b3dec93da1a"
2363−checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d"
2364dependencies = [
2181dependencies = [
2365 "cc",
2182 "cc",
2366 "regex",
2183 "regex",
@@ -2401,13 +2218,9 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
2401
2218
2402[[package]]
2219[[package]]
2403name = "unicode-linebreak"
2220name = "unicode-linebreak"
2404−version = "0.1.4"
2221+version = "0.1.5"
2405source = "registry+https://github.com/rust-lang/crates.io-index"
2222source = "registry+https://github.com/rust-lang/crates.io-index"
2406−checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
2223+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
2407−dependencies = [
2408− "hashbrown 0.12.3",
2409− "regex",
2410−]
2411
2224
2412[[package]]
2225[[package]]
2413name = "unicode-normalization"
2226name = "unicode-normalization"
@@ -2426,15 +2239,15 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
2426
2239
2427[[package]]
2240[[package]]
2428name = "unicode-width"
2241name = "unicode-width"
2429−version = "0.1.10"
2242+version = "0.1.11"
2430source = "registry+https://github.com/rust-lang/crates.io-index"
2243source = "registry+https://github.com/rust-lang/crates.io-index"
2431−checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
2244+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
2432
2245
2433[[package]]
2246[[package]]
2434name = "url"
2247name = "url"
2435−version = "2.4.0"
2248+version = "2.5.0"
2436source = "registry+https://github.com/rust-lang/crates.io-index"
2249source = "registry+https://github.com/rust-lang/crates.io-index"
2437−checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
2250+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
2438dependencies = [
2251dependencies = [
2439 "form_urlencoded",
2252 "form_urlencoded",
2440 "idna",
2253 "idna",
@@ -2450,9 +2263,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
2450
2263
2451[[package]]
2264[[package]]
2452name = "walkdir"
2265name = "walkdir"
2453−version = "2.3.3"
2266+version = "2.4.0"
2454source = "registry+https://github.com/rust-lang/crates.io-index"
2267source = "registry+https://github.com/rust-lang/crates.io-index"
2455−checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
2268+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
2456dependencies = [
2269dependencies = [
2457 "same-file",
2270 "same-file",
2458 "winapi-util",
2271 "winapi-util",
@@ -2520,13 +2333,15 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
2520
2333
2521[[package]]
2334[[package]]
2522name = "which"
2335name = "which"
2523−version = "4.4.0"
2336+version = "5.0.0"
2524source = "registry+https://github.com/rust-lang/crates.io-index"
2337source = "registry+https://github.com/rust-lang/crates.io-index"
2525−checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
2338+checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14"
2526dependencies = [
2339dependencies = [
2527 "either",
2340 "either",
2528− "libc",
2341+ "home",
2529 "once_cell",
2342 "once_cell",
2343+ "rustix",
2344+ "windows-sys 0.48.0",
2530]
2345]
2531
2346
2532[[package]]
2347[[package]]
@@ -2569,6 +2384,25 @@ dependencies = [
2569 "windows-targets 0.48.0",
2384 "windows-targets 0.48.0",
2570]
2385]
2571
2386
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+
2572[[package]]
2406[[package]]
2573name = "windows-sys"
2407name = "windows-sys"
2574version = "0.45.0"
2408version = "0.45.0"
@@ -2587,6 +2421,15 @@ dependencies = [
2587 "windows-targets 0.48.0",
2421 "windows-targets 0.48.0",
2588]
2422]
2589
2423
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+
2590[[package]]
2433[[package]]
2591name = "windows-targets"
2434name = "windows-targets"
2592version = "0.42.2"
2435version = "0.42.2"
@@ -2617,6 +2460,21 @@ dependencies = [
2617 "windows_x86_64_msvc 0.48.0",
2460 "windows_x86_64_msvc 0.48.0",
2618]
2461]
2619
2462
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+
2620[[package]]
2478[[package]]
2621name = "windows_aarch64_gnullvm"
2479name = "windows_aarch64_gnullvm"
2622version = "0.42.2"
2480version = "0.42.2"
@@ -2629,6 +2487,12 @@ version = "0.48.0"
2629source = "registry+https://github.com/rust-lang/crates.io-index"
2487source = "registry+https://github.com/rust-lang/crates.io-index"
2630checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
2488checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
2631
2489
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+
2632[[package]]
2496[[package]]
2633name = "windows_aarch64_msvc"
2497name = "windows_aarch64_msvc"
2634version = "0.42.2"
2498version = "0.42.2"
@@ -2641,6 +2505,12 @@ version = "0.48.0"
2641source = "registry+https://github.com/rust-lang/crates.io-index"
2505source = "registry+https://github.com/rust-lang/crates.io-index"
2642checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
2506checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
2643
2507
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+
2644[[package]]
2514[[package]]
2645name = "windows_i686_gnu"
2515name = "windows_i686_gnu"
2646version = "0.42.2"
2516version = "0.42.2"
@@ -2653,6 +2523,12 @@ version = "0.48.0"
2653source = "registry+https://github.com/rust-lang/crates.io-index"
2523source = "registry+https://github.com/rust-lang/crates.io-index"
2654checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
2524checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
2655
2525
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+
2656[[package]]
2532[[package]]
2657name = "windows_i686_msvc"
2533name = "windows_i686_msvc"
2658version = "0.42.2"
2534version = "0.42.2"
@@ -2665,6 +2541,12 @@ version = "0.48.0"
2665source = "registry+https://github.com/rust-lang/crates.io-index"
2541source = "registry+https://github.com/rust-lang/crates.io-index"
2666checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
2542checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
2667
2543
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+
2668[[package]]
2550[[package]]
2669name = "windows_x86_64_gnu"
2551name = "windows_x86_64_gnu"
2670version = "0.42.2"
2552version = "0.42.2"
@@ -2677,6 +2559,12 @@ version = "0.48.0"
2677source = "registry+https://github.com/rust-lang/crates.io-index"
2559source = "registry+https://github.com/rust-lang/crates.io-index"
2678checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
2560checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
2679
2561
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+
2680[[package]]
2568[[package]]
2681name = "windows_x86_64_gnullvm"
2569name = "windows_x86_64_gnullvm"
2682version = "0.42.2"
2570version = "0.42.2"
@@ -2689,6 +2577,12 @@ version = "0.48.0"
2689source = "registry+https://github.com/rust-lang/crates.io-index"
2577source = "registry+https://github.com/rust-lang/crates.io-index"
2690checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
2578checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
2691
2579
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+
2692[[package]]
2586[[package]]
2693name = "windows_x86_64_msvc"
2587name = "windows_x86_64_msvc"
2694version = "0.42.2"
2588version = "0.42.2"
@@ -2701,6 +2595,12 @@ version = "0.48.0"
2701source = "registry+https://github.com/rust-lang/crates.io-index"
2595source = "registry+https://github.com/rust-lang/crates.io-index"
2702checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
2596checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
2703
2597
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+
2704[[package]]
2604[[package]]
2705name = "winnow"
2605name = "winnow"
2706version = "0.4.6"
2606version = "0.4.6"
@@ -2710,9 +2610,18 @@ dependencies = [
2710 "memchr",
2610 "memchr",
2711]
2611]
2712
2612
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+
2713[[package]]
2622[[package]]
2714name = "xtask"
2623name = "xtask"
2715−version = "0.6.0"
2624+version = "23.10.0"
2716dependencies = [
2625dependencies = [
2717 "helix-core",
2626 "helix-core",
2718 "helix-loader",
2627 "helix-loader",
@@ -2720,3 +2629,23 @@ dependencies = [
2720 "helix-view",
2629 "helix-view",
2721 "toml",
2630 "toml",
2722]
2631]
2632+
2633+[[package]]
2634+name = "zerocopy"
2635+version = "0.7.31"
2636+source = "registry+https://github.com/rust-lang/crates.io-index"
2637+checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d"
2638+dependencies = [
2639+ "zerocopy-derive",
2640+]
2641+
2642+[[package]]
2643+name = "zerocopy-derive"
2644+version = "0.7.31"
2645+source = "registry+https://github.com/rust-lang/crates.io-index"
2646+checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
2647+dependencies = [
2648+ "proc-macro2",
2649+ "quote",
2650+ "syn 2.0.38",
2651+]
Cargo.toml+16
@@ -1,10 +1,12 @@
1[workspace]
1[workspace]
2+resolver = "2"
2members = [
3members = [
3 "helix-core",
4 "helix-core",
4 "helix-view",
5 "helix-view",
5 "helix-term",
6 "helix-term",
6 "helix-tui",
7 "helix-tui",
7 "helix-lsp",
8 "helix-lsp",
9+ "helix-event",
8 "helix-dap",
10 "helix-dap",
9 "helix-loader",
11 "helix-loader",
10 "helix-vcs",
12 "helix-vcs",
@@ -32,3 +34,17 @@ inherits = "test"
32package.helix-core.opt-level = 2
34package.helix-core.opt-level = 2
33package.helix-tui.opt-level = 2
35package.helix-tui.opt-level = 2
34package.helix-term.opt-level = 2
36package.helix-term.opt-level = 2
37+
38+[workspace.dependencies]
39+tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
40+nucleo = "0.2.0"
41+
42+[workspace.package]
43+version = "23.10.0"
44+edition = "2021"
45+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
46+categories = ["editor"]
47+repository = "https://github.com/helix-editor/helix"
48+homepage = "https://helix-editor.com"
49+license = "MPL-2.0"
50+rust-version = "1.70"
README.md+1−1
@@ -18,7 +18,7 @@
18
18
19![Screenshot](./screenshot.png)
19![Screenshot](./screenshot.png)
20
20
21−A Kakoune / Neovim inspired editor, written in Rust.
21+A [Kakoune](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.
22
22
23The editing model is very heavily based on Kakoune; during development I found
23The editing model is very heavily based on Kakoune; during development I found
24myself agreeing with most of Kakoune's design decisions.
24myself agreeing with most of Kakoune's design decisions.
VERSIONdeleted−1
@@ -1 +0,0 @@
1−23.05
book/book.toml+3
@@ -11,3 +11,6 @@ preferred-dark-theme = "colibri"
11git-repository-url = "https://github.com/helix-editor/helix"
11git-repository-url = "https://github.com/helix-editor/helix"
12edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
12edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
13additional-css = ["custom.css"]
13additional-css = ["custom.css"]
14+
15+[output.html.search]
16+use-boolean-and = true
book/custom.css+1−1
@@ -164,7 +164,7 @@ code.hljs {
164 --searchresults-header-fg: #5f5f71;
164 --searchresults-header-fg: #5f5f71;
165 --searchresults-border-color: #5c5c68;
165 --searchresults-border-color: #5c5c68;
166 --searchresults-li-bg: #242430;
166 --searchresults-li-bg: #242430;
167− --search-mark-bg: #acff5;
167+ --search-mark-bg: #a2cff5;
168}
168}
169
169
170.colibri .content .header {
170.colibri .content .header {
book/src/configuration.md+37−10
@@ -51,7 +51,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
51| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
51| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
52| `auto-format` | Enable automatic formatting on save | `true` |
52| `auto-format` | Enable automatic formatting on save | `true` |
53| `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` |
53| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
54−| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
54+| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
55| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
55| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
56| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
56| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
57| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
57| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
@@ -64,6 +64,9 @@ Its settings will be merged with the configuration directory `config.toml` and t
64| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
64| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
65| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
65| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
66| `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` |
66| `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`
67
70
68### `[editor.statusline]` Section
71### `[editor.statusline]` Section
69
72
@@ -89,7 +92,7 @@ The `[editor.statusline]` key takes the following sub-keys:
89
92
90| Key | Description | Default |
93| Key | Description | Default |
91| --- | --- | --- |
94| --- | --- | --- |
92−| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "file-modification-indicator"]` |
95+| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
93| `center` | A list of elements aligned to the middle of the statusline | `[]` |
96| `center` | A list of elements aligned to the middle of the statusline | `[]` |
94| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
97| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
95| `separator` | The character used to separate elements in the statusline | `"│"` |
98| `separator` | The character used to separate elements in the statusline | `"│"` |
@@ -108,6 +111,7 @@ The following statusline elements can be configured:
108| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
111| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
109| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
112| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
110| `file-line-ending` | The file line endings (CRLF or LF) |
113| `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 |
111| `total-line-numbers` | The total line numbers of the opened file |
115| `total-line-numbers` | The total line numbers of the opened file |
112| `file-type` | The type of the opened file |
116| `file-type` | The type of the opened file |
113| `diagnostics` | The number of warnings and/or errors |
117| `diagnostics` | The number of warnings and/or errors |
@@ -164,16 +168,31 @@ All git related options are only enabled in a git repository.
164
168
165| Key | Description | Default |
169| Key | Description | Default |
166|--|--|---------|
170|--|--|---------|
167−|`hidden` | Enables ignoring hidden files | true
171+|`hidden` | Enables ignoring hidden files | `true`
168−|`follow-symlinks` | Follow symlinks instead of ignoring them | true
172+|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
169−|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | true
173+|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true`
170−|`parents` | Enables reading ignore files from parent directories | true
174+|`parents` | Enables reading ignore files from parent directories | `true`
171−|`ignore` | Enables reading `.ignore` files | true
175+|`ignore` | Enables reading `.ignore` files | `true`
172−|`git-ignore` | Enables reading `.gitignore` files | true
176+|`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
177+|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
174−|`git-exclude` | Enables reading `.git/info/exclude` files | true
178+|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
175|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
179|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
176
180
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+
177### `[editor.auto-pairs]` Section
196### `[editor.auto-pairs]` Section
178
197
179Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
198Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
@@ -347,3 +366,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
347max-indent-retain = 0
366max-indent-retain = 0
348wrap-indicator = "" # set wrap-indicator to "" to hide it
367wrap-indicator = "" # set wrap-indicator to "" to hide it
349```
368```
369+
370+### `[editor.smart-tab]` Section
371+
372+
373+| Key | Description | Default |
374+|------------|-------------|---------|
375+| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
376+| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
book/src/generated/lang-support.md+34−15
@@ -1,8 +1,9 @@
1| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
1| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
2| --- | --- | --- | --- | --- |
2| --- | --- | --- | --- | --- |
3+| agda | ✓ | | | |
3| astro | ✓ | | | |
4| astro | ✓ | | | |
4| awk | ✓ | ✓ | | `awk-language-server` |
5| awk | ✓ | ✓ | | `awk-language-server` |
5−| bash | ✓ | | ✓ | `bash-language-server` |
6+| bash | ✓ | ✓ | ✓ | `bash-language-server` |
6| bass | ✓ | | | `bass` |
7| bass | ✓ | | | `bass` |
7| beancount | ✓ | | | |
8| beancount | ✓ | | | |
8| bibtex | ✓ | | | `texlab` |
9| bibtex | ✓ | | | `texlab` |
@@ -10,13 +11,13 @@
10| blueprint | ✓ | | | `blueprint-compiler` |
11| blueprint | ✓ | | | `blueprint-compiler` |
11| c | ✓ | ✓ | ✓ | `clangd` |
12| c | ✓ | ✓ | ✓ | `clangd` |
12| c-sharp | ✓ | ✓ | | `OmniSharp` |
13| c-sharp | ✓ | ✓ | | `OmniSharp` |
13−| cabal | | | | |
14+| cabal | | | | `haskell-language-server-wrapper` |
14| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
15| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
15| capnp | ✓ | | ✓ | |
16| capnp | ✓ | | ✓ | |
16| clojure | ✓ | | | `clojure-lsp` |
17| clojure | ✓ | | | `clojure-lsp` |
17| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
18| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
18| comment | ✓ | | | |
19| comment | ✓ | | | |
19−| common-lisp | ✓ | | | `cl-lsp` |
20+| common-lisp | ✓ | | ✓ | `cl-lsp` |
20| cpon | ✓ | | ✓ | |
21| cpon | ✓ | | ✓ | |
21| cpp | ✓ | ✓ | ✓ | `clangd` |
22| cpp | ✓ | ✓ | ✓ | `clangd` |
22| crystal | ✓ | ✓ | | `crystalline` |
23| crystal | ✓ | ✓ | | `crystalline` |
@@ -24,6 +25,7 @@
24| cue | ✓ | | | `cuelsp` |
25| cue | ✓ | | | `cuelsp` |
25| d | ✓ | ✓ | ✓ | `serve-d` |
26| d | ✓ | ✓ | ✓ | `serve-d` |
26| dart | ✓ | | ✓ | `dart` |
27| dart | ✓ | | ✓ | `dart` |
28+| dbml | ✓ | | | |
27| devicetree | ✓ | | | |
29| devicetree | ✓ | | | |
28| dhall | ✓ | ✓ | | `dhall-lsp-server` |
30| dhall | ✓ | ✓ | | `dhall-lsp-server` |
29| diff | ✓ | | | |
31| diff | ✓ | | | |
@@ -44,7 +46,9 @@
44| forth | ✓ | | | `forth-lsp` |
46| forth | ✓ | | | `forth-lsp` |
45| fortran | ✓ | | ✓ | `fortls` |
47| fortran | ✓ | | ✓ | `fortls` |
46| fsharp | ✓ | | | `fsautocomplete` |
48| fsharp | ✓ | | | `fsautocomplete` |
49+| gas | ✓ | ✓ | | |
47| gdscript | ✓ | ✓ | ✓ | |
50| gdscript | ✓ | ✓ | ✓ | |
51+| gemini | ✓ | | | |
48| git-attributes | ✓ | | | |
52| git-attributes | ✓ | | | |
49| git-commit | ✓ | ✓ | | |
53| git-commit | ✓ | ✓ | | |
50| git-config | ✓ | | | |
54| git-config | ✓ | | | |
@@ -52,12 +56,13 @@
52| git-rebase | ✓ | | | |
56| git-rebase | ✓ | | | |
53| gleam | ✓ | ✓ | | `gleam` |
57| gleam | ✓ | ✓ | | `gleam` |
54| glsl | ✓ | ✓ | ✓ | |
58| glsl | ✓ | ✓ | ✓ | |
55−| go | ✓ | ✓ | ✓ | `gopls` |
59+| gn | ✓ | | | |
60+| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
56| godot-resource | ✓ | | | |
61| godot-resource | ✓ | | | |
57| gomod | ✓ | | | `gopls` |
62| gomod | ✓ | | | `gopls` |
58| gotmpl | ✓ | | | `gopls` |
63| gotmpl | ✓ | | | `gopls` |
59| gowork | ✓ | | | `gopls` |
64| gowork | ✓ | | | `gopls` |
60−| graphql | ✓ | | | |
65+| graphql | ✓ | | | `graphql-lsp` |
61| hare | ✓ | | | |
66| hare | ✓ | | | |
62| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
67| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
63| haskell-persistent | ✓ | | | |
68| haskell-persistent | ✓ | | | |
@@ -69,15 +74,18 @@
69| idris | | | | `idris2-lsp` |
74| idris | | | | `idris2-lsp` |
70| iex | ✓ | | | |
75| iex | ✓ | | | |
71| ini | ✓ | | | |
76| ini | ✓ | | | |
77+| janet | ✓ | | | |
72−| java | ✓ | ✓ | | `jdtls` |
78+| java | ✓ | ✓ | ✓ | `jdtls` |
73| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
79| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
80+| jinja | ✓ | | | |
74| jsdoc | ✓ | | | |
81| jsdoc | ✓ | | | |
75| json | ✓ | | ✓ | `vscode-json-language-server` |
82| json | ✓ | | ✓ | `vscode-json-language-server` |
83+| json5 | ✓ | | | |
76| jsonnet | ✓ | | | `jsonnet-language-server` |
84| jsonnet | ✓ | | | `jsonnet-language-server` |
77| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
85| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
78| julia | ✓ | ✓ | ✓ | `julia` |
86| julia | ✓ | ✓ | ✓ | `julia` |
79| just | ✓ | ✓ | ✓ | |
87| just | ✓ | ✓ | ✓ | |
80−| kdl | ✓ | | | |
88+| kdl | ✓ | ✓ | ✓ | |
81| kotlin | ✓ | | | `kotlin-language-server` |
89| kotlin | ✓ | | | `kotlin-language-server` |
82| latex | ✓ | ✓ | | `texlab` |
90| latex | ✓ | ✓ | | `texlab` |
83| lean | ✓ | | | `lean` |
91| lean | ✓ | | | `lean` |
@@ -85,6 +93,8 @@
85| llvm | ✓ | ✓ | ✓ | |
93| llvm | ✓ | ✓ | ✓ | |
86| llvm-mir | ✓ | ✓ | ✓ | |
94| llvm-mir | ✓ | ✓ | ✓ | |
87| llvm-mir-yaml | ✓ | | ✓ | |
95| llvm-mir-yaml | ✓ | | ✓ | |
96+| log | ✓ | | | |
97+| lpf | ✓ | | | |
88| lua | ✓ | ✓ | ✓ | `lua-language-server` |
98| lua | ✓ | ✓ | ✓ | `lua-language-server` |
89| make | ✓ | | | |
99| make | ✓ | | | |
90| markdoc | ✓ | | | `markdoc-ls` |
100| markdoc | ✓ | | | `markdoc-ls` |
@@ -99,7 +109,8 @@
99| nickel | ✓ | | ✓ | `nls` |
109| nickel | ✓ | | ✓ | `nls` |
100| nim | ✓ | ✓ | ✓ | `nimlangserver` |
110| nim | ✓ | ✓ | ✓ | `nimlangserver` |
101| nix | ✓ | | | `nil` |
111| nix | ✓ | | | `nil` |
102−| nu | ✓ | | | |
112+| nu | ✓ | | | `nu` |
113+| nunjucks | ✓ | | | |
103| ocaml | ✓ | | ✓ | `ocamllsp` |
114| ocaml | ✓ | | ✓ | `ocamllsp` |
104| ocaml-interface | ✓ | | | `ocamllsp` |
115| ocaml-interface | ✓ | | | `ocamllsp` |
105| odin | ✓ | | ✓ | `ols` |
116| odin | ✓ | | ✓ | `ols` |
@@ -109,19 +120,20 @@
109| pascal | ✓ | ✓ | | `pasls` |
120| pascal | ✓ | ✓ | | `pasls` |
110| passwd | ✓ | | | |
121| passwd | ✓ | | | |
111| pem | ✓ | | | |
122| pem | ✓ | | | |
112−| perl | ✓ | | | `perlnavigator` |
123+| perl | ✓ | ✓ | ✓ | `perlnavigator` |
113| php | ✓ | ✓ | ✓ | `intelephense` |
124| php | ✓ | ✓ | ✓ | `intelephense` |
114| po | ✓ | ✓ | | |
125| po | ✓ | ✓ | | |
126+| pod | ✓ | | | |
115| ponylang | ✓ | ✓ | ✓ | |
127| ponylang | ✓ | ✓ | ✓ | |
116| prisma | ✓ | | | `prisma-language-server` |
128| prisma | ✓ | | | `prisma-language-server` |
117| prolog | | | | `swipl` |
129| prolog | | | | `swipl` |
118−| protobuf | ✓ | | ✓ | |
130+| protobuf | ✓ | | ✓ | `bufls`, `pb` |
119| prql | ✓ | | | |
131| prql | ✓ | | | |
120−| purescript | ✓ | | | `purescript-language-server` |
132+| purescript | ✓ | ✓ | | `purescript-language-server` |
121| python | ✓ | ✓ | ✓ | `pylsp` |
133| python | ✓ | ✓ | ✓ | `pylsp` |
122| qml | ✓ | | ✓ | `qmlls` |
134| qml | ✓ | | ✓ | `qmlls` |
123| r | ✓ | | | `R` |
135| r | ✓ | | | `R` |
124−| racket | ✓ | | | `racket` |
136+| racket | ✓ | | ✓ | `racket` |
125| regex | ✓ | | | |
137| regex | ✓ | | | |
126| rego | ✓ | | | `regols` |
138| rego | ✓ | | | `regols` |
127| rescript | ✓ | ✓ | | `rescript-language-server` |
139| rescript | ✓ | ✓ | | `rescript-language-server` |
@@ -133,30 +145,36 @@
133| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
145| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
134| sage | ✓ | ✓ | | |
146| sage | ✓ | ✓ | | |
135| scala | ✓ | | ✓ | `metals` |
147| scala | ✓ | | ✓ | `metals` |
136−| scheme | ✓ | | | |
148+| scheme | ✓ | | ✓ | |
137| scss | ✓ | | | `vscode-css-language-server` |
149| scss | ✓ | | | `vscode-css-language-server` |
138| slint | ✓ | | ✓ | `slint-lsp` |
150| slint | ✓ | | ✓ | `slint-lsp` |
151+| smali | ✓ | | ✓ | |
139| smithy | ✓ | | | `cs` |
152| smithy | ✓ | | | `cs` |
140| sml | ✓ | | | |
153| sml | ✓ | | | |
141| solidity | ✓ | | | `solc` |
154| solidity | ✓ | | | `solc` |
142| sql | ✓ | | | |
155| sql | ✓ | | | |
143| sshclientconfig | ✓ | | | |
156| sshclientconfig | ✓ | | | |
144| starlark | ✓ | ✓ | | |
157| starlark | ✓ | ✓ | | |
158+| strace | ✓ | | | |
145| svelte | ✓ | | ✓ | `svelteserver` |
159| svelte | ✓ | | ✓ | `svelteserver` |
146| sway | ✓ | ✓ | ✓ | `forc` |
160| sway | ✓ | ✓ | ✓ | `forc` |
147| swift | ✓ | | | `sourcekit-lsp` |
161| swift | ✓ | | | `sourcekit-lsp` |
148| t32 | ✓ | | | |
162| t32 | ✓ | | | |
149| tablegen | ✓ | ✓ | ✓ | |
163| tablegen | ✓ | ✓ | ✓ | |
150| task | ✓ | | | |
164| task | ✓ | | | |
165+| templ | ✓ | | | `templ` |
151| tfvars | ✓ | | ✓ | `terraform-ls` |
166| tfvars | ✓ | | ✓ | `terraform-ls` |
167+| todotxt | ✓ | | | |
152| toml | ✓ | | | `taplo` |
168| toml | ✓ | | | `taplo` |
153| tsq | ✓ | | | |
169| tsq | ✓ | | | |
154| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
170| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
155| twig | ✓ | | | |
171| twig | ✓ | | | |
156| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
172| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
173+| typst | ✓ | | | `typst-lsp` |
157| ungrammar | ✓ | | | |
174| ungrammar | ✓ | | | |
175+| unison | ✓ | | | |
158| uxntal | ✓ | | | |
176| uxntal | ✓ | | | |
159−| v | ✓ | ✓ | ✓ | `v` |
177+| v | ✓ | ✓ | ✓ | `v-analyzer` |
160| vala | ✓ | | | `vala-language-server` |
178| vala | ✓ | | | `vala-language-server` |
161| verilog | ✓ | ✓ | | `svlangserver` |
179| verilog | ✓ | ✓ | | `svlangserver` |
162| vhdl | ✓ | | | `vhdl_ls` |
180| vhdl | ✓ | | | `vhdl_ls` |
@@ -167,8 +185,9 @@
167| webc | ✓ | | | |
185| webc | ✓ | | | |
168| wgsl | ✓ | | | `wgsl_analyzer` |
186| wgsl | ✓ | | | `wgsl_analyzer` |
169| wit | ✓ | | ✓ | |
187| wit | ✓ | | ✓ | |
188+| wren | ✓ | ✓ | ✓ | |
170| xit | ✓ | | | |
189| xit | ✓ | | | |
171| xml | ✓ | | ✓ | |
190| xml | ✓ | | ✓ | |
172−| yaml | ✓ | | ✓ | `yaml-language-server` |
191+| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
173| yuck | ✓ | | | |
192| yuck | ✓ | | | |
174| zig | ✓ | ✓ | ✓ | `zls` |
193| zig | ✓ | ✓ | ✓ | `zls` |
book/src/generated/typable-cmd.md+4−1
@@ -17,7 +17,7 @@
17| `: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) |
17| `: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) |
18| `:new`, `:n` | Create a new scratch buffer. |
18| `:new`, `:n` | Create a new scratch buffer. |
19| `:format`, `:fmt` | Format the file using the LSP formatter. |
19| `:format`, `:fmt` | Format the file using the LSP formatter. |
20−| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
20+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.) |
21| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
21| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
22| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
22| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
23| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
23| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
@@ -55,6 +55,7 @@
55| `:lsp-restart` | Restarts the language servers used by the current doc |
55| `:lsp-restart` | Restarts the language servers used by the current doc |
56| `:lsp-stop` | Stops the language servers that are used by the current doc |
56| `:lsp-stop` | Stops the language servers that are used by the current doc |
57| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
57| `: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. |
58| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
59| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
59| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
60| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
60| `:debug-eval` | Evaluate expression in current debug context. |
61| `:debug-eval` | Evaluate expression in current debug context. |
@@ -83,3 +84,5 @@
83| `:run-shell-command`, `:sh` | Run a shell command |
84| `:run-shell-command`, `:sh` | Run a shell command |
84| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
85| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
85| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
86| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
87+| `:redraw` | Clear and re-render the whole UI |
88+| `:move` | Move the current buffer and its corresponding file to a different path |
book/src/guides/adding_languages.md+2
@@ -36,6 +36,7 @@ below.
363. Refer to the
363. Refer to the
37 [tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
37 [tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
38 for more information on writing queries.
38 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).
39
40
40> 💡 In Helix, the first matching query takes precedence when evaluating
41> 💡 In Helix, the first matching query takes precedence when evaluating
41> queries, which is different from other editors such as Neovim where the last
42> queries, which is different from other editors such as Neovim where the last
@@ -51,3 +52,4 @@ below.
51 grammars.
52 grammars.
52- If a parser is causing a segfault, or you want to remove it, make sure to
53- If a parser is causing a segfault, or you want to remove it, make sure to
53 remove the compiled parser located at `runtime/grammars/<name>.so`.
54 remove the compiled parser located at `runtime/grammars/<name>.so`.
55+- If you are attempting to add queries and Helix is unable to locate them, ensure that the environment variable `HELIX_RUNTIME` is set to the location of the `runtime` folder you're developing in.
book/src/guides/indent.md+319−43
@@ -1,76 +1,308 @@
1# Adding indent queries
1# Adding indent queries
2
2
3−Helix uses tree-sitter to correctly indent new lines. This requires
3+Helix uses tree-sitter to correctly indent new lines. This requires a tree-
4−a tree-sitter grammar and an `indent.scm` query file placed in
4+sitter grammar and an `indent.scm` query file placed in `runtime/queries/
5−`runtime/queries/{language}/indents.scm`. The indentation for a line
5+{language}/indents.scm`. The indentation for a line is calculated by traversing
6−is calculated by traversing the syntax tree from the lowest node at the
6+the syntax tree from the lowest node at the beginning of the new line (see
7−beginning of the new line. Each of these nodes contributes to the total
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
8+indent when it is captured by the query (in what way depends on the name of
9−of the capture).
9+the capture.
10
10
11Note that it matters where these added indents begin. For example,
11Note that it matters where these added indents begin. For example,
12multiple indent level increases that start on the same line only increase
12multiple indent level increases that start on the same line only increase
13−the total indent level by 1.
13+the total indent level by 1. See [Capture types](#capture-types).
14
14
15−## Scopes
15+By default, Helix uses the `hybrid` indentation heuristic. This means that
16+indent queries are not used to compute the expected absolute indentation of a
17+line but rather the expected difference in indentation between the new and an
18+already existing line. This difference is then added to the actual indentation
19+of the already existing line. Since this makes errors in the indent queries
20+harder to find, it is recommended to disable it when testing via
21+`:set indent-heuristic tree-sitter`. The rest of this guide assumes that
22+the `tree-sitter` heuristic is used.
16
23
17−Added indents don't always apply to the whole node. For example, in most
24+## Indent queries
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):
21
25
22−- `all`:
26+When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
23−This scope applies to the whole captured node. This is only different from
27+the indent level for the new line, the query in `indents.scm` is run on the
24−`tail` when the captured node is the first node on its line.
28+document. The starting position of the query is the end of the line above where
29+a new line will be inserted.
25
30
26−- `tail`:
31+For `o`, the inserted line is the line below the cursor, so that starting
27−This scope applies to everything except for the first line of the
32+position of the query is the end of the current line.
28−captured node.
29
33
30−Every capture type has a default scope which should do the right thing
34+```rust
31−in most situations. When a different scope is required, this can be
35+fn need_hero(some_hero: Hero, life: Life) -> {
32−changed by using a `#set!` declaration anywhere in the pattern:
36+ matches!(some_hero, Hero { // ←─────────────────╮
33−```scm
37+ strong: true,//←╮ ↑ ↑ │
34−(assignment_expression
38+ fast: true, // │ │ ╰── query start │
35− right: (_) @indent
39+ sure: true, // │ ╰───── cursor ├─ traversal
36− (#set! "scope" "all"))
40+ soon: true, // ╰──────── new line inserted │ start node
41+ }) && // │
42+// ↑ │
43+// ╰───────────────────────────────────────────────╯
44+ some_hero > life
45+}
37```
46```
38
47
39−## Capture types
48+For `O`, the newly inserted line is the *current* line, so the starting position
49+of the query is the end of the line above the cursor.
40
50
41−- `@indent` (default scope `tail`):
51+```rust
42−Increase the indent level by 1. Multiple occurrences in the same line
52+fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
43−don't stack. If there is at least one `@indent` and one `@outdent`
53+ matches!(some_hero, Hero { // ←╮ ↑ │
44−capture on the same line, the indent level isn't changed at all.
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+```
45
62
46−- `@outdent` (default scope `all`):
63+From this starting node, the syntax tree is traversed up until the root node.
47−Decrease the indent level by 1. The same rules as for `@indent` apply.
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.
48
67
49−- `@extend`:
68+### Capture types
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.
54
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.
55- `@extend.prevent-once`:
94- `@extend.prevent-once`:
56−Prevents the first extension of an ancestor of this node. For example, in Python
95+ 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
96+ a return expression always ends the block that it is in. Note that this only
58−extension of the next `@extend` capture. If multiple ancestors are captured,
97+ stops the extension of the next `@extend` capture. If multiple ancestors are
59−only the extension of the innermost one is prevented. All other ancestors are unaffected
98+ captured, only the extension of the innermost one is prevented. All other
60−(regardless of whether the innermost ancestor would actually have been extended).
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+```
61
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+```
231+
232+In Python, the are a few tokens that will always end a scope, such as a return
233+statement. Since the scope ends, so should the indent level. But because the
234+function span is extended to every line with a greater indent level, a new line
235+would just continue on the same level. And an `@outdent` would not help us here
236+either, since it would cause everything in the parentheses to become outdented
237+as well.
238+
239+To help, we need to signal an end to the extension. We can do this with
240+`@extend.prevent-once`.
241+
242+```scm
243+(parenthesized_expression) @indent
244+
245+]
246+ (function_definition)
247+ (class_definition)
248+] @indent @extend
249+
250+(return_statement) @extend.prevent-once
251+```
252+
253+#### `@indent.always` / `@outdent.always`
254+
255+As mentioned before, normally if there is more than one `@indent` or `@outdent`
256+capture on the same line, they are combined.
257+
258+Sometimes, there are cases when you may want to ensure that every indent capture
259+is additive, regardless of how many occur on the same line. Consider this
260+example in YAML.
261+
262+```yaml
263+ - foo: bar
264+# ↑ ↑
265+# │ ╰─────────────── start of map
266+# ╰───────────────── start of list element
267+ baz: quux # ←─── cursor
268+ # ←───────────── where the cursor should go on a new line
269+ garply: waldo
270+ - quux:
271+ bar: baz
272+ xyzzy: thud
273+ fred: plugh
274+```
275+
276+In YAML, you often have lists of maps. In these cases, the syntax is such that
277+the list element and the map both start on the same line. But we really do want
278+to start an indentation for each of these so that subsequent keys in the map
279+hang over the list and align properly. This is where `@indent.always` helps.
280+
281+```scm
282+((block_sequence_item) @item @indent.always @extend
283+ (#not-one-line? @item))
284+
285+((block_mapping_pair
286+ key: (_) @key
287+ value: (_) @val
288+ (#not-same-line? @key @val)
289+ ) @indent.always @extend
290+)
291+```
62
292
63## Predicates
293## Predicates
64
294
65In some cases, an S-expression cannot express exactly what pattern should be matched.
295In some cases, an S-expression cannot express exactly what pattern should be matched.
66For that, tree-sitter allows for predicates to appear anywhere within a pattern,
296For that, tree-sitter allows for predicates to appear anywhere within a pattern,
67similar to how `#set!` declarations work:
297similar to how `#set!` declarations work:
298+
68```scm
299```scm
69(some_kind
300(some_kind
70 (child_kind) @indent
301 (child_kind) @indent
71 (#predicate? arg1 arg2 ...)
302 (#predicate? arg1 arg2 ...)
72)
303)
73```
304```
305+
74The number of arguments depends on the predicate that's used.
306The number of arguments depends on the predicate that's used.
75Each argument is either a capture (`@name`) or a string (`"some string"`).
307Each argument is either a capture (`@name`) or a string (`"some string"`).
76The following predicates are supported by tree-sitter:
308The following predicates are supported by tree-sitter:
@@ -91,3 +323,47 @@ argument (a string).
91
323
92- `#same-line?`/`#not-same-line?`:
324- `#same-line?`/`#not-same-line?`:
93The captures given by the 2 arguments must/must not start on the same line.
325The 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 @@
1# Adding Injection Queries
1# Adding Injection Queries
2
2
3Writing language injection queries allows one to highlight a specific node as a different language.
3Writing 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
5are a few Helix specific extensions that allow for more control.
5are a few Helix specific extensions that allow for more control.
6
6
7And example of a simple query that would highlight all strings as bash in Nix:
7And example of a simple query that would highlight all strings as bash in Nix:
book/src/install.md+30−6
@@ -13,6 +13,7 @@
13 - [AppImage](#appimage)
13 - [AppImage](#appimage)
14- [macOS](#macos)
14- [macOS](#macos)
15 - [Homebrew Core](#homebrew-core)
15 - [Homebrew Core](#homebrew-core)
16+ - [MacPorts](#macports)
16- [Windows](#windows)
17- [Windows](#windows)
17 - [Winget](#winget)
18 - [Winget](#winget)
18 - [Scoop](#scoop)
19 - [Scoop](#scoop)
@@ -64,10 +65,7 @@ sudo apt install helix
64
65
65### Fedora/RHEL
66### Fedora/RHEL
66
67
67−Enable the `COPR` repository for Helix:
68−
69```sh
68```sh
70−sudo dnf copr enable varlad/helix
71sudo dnf install helix
69sudo dnf install helix
72```
70```
73
71
@@ -133,6 +131,12 @@ chmod +x helix-*.AppImage # change permission for executable mode
133brew install helix
131brew install helix
134```
132```
135
133
134+### MacPorts
135+
136+```sh
137+port install helix
138+```
139+
136## Windows
140## Windows
137
141
138Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
142Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
@@ -224,7 +228,7 @@ Or, create a symbolic link:
224ln -Ts $PWD/runtime ~/.config/helix/runtime
228ln -Ts $PWD/runtime ~/.config/helix/runtime
225```
229```
226
230
227−If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
231+If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
228
232
229#### Windows
233#### Windows
230
234
@@ -257,12 +261,32 @@ following order:
2571. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
2611. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
258 developing and testing helix only).
262 developing and testing helix only).
2592. `runtime/` subdirectory of OS-dependent helix user config directory.
2632. `runtime/` subdirectory of OS-dependent helix user config directory.
260−3. `$HELIX_RUNTIME`.
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)
261−4. `runtime/` subdirectory of path to Helix executable.
267+5. `runtime/` subdirectory of path to Helix executable.
262
268
263This order also sets the priority for selecting which file will be used if multiple runtime
269This order also sets the priority for selecting which file will be used if multiple runtime
264directories have files with the same name.
270directories have files with the same name.
265
271
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+
266### Validating the installation
290### Validating the installation
267
291
268To make sure everything is set up as expected you should run the Helix health
292To make sure everything is set up as expected you should run the Helix health
book/src/keymap.md+16−15
@@ -25,7 +25,7 @@
25
25
26## Normal mode
26## Normal mode
27
27
28−Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
28+Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
29
29
30### Movement
30### Movement
31
31
@@ -291,7 +291,7 @@ This layer is a kludge of mappings, mostly pickers.
291| `w` | Enter [window mode](#window-mode) | N/A |
291| `w` | Enter [window mode](#window-mode) | N/A |
292| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
292| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
293| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
293| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
294−| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
294+| `y` | Yank selections to clipboard | `yank_to_clipboard` |
295| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
295| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
296| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
296| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
297| `/` | Global search in workspace folder | `global_search` |
297| `/` | Global search in workspace folder | `global_search` |
@@ -411,19 +411,20 @@ you to selectively add search terms to your selections.
411
411
412Keys to use within picker. Remapping currently not supported.
412Keys to use within picker. Remapping currently not supported.
413
413
414−| Key | Description |
414+| Key | Description |
415−| ----- | ------------- |
415+| ----- | ------------- |
416−| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
416+| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
417−| `Tab`, `Down`, `Ctrl-n` | Next entry |
417+| `Tab`, `Down`, `Ctrl-n` | Next entry |
418−| `PageUp`, `Ctrl-u` | Page up |
418+| `PageUp`, `Ctrl-u` | Page up |
419−| `PageDown`, `Ctrl-d` | Page down |
419+| `PageDown`, `Ctrl-d` | Page down |
420−| `Home` | Go to first entry |
420+| `Home` | Go to first entry |
421−| `End` | Go to last entry |
421+| `End` | Go to last entry |
422−| `Enter` | Open selected |
422+| `Enter` | Open selected |
423+| `Alt-Enter` | Open selected in the background without closing the picker |
423−| `Ctrl-s` | Open horizontally |
424+| `Ctrl-s` | Open horizontally |
424−| `Ctrl-v` | Open vertically |
425+| `Ctrl-v` | Open vertically |
425−| `Ctrl-t` | Toggle preview |
426+| `Ctrl-t` | Toggle preview |
426−| `Escape`, `Ctrl-c` | Close picker |
427+| `Escape`, `Ctrl-c` | Close picker |
427
428
428## Prompt
429## Prompt
429
430
book/src/languages.md+3−1
@@ -66,8 +66,10 @@ These configuration keys are available:
66| `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) |
66| `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) |
67| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
67| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
68| `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 |
68| `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)
69| `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` |
70| `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` |
70| `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. |
71| `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.
71
73
72### File-type detection and the `file-types` key
74### File-type detection and the `file-types` key
73
75
@@ -127,7 +129,7 @@ These are the available options for a language server.
127| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
129| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
128
130
129A `format` sub-table within `config` can be used to pass extra formatting options to
131A `format` sub-table within `config` can be used to pass extra formatting options to
130−[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
132+[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
131For example, with typescript:
133For example, with typescript:
132
134
133```toml
135```toml
book/src/themes.md+9−1
@@ -70,6 +70,7 @@ over it and is merged into the default palette.
70
70
71| Color Name |
71| Color Name |
72| --- |
72| --- |
73+| `default` |
73| `black` |
74| `black` |
74| `red` |
75| `red` |
75| `green` |
76| `green` |
@@ -154,6 +155,7 @@ We use a similar set of scopes as
154
155
155- `type` - Types
156- `type` - Types
156 - `builtin` - Primitive types provided by the language (`int`, `usize`)
157 - `builtin` - Primitive types provided by the language (`int`, `usize`)
158+ - `parameter` - Generic type parameters (`T`)
157 - `enum`
159 - `enum`
158 - `variant`
160 - `variant`
159- `constructor`
161- `constructor`
@@ -244,9 +246,12 @@ We use a similar set of scopes as
244
246
245- `diff` - version control changes
247- `diff` - version control changes
246 - `plus` - additions
248 - `plus` - additions
249+ - `gutter` - gutter indicator
247 - `minus` - deletions
250 - `minus` - deletions
251+ - `gutter` - gutter indicator
248 - `delta` - modifications
252 - `delta` - modifications
249 - `moved` - renamed or moved files/changes
253 - `moved` - renamed or moved files/changes
254+ - `gutter` - gutter indicator
250
255
251#### Interface
256#### Interface
252
257
@@ -291,11 +296,14 @@ These scopes are used for theming the editor interface:
291| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
296| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
292| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
297| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
293| `ui.statusline.separator` | Separator character in statusline |
298| `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 |
294| `ui.popup` | Documentation popups (e.g. Space + k) |
302| `ui.popup` | Documentation popups (e.g. Space + k) |
295| `ui.popup.info` | Prompt for multiple key options |
303| `ui.popup.info` | Prompt for multiple key options |
296| `ui.window` | Borderlines separating splits |
304| `ui.window` | Borderlines separating splits |
297| `ui.help` | Description box for commands |
305| `ui.help` | Description box for commands |
298−| `ui.text` | Command prompts, popup text, etc. |
306+| `ui.text` | Default text style, command prompts, popup text, etc. |
299| `ui.text.focus` | The currently selected line in the picker |
307| `ui.text.focus` | The currently selected line in the picker |
300| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
308| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
301| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
309| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
book/src/usage.md+21−5
@@ -37,19 +37,35 @@ If a register is selected before invoking a change or delete command, the select
37- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
37- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
38- `"md` - Store the selection in register `m` and delete it.
38- `"md` - Store the selection in register `m` and delete it.
39
39
40−### Special registers
40+### Default registers
41+
42+Commands that use registers, like yank (`y`), use a default register if none is specified.
43+These registers are used as defaults:
41
44
42| Register character | Contains |
45| Register character | Contains |
43| --- | --- |
46| --- | --- |
44| `/` | Last search |
47| `/` | Last search |
45| `:` | Last executed command |
48| `:` | Last executed command |
46| `"` | Last yanked text |
49| `"` | Last yanked text |
47−| `_` | Black hole |
50+| `@` | Last recorded macro |
48
51
49−The system clipboard is not directly supported by a special register. Instead, special commands and keybindings are provided. Refer to the
52+### Special registers
50−[key map](keymap.md#space-mode) for more details.
51
53
52−The black hole register is a no-op register, meaning that no data will be read or written to it.
54+Some registers have special behavior when read from and written to.
55+
56+| Register character | When read | When written |
57+| --- | --- | --- |
58+| `_` | No values are returned | All values are discarded |
59+| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
60+| `.` | Contents of the current selections | This register is not writable |
61+| `%` | Name of the current file | This register is not writable |
62+| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
63+| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
64+
65+When yanking multiple selections to the clipboard registers, the selections
66+are joined with newlines. Pasting from these registers will paste multiple
67+selections if the clipboard was last yanked to by the Helix session. Otherwise
68+the clipboard contents are pasted as one selection.
53
69
54## Surround
70## Surround
55
71
book/theme/index.hbs+18−15
@@ -1,11 +1,11 @@
1<!DOCTYPE HTML>
1<!DOCTYPE HTML>
2−<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
2+<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
3 <head>
3 <head>
4 <!-- Book generated using mdBook -->
4 <!-- Book generated using mdBook -->
5 <meta charset="UTF-8">
5 <meta charset="UTF-8">
6 <title>{{ title }}</title>
6 <title>{{ title }}</title>
7 {{#if is_print }}
7 {{#if is_print }}
8− <meta name="robots" content="noindex" />
8+ <meta name="robots" content="noindex">
9 {{/if}}
9 {{/if}}
10 {{#if base_url}}
10 {{#if base_url}}
11 <base href="{{ base_url }}">
11 <base href="{{ base_url }}">
@@ -17,7 +17,7 @@
17
17
18 <meta name="description" content="{{ description }}">
18 <meta name="description" content="{{ description }}">
19 <meta name="viewport" content="width=device-width, initial-scale=1">
19 <meta name="viewport" content="width=device-width, initial-scale=1">
20− <meta name="theme-color" content="#ffffff" />
20+ <meta name="theme-color" content="#ffffff">
21
21
22 {{#if favicon_svg}}
22 {{#if favicon_svg}}
23 <link rel="icon" href="{{ path_to_root }}favicon.svg">
23 <link rel="icon" href="{{ path_to_root }}favicon.svg">
@@ -34,8 +34,6 @@
34
34
35 <!-- Fonts -->
35 <!-- Fonts -->
36 <link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
36 <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">
39 {{#if copy_fonts}}
37 {{#if copy_fonts}}
40 <link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
38 <link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
41 {{/if}}
39 {{/if}}
@@ -55,7 +53,7 @@
55 <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
53 <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
56 {{/if}}
54 {{/if}}
57 </head>
55 </head>
58− <body>
56+ <body class="sidebar-visible no-js">
59 <div id="body-container">
57 <div id="body-container">
60 <!-- Provide site root to javascript -->
58 <!-- Provide site root to javascript -->
61 <script>
59 <script>
@@ -85,24 +83,29 @@
85 try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
83 try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
86 if (theme === null || theme === undefined) { theme = default_theme; }
84 if (theme === null || theme === undefined) { theme = default_theme; }
87 var html = document.querySelector('html');
85 var html = document.querySelector('html');
88− html.classList.remove('no-js')
89 html.classList.remove('{{ default_theme }}')
86 html.classList.remove('{{ default_theme }}')
90 html.classList.add(theme);
87 html.classList.add(theme);
88+ var body = document.querySelector('body');
89+ body.classList.remove('no-js')
91− html.classList.add('js');
90+ body.classList.add('js');
92 </script>
91 </script>
93
92
93+ <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
94+
94 <!-- Hide / unhide sidebar before it is displayed -->
95 <!-- Hide / unhide sidebar before it is displayed -->
95 <script>
96 <script>
96− var html = document.querySelector('html');
97+ var body = document.querySelector('body');
97 var sidebar = null;
98 var sidebar = null;
99+ var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
98 if (document.body.clientWidth >= 1080) {
100 if (document.body.clientWidth >= 1080) {
99 try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
101 try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
100 sidebar = sidebar || 'visible';
102 sidebar = sidebar || 'visible';
101 } else {
103 } else {
102 sidebar = 'hidden';
104 sidebar = 'hidden';
103 }
105 }
106+ sidebar_toggle.checked = sidebar === 'visible';
104− html.classList.remove('sidebar-visible');
107+ body.classList.remove('sidebar-visible');
105− html.classList.add("sidebar-" + sidebar);
108+ body.classList.add("sidebar-" + sidebar);
106 </script>
109 </script>
107
110
108 <nav id="sidebar" class="sidebar" aria-label="Table of contents">
111 <nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -141,9 +144,9 @@
141 <div id="menu-bar-hover-placeholder"></div>
144 <div id="menu-bar-hover-placeholder"></div>
142 <div id="menu-bar" class="menu-bar sticky">
145 <div id="menu-bar" class="menu-bar sticky">
143 <div class="left-buttons">
146 <div class="left-buttons">
144− <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
147+ <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
145 <i class="fa fa-bars"></i>
148 <i class="fa fa-bars"></i>
146− </button>
149+ </label>
147 <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">
150 <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">
148 <i class="fa fa-paint-brush"></i>
151 <i class="fa fa-paint-brush"></i>
149 </button>
152 </button>
@@ -220,7 +223,7 @@
220 {{/previous}}
223 {{/previous}}
221
224
222 {{#next}}
225 {{#next}}
223− <a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
226+ <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
224 <i class="fa fa-angle-right"></i>
227 <i class="fa fa-angle-right"></i>
225 </a>
228 </a>
226 {{/next}}
229 {{/next}}
@@ -238,7 +241,7 @@
238 {{/previous}}
241 {{/previous}}
239
242
240 {{#next}}
243 {{#next}}
241− <a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
244+ <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
242 <i class="fa fa-angle-right"></i>
245 <i class="fa fa-angle-right"></i>
243 </a>
246 </a>
244 {{/next}}
247 {{/next}}
contrib/Helix.appdata.xml+15
@@ -5,6 +5,7 @@
5 <project_license>MPL-2.0</project_license>
5 <project_license>MPL-2.0</project_license>
6 <name>Helix</name>
6 <name>Helix</name>
7 <summary>A post-modern text editor</summary>
7 <summary>A post-modern text editor</summary>
8+ <summary xml:lang="ar">مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه</summary>
8
9
9 <description>
10 <description>
10 <p>
11 <p>
@@ -17,6 +18,17 @@
17 <li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
18 <li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
18 </ul>
19 </ul>
19 </description>
20 </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>
20
32
21 <launchable type="desktop-id">Helix.desktop</launchable>
33 <launchable type="desktop-id">Helix.desktop</launchable>
22
34
@@ -36,6 +48,9 @@
36 <content_rating type="oars-1.1" />
48 <content_rating type="oars-1.1" />
37
49
38 <releases>
50 <releases>
51+ <release version="23.10" date="2023-10-24">
52+ <url>https://helix-editor.com/news/release-23-10-highlights/</url>
53+ </release>
39 <release version="23.05" date="2023-05-18">
54 <release version="23.05" date="2023-05-18">
40 <url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
55 <url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
41 </release>
56 </release>
contrib/Helix.desktop+3−1
@@ -1,6 +1,7 @@
1[Desktop Entry]
1[Desktop Entry]
2Name=Helix
2Name=Helix
3GenericName=Text Editor
3GenericName=Text Editor
4+GenericName[ar]=مُحَرِّرُ نُصُوص
4GenericName[de]=Texteditor
5GenericName[de]=Texteditor
5GenericName[fr]=Éditeur de texte
6GenericName[fr]=Éditeur de texte
6GenericName[ru]=Текстовый редактор
7GenericName[ru]=Текстовый редактор
@@ -9,7 +10,7 @@ GenericName[tr]=Metin Düzenleyici
9Comment=Edit text files
10Comment=Edit text files
10Comment[af]=Redigeer tekslêers
11Comment[af]=Redigeer tekslêers
11Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
12Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
12−Comment[ar]=حرّر ملفات نصية
13+Comment[ar]=مُحَرِّرُ مِلَفَّاتٍ نَصِّيَّة
13Comment[az]=Mətn fayllarını redaktə edin
14Comment[az]=Mətn fayllarını redaktə edin
14Comment[be]=Рэдагаваньне тэкставых файлаў
15Comment[be]=Рэдагаваньне тэкставых файлаў
15Comment[bg]=Редактиране на текстови файлове
16Comment[bg]=Редактиране на текстови файлове
@@ -79,6 +80,7 @@ Exec=hx %F
79Terminal=true
80Terminal=true
80Type=Application
81Type=Application
81Keywords=Text;editor;
82Keywords=Text;editor;
83+Keywords[ar]=نص;نصوص;محرر;
82Keywords[fr]=Texte;éditeur;
84Keywords[fr]=Texte;éditeur;
83Keywords[ru]=текст;текстовый редактор;
85Keywords[ru]=текст;текстовый редактор;
84Keywords[sr]=Текст;едитор;
86Keywords[sr]=Текст;едитор;
docs/CONTRIBUTING.md+10−3
@@ -29,9 +29,15 @@ files, run
29cargo xtask docgen
29cargo xtask docgen
30```
30```
31
31
32−inside the project. We use [xtask][xtask] as an ad-hoc task runner and
32+inside the project. We use [xtask][xtask] as an ad-hoc task runner.
33−thus do not require any dependencies other than `cargo` (You don't have
33+
34−to `cargo install` anything either).
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).
35
41
36# Testing
42# Testing
37
43
@@ -58,4 +64,5 @@ The current MSRV and future changes to the MSRV are listed in the [Firefox docum
58[architecture.md]: ./architecture.md
64[architecture.md]: ./architecture.md
59[docs]: https://docs.helix-editor.com/
65[docs]: https://docs.helix-editor.com/
60[xtask]: https://github.com/matklad/cargo-xtask
66[xtask]: https://github.com/matklad/cargo-xtask
67+[mdbook]: https://rust-lang.github.io/mdBook/guide/installation.html
61[helpers.rs]: ../helix-term/tests/test/helpers.rs
68[helpers.rs]: ../helix-term/tests/test/helpers.rs
docs/releases.md+12−5
@@ -1,16 +1,23 @@
1## Checklist
1## Checklist
2
2
3Helix releases are versioned in the Calendar Versioning scheme:
3Helix releases are versioned in the Calendar Versioning scheme:
4−`YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions
4+`YY.0M(.MICRO)`, for example, `22.05` for May of 2022, or in a patch release,
5−we'll use `<tag>` as a placeholder for the tag being published.
5+`22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag
6+being published.
6
7
7* Merge the changelog PR
8* Merge the changelog PR
8* 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)
9* 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)
9* Tag and push
10* Tag and push
10 * `git tag -s -m "<tag>" -a <tag> && git push`
11 * `git tag -s -m "<tag>" -a <tag> && git push`
11 * Make sure to switch to master and pull first
12 * Make sure to switch to master and pull first
12−* Edit the `VERSION` file and change the date to the next planned release
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.
13− * Releases are planned to happen every two months, so `22.05` would change to `22.07`
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`
14* Wait for the Release CI to finish
21* Wait for the Release CI to finish
15 * It will automatically turn the git tag into a GitHub release when it uploads artifacts
22 * It will automatically turn the git tag into a GitHub release when it uploads artifacts
16* Edit the new release
23* Edit the new release
@@ -57,4 +64,4 @@ versions for convenience:
57> release. For the full log, check out the git log.
64> release. For the full log, check out the git log.
58
65
59Typically, small changes like dependencies or documentation updates, refactors,
66Typically, small changes like dependencies or documentation updates, refactors,
60−or meta changes like GitHub Actions work are left out.
67+or meta changes like GitHub Actions work are left out.
flake.lock+21−237
@@ -1,126 +1,22 @@
1{
1{
2 "nodes": {
2 "nodes": {
3 "crane": {
3 "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": {
21 "inputs": {
4 "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− ],
50 "nixpkgs": [
5 "nixpkgs": [
51− "nci",
52 "nixpkgs"
6 "nixpkgs"
53− ],
54− "nixpkgsV1": "nixpkgsV1",
55− "poetry2nix": [
56− "nci"
57− ],
58− "pre-commit-hooks": [
59− "nci"
60− ],
61− "pruned-racket-catalog": [
62− "nci"
63 ]
7 ]
64 },
8 },
65 "locked": {
9 "locked": {
66− "lastModified": 1683212002,
10+ "lastModified": 1701025348,
67− "narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=",
11+ "narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
68− "owner": "nix-community",
69− "repo": "dream2nix",
70− "rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096",
71− "type": "github"
72− },
73− "original": {
74− "owner": "nix-community",
75− "repo": "dream2nix",
76− "type": "github"
77− }
78− },
79− "drv-parts": {
80− "inputs": {
81− "flake-compat": [
82− "nci",
83− "dream2nix",
84− "flake-compat"
85− ],
86− "flake-parts": [
87− "nci",
88− "dream2nix",
89− "flake-parts"
90− ],
91− "nixpkgs": [
92− "nci",
93− "dream2nix",
94− "nixpkgs"
95− ]
96− },
97− "locked": {
98− "lastModified": 1680698112,
99− "narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=",
100− "owner": "davhau",
101− "repo": "drv-parts",
102− "rev": "e8c2ec1157dc1edb002989669a0dbd935f430201",
103− "type": "github"
104− },
105− "original": {
106− "owner": "davhau",
107− "repo": "drv-parts",
108− "type": "github"
109− }
110− },
111− "flake-compat": {
112− "flake": false,
113− "locked": {
114− "lastModified": 1673956053,
115− "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
116− "owner": "edolstra",
12+ "owner": "ipetkov",
117− "repo": "flake-compat",
13+ "repo": "crane",
118− "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
14+ "rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
119 "type": "github"
15 "type": "github"
120 },
16 },
121 "original": {
17 "original": {
122− "owner": "edolstra",
18+ "owner": "ipetkov",
123− "repo": "flake-compat",
19+ "repo": "crane",
124 "type": "github"
20 "type": "github"
125 }
21 }
126 },
22 },
@@ -129,11 +25,11 @@
129 "systems": "systems"
25 "systems": "systems"
130 },
26 },
131 "locked": {
27 "locked": {
132− "lastModified": 1681202837,
28+ "lastModified": 1694529238,
133− "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
29+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
134 "owner": "numtide",
30 "owner": "numtide",
135 "repo": "flake-utils",
31 "repo": "flake-utils",
136− "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
32+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
137 "type": "github"
33 "type": "github"
138 },
34 },
139 "original": {
35 "original": {
@@ -142,55 +38,13 @@
142 "type": "github"
38 "type": "github"
143 }
39 }
144 },
40 },
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− },
187 "nixpkgs": {
41 "nixpkgs": {
188 "locked": {
42 "locked": {
189− "lastModified": 1683408522,
43+ "lastModified": 1700794826,
190− "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
44+ "narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
191 "owner": "nixos",
45 "owner": "nixos",
192 "repo": "nixpkgs",
46 "repo": "nixpkgs",
193− "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
47+ "rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
194 "type": "github"
48 "type": "github"
195 },
49 },
196 "original": {
50 "original": {
@@ -200,99 +54,29 @@
200 "type": "github"
54 "type": "github"
201 }
55 }
202 },
56 },
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− },
275 "root": {
57 "root": {
276 "inputs": {
58 "inputs": {
277− "nci": "nci",
59+ "crane": "crane",
60+ "flake-utils": "flake-utils",
278 "nixpkgs": "nixpkgs",
61 "nixpkgs": "nixpkgs",
279− "parts": "parts_2",
280 "rust-overlay": "rust-overlay"
62 "rust-overlay": "rust-overlay"
281 }
63 }
282 },
64 },
283 "rust-overlay": {
65 "rust-overlay": {
284 "inputs": {
66 "inputs": {
285− "flake-utils": "flake-utils",
67+ "flake-utils": [
68+ "flake-utils"
69+ ],
286 "nixpkgs": [
70 "nixpkgs": [
287 "nixpkgs"
71 "nixpkgs"
288 ]
72 ]
289 },
73 },
290 "locked": {
74 "locked": {
291− "lastModified": 1683771545,
75+ "lastModified": 1701137803,
292− "narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=",
76+ "narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
293 "owner": "oxalica",
77 "owner": "oxalica",
294 "repo": "rust-overlay",
78 "repo": "rust-overlay",
295− "rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed",
79+ "rev": "9dd940c967502f844eacea52a61e9596268d4f70",
296 "type": "github"
80 "type": "github"
297 },
81 },
298 "original": {
82 "original": {
flake.nix+170−143
@@ -3,166 +3,193 @@
3
3
4 inputs = {
4 inputs = {
5 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
5 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6+ flake-utils.url = "github:numtide/flake-utils";
6 rust-overlay = {
7 rust-overlay = {
7 url = "github:oxalica/rust-overlay";
8 url = "github:oxalica/rust-overlay";
9+ inputs = {
8− inputs.nixpkgs.follows = "nixpkgs";
10+ nixpkgs.follows = "nixpkgs";
11+ flake-utils.follows = "flake-utils";
12+ };
9 };
13 };
10− nci = {
14+ crane = {
11− url = "github:yusdacra/nix-cargo-integration";
15+ url = "github:ipetkov/crane";
12 inputs.nixpkgs.follows = "nixpkgs";
16 inputs.nixpkgs.follows = "nixpkgs";
13− inputs.rust-overlay.follows = "rust-overlay";
14 };
17 };
15− parts.url = "github:hercules-ci/flake-parts";
16 };
18 };
17
19
18− outputs = inp: let
20+ outputs = {
19− mkRootPath = rel:
21+ self,
20− builtins.path {
22+ nixpkgs,
21− path = "${toString ./.}/${rel}";
23+ crane,
22− name = rel;
24+ flake-utils,
23− };
25+ rust-overlay,
24− filteredSource = let
26+ ...
25− pathsToIgnore = [
27+ }:
26− ".envrc"
28+ flake-utils.lib.eachDefaultSystem (system: let
27− ".ignore"
29+ pkgs = import nixpkgs {
28− ".github"
30+ inherit system;
29− ".gitignore"
31+ overlays = [(import rust-overlay)];
30− "logo.svg"
31− "logo_dark.svg"
32− "logo_light.svg"
33− "rust-toolchain.toml"
34− "rustfmt.toml"
35− "runtime"
36− "screenshot.png"
37− "book"
38− "contrib"
39− "docs"
40− "README.md"
41− "CHANGELOG.md"
42− "shell.nix"
43− "default.nix"
44− "grammars.nix"
45− "flake.nix"
46− "flake.lock"
47− ];
48− ignorePaths = path: type: let
49− inherit (inp.nixpkgs) lib;
50− # split the nix store path into its components
51− components = lib.splitString "/" path;
52− # drop off the `/nix/hash-source` section from the path
53− relPathComponents = lib.drop 4 components;
54− # reassemble the path components
55− relPath = lib.concatStringsSep "/" relPathComponents;
56− in
57− lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
58− in
59− builtins.path {
60− name = "helix-source";
61− path = toString ./.;
62− # filter out unnecessary paths
63− filter = ignorePaths;
64 };
32 };
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
33+ mkRootPath = rel:
86− rm -r $out/grammars
34+ builtins.path {
87− ln -s ${grammars} $out/grammars
35+ path = "${toString ./.}/${rel}";
88− '';
89− helix-wrapped =
90− pkgs.runCommand
91− old.name
36+ name = rel;
92− {
37+ };
93− inherit (old) pname version;
38+ filteredSource = let
94− meta = old.meta or {};
39+ pathsToIgnore = [
95− passthru =
40+ ".envrc"
96− (old.passthru or {})
41+ ".ignore"
97− // {
42+ ".github"
98− unwrapped = old;
43+ ".gitignore"
99− };
44+ "logo_dark.svg"
100− nativeBuildInputs = [pkgs.makeWrapper];
45+ "logo_light.svg"
101− makeWrapperArgs = config.makeWrapperArgs or [];
46+ "rust-toolchain.toml"
102− }
47+ "rustfmt.toml"
103− ''
48+ "runtime"
104− cp -rs --no-preserve=mode,ownership ${old} $out
49+ "screenshot.png"
105− wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
50+ "book"
106− '';
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;
107 in
68 in
108− helix-wrapped
69+ lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
109− // {
70+ in
71+ builtins.path {
72+ name = "helix-source";
73+ path = toString ./.;
74+ # filter out unnecessary paths
75+ filter = ignorePaths;
76+ };
110− override = makeOverridableHelix old;
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 {};
111 passthru =
91 passthru =
112− helix-wrapped.passthru
92+ (old.passthru or {})
113 // {
93 // {
114− wrapper = old: makeOverridableHelix old config;
94+ unwrapped = old;
115 };
95 };
116− };
96+ nativeBuildInputs = [pkgs.makeWrapper];
117− stdenv =
97+ makeWrapperArgs = config.makeWrapperArgs or [];
118− if pkgs.stdenv.isLinux
98+ }
119− then pkgs.stdenv
99+ ''
120− else pkgs.clangStdenv;
100+ cp -rs --no-preserve=mode,ownership ${old} $out
121− rustFlagsEnv =
101+ wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
122− if stdenv.isLinux
102+ '';
123− then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
124− else "$RUSTFLAGS";
125− in {
103+ in
126− nci.projects."helix-project".relPath = "";
104+ helix-wrapped
127− nci.crates."helix-term" = {
105+ // {
128− overrides = {
106+ override = makeOverridableHelix old;
129− add-meta.override = _: {meta.mainProgram = "hx";};
107+ passthru =
130− add-inputs.overrideAttrs = prev: {
108+ helix-wrapped.passthru
131− buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib];
109+ // {
132− };
110+ wrapper = old: makeOverridableHelix old config;
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";
136 };
111 };
137− disable-tests = {checkPhase = ":";};
138− set-stdenv.override = _: {inherit stdenv;};
139− set-filtered-src.override = _: {src = filteredSource;};
140− };
141 };
112 };
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+ };
142
152
143− packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release;
153+ checks = {
144− packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev;
154+ # Build the crate itself
145− packages.helix = makeOverridableHelix config.packages.helix-unwrapped {};
155+ inherit (self.packages.${system}) helix;
146− packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
147− packages.default = config.packages.helix;
148
156
149− overlayAttrs = {
157+ clippy = craneLibMSRV.cargoClippy (commonArgs
150− inherit (config.packages) helix;
158+ // {
159+ inherit cargoArtifacts;
160+ cargoClippyExtraArgs = "--all-targets -- --deny warnings";
151− };
161+ });
152
162
153− devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: {
163+ fmt = craneLibMSRV.cargoFmt commonArgs;
154− nativeBuildInputs =
164+
165+ doc = craneLibMSRV.cargoDoc (commonArgs
166+ // {
167+ inherit cargoArtifacts;
168+ });
169+
170+ test = craneLibMSRV.cargoTest (commonArgs
171+ // {
172+ inherit cargoArtifacts;
173+ });
174+ };
175+
176+ devShells.default = pkgs.mkShell {
177+ inputsFrom = builtins.attrValues self.checks.${system};
155− (old.nativeBuildInputs or [])
178+ nativeBuildInputs = with pkgs;
156− ++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer])
179+ [lld_13 cargo-flamegraph rust-analyzer]
157− ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
180+ ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
158− ++ (lib.optional stdenv.isLinux pkgs.lldb)
181+ ++ (lib.optional stdenv.isLinux pkgs.lldb)
159− ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
182+ ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
160− shellHook = ''
183+ shellHook = ''
161− export HELIX_RUNTIME="$PWD/runtime"
184+ export HELIX_RUNTIME="$PWD/runtime"
162− export RUST_BACKTRACE="1"
185+ export RUST_BACKTRACE="1"
163− export RUSTFLAGS="${rustFlagsEnv}"
186+ export RUSTFLAGS="${rustFlagsEnv}"
164− '';
187+ '';
165− });
188+ };
189+ })
190+ // {
191+ overlays.default = final: prev: {
192+ inherit (self.packages.${final.system}) helix;
166 };
193 };
167 };
194 };
168
195
grammars.nix+24−21
@@ -5,6 +5,7 @@
5 runCommand,
5 runCommand,
6 yj,
6 yj,
7 includeGrammarIf ? _: true,
7 includeGrammarIf ? _: true,
8+ grammarOverlays ? [],
8 ...
9 ...
9}: let
10}: let
10 # HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
11 # HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
@@ -48,22 +49,22 @@
48 then sourceGitHub
49 then sourceGitHub
49 else sourceGit;
50 else sourceGit;
50 in
51 in
51− stdenv.mkDerivation rec {
52+ stdenv.mkDerivation {
52 # see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
53 # see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
53
54
54 pname = "helix-tree-sitter-${grammar.name}";
55 pname = "helix-tree-sitter-${grammar.name}";
55 version = grammar.source.rev;
56 version = grammar.source.rev;
56
57
57− src =
58+ src = source;
58− if builtins.hasAttr "subpath" grammar.source
59+ sourceRoot = if builtins.hasAttr "subpath" grammar.source then
59− then "${source}/${grammar.source.subpath}"
60+ "source/${grammar.source.subpath}"
61+ else
60− else source;
62+ "source";
61
63
62− dontUnpack = true;
63 dontConfigure = true;
64 dontConfigure = true;
64
65
65 FLAGS = [
66 FLAGS = [
66− "-I${src}/src"
67+ "-Isrc"
67 "-g"
68 "-g"
68 "-O3"
69 "-O3"
69 "-fPIC"
70 "-fPIC"
@@ -76,13 +77,13 @@
76 buildPhase = ''
77 buildPhase = ''
77 runHook preBuild
78 runHook preBuild
78
79
79− if [[ -e "$src/src/scanner.cc" ]]; then
80+ if [[ -e src/scanner.cc ]]; then
80− $CXX -c "$src/src/scanner.cc" -o scanner.o $FLAGS
81+ $CXX -c src/scanner.cc -o scanner.o $FLAGS
81− elif [[ -e "$src/src/scanner.c" ]]; then
82+ elif [[ -e src/scanner.c ]]; then
82− $CC -c "$src/src/scanner.c" -o scanner.o $FLAGS
83+ $CC -c src/scanner.c -o scanner.o $FLAGS
83 fi
84 fi
84
85
85− $CC -c "$src/src/parser.c" -o parser.o $FLAGS
86+ $CC -c src/parser.c -o parser.o $FLAGS
86 $CXX -shared -o $NAME.so *.o
87 $CXX -shared -o $NAME.so *.o
87
88
88 ls -al
89 ls -al
@@ -105,15 +106,17 @@
105 '';
106 '';
106 };
107 };
107 grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
108 grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
108− builtGrammars =
109− builtins.map (grammar: {
109+ builtGrammars = builtins.map (grammar: {
110− inherit (grammar) name;
110+ inherit (grammar) name;
111− artifact = buildGrammar grammar;
111+ value = buildGrammar grammar;
112− })
113− grammarsToBuild;
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);
114− grammarLinks =
117+ grammarLinks = lib.mapAttrsToList
115− builtins.map (grammar: "ln -s ${grammar.artifact}/${grammar.name}.so $out/${grammar.name}.so")
118+ (name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
116− builtGrammars;
119+ (lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
117in
120in
118 runCommand "consolidated-helix-grammars" {} ''
121 runCommand "consolidated-helix-grammars" {} ''
119 mkdir -p $out
122 mkdir -p $out
helix-core/Cargo.toml+19−15
@@ -1,23 +1,24 @@
1[package]
1[package]
2name = "helix-core"
2name = "helix-core"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
7description = "Helix editor core editing primitives"
3description = "Helix editor core editing primitives"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
11include = ["src/**/*", "README.md"]
4include = ["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
12
13
13[features]
14[features]
14unicode-lines = ["ropey/unicode_lines"]
15unicode-lines = ["ropey/unicode_lines"]
15integration = []
16integration = []
16
17
17[dependencies]
18[dependencies]
18−helix-loader = { version = "0.6", path = "../helix-loader" }
19+helix-loader = { path = "../helix-loader" }
19
20
20−ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
21+ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
21smallvec = "1.11"
22smallvec = "1.11"
22smartstring = "1.0.1"
23smartstring = "1.0.1"
23unicode-segmentation = "1.10"
24unicode-segmentation = "1.10"
@@ -25,13 +26,13 @@ unicode-width = "0.1"
25unicode-general-category = "0.6"
26unicode-general-category = "0.6"
26# slab = "0.4.2"
27# slab = "0.4.2"
27slotmap = "1.0"
28slotmap = "1.0"
28−tree-sitter = "0.20"
29+tree-sitter.workspace = true
29−once_cell = "1.18"
30+once_cell = "1.19"
30arc-swap = "1"
31arc-swap = "1"
31regex = "1"
32regex = "1"
32−bitflags = "2.3"
33+bitflags = "2.4"
33−ahash = "0.8.3"
34+ahash = "0.8.6"
34−hashbrown = { version = "0.14.0", features = ["raw"] }
35+hashbrown = { version = "0.14.3", features = ["raw"] }
35dunce = "1.0"
36dunce = "1.0"
36
37
37log = "0.4"
38log = "0.4"
@@ -48,6 +49,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
48etcetera = "0.8"
49etcetera = "0.8"
49textwrap = "0.16.0"
50textwrap = "0.16.0"
50
51
52+nucleo.workspace = true
53+parking_lot = "0.12"
54+
51[dev-dependencies]
55[dev-dependencies]
52quickcheck = { version = "1", default-features = false }
56quickcheck = { version = "1", default-features = false }
53−indoc = "2.0.3"
57+indoc = "2.0.4"
helix-core/src/diagnostic.rs+4
@@ -39,6 +39,10 @@ pub enum DiagnosticTag {
39#[derive(Debug, Clone)]
39#[derive(Debug, Clone)]
40pub struct Diagnostic {
40pub struct Diagnostic {
41 pub range: Range,
41 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,
42 pub line: usize,
46 pub line: usize,
43 pub message: String,
47 pub message: String,
44 pub severity: Option<Severity>,
48 pub severity: Option<Severity>,
helix-core/src/fuzzy.rsnew file+43
@@ -0,0 +1,43 @@
1+use std::ops::DerefMut;
2+
3+use nucleo::pattern::{Atom, AtomKind, CaseMatching};
4+use nucleo::Config;
5+use parking_lot::Mutex;
6+
7+pub struct LazyMutex<T> {
8+ inner: Mutex<Option<T>>,
9+ init: fn() -> T,
10+}
11+
12+impl<T> LazyMutex<T> {
13+ pub const fn new(init: fn() -> T) -> Self {
14+ Self {
15+ inner: Mutex::new(None),
16+ init,
17+ }
18+ }
19+
20+ pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
21+ parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
22+ }
23+}
24+
25+pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
26+
27+/// convenience function to easily fuzzy match
28+/// on a (relatively small list of inputs). This is not recommended for building a full tui
29+/// application that can match large numbers of matches as all matching is done on the current
30+/// thread, effectively blocking the UI
31+pub fn fuzzy_match<T: AsRef<str>>(
32+ pattern: &str,
33+ items: impl IntoIterator<Item = T>,
34+ path: bool,
35+) -> Vec<(T, u16)> {
36+ let mut matcher = MATCHER.lock();
37+ matcher.config = Config::DEFAULT;
38+ if path {
39+ matcher.config.set_match_paths();
40+ }
41+ let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
42+ pattern.match_list(items, &mut matcher)
43+}
helix-core/src/graphemes.rs+1−1
@@ -481,7 +481,7 @@ impl<'a> From<String> for GraphemeStr<'a> {
481 let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
481 let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
482 GraphemeStr {
482 GraphemeStr {
483 ptr: unsafe { NonNull::new_unchecked(ptr) },
483 ptr: unsafe { NonNull::new_unchecked(ptr) },
484− len: i32::try_from(len).unwrap() as u32,
484+ len: (i32::try_from(len).unwrap() as u32) | Self::MASK_OWNED,
485 phantom: PhantomData,
485 phantom: PhantomData,
486 }
486 }
487 }
487 }
helix-core/src/indent.rs+586−127
@@ -1,13 +1,14 @@
1−use std::collections::HashMap;
1+use std::{borrow::Cow, collections::HashMap};
2
2
3use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
3use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
4
4
5use crate::{
5use crate::{
6 chars::{char_is_line_ending, char_is_whitespace},
6 chars::{char_is_line_ending, char_is_whitespace},
7+ find_first_non_whitespace_char,
7− graphemes::tab_width_at,
8+ graphemes::{grapheme_width, tab_width_at},
8− syntax::{LanguageConfiguration, RopeProvider, Syntax},
9+ syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
9 tree_sitter::Node,
10 tree_sitter::Node,
10− Rope, RopeSlice,
11+ Position, Rope, RopeGraphemes, RopeSlice,
11};
12};
12
13
13/// Enum representing indentation style.
14/// Enum representing indentation style.
@@ -21,7 +22,7 @@ pub enum IndentStyle {
21
22
22// 16 spaces
23// 16 spaces
23const INDENTS: &str = " ";
24const INDENTS: &str = " ";
24−const MAX_INDENT: u8 = 16;
25+pub const MAX_INDENT: u8 = 16;
25
26
26impl IndentStyle {
27impl IndentStyle {
27 /// Creates an `IndentStyle` from an indentation string.
28 /// Creates an `IndentStyle` from an indentation string.
@@ -196,6 +197,56 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
196 len / indent_width
197 len / indent_width
197}
198}
198
199
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+
199/// Computes for node and all ancestors whether they are the first node on their line.
250/// Computes for node and all ancestors whether they are the first node on their line.
200/// The first entry in the return value represents the root node, the last one the node itself
251/// The first entry in the return value represents the root node, the last one the node itself
201fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
252fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
@@ -237,68 +288,140 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
237/// This is usually constructed in one of 2 ways:
288/// This is usually constructed in one of 2 ways:
238/// - Successively add indent captures to get the (added) indent from a single line
289/// - Successively add indent captures to get the (added) indent from a single line
239/// - Successively add the indent results for each line
290/// - 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
240−#[derive(Default)]
294+#[derive(Default, Debug, PartialEq, Eq, Clone)]
241−pub struct Indentation {
295+pub struct Indentation<'a> {
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.)
244 indent: usize,
296 indent: usize,
297+ indent_always: usize,
245 outdent: usize,
298 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>>,
246}
303}
304+
247−impl Indentation {
305+impl<'a> Indentation<'a> {
248 /// Add some other [Indentation] to this.
306 /// Add some other [Indentation] to this.
249− /// The added indent should be the total added indent from one line
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).
250− fn add_line(&mut self, added: &Indentation) {
309+ fn add_line(&mut self, added: Indentation<'a>) {
251− if added.indent > 0 && added.outdent == 0 {
310+ // Align overrides the indent from outer scopes.
252− self.indent += 1;
311+ if self.align.is_some() {
253− } else if added.outdent > 0 && added.indent == 0 {
312+ return;
254− self.outdent += 1;
313+ }
314+ if added.align.is_some() {
315+ self.align = added.align;
316+ return;
255 }
317 }
318+ self.indent += added.indent;
319+ self.indent_always += added.indent_always;
320+ self.outdent += added.outdent;
321+ self.outdent_always += added.outdent_always;
256 }
322 }
323+
257 /// Add an indent capture to this indent.
324 /// Add an indent capture to this indent.
258− /// All the captures that are added in this way should be on the same line.
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).
259− fn add_capture(&mut self, added: IndentCaptureType) {
328+ fn add_capture(&mut self, added: IndentCaptureType<'a>) {
260 match added {
329 match added {
261 IndentCaptureType::Indent => {
330 IndentCaptureType::Indent => {
331+ if self.indent_always == 0 {
262− self.indent = 1;
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;
263 }
340 }
264 IndentCaptureType::Outdent => {
341 IndentCaptureType::Outdent => {
342+ if self.outdent_always == 0 {
265− self.outdent = 1;
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+ }
266 }
354 }
267 }
355 }
268 }
356 }
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,
269− fn as_string(&self, indent_style: &IndentStyle) -> String {
366+ indent_style: &IndentStyle,
270− let indent_level = if self.indent >= self.outdent {
367+ tab_width: usize,
271− self.indent - self.outdent
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+ ))
272 } else {
380 } else {
273− log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
381+ // If the alignment of both lines is different, we cannot compare their indentation in any meaningful way
274− 0
382+ None
275− };
383+ }
276− indent_style.as_str().repeat(indent_level)
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+ )
277 }
393 }
278}
394}
279
395
280/// An indent definition which corresponds to a capture from the indent query
396/// An indent definition which corresponds to a capture from the indent query
397+#[derive(Debug)]
281−struct IndentCapture {
398+struct IndentCapture<'a> {
282− capture_type: IndentCaptureType,
399+ capture_type: IndentCaptureType<'a>,
283 scope: IndentScope,
400 scope: IndentScope,
284}
401}
285−#[derive(Clone, Copy)]
402+#[derive(Debug, Clone, PartialEq)]
286−enum IndentCaptureType {
403+enum IndentCaptureType<'a> {
287 Indent,
404 Indent,
405+ IndentAlways,
288 Outdent,
406 Outdent,
407+ OutdentAlways,
408+ /// Alignment given as a string of whitespace
409+ Align(RopeSlice<'a>),
289}
410}
411+
290−impl IndentCaptureType {
412+impl<'a> IndentCaptureType<'a> {
291 fn default_scope(&self) -> IndentScope {
413 fn default_scope(&self) -> IndentScope {
292 match self {
414 match self {
293− IndentCaptureType::Indent => IndentScope::Tail,
415+ IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
294− IndentCaptureType::Outdent => IndentScope::All,
416+ IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
417+ IndentCaptureType::Align(_) => IndentScope::All,
295 }
418 }
296 }
419 }
297}
420}
298/// This defines which part of a node an [IndentCapture] applies to.
421/// This defines which part of a node an [IndentCapture] applies to.
299/// Each [IndentCaptureType] has a default scope, but the scope can be changed
422/// Each [IndentCaptureType] has a default scope, but the scope can be changed
300/// with `#set!` property declarations.
423/// with `#set!` property declarations.
301−#[derive(Clone, Copy)]
424+#[derive(Debug, Clone, Copy)]
302enum IndentScope {
425enum IndentScope {
303 /// The indent applies to the whole node
426 /// The indent applies to the whole node
304 All,
427 All,
@@ -308,6 +431,7 @@ enum IndentScope {
308
431
309/// A capture from the indent query which does not define an indent but extends
432/// A capture from the indent query which does not define an indent but extends
310/// the range of a node. This is used before the indent is calculated.
433/// the range of a node. This is used before the indent is calculated.
434+#[derive(Debug)]
311enum ExtendCapture {
435enum ExtendCapture {
312 Extend,
436 Extend,
313 PreventOnce,
437 PreventOnce,
@@ -316,24 +440,41 @@ enum ExtendCapture {
316/// The result of running a tree-sitter indent query. This stores for
440/// The result of running a tree-sitter indent query. This stores for
317/// each node (identified by its ID) the relevant captures (already filtered
441/// each node (identified by its ID) the relevant captures (already filtered
318/// by predicates).
442/// by predicates).
443+#[derive(Debug)]
319−struct IndentQueryResult {
444+struct IndentQueryResult<'a> {
320− indent_captures: HashMap<usize, Vec<IndentCapture>>,
445+ indent_captures: HashMap<usize, Vec<IndentCapture<'a>>>,
321 extend_captures: HashMap<usize, Vec<ExtendCapture>>,
446 extend_captures: HashMap<usize, Vec<ExtendCapture>>,
322}
447}
323
448
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+
324−fn query_indents(
466+fn query_indents<'a>(
325 query: &Query,
467 query: &Query,
326 syntax: &Syntax,
468 syntax: &Syntax,
327 cursor: &mut QueryCursor,
469 cursor: &mut QueryCursor,
328− text: RopeSlice,
470+ text: RopeSlice<'a>,
329 range: std::ops::Range<usize>,
471 range: std::ops::Range<usize>,
330− // Position of the (optional) newly inserted line break.
472+ new_line_byte_pos: Option<usize>,
331− // Given as (line, byte_pos)
332− new_line_break: Option<(usize, usize)>,
333−) -> IndentQueryResult {
473+) -> IndentQueryResult<'a> {
334 let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
474 let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
335 let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
475 let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
336 cursor.set_byte_range(range);
476 cursor.set_byte_range(range);
477+
337 // Iterate over all captures from the query
478 // Iterate over all captures from the query
338 for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
479 for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
339 // Skip matches where not all custom predicates are fulfilled
480 // Skip matches where not all custom predicates are fulfilled
@@ -360,21 +501,13 @@ fn query_indents(
360 Some(QueryPredicateArg::Capture(capt1)),
501 Some(QueryPredicateArg::Capture(capt1)),
361 Some(QueryPredicateArg::Capture(capt2))
502 Some(QueryPredicateArg::Capture(capt2))
362 ) => {
503 ) => {
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− };
373 let n1 = m.nodes_for_capture_index(*capt1).next();
504 let n1 = m.nodes_for_capture_index(*capt1).next();
374 let n2 = m.nodes_for_capture_index(*capt2).next();
505 let n2 = m.nodes_for_capture_index(*capt2).next();
375 match (n1, n2) {
506 match (n1, n2) {
376 (Some(n1), Some(n2)) => {
507 (Some(n1), Some(n2)) => {
377− let same_line = get_line_num(n1)==get_line_num(n2);
508+ let n1_line = get_node_start_line(n1, new_line_byte_pos);
509+ let n2_line = get_node_start_line(n2, new_line_byte_pos);
510+ let same_line = n1_line == n2_line;
378 same_line==(pred.operator.as_ref()=="same-line?")
511 same_line==(pred.operator.as_ref()=="same-line?")
379 }
512 }
380 _ => true,
513 _ => true,
@@ -385,6 +518,23 @@ fn query_indents(
385 }
518 }
386 }
519 }
387 }
520 }
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+ },
388 _ => {
538 _ => {
389 panic!(
539 panic!(
390 "Invalid indent query: Unknown predicate (\"{}\")",
540 "Invalid indent query: Unknown predicate (\"{}\")",
@@ -395,11 +545,28 @@ fn query_indents(
395 }) {
545 }) {
396 continue;
546 continue;
397 }
547 }
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;
398 for capture in m.captures {
553 for capture in m.captures {
399 let capture_name = query.capture_names()[capture.index as usize].as_str();
554 let capture_name = query.capture_names()[capture.index as usize].as_str();
400 let capture_type = match capture_name {
555 let capture_type = match capture_name {
401 "indent" => IndentCaptureType::Indent,
556 "indent" => IndentCaptureType::Indent,
557+ "indent.always" => IndentCaptureType::IndentAlways,
402 "outdent" => IndentCaptureType::Outdent,
558 "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+ }
403 "extend" => {
570 "extend" => {
404 extend_captures
571 extend_captures
405 .entry(capture.node.id())
572 .entry(capture.node.id())
@@ -449,17 +616,40 @@ fn query_indents(
449 }
616 }
450 }
617 }
451 }
618 }
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+ }
452 indent_captures
638 indent_captures
453− .entry(capture.node.id())
639+ .entry(node_id)
454− // Most entries only need to contain a single IndentCapture
455 .or_insert_with(|| Vec::with_capacity(1))
640 .or_insert_with(|| Vec::with_capacity(1))
456− .push(indent_capture);
641+ .push(capture);
457 }
642 }
458 }
643 }
644+
459− IndentQueryResult {
645+ let result = IndentQueryResult {
460 indent_captures,
646 indent_captures,
461 extend_captures,
647 extend_captures,
462− }
648+ };
649+
650+ log::trace!("indent result = {:?}", result);
651+
652+ result
463}
653}
464
654
465/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
655/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
@@ -529,61 +719,27 @@ fn extend_nodes<'a>(
529 }
719 }
530}
720}
531
721
532−/// Use the syntax tree to determine the indentation for a given position.
722+/// Prepare an indent query by computing:
533−/// This can be used in 2 ways:
723+/// - The node from which to start the query (this is non-trivial due to `@extend` captures)
534−///
724+/// - The indent captures for all relevant nodes.
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−/// ```
568#[allow(clippy::too_many_arguments)]
725#[allow(clippy::too_many_arguments)]
569−pub fn treesitter_indent_for_pos(
726+fn init_indent_query<'a, 'b>(
570 query: &Query,
727 query: &Query,
571− syntax: &Syntax,
728+ syntax: &'a Syntax,
572− indent_style: &IndentStyle,
729+ text: RopeSlice<'b>,
573 tab_width: usize,
730 tab_width: usize,
574 indent_width: usize,
731 indent_width: usize,
575− text: RopeSlice,
576 line: usize,
732 line: usize,
577− pos: usize,
733+ byte_pos: usize,
578− new_line: bool,
734+ new_line_byte_pos: Option<usize>,
579−) -> Option<String> {
735+) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
580− let byte_pos = text.char_to_byte(pos);
581 // The innermost tree-sitter node which is considered for the indent
736 // The innermost tree-sitter node which is considered for the indent
582 // computation. It may change if some predeceding node is extended
737 // computation. It may change if some predeceding node is extended
583 let mut node = syntax
738 let mut node = syntax
584 .tree()
739 .tree()
585 .root_node()
740 .root_node()
586 .descendant_for_byte_range(byte_pos, byte_pos)?;
741 .descendant_for_byte_range(byte_pos, byte_pos)?;
742+
587 let (query_result, deepest_preceding) = {
743 let (query_result, deepest_preceding) = {
588 // The query range should intersect with all nodes directly preceding
744 // The query range should intersect with all nodes directly preceding
589 // the position of the indent query in case one of them is extended.
745 // the position of the indent query in case one of them is extended.
@@ -614,13 +770,12 @@ pub fn treesitter_indent_for_pos(
614 &mut cursor,
770 &mut cursor,
615 text,
771 text,
616 query_range,
772 query_range,
617− new_line.then_some((line, byte_pos)),
773+ new_line_byte_pos,
618 );
774 );
619 ts_parser.cursors.push(cursor);
775 ts_parser.cursors.push(cursor);
620 (query_result, deepest_preceding)
776 (query_result, deepest_preceding)
621 })
777 })
622 };
778 };
623− let indent_captures = query_result.indent_captures;
624 let extend_captures = query_result.extend_captures;
779 let extend_captures = query_result.extend_captures;
625
780
626 // Check for extend captures, potentially changing the node that the indent calculation starts with
781 // Check for extend captures, potentially changing the node that the indent calculation starts with
@@ -635,6 +790,68 @@ pub fn treesitter_indent_for_pos(
635 indent_width,
790 indent_width,
636 );
791 );
637 }
792 }
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+ )?;
638 let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
855 let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
639
856
640 let mut result = Indentation::default();
857 let mut result = Indentation::default();
@@ -642,12 +859,16 @@ pub fn treesitter_indent_for_pos(
642 // even if there are multiple "indent" nodes on the same line
859 // even if there are multiple "indent" nodes on the same line
643 let mut indent_for_line = Indentation::default();
860 let mut indent_for_line = Indentation::default();
644 let mut indent_for_line_below = Indentation::default();
861 let mut indent_for_line_below = Indentation::default();
862+
645 loop {
863 loop {
646 // This can safely be unwrapped because `first_in_line` contains
864 // This can safely be unwrapped because `first_in_line` contains
647 // one entry for each ancestor of the node (which is what we iterate over)
865 // one entry for each ancestor of the node (which is what we iterate over)
648 let is_first = *first_in_line.last().unwrap();
866 let is_first = *first_in_line.last().unwrap();
867+
649− // Apply all indent definitions for this node
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.
650− if let Some(definitions) = indent_captures.get(&node.id()) {
871+ if let Some(definitions) = indent_captures.remove(&node.id()) {
651 for definition in definitions {
872 for definition in definitions {
652 match definition.scope {
873 match definition.scope {
653 IndentScope::All => {
874 IndentScope::All => {
@@ -665,28 +886,22 @@ pub fn treesitter_indent_for_pos(
665 }
886 }
666
887
667 if let Some(parent) = node.parent() {
888 if let Some(parent) = node.parent() {
668− let mut node_line = node.start_position().row;
889+ let node_line = get_node_start_line(node, new_line_byte_pos);
669− let mut parent_line = parent.start_position().row;
890+ let parent_line = get_node_start_line(parent, new_line_byte_pos);
670− if node_line == line && new_line {
891+
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− };
679 if node_line != parent_line {
892 if node_line != parent_line {
893+ // Don't add indent for the line below the line of the query
680 if node_line < line + (new_line as usize) {
894 if node_line < line + (new_line as usize) {
681− // Don't add indent for the line below the line of the query
682− result.add_line(&indent_for_line_below);
895+ result.add_line(indent_for_line_below);
683 }
896 }
897+
684 if node_line == parent_line + 1 {
898 if node_line == parent_line + 1 {
685 indent_for_line_below = indent_for_line;
899 indent_for_line_below = indent_for_line;
686 } else {
900 } else {
687− result.add_line(&indent_for_line);
901+ result.add_line(indent_for_line);
688 indent_for_line_below = Indentation::default();
902 indent_for_line_below = Indentation::default();
689 }
903 }
904+
690 indent_for_line = Indentation::default();
905 indent_for_line = Indentation::default();
691 }
906 }
692
907
@@ -698,13 +913,13 @@ pub fn treesitter_indent_for_pos(
698 if (node.start_position().row < line)
913 if (node.start_position().row < line)
699 || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
914 || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
700 {
915 {
701− result.add_line(&indent_for_line_below);
916+ result.add_line(indent_for_line_below);
702 }
917 }
703− result.add_line(&indent_for_line);
918+ result.add_line(indent_for_line);
704 break;
919 break;
705 }
920 }
706 }
921 }
707− Some(result.as_string(indent_style))
922+ Some(result)
708}
923}
709
924
710/// Returns the indentation for a new line.
925/// Returns the indentation for a new line.
@@ -713,6 +928,7 @@ pub fn treesitter_indent_for_pos(
713pub fn indent_for_newline(
928pub fn indent_for_newline(
714 language_config: Option<&LanguageConfiguration>,
929 language_config: Option<&LanguageConfiguration>,
715 syntax: Option<&Syntax>,
930 syntax: Option<&Syntax>,
931+ indent_heuristic: &IndentationHeuristic,
716 indent_style: &IndentStyle,
932 indent_style: &IndentStyle,
717 tab_width: usize,
933 tab_width: usize,
718 text: RopeSlice,
934 text: RopeSlice,
@@ -721,14 +937,18 @@ pub fn indent_for_newline(
721 current_line: usize,
937 current_line: usize,
722) -> String {
938) -> String {
723 let indent_width = indent_style.indent_width(tab_width);
939 let indent_width = indent_style.indent_width(tab_width);
940+ if let (
941+ IndentationHeuristic::TreeSitter | IndentationHeuristic::Hybrid,
942+ Some(query),
724− if let (Some(query), Some(syntax)) = (
943+ Some(syntax),
944+ ) = (
945+ indent_heuristic,
725 language_config.and_then(|config| config.indent_query()),
946 language_config.and_then(|config| config.indent_query()),
726 syntax,
947 syntax,
727 ) {
948 ) {
728 if let Some(indent) = treesitter_indent_for_pos(
949 if let Some(indent) = treesitter_indent_for_pos(
729 query,
950 query,
730 syntax,
951 syntax,
731− indent_style,
732 tab_width,
952 tab_width,
733 indent_width,
953 indent_width,
734 text,
954 text,
@@ -736,9 +956,57 @@ pub fn indent_for_newline(
736 line_before_end_pos,
956 line_before_end_pos,
737 true,
957 true,
738 ) {
958 ) {
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+ })() {
739− return indent;
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);
740 };
1007 };
741 }
1008 }
1009+ // Fallback in case we either don't have indent queries or they failed for some reason
742 let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
1010 let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
743 indent_style.as_str().repeat(indent_level)
1011 indent_style.as_str().repeat(indent_level)
744}
1012}
@@ -810,4 +1078,195 @@ mod test {
810 2
1078 2
811 );
1079 );
812 }
1080 }
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+ }
813}
1272}
helix-core/src/lib.rs+1−1
@@ -7,6 +7,7 @@ pub mod config;
7pub mod diagnostic;
7pub mod diagnostic;
8pub mod diff;
8pub mod diff;
9pub mod doc_formatter;
9pub mod doc_formatter;
10+pub mod fuzzy;
10pub mod graphemes;
11pub mod graphemes;
11pub mod history;
12pub mod history;
12pub mod increment;
13pub mod increment;
@@ -18,7 +19,6 @@ pub mod movement;
18pub mod object;
19pub mod object;
19pub mod path;
20pub mod path;
20mod position;
21mod position;
21−pub mod register;
22pub mod search;
22pub mod search;
23pub mod selection;
23pub mod selection;
24pub mod shellwords;
24pub mod shellwords;
helix-core/src/match_brackets.rs+6−2
@@ -106,12 +106,16 @@ fn find_pair(
106 for close in
106 for close in
107 iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
107 iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
108 {
108 {
109− let Some(open) = as_close_pair(doc, &close) else { continue; };
109+ let Some(open) = as_close_pair(doc, &close) else {
110+ continue;
111+ };
110 if find_pair_end(doc, Some(node), open, Backward).is_some() {
112 if find_pair_end(doc, Some(node), open, Backward).is_some() {
111 return doc.try_byte_to_char(close.start_byte()).ok();
113 return doc.try_byte_to_char(close.start_byte()).ok();
112 }
114 }
113 }
115 }
114− let Some(parent) = node.parent() else { break; };
116+ let Some(parent) = node.parent() else {
117+ break;
118+ };
115 node = parent;
119 node = parent;
116 }
120 }
117 let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
121 let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
helix-core/src/movement.rs+80−1
@@ -16,7 +16,7 @@ use crate::{
16 syntax::LanguageConfiguration,
16 syntax::LanguageConfiguration,
17 text_annotations::TextAnnotations,
17 text_annotations::TextAnnotations,
18 textobject::TextObject,
18 textobject::TextObject,
19− visual_offset_from_block, Range, RopeSlice,
19+ visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
20};
20};
21
21
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
556 last_range
556 last_range
557}
557}
558
558
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+
559#[cfg(test)]
638#[cfg(test)]
560mod test {
639mod test {
561 use ropey::Rope;
640 use ropey::Rope;
helix-core/src/path.rs+2−2
@@ -85,7 +85,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
85///
85///
86/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
86/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
87/// here if the path exists, just normalize it's components.
87/// here if the path exists, just normalize it's components.
88−pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
88+pub fn get_canonicalized_path(path: &Path) -> PathBuf {
89 let path = expand_tilde(path);
89 let path = expand_tilde(path);
90 let path = if path.is_relative() {
90 let path = if path.is_relative() {
91 helix_loader::current_working_dir().join(path)
91 helix_loader::current_working_dir().join(path)
@@ -93,7 +93,7 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
93 path
93 path
94 };
94 };
95
95
96− Ok(get_normalized_path(path.as_path()))
96+ get_normalized_path(path.as_path())
97}
97}
98
98
99pub fn get_relative_path(path: &Path) -> PathBuf {
99pub fn get_relative_path(path: &Path) -> PathBuf {
helix-core/src/register.rsdeleted−89
@@ -1,89 +0,0 @@
1−use std::collections::HashMap;
2−
3−#[derive(Debug)]
4−pub struct Register {
5− name: char,
6− values: Vec<String>,
7−}
8−
9−impl Register {
10− pub const fn new(name: char) -> Self {
11− Self {
12− name,
13− values: Vec::new(),
14− }
15− }
16−
17− pub fn new_with_values(name: char, values: Vec<String>) -> Self {
18− Self { name, values }
19− }
20−
21− pub const fn name(&self) -> char {
22− self.name
23− }
24−
25− pub fn read(&self) -> &[String] {
26− &self.values
27− }
28−
29− pub fn write(&mut self, values: Vec<String>) {
30− self.values = values;
31− }
32−
33− pub fn push(&mut self, value: String) {
34− self.values.push(value);
35− }
36−}
37−
38−/// Currently just wraps a `HashMap` of `Register`s
39−#[derive(Debug, Default)]
40−pub struct Registers {
41− inner: HashMap<char, Register>,
42−}
43−
44−impl Registers {
45− pub fn get(&self, name: char) -> Option<&Register> {
46− self.inner.get(&name)
47− }
48−
49− pub fn read(&self, name: char) -> Option<&[String]> {
50− self.get(name).map(|reg| reg.read())
51− }
52−
53− pub fn write(&mut self, name: char, values: Vec<String>) {
54− if name != '_' {
55− self.inner
56− .insert(name, Register::new_with_values(name, values));
57− }
58− }
59−
60− pub fn push(&mut self, name: char, value: String) {
61− if name != '_' {
62− if let Some(r) = self.inner.get_mut(&name) {
63− r.push(value);
64− } else {
65− self.write(name, vec![value]);
66− }
67− }
68− }
69−
70− pub fn first(&self, name: char) -> Option<&String> {
71− self.read(name).and_then(|entries| entries.first())
72− }
73−
74− pub fn last(&self, name: char) -> Option<&String> {
75− self.read(name).and_then(|entries| entries.last())
76− }
77−
78− pub fn inner(&self) -> &HashMap<char, Register> {
79− &self.inner
80− }
81−
82− pub fn clear(&mut self) {
83− self.inner.clear();
84− }
85−
86− pub fn remove(&mut self, name: char) -> Option<Register> {
87− self.inner.remove(&name)
88− }
89−}
helix-core/src/selection.rs+10−2
@@ -630,11 +630,19 @@ impl Selection {
630 self.transform(|range| Range::point(range.cursor(text)))
630 self.transform(|range| Range::point(range.cursor(text)))
631 }
631 }
632
632
633− pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
633+ pub fn fragments<'a>(
634+ &'a self,
635+ text: RopeSlice<'a>,
636+ ) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<str>> + 'a
637+ {
634 self.ranges.iter().map(move |range| range.fragment(text))
638 self.ranges.iter().map(move |range| range.fragment(text))
635 }
639 }
636
640
637− pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a {
641+ pub fn slices<'a>(
642+ &'a self,
643+ text: RopeSlice<'a>,
644+ ) -> impl DoubleEndedIterator<Item = RopeSlice<'a>> + ExactSizeIterator<Item = RopeSlice<'a>> + 'a
645+ {
638 self.ranges.iter().map(move |range| range.slice(text))
646 self.ranges.iter().map(move |range| range.slice(text))
639 }
647 }
640
648
helix-core/src/syntax.rs+24−8
@@ -101,7 +101,8 @@ pub struct LanguageConfiguration {
101 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
101 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
102 #[serde(default)]
102 #[serde(default)]
103 pub shebangs: Vec<String>, // interpreter(s) associated with language
103 pub shebangs: Vec<String>, // interpreter(s) associated with language
104+ #[serde(default)]
104− pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
105+ pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
105 pub comment_token: Option<String>,
106 pub comment_token: Option<String>,
106 pub text_width: Option<usize>,
107 pub text_width: Option<usize>,
107 pub soft_wrap: Option<SoftWrap>,
108 pub soft_wrap: Option<SoftWrap>,
@@ -154,6 +155,8 @@ pub struct LanguageConfiguration {
154 /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
155 /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
155 /// Falling back to the current working directory if none are configured.
156 /// Falling back to the current working directory if none are configured.
156 pub workspace_lsp_roots: Option<Vec<PathBuf>>,
157 pub workspace_lsp_roots: Option<Vec<PathBuf>>,
158+ #[serde(default)]
159+ pub persistent_diagnostic_sources: Vec<String>,
157}
160}
158
161
159#[derive(Debug, PartialEq, Eq, Hash)]
162#[derive(Debug, PartialEq, Eq, Hash)]
@@ -211,10 +214,7 @@ impl<'de> Deserialize<'de> for FileType {
211 {
214 {
212 match map.next_entry::<String, String>()? {
215 match map.next_entry::<String, String>()? {
213 Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
216 Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
214− // FIXME: use `suffix.replace('/', std::path::MAIN_SEPARATOR_STR)`
217+ 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))
218 })),
218 })),
219 Some((key, _value)) => Err(serde::de::Error::custom(format!(
219 Some((key, _value)) => Err(serde::de::Error::custom(format!(
220 "unknown key in `file-types` list: {}",
220 "unknown key in `file-types` list: {}",
@@ -444,6 +444,22 @@ pub struct IndentationConfiguration {
444 pub unit: String,
444 pub unit: String,
445}
445}
446
446
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+
447/// Configuration for auto pairs
463/// Configuration for auto pairs
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
465#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
@@ -962,7 +978,7 @@ impl Syntax {
962 let res = syntax.update(source, source, &ChangeSet::new(source));
978 let res = syntax.update(source, source, &ChangeSet::new(source));
963
979
964 if res.is_err() {
980 if res.is_err() {
965− log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
981+ log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
966 return None;
982 return None;
967 }
983 }
968 Some(syntax)
984 Some(syntax)
@@ -1614,7 +1630,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
1614}
1630}
1615
1631
1616pub struct RopeProvider<'a>(pub RopeSlice<'a>);
1632pub struct RopeProvider<'a>(pub RopeSlice<'a>);
1617−impl<'a> TextProvider<'a> for RopeProvider<'a> {
1633+impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
1618 type I = ChunksBytes<'a>;
1634 type I = ChunksBytes<'a>;
1619
1635
1620 fn text(&mut self, node: Node) -> Self::I {
1636 fn text(&mut self, node: Node) -> Self::I {
@@ -1628,7 +1644,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
1628struct HighlightIterLayer<'a> {
1644struct HighlightIterLayer<'a> {
1629 _tree: Option<Tree>,
1645 _tree: Option<Tree>,
1630 cursor: QueryCursor,
1646 cursor: QueryCursor,
1631− captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
1647+ captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
1632 config: &'a HighlightConfiguration,
1648 config: &'a HighlightConfiguration,
1633 highlight_end_stack: Vec<usize>,
1649 highlight_end_stack: Vec<usize>,
1634 scope_stack: Vec<LocalScope<'a>>,
1650 scope_stack: Vec<LocalScope<'a>>,
helix-core/src/transaction.rs+77−15
@@ -1,7 +1,7 @@
1use ropey::RopeSlice;
1use ropey::RopeSlice;
2use smallvec::SmallVec;
2use smallvec::SmallVec;
3
3
4−use crate::{Range, Rope, Selection, Tendril};
4+use crate::{chars::char_is_word, Range, Rope, Selection, Tendril};
5use std::{borrow::Cow, iter::once};
5use std::{borrow::Cow, iter::once};
6
6
7/// (from, to, replacement)
7/// (from, to, replacement)
@@ -23,6 +23,30 @@ pub enum Operation {
23pub enum Assoc {
23pub enum Assoc {
24 Before,
24 Before,
25 After,
25 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+ }
26}
50}
27
51
28#[derive(Debug, Default, Clone, PartialEq, Eq)]
52#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -389,7 +413,10 @@ impl ChangeSet {
389 }
413 }
390
414
391 let Some((i, change)) = iter.next() else {
415 let Some((i, change)) = iter.next() else {
416+ map!(
392− map!(|pos, _| (old_pos == pos).then_some(new_pos), self.changes.len());
417+ |pos, _| (old_pos == pos).then_some(new_pos),
418+ self.changes.len()
419+ );
393 break;
420 break;
394 };
421 };
395
422
@@ -412,8 +439,6 @@ impl ChangeSet {
412 map!(|pos, _| (old_end > pos).then_some(new_pos), i);
439 map!(|pos, _| (old_end > pos).then_some(new_pos), i);
413 }
440 }
414 Insert(s) => {
441 Insert(s) => {
415− let ins = s.chars().count();
416−
417 // a subsequent delete means a replace, consume it
442 // a subsequent delete means a replace, consume it
418 if let Some((_, Delete(len))) = iter.peek() {
443 if let Some((_, Delete(len))) = iter.peek() {
419 iter.next();
444 iter.next();
@@ -421,13 +446,13 @@ impl ChangeSet {
421 old_end = old_pos + len;
446 old_end = old_pos + len;
422 // in range of replaced text
447 // in range of replaced text
423 map!(
448 map!(
424− |pos, assoc| (old_end > pos).then(|| {
449+ |pos, assoc: Assoc| (old_end > pos).then(|| {
425 // at point or tracking before
450 // at point or tracking before
426− if pos == old_pos || assoc == Assoc::Before {
451+ if pos == old_pos && assoc.stay_at_gaps() {
427 new_pos
452 new_pos
428 } else {
453 } else {
429 // place to end of insert
454 // place to end of insert
430− new_pos + ins
455+ new_pos + assoc.insert_offset(s)
431 }
456 }
432 }),
457 }),
433 i
458 i
@@ -435,20 +460,15 @@ impl ChangeSet {
435 } else {
460 } else {
436 // at insert point
461 // at insert point
437 map!(
462 map!(
438− |pos, assoc| (old_pos == pos).then(|| {
463+ |pos, assoc: Assoc| (old_pos == pos).then(|| {
439 // return position before inserted text
464 // return position before inserted text
440− if assoc == Assoc::Before {
465+ new_pos + assoc.insert_offset(s)
441− new_pos
442− } else {
443− // after text
444− new_pos + ins
445− }
446 }),
466 }),
447 i
467 i
448 );
468 );
449 }
469 }
450
470
451− new_pos += ins;
471+ new_pos += s.chars().count();
452 }
472 }
453 }
473 }
454 old_pos = old_end;
474 old_pos = old_end;
@@ -881,6 +901,48 @@ mod test {
881 let mut positions = [4, 2];
901 let mut positions = [4, 2];
882 cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
902 cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
883 assert_eq!(positions, [4, 2]);
903 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);
884 }
946 }
885
947
886 #[test]
948 #[test]
helix-core/src/wrap.rs+3−1
@@ -1,7 +1,9 @@
1use smartstring::{LazyCompact, SmartString};
1use smartstring::{LazyCompact, SmartString};
2+use textwrap::{Options, WordSplitter::NoHyphenation};
2
3
3/// Given a slice of text, return the text re-wrapped to fit it
4/// Given a slice of text, return the text re-wrapped to fit it
4/// within the given width.
5/// within the given width.
5pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
6pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
7+ let options = Options::new(text_width).word_splitter(NoHyphenation);
6− textwrap::refill(text, text_width).into()
8+ textwrap::refill(text, options).into()
7}
9}
helix-core/tests/data/indent/cpp.cppnew file+48
@@ -0,0 +1,48 @@
1+std::vector<std::string>
2+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
3+ char* parm5, bool parm6);
4+
5+std::vector<std::string>
6+fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
7+ char* parm5, bool parm6) {
8+ auto lambda = []() {
9+ return 0;
10+ };
11+ auto lambda_with_a_really_long_name_that_uses_a_whole_line
12+ = [](int some_more_aligned_parameters,
13+ std::string parm2) {
14+ do_smth();
15+ };
16+ if (brace_on_same_line) {
17+ do_smth();
18+ } else if (brace_on_next_line)
19+ {
20+ do_smth();
21+ } else if (another_condition) {
22+ do_smth();
23+ }
24+ else {
25+ do_smth();
26+ }
27+ if (inline_if_statement)
28+ do_smth();
29+ if (another_inline_if_statement)
30+ return [](int parm1, char* parm2) {
31+ this_is_a_really_pointless_lambda();
32+ };
33+
34+ switch (var) {
35+ case true:
36+ return -1;
37+ case false:
38+ return 42;
39+ }
40+}
41+
42+class MyClass : public MyBaseClass {
43+public:
44+ MyClass();
45+ void public_fn();
46+private:
47+ super_secret_private_fn();
48+}
helix-core/tests/data/indent/indent.rsdeleted−1
@@ -1 +0,0 @@
1−../../../src/indent.rs
helix-core/tests/data/indent/languages.toml+13
@@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " }
11[[grammar]]
11[[grammar]]
12name = "rust"
12name = "rust"
13source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
13source = { 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+151−23
@@ -1,20 +1,122 @@
1use helix_core::{
1use helix_core::{
2 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
2 indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
3− syntax::Loader,
3+ syntax::{Configuration, Loader},
4 Syntax,
4 Syntax,
5};
5};
6+use ropey::Rope;
6−use std::path::PathBuf;
7+use std::{ops::Range, path::PathBuf, process::Command};
7
8
8#[test]
9#[test]
9fn test_treesitter_indent_rust() {
10fn test_treesitter_indent_rust() {
10− test_treesitter_indent("rust.rs", "source.rust");
11+ standard_treesitter_test("rust.rs", "source.rust");
11}
12}
13+
14+#[test]
15+fn test_treesitter_indent_cpp() {
16+ standard_treesitter_test("cpp.cpp", "source.cpp");
17+}
18+
12#[test]
19#[test]
13−fn test_treesitter_indent_rust_2() {
20+fn test_treesitter_indent_rust_helix() {
14− test_treesitter_indent("indent.rs", "source.rust");
21+ // We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
15− // TODO Use commands.rs as indentation test.
22+ // Ideally, someone updates this once in a while and fixes any errors that occur.
16− // Currently this fails because we can't align the parameters of a closure yet
23+ let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
17− // test_treesitter_indent("commands.rs", "source.rust");
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+ }
18}
120}
19
121
20#[test]
122#[test]
@@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
50 assert_eq!(indent_level, 2)
152 assert_eq!(indent_level, 2)
51}
153}
52
154
53−fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
155+fn indent_tests_dir() -> PathBuf {
54 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
156 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
55 test_dir.push("tests/data/indent");
157 test_dir.push("tests/data/indent");
158+ test_dir
159+}
56
160
57− let mut test_file = test_dir.clone();
161+fn indent_test_path(name: &str) -> PathBuf {
58− test_file.push(file_name);
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);
59− let test_file = std::fs::File::open(test_file).unwrap();
176+ let test_file = std::fs::File::open(test_path).unwrap();
60 let doc = ropey::Rope::from_reader(test_file).unwrap();
177 let doc = ropey::Rope::from_reader(test_file).unwrap();
178+ test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
179+}
61
180
62− let mut config_file = test_dir;
181+/// Test that all the lines in the given file are indented as expected.
63− config_file.push("languages.toml");
182+/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
64− let config = std::fs::read_to_string(config_file).unwrap();
183+fn test_treesitter_indent(
65− let config = toml::from_str(&config).unwrap();
184+ test_name: &str,
185+ doc: Rope,
186+ lang_scope: &str,
187+ ignored_lines: Vec<std::ops::Range<usize>>,
188+) {
66− let loader = Loader::new(config);
189+ let loader = Loader::new(indent_tests_config());
67
190
68 // set runtime path so we can find the queries
191 // set runtime path so we can find the queries
69 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
192 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -71,6 +194,7 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
71 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
194 std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
72
195
73 let language_config = loader.language_config_for_scope(lang_scope).unwrap();
196 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);
74 let highlight_config = language_config.highlight_config(&[]).unwrap();
198 let highlight_config = language_config.highlight_config(&[]).unwrap();
75 let text = doc.slice(..);
199 let text = doc.slice(..);
76 let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
200 let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
@@ -78,23 +202,27 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
78
202
79 for i in 0..doc.len_lines() {
203 for i in 0..doc.len_lines() {
80 let line = text.line(i);
204 let line = text.line(i);
205+ if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
206+ continue;
207+ }
81 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
208 if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
82− let tab_and_indent_width: usize = 4;
209+ let tab_width: usize = 4;
83 let suggested_indent = treesitter_indent_for_pos(
210 let suggested_indent = treesitter_indent_for_pos(
84 indent_query,
211 indent_query,
85 &syntax,
212 &syntax,
86− &IndentStyle::Spaces(tab_and_indent_width as u8),
213+ tab_width,
87− tab_and_indent_width,
214+ indent_style.indent_width(tab_width),
88− tab_and_indent_width,
89 text,
215 text,
90 i,
216 i,
91 text.line_to_char(i) + pos,
217 text.line_to_char(i) + pos,
92 false,
218 false,
93 )
219 )
94− .unwrap();
220+ .unwrap()
221+ .to_string(&indent_style, tab_width);
95 assert!(
222 assert!(
96 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
223 line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
97− "Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
224+ "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
225+ test_name,
98 i+1,
226 i+1,
99 line.slice(..line.len_chars()-1),
227 line.slice(..line.len_chars()-1),
100 suggested_indent,
228 suggested_indent,
helix-dap/Cargo.toml+11−9
@@ -1,25 +1,27 @@
1[package]
1[package]
2name = "helix-dap"
2name = "helix-dap"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
6−license = "MPL-2.0"
7description = "DAP client implementation for Helix project"
3description = "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
8−categories = ["editor"]
9+categories.workspace = true
9−repository = "https://github.com/helix-editor/helix"
10+repository.workspace = true
10−homepage = "https://helix-editor.com"
11+homepage.workspace = true
11
12
12# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13
14
14[dependencies]
15[dependencies]
15−helix-core = { version = "0.6", path = "../helix-core" }
16+helix-core = { path = "../helix-core" }
17+
16anyhow = "1.0"
18anyhow = "1.0"
17log = "0.4"
19log = "0.4"
18serde = { version = "1.0", features = ["derive"] }
20serde = { version = "1.0", features = ["derive"] }
19serde_json = "1.0"
21serde_json = "1.0"
20thiserror = "1.0"
22thiserror = "1.0"
21tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
23tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
22−which = "4.4"
24+which = "5.0.0"
23
25
24[dev-dependencies]
26[dev-dependencies]
25fern = "0.6"
27fern = "0.6"
helix-event/Cargo.tomlnew file+16
@@ -0,0 +1,16 @@
1+[package]
2+name = "helix-event"
3+version.workspace = true
4+authors.workspace = true
5+edition.workspace = true
6+license.workspace = true
7+rust-version.workspace = true
8+categories.workspace = true
9+repository.workspace = true
10+homepage.workspace = true
11+
12+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13+
14+[dependencies]
15+tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
16+parking_lot = { version = "0.12", features = ["send_guard"] }
helix-event/src/lib.rsnew file+8
@@ -0,0 +1,8 @@
1+//! `helix-event` contains systems that allow (often async) communication between
2+//! different editor components without strongly coupling them. Currently this
3+//! crate only contains some smaller facilities but the intend is to add more
4+//! functionality in the future ( like a generic hook system)
5+
6+pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
7+
8+mod redraw;
helix-event/src/redraw.rsnew file+49
@@ -0,0 +1,49 @@
1+//! Signals that control when/if the editor redraws
2+
3+use std::future::Future;
4+
5+use parking_lot::{RwLock, RwLockReadGuard};
6+use tokio::sync::Notify;
7+
8+/// A `Notify` instance that can be used to (asynchronously) request
9+/// the editor the render a new frame.
10+static REDRAW_NOTIFY: Notify = Notify::const_new();
11+
12+/// A `RwLock` that prevents the next frame from being
13+/// drawn until an exclusive (write) lock can be acquired.
14+/// This allows asynchsonous tasks to acquire `non-exclusive`
15+/// locks (read) to prevent the next frame from being drawn
16+/// until a certain computation has finished.
17+static RENDER_LOCK: RwLock<()> = RwLock::new(());
18+
19+pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
20+
21+/// Requests that the editor is redrawn. The redraws are debounced (currently to
22+/// 30FPS) so this can be called many times without causing a ton of frames to
23+/// be rendered.
24+pub fn request_redraw() {
25+ REDRAW_NOTIFY.notify_one();
26+}
27+
28+/// Returns a future that will yield once a redraw has been asynchronously
29+/// requested using [`request_redraw`].
30+pub fn redraw_requested() -> impl Future<Output = ()> {
31+ REDRAW_NOTIFY.notified()
32+}
33+
34+/// Wait until all locks acquired with [`lock_frame`] have been released.
35+/// This function is called before rendering and is intended to allow the frame
36+/// to wait for async computations that should be included in the current frame.
37+pub fn start_frame() {
38+ drop(RENDER_LOCK.write());
39+ // exhaust any leftover redraw notifications
40+ let notify = REDRAW_NOTIFY.notified();
41+ tokio::pin!(notify);
42+ notify.enable();
43+}
44+
45+/// Acquires the render lock which will prevent the next frame from being drawn
46+/// until the returned guard is dropped.
47+pub fn lock_frame() -> RenderLockGuard {
48+ RENDER_LOCK.read()
49+}
helix-loader/Cargo.toml+13−12
@@ -1,13 +1,14 @@
1[package]
1[package]
2name = "helix-loader"
2name = "helix-loader"
3−version = "0.6.0"
3+description = "Build bootstrapping for Helix crates"
4−description = "A post-modern text editor."
4+version.workspace = true
5−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5+authors.workspace = true
6−edition = "2021"
6+edition.workspace = true
7−license = "MPL-2.0"
7+license.workspace = true
8+rust-version.workspace = true
8−categories = ["editor"]
9+categories.workspace = true
9−repository = "https://github.com/helix-editor/helix"
10+repository.workspace = true
10−homepage = "https://helix-editor.com"
11+homepage.workspace = true
11
12
12[[bin]]
13[[bin]]
13name = "hx-loader"
14name = "hx-loader"
@@ -18,17 +19,17 @@ anyhow = "1"
18serde = { version = "1.0", features = ["derive"] }
19serde = { version = "1.0", features = ["derive"] }
19toml = "0.7"
20toml = "0.7"
20etcetera = "0.8"
21etcetera = "0.8"
21−tree-sitter = "0.20"
22+tree-sitter.workspace = true
22−once_cell = "1.18"
23+once_cell = "1.19"
23log = "0.4"
24log = "0.4"
24−which = "4.4"
25+which = "5.0.0"
25
26
26# TODO: these two should be on !wasm32 only
27# TODO: these two should be on !wasm32 only
27
28
28# cloning/compiling tree-sitter grammars
29# cloning/compiling tree-sitter grammars
29cc = { version = "1" }
30cc = { version = "1" }
30threadpool = { version = "1.0" }
31threadpool = { version = "1.0" }
31−tempfile = "3.7.0"
32+tempfile = "3.8.1"
32dunce = "1.0.4"
33dunce = "1.0.4"
33
34
34[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
35[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
helix-loader/build.rs+20−6
@@ -2,7 +2,17 @@ use std::borrow::Cow;
2use std::path::Path;
2use std::path::Path;
3use std::process::Command;
3use std::process::Command;
4
4
5−const VERSION: &str = include_str!("../VERSION");
5+const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
6+const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
7+const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
8+
9+fn get_calver() -> String {
10+ if PATCH == "0" {
11+ format!("{MAJOR}.{MINOR}")
12+ } else {
13+ format!("{MAJOR}.{MINOR}.{PATCH}")
14+ }
15+}
6
16
7fn main() {
17fn main() {
8 let git_hash = Command::new("git")
18 let git_hash = Command::new("git")
@@ -12,9 +22,10 @@ fn main() {
12 .filter(|output| output.status.success())
22 .filter(|output| output.status.success())
13 .and_then(|x| String::from_utf8(x.stdout).ok());
23 .and_then(|x| String::from_utf8(x.stdout).ok());
14
24
25+ let calver = get_calver();
15 let version: Cow<_> = match &git_hash {
26 let version: Cow<_> = match &git_hash {
16− Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
27+ Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(),
17− None => VERSION.into(),
28+ None => calver.into(),
18 };
29 };
19
30
20 println!(
31 println!(
@@ -22,7 +33,6 @@ fn main() {
22 std::env::var("TARGET").unwrap()
33 std::env::var("TARGET").unwrap()
23 );
34 );
24
35
25− println!("cargo:rerun-if-changed=../VERSION");
26 println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
36 println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
27
37
28 if git_hash.is_none() {
38 if git_hash.is_none() {
@@ -40,7 +50,9 @@ fn main() {
40 .ok()
50 .ok()
41 .filter(|output| output.status.success())
51 .filter(|output| output.status.success())
42 .and_then(|x| String::from_utf8(x.stdout).ok())
52 .and_then(|x| String::from_utf8(x.stdout).ok())
53+ else {
43− else{ return; };
54+ return;
55+ };
44 // If heads starts pointing at something else (different branch)
56 // If heads starts pointing at something else (different branch)
45 // we need to return
57 // we need to return
46 let head = Path::new(&git_dir).join("HEAD");
58 let head = Path::new(&git_dir).join("HEAD");
@@ -55,7 +67,9 @@ fn main() {
55 .ok()
67 .ok()
56 .filter(|output| output.status.success())
68 .filter(|output| output.status.success())
57 .and_then(|x| String::from_utf8(x.stdout).ok())
69 .and_then(|x| String::from_utf8(x.stdout).ok())
70+ else {
58− else{ return; };
71+ return;
72+ };
59 let head_ref = Path::new(&git_dir).join(head_ref);
73 let head_ref = Path::new(&git_dir).join(head_ref);
60 if head_ref.exists() {
74 if head_ref.exists() {
61 println!("cargo:rerun-if-changed={}", head_ref.display());
75 println!("cargo:rerun-if-changed={}", head_ref.display());
helix-loader/src/lib.rs+13−4
@@ -33,9 +33,9 @@ pub fn current_working_dir() -> PathBuf {
33 path
33 path
34}
34}
35
35
36−pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> {
36+pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
37 let path = dunce::canonicalize(path)?;
37 let path = dunce::canonicalize(path)?;
38− std::env::set_current_dir(path.clone())?;
38+ std::env::set_current_dir(&path)?;
39 let mut cwd = CWD.write().unwrap();
39 let mut cwd = CWD.write().unwrap();
40 *cwd = Some(path);
40 *cwd = Some(path);
41 Ok(())
41 Ok(())
@@ -60,7 +60,8 @@ pub fn initialize_log_file(specified_file: Option<PathBuf>) {
60/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
60/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set)
61/// 2. subdirectory of user config directory (always included)
61/// 2. subdirectory of user config directory (always included)
62/// 3. `HELIX_RUNTIME` (if environment variable is set)
62/// 3. `HELIX_RUNTIME` (if environment variable is set)
63+/// 4. `HELIX_DEFAULT_RUNTIME` (if environment variable is set *at build time*)
63−/// 4. subdirectory of path to helix executable (always included)
64+/// 5. subdirectory of path to helix executable (always included)
64///
65///
65/// Postcondition: returns at least two paths (they might not exist).
66/// Postcondition: returns at least two paths (they might not exist).
66fn prioritize_runtime_dirs() -> Vec<PathBuf> {
67fn prioritize_runtime_dirs() -> Vec<PathBuf> {
@@ -81,6 +82,14 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
81 rt_dirs.push(dir.into());
82 rt_dirs.push(dir.into());
82 }
83 }
83
84
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+
84 // fallback to location of the executable being run
93 // fallback to location of the executable being run
85 // canonicalize the path in case the executable is symlinked
94 // canonicalize the path in case the executable is symlinked
86 let exe_rt_dir = std::env::current_exe()
95 let exe_rt_dir = std::env::current_exe()
@@ -280,7 +289,7 @@ mod merge_toml_tests {
280 let cwd = current_working_dir();
289 let cwd = current_working_dir();
281 assert_ne!(cwd, new_path);
290 assert_ne!(cwd, new_path);
282
291
283− set_current_working_dir(new_path.clone()).expect("Couldn't set new path");
292+ set_current_working_dir(&new_path).expect("Couldn't set new path");
284
293
285 let cwd = current_working_dir();
294 let cwd = current_working_dir();
286 assert_eq!(cwd, new_path);
295 assert_eq!(cwd, new_path);
helix-lsp/Cargo.toml+15−14
@@ -1,31 +1,32 @@
1[package]
1[package]
2name = "helix-lsp"
2name = "helix-lsp"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
7description = "LSP client implementation for Helix project"
3description = "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
8−categories = ["editor"]
9+categories.workspace = true
9−repository = "https://github.com/helix-editor/helix"
10+repository.workspace = true
10−homepage = "https://helix-editor.com"
11+homepage.workspace = true
11
12
12# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13
14
14[dependencies]
15[dependencies]
15−helix-core = { version = "0.6", path = "../helix-core" }
16+helix-core = { path = "../helix-core" }
16−helix-loader = { version = "0.6", path = "../helix-loader" }
17+helix-loader = { path = "../helix-loader" }
17−helix-parsec = { version = "0.6", path = "../helix-parsec" }
18+helix-parsec = { path = "../helix-parsec" }
18
19
19anyhow = "1.0"
20anyhow = "1.0"
20futures-executor = "0.3"
21futures-executor = "0.3"
21futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
22futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
22−globset = "0.4.11"
23+globset = "0.4.14"
23log = "0.4"
24log = "0.4"
24−lsp-types = { version = "0.94" }
25+lsp-types = { version = "0.95" }
25serde = { version = "1.0", features = ["derive"] }
26serde = { version = "1.0", features = ["derive"] }
26serde_json = "1.0"
27serde_json = "1.0"
27thiserror = "1.0"
28thiserror = "1.0"
28−tokio = { version = "1.29", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
29+tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
29tokio-stream = "0.1.14"
30tokio-stream = "0.1.14"
30−which = "4.4"
31+which = "5.0.0"
31parking_lot = "0.12.1"
32parking_lot = "0.12.1"
helix-lsp/src/client.rs+87−13
@@ -401,12 +401,22 @@ impl Client {
401 &self,
401 &self,
402 params: R::Params,
402 params: R::Params,
403 ) -> impl Future<Output = Result<Value>>
403 ) -> 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>>
404 where
415 where
405 R::Params: serde::Serialize,
416 R::Params: serde::Serialize,
406 {
417 {
407 let server_tx = self.server_tx.clone();
418 let server_tx = self.server_tx.clone();
408 let id = self.next_request_id();
419 let id = self.next_request_id();
409− let timeout_secs = self.req_timeout;
410
420
411 async move {
421 async move {
412 use std::time::Duration;
422 use std::time::Duration;
@@ -548,6 +558,11 @@ impl Client {
548 dynamic_registration: Some(true),
558 dynamic_registration: Some(true),
549 relative_pattern_support: Some(false),
559 relative_pattern_support: Some(false),
550 }),
560 }),
561+ file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities {
562+ will_rename: Some(true),
563+ did_rename: Some(true),
564+ ..Default::default()
565+ }),
551 ..Default::default()
566 ..Default::default()
552 }),
567 }),
553 text_document: Some(lsp::TextDocumentClientCapabilities {
568 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -652,6 +667,7 @@ impl Client {
652 version: Some(String::from(VERSION_AND_GIT_HASH)),
667 version: Some(String::from(VERSION_AND_GIT_HASH)),
653 }),
668 }),
654 locale: None, // TODO
669 locale: None, // TODO
670+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
655 };
671 };
656
672
657 self.request::<lsp::request::Initialize>(params).await
673 self.request::<lsp::request::Initialize>(params).await
@@ -700,6 +716,65 @@ impl Client {
700 })
716 })
701 }
717 }
702
718
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+
703 // -------------------------------------------------------------------------------------------
778 // -------------------------------------------------------------------------------------------
704 // Text document
779 // Text document
705 // -------------------------------------------------------------------------------------------
780 // -------------------------------------------------------------------------------------------
@@ -895,20 +970,19 @@ impl Client {
895 ) -> Option<impl Future<Output = Result<()>>> {
970 ) -> Option<impl Future<Output = Result<()>>> {
896 let capabilities = self.capabilities.get().unwrap();
971 let capabilities = self.capabilities.get().unwrap();
897
972
898− let include_text = match &capabilities.text_document_sync {
973+ let include_text = match &capabilities.text_document_sync.as_ref()? {
899− Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
974+ lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
900− save: Some(options),
975+ save: options,
901 ..
976 ..
902− })) => match options {
977+ }) => match options.as_ref()? {
903 lsp::TextDocumentSyncSaveOptions::Supported(true) => false,
978 lsp::TextDocumentSyncSaveOptions::Supported(true) => false,
904 lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions {
979 lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions {
905 include_text,
980 include_text,
906 }) => include_text.unwrap_or(false),
981 }) => include_text.unwrap_or(false),
907− // Supported(false)
982+ lsp::TextDocumentSyncSaveOptions::Supported(false) => return None,
908− _ => return None,
909 },
983 },
910− // unsupported
984+ // see: https://github.com/microsoft/language-server-protocol/issues/288
911− _ => return None,
985+ lsp::TextDocumentSyncCapability::Kind(..) => false,
912 };
986 };
913
987
914 Some(self.notify::<lsp::notification::DidSaveTextDocument>(
988 Some(self.notify::<lsp::notification::DidSaveTextDocument>(
@@ -971,12 +1045,12 @@ impl Client {
971 ) -> Option<impl Future<Output = Result<Value>>> {
1045 ) -> Option<impl Future<Output = Result<Value>>> {
972 let capabilities = self.capabilities.get().unwrap();
1046 let capabilities = self.capabilities.get().unwrap();
973
1047
974− // Return early if the server does not support resolving code action.
1048+ // Return early if the server does not support resolving code actions.
975− match capabilities.completion_provider {
1049+ match capabilities.code_action_provider {
976− Some(lsp::CompletionOptions {
1050+ Some(lsp::CodeActionProviderCapability::Options(lsp::CodeActionOptions {
977 resolve_provider: Some(true),
1051 resolve_provider: Some(true),
978 ..
1052 ..
979− }) => (),
1053+ })) => (),
980 _ => return None,
1054 _ => return None,
981 }
1055 }
982
1056
helix-lsp/src/lib.rs+33−29
@@ -435,7 +435,7 @@ pub mod util {
435 }
435 }
436
436
437 let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
437 let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
438− let Some(tabstops) = tabstops else{
438+ let Some(tabstops) = tabstops else {
439 // no tabstop normal mapping
439 // no tabstop normal mapping
440 mapped_selection.push(range);
440 mapped_selection.push(range);
441 continue;
441 continue;
@@ -445,36 +445,36 @@ pub mod util {
445 // the tabstop closest to the range simply replaces `head` while anchor remains in place
445 // the tabstop closest to the range simply replaces `head` while anchor remains in place
446 // the remaining tabstops receive their own single-width cursor
446 // the remaining tabstops receive their own single-width cursor
447 if range.head < range.anchor {
447 if range.head < range.anchor {
448+ let last_idx = tabstops.len() - 1;
448− let first_tabstop = tabstop_anchor + tabstops[0].1;
449+ let last_tabstop = tabstop_anchor + tabstops[last_idx].0;
449
450
450 // if selection is forward but was moved to the right it is
451 // if selection is forward but was moved to the right it is
451 // contained entirely in the replacement text, just do a point
452 // contained entirely in the replacement text, just do a point
452 // selection (fallback below)
453 // selection (fallback below)
453− if range.anchor >= first_tabstop {
454+ if range.anchor > last_tabstop {
454− let range = Range::new(range.anchor, first_tabstop);
455+ let range = Range::new(range.anchor, last_tabstop);
455 mapped_selection.push(range);
456 mapped_selection.push(range);
456− let rem_tabstops = tabstops[1..]
457+ let rem_tabstops = tabstops[..last_idx]
457 .iter()
458 .iter()
458− .map(|tabstop| Range::point(tabstop_anchor + tabstop.1));
459+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
459 mapped_selection.extend(rem_tabstops);
460 mapped_selection.extend(rem_tabstops);
460 continue;
461 continue;
461 }
462 }
462 } else {
463 } else {
463− let last_idx = tabstops.len() - 1;
464− let last_tabstop = tabstop_anchor + tabstops[last_idx].1;
464+ let first_tabstop = tabstop_anchor + tabstops[0].0;
465
465
466 // if selection is forward but was moved to the right it is
466 // if selection is forward but was moved to the right it is
467 // contained entirely in the replacement text, just do a point
467 // contained entirely in the replacement text, just do a point
468 // selection (fallback below)
468 // selection (fallback below)
469− if range.anchor <= last_tabstop {
469+ if range.anchor < first_tabstop {
470 // we can't properly compute the the next grapheme
470 // we can't properly compute the the next grapheme
471 // here because the transaction hasn't been applied yet
471 // here because the transaction hasn't been applied yet
472 // that is not a problem because the range gets grapheme aligned anyway
472 // that is not a problem because the range gets grapheme aligned anyway
473 // tough so just adding one will always cause head to be grapheme
473 // tough so just adding one will always cause head to be grapheme
474 // aligned correctly when applied to the document
474 // aligned correctly when applied to the document
475− let range = Range::new(range.anchor, last_tabstop + 1);
475+ let range = Range::new(range.anchor, first_tabstop + 1);
476 mapped_selection.push(range);
476 mapped_selection.push(range);
477− let rem_tabstops = tabstops[..last_idx]
477+ let rem_tabstops = tabstops[1..]
478 .iter()
478 .iter()
479 .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
479 .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
480 mapped_selection.extend(rem_tabstops);
480 mapped_selection.extend(rem_tabstops);
@@ -749,36 +749,40 @@ impl Registry {
749 }
749 }
750 }
750 }
751
751
752− pub fn get(
752+ pub fn get<'a>(
753− &mut self,
753+ &'a mut self,
754− language_config: &LanguageConfiguration,
754+ language_config: &'a LanguageConfiguration,
755− doc_path: Option<&std::path::PathBuf>,
755+ doc_path: Option<&'a std::path::PathBuf>,
756− root_dirs: &[PathBuf],
756+ root_dirs: &'a [PathBuf],
757 enable_snippets: bool,
757 enable_snippets: bool,
758− ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
758+ ) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
759− language_config
760− .language_servers
759+ language_config.language_servers.iter().map(
761− .iter()
762− .map(|LanguageServerFeatures { name, .. }| {
760+ move |LanguageServerFeatures { name, .. }| {
763 if let Some(clients) = self.inner.get(name) {
761 if let Some(clients) = self.inner.get(name) {
764 if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
762 if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
765 client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
763 client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
766 }) {
764 }) {
767− return Ok((name.to_owned(), client.clone()));
765+ return (name.to_owned(), Ok(client.clone()));
768 }
766 }
769 }
767 }
770− let client = self.start_client(
768+ match self.start_client(
771 name.clone(),
769 name.clone(),
772 language_config,
770 language_config,
773 doc_path,
771 doc_path,
774 root_dirs,
772 root_dirs,
775 enable_snippets,
773 enable_snippets,
776− )?;
774+ ) {
777− let clients = self.inner.entry(name.clone()).or_default();
775+ Ok(client) => {
776+ self.inner
777+ .entry(name.to_owned())
778+ .or_default()
778− clients.push(client.clone());
779+ .push(client.clone());
779− Ok((name.clone(), client))
780+ (name.clone(), Ok(client))
780− })
781+ }
781− .collect()
782+ Err(err) => (name.to_owned(), Err(err)),
783+ }
784+ },
785+ )
782 }
786 }
783
787
784 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
788 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
helix-parsec/Cargo.toml+8−7
@@ -1,13 +1,14 @@
1[package]
1[package]
2name = "helix-parsec"
2name = "helix-parsec"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
7description = "Parser combinators for Helix"
3description = "Parser combinators for Helix"
8−categories = ["editor"]
9−repository = "https://github.com/helix-editor/helix"
10−homepage = "https://helix-editor.com"
11include = ["src/**/*", "README.md"]
4include = ["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
12
13
13[dependencies]
14[dependencies]
helix-term/Cargo.toml+30−25
@@ -1,16 +1,16 @@
1[package]
1[package]
2name = "helix-term"
2name = "helix-term"
3−version = "0.6.0"
4description = "A post-modern text editor."
3description = "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"
11include = ["src/**/*", "README.md"]
4include = ["src/**/*", "README.md"]
12default-run = "hx"
5default-run = "hx"
6+version.workspace = true
7+authors.workspace = true
8+edition.workspace = true
9+license.workspace = true
13−rust-version = "1.65"
10+rust-version.workspace = true
11+categories.workspace = true
12+repository.workspace = true
13+homepage.workspace = true
14
14
15[features]
15[features]
16default = ["git"]
16default = ["git"]
@@ -23,21 +23,22 @@ name = "hx"
23path = "src/main.rs"
23path = "src/main.rs"
24
24
25[dependencies]
25[dependencies]
26−helix-core = { version = "0.6", path = "../helix-core" }
26+helix-core = { path = "../helix-core" }
27+helix-event = { path = "../helix-event" }
27−helix-view = { version = "0.6", path = "../helix-view" }
28+helix-view = { path = "../helix-view" }
28−helix-lsp = { version = "0.6", path = "../helix-lsp" }
29+helix-lsp = { path = "../helix-lsp" }
29−helix-dap = { version = "0.6", path = "../helix-dap" }
30+helix-dap = { path = "../helix-dap" }
30−helix-vcs = { version = "0.6", path = "../helix-vcs" }
31+helix-vcs = { path = "../helix-vcs" }
31−helix-loader = { version = "0.6", path = "../helix-loader" }
32+helix-loader = { path = "../helix-loader" }
32
33
33anyhow = "1"
34anyhow = "1"
34−once_cell = "1.18"
35+once_cell = "1.19"
35
36
36−which = "4.4"
37+which = "5.0.0"
37
38
38tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
39tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
39tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
40tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
40−crossterm = { version = "0.26", features = ["event-stream"] }
41+crossterm = { version = "0.27", features = ["event-stream"] }
41signal-hook = "0.3"
42signal-hook = "0.3"
42tokio-stream = "0.1"
43tokio-stream = "0.1"
43futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
44futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -49,13 +50,17 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
49log = "0.4"
50log = "0.4"
50
51
51# File picker
52# File picker
52−fuzzy-matcher = "0.3"
53+nucleo.workspace = true
53ignore = "0.4"
54ignore = "0.4"
54# markdown doc rendering
55# markdown doc rendering
55pulldown-cmark = { version = "0.9", default-features = false }
56pulldown-cmark = { version = "0.9", default-features = false }
56# file type detection
57# file type detection
57content_inspector = "0.2.4"
58content_inspector = "0.2.4"
58
59
60+# opening URLs
61+open = "5.0.1"
62+url = "2.5.0"
63+
59# config
64# config
60toml = "0.7"
65toml = "0.7"
61
66
@@ -63,20 +68,20 @@ serde_json = "1.0"
63serde = { version = "1.0", features = ["derive"] }
68serde = { version = "1.0", features = ["derive"] }
64
69
65# ripgrep for global search
70# ripgrep for global search
66−grep-regex = "0.1.11"
71+grep-regex = "0.1.12"
67−grep-searcher = "0.1.11"
72+grep-searcher = "0.1.13"
68
73
69[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
74[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
70signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
75signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
71−libc = "0.2.147"
76+libc = "0.2.151"
72
77
73[target.'cfg(target_os = "macos")'.dependencies]
78[target.'cfg(target_os = "macos")'.dependencies]
74−crossterm = { version = "0.26", features = ["event-stream", "use-dev-tty"] }
79+crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
75
80
76[build-dependencies]
81[build-dependencies]
77−helix-loader = { version = "0.6", path = "../helix-loader" }
82+helix-loader = { path = "../helix-loader" }
78
83
79[dev-dependencies]
84[dev-dependencies]
80smallvec = "1.11"
85smallvec = "1.11"
81−indoc = "2.0.3"
86+indoc = "2.0.4"
82−tempfile = "3.7.0"
87+tempfile = "3.8.1"
helix-term/src/application.rs+114−81
@@ -1,6 +1,7 @@
1use arc_swap::{access::Map, ArcSwap};
1use arc_swap::{access::Map, ArcSwap};
2use futures_util::Stream;
2use futures_util::Stream;
3use helix_core::{
3use helix_core::{
4+ chars::char_is_word,
4 diagnostic::{DiagnosticTag, NumberOrString},
5 diagnostic::{DiagnosticTag, NumberOrString},
5 path::get_relative_path,
6 path::get_relative_path,
6 pos_at_coords, syntax, Selection,
7 pos_at_coords, syntax, Selection,
@@ -33,12 +34,9 @@ use crate::{
33};
34};
34
35
35use log::{debug, error, warn};
36use log::{debug, error, warn};
36−use std::{
37+#[cfg(not(feature = "integration"))]
38+use std::io::stdout;
37− collections::btree_map::Entry,
39+use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
38− io::{stdin, stdout},
39− path::Path,
40− sync::Arc,
41−};
42
40
43use anyhow::{Context, Error};
41use anyhow::{Context, Error};
44
42
@@ -163,17 +161,21 @@ impl Application {
163 let path = helix_loader::runtime_file(Path::new("tutor"));
161 let path = helix_loader::runtime_file(Path::new("tutor"));
164 editor.open(&path, Action::VerticalSplit)?;
162 editor.open(&path, Action::VerticalSplit)?;
165 // Unset path to prevent accidentally saving to the original tutor file.
163 // Unset path to prevent accidentally saving to the original tutor file.
166− doc_mut!(editor).set_path(None)?;
164+ doc_mut!(editor).set_path(None);
167 } else if !args.files.is_empty() {
165 } else if !args.files.is_empty() {
168− let first = &args.files[0].0; // we know it's not 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 first.is_dir() {
169+ if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
170− helix_loader::set_current_working_dir(first.clone())?;
171− editor.new_file(Action::VerticalSplit);
172− let picker = ui::file_picker(".".into(), &config.load().editor);
170+ let picker = ui::file_picker(first, &config.load().editor);
173 compositor.push(Box::new(overlaid(picker)));
171 compositor.push(Box::new(overlaid(picker)));
174− } else {
172+ }
173+
174+ // If there are any more files specified, open them
175+ if files_it.peek().is_some() {
175− let nr_of_files = args.files.len();
176+ let mut nr_of_files = 0;
176− for (i, (file, pos)) in args.files.into_iter().enumerate() {
177+ for (file, pos) in files_it {
178+ nr_of_files += 1;
177 if file.is_dir() {
179 if file.is_dir() {
178 return Err(anyhow::anyhow!(
180 return Err(anyhow::anyhow!(
179 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
181 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
@@ -185,7 +187,7 @@ impl Application {
185 // option. If neither of those two arguments are passed
187 // option. If neither of those two arguments are passed
186 // in, just load the files normally.
188 // in, just load the files normally.
187 let action = match args.split {
189 let action = match args.split {
188− _ if i == 0 => Action::VerticalSplit,
190+ _ if nr_of_files == 1 => Action::VerticalSplit,
189 Some(Layout::Vertical) => Action::VerticalSplit,
191 Some(Layout::Vertical) => Action::VerticalSplit,
190 Some(Layout::Horizontal) => Action::HorizontalSplit,
192 Some(Layout::Horizontal) => Action::HorizontalSplit,
191 None => Action::Load,
193 None => Action::Load,
@@ -212,6 +214,8 @@ impl Application {
212 // does not affect views without pos since it is at the top
214 // does not affect views without pos since it is at the top
213 let (view, doc) = current!(editor);
215 let (view, doc) = current!(editor);
214 align_view(doc, view, Align::Center);
216 align_view(doc, view, Align::Center);
217+ } else {
218+ editor.new_file(Action::VerticalSplit);
215 }
219 }
216 } else if stdin().is_tty() || cfg!(feature = "integration") {
220 } else if stdin().is_tty() || cfg!(feature = "integration") {
217 editor.new_file(Action::VerticalSplit);
221 editor.new_file(Action::VerticalSplit);
@@ -254,22 +258,19 @@ impl Application {
254 }
258 }
255
259
256 async fn render(&mut self) {
260 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+
257 let mut cx = crate::compositor::Context {
266 let mut cx = crate::compositor::Context {
258 editor: &mut self.editor,
267 editor: &mut self.editor,
259 jobs: &mut self.jobs,
268 jobs: &mut self.jobs,
260 scroll: None,
269 scroll: None,
261 };
270 };
262
271
263− // Acquire mutable access to the redraw_handle lock
272+ helix_event::start_frame();
264− // to ensure that there are no tasks running that want to block rendering
265− drop(cx.editor.redraw_handle.1.write().await);
266 cx.editor.needs_redraw = false;
273 cx.editor.needs_redraw = false;
267− {
268− // exhaust any leftover redraw notifications
269− let notify = cx.editor.redraw_handle.0.notified();
270− tokio::pin!(notify);
271− notify.enable();
272− }
273
274
274 let area = self
275 let area = self
275 .terminal
276 .terminal
@@ -291,7 +292,7 @@ impl Application {
291
292
292 pub async fn event_loop<S>(&mut self, input_stream: &mut S)
293 pub async fn event_loop<S>(&mut self, input_stream: &mut S)
293 where
294 where
294− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
295+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
295 {
296 {
296 self.render().await;
297 self.render().await;
297
298
@@ -304,7 +305,7 @@ impl Application {
304
305
305 pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
306 pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
306 where
307 where
307− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
308+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
308 {
309 {
309 loop {
310 loop {
310 if self.editor.should_close() {
311 if self.editor.should_close() {
@@ -557,16 +558,7 @@ impl Application {
557 let bytes = doc_save_event.text.len_bytes();
558 let bytes = doc_save_event.text.len_bytes();
558
559
559 if doc.path() != Some(&doc_save_event.path) {
560 if doc.path() != Some(&doc_save_event.path) {
560− if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
561+ doc.set_path(Some(&doc_save_event.path));
561− log::error!(
562− "error setting path for doc '{:?}': {}",
563− doc.path(),
564− err.to_string(),
565− );
566−
567− self.editor.set_error(err.to_string());
568− return;
569− }
570
562
571 let loader = self.editor.syn_loader.clone();
563 let loader = self.editor.syn_loader.clone();
572
564
@@ -602,7 +594,7 @@ impl Application {
602 EditorEvent::LanguageServerMessage((id, call)) => {
594 EditorEvent::LanguageServerMessage((id, call)) => {
603 self.handle_language_server_message(call, id).await;
595 self.handle_language_server_message(call, id).await;
604 // limit render calls for fast language server messages
596 // limit render calls for fast language server messages
605− self.editor.redraw_handle.0.notify_one();
597+ helix_event::request_redraw();
606 }
598 }
607 EditorEvent::DebuggerEvent(payload) => {
599 EditorEvent::DebuggerEvent(payload) => {
608 let needs_render = self.editor.handle_debugger_message(payload).await;
600 let needs_render = self.editor.handle_debugger_message(payload).await;
@@ -610,6 +602,9 @@ impl Application {
610 self.render().await;
602 self.render().await;
611 }
603 }
612 }
604 }
605+ EditorEvent::Redraw => {
606+ self.render().await;
607+ }
613 EditorEvent::IdleTimer => {
608 EditorEvent::IdleTimer => {
614 self.editor.clear_idle_timer();
609 self.editor.clear_idle_timer();
615 self.handle_idle_timeout().await;
610 self.handle_idle_timeout().await;
@@ -624,10 +619,7 @@ impl Application {
624 false
619 false
625 }
620 }
626
621
627− pub async fn handle_terminal_events(
622+ pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
628− &mut self,
629− event: Result<CrosstermEvent, crossterm::ErrorKind>,
630− ) {
631 let mut cx = crate::compositor::Context {
623 let mut cx = crate::compositor::Context {
632 editor: &mut self.editor,
624 editor: &mut self.editor,
633 jobs: &mut self.jobs,
625 jobs: &mut self.jobs,
@@ -726,7 +718,7 @@ impl Application {
726 ));
718 ));
727 }
719 }
728 }
720 }
729− Notification::PublishDiagnostics(params) => {
721+ Notification::PublishDiagnostics(mut params) => {
730 let path = match params.uri.to_file_path() {
722 let path = match params.uri.to_file_path() {
731 Ok(path) => path,
723 Ok(path) => path,
732 Err(_) => {
724 Err(_) => {
@@ -740,31 +732,69 @@ impl Application {
740 return;
732 return;
741 }
733 }
742 let offset_encoding = language_server.offset_encoding();
734 let offset_encoding = language_server.offset_encoding();
743− let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
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| {
744− if let Some(version) = params.version {
739+ if let Some(version) = params.version {
745− if version != doc.version() {
740+ if version != doc.version() {
746− log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
741+ log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
747− return false;
742+ return false;
743+ }
748 }
744 }
749− }
750−
751− true
745+ true
752− });
746+ });
753
747
754 if let Some(doc) = doc {
748 if let Some(doc) = doc {
755− let lang_conf = doc.language_config();
749+ let lang_conf = doc.language.clone();
756− let text = doc.text();
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+ }
757
782
783+ let unchaged_diag_sources = &unchaged_diag_sources_;
758− let diagnostics = params
784+ let diagnostics =
759− .diagnostics
760− .iter()
761− .filter_map(|diagnostic| {
785+ params.diagnostics.iter().filter_map(move |diagnostic| {
762 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
786 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
763 use lsp::DiagnosticSeverity;
787 use lsp::DiagnosticSeverity;
764
788
789+ if diagnostic.source.as_ref().map_or(false, |source| {
790+ unchaged_diag_sources.contains(source)
791+ }) {
792+ return None;
793+ }
794+
765 // TODO: convert inside server
795 // TODO: convert inside server
766 let start = if let Some(start) = lsp_pos_to_pos(
796 let start = if let Some(start) = lsp_pos_to_pos(
767− text,
797+ &text,
768 diagnostic.range.start,
798 diagnostic.range.start,
769 offset_encoding,
799 offset_encoding,
770 ) {
800 ) {
@@ -775,14 +805,13 @@ impl Application {
775 };
805 };
776
806
777 let end = if let Some(end) =
807 let end = if let Some(end) =
778− lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
808+ lsp_pos_to_pos(&text, diagnostic.range.end, offset_encoding)
779 {
809 {
780 end
810 end
781 } else {
811 } else {
782 log::warn!("lsp position out of bounds - {:?}", diagnostic);
812 log::warn!("lsp position out of bounds - {:?}", diagnostic);
783 return None;
813 return None;
784 };
814 };
785−
786 let severity =
815 let severity =
787 diagnostic.severity.map(|severity| match severity {
816 diagnostic.severity.map(|severity| match severity {
788 DiagnosticSeverity::ERROR => Error,
817 DiagnosticSeverity::ERROR => Error,
@@ -795,7 +824,7 @@ impl Application {
795 ),
824 ),
796 });
825 });
797
826
798− if let Some(lang_conf) = lang_conf {
827+ if let Some(lang_conf) = &lang_conf {
799 if let Some(severity) = severity {
828 if let Some(severity) = severity {
800 if severity < lang_conf.diagnostic_severity {
829 if severity < lang_conf.diagnostic_severity {
801 return None;
830 return None;
@@ -834,8 +863,17 @@ impl Application {
834 Vec::new()
863 Vec::new()
835 };
864 };
836
865
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+
837 Some(Diagnostic {
872 Some(Diagnostic {
838 range: Range { start, end },
873 range: Range { start, end },
874+ ends_at_word,
875+ starts_at_word,
876+ zero_width: start == end,
839 line: diagnostic.range.start.line as usize,
877 line: diagnostic.range.start.line as usize,
840 message: diagnostic.message.clone(),
878 message: diagnostic.message.clone(),
841 severity,
879 severity,
@@ -845,38 +883,33 @@ impl Application {
845 data: diagnostic.data.clone(),
883 data: diagnostic.data.clone(),
846 language_server_id: server_id,
884 language_server_id: server_id,
847 })
885 })
848− })
886+ });
849− .collect();
850
887
851− doc.replace_diagnostics(diagnostics, server_id);
888+ doc.replace_diagnostics(diagnostics, unchaged_diag_sources, server_id);
852 }
889 }
853
890
854− let mut diagnostics = params
891+ let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
855− .diagnostics
856− .into_iter()
857− .map(|d| (d, server_id))
858− .collect();
859
892
860 // Insert the original lsp::Diagnostics here because we may have no open document
893 // Insert the original lsp::Diagnostics here because we may have no open document
861 // for diagnosic message and so we can't calculate the exact position.
894 // for diagnosic message and so we can't calculate the exact position.
862 // When using them later in the diagnostics picker, we calculate them on-demand.
895 // When using them later in the diagnostics picker, we calculate them on-demand.
863− match self.editor.diagnostics.entry(params.uri) {
896+ let diagnostics = match self.editor.diagnostics.entry(params.uri) {
864 Entry::Occupied(o) => {
897 Entry::Occupied(o) => {
865 let current_diagnostics = o.into_mut();
898 let current_diagnostics = o.into_mut();
866 // there may entries of other language servers, which is why we can't overwrite the whole entry
899 // there may entries of other language servers, which is why we can't overwrite the whole entry
867 current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
900 current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
868− current_diagnostics.append(&mut diagnostics);
901+ current_diagnostics.extend(diagnostics);
869− // Sort diagnostics first by severity and then by line numbers.
870− // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
871 current_diagnostics
902 current_diagnostics
872− .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
903+ // Sort diagnostics first by severity and then by line numbers.
873− }
874− Entry::Vacant(v) => {
875− diagnostics
876− .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
877− v.insert(diagnostics);
878 }
904 }
905+ Entry::Vacant(v) => v.insert(diagnostics.collect()),
879 };
906 };
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+ });
880 }
913 }
881 Notification::ShowMessage(params) => {
914 Notification::ShowMessage(params) => {
882 log::warn!("unhandled window/showMessage: {:?}", params);
915 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -1168,7 +1201,7 @@ impl Application {
1168
1201
1169 pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
1202 pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
1170 where
1203 where
1171− S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
1204+ S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
1172 {
1205 {
1173 self.claim_term().await?;
1206 self.claim_term().await?;
1174
1207
helix-term/src/args.rs+29
@@ -17,12 +17,14 @@ pub struct Args {
17 pub log_file: Option<PathBuf>,
17 pub log_file: Option<PathBuf>,
18 pub config_file: Option<PathBuf>,
18 pub config_file: Option<PathBuf>,
19 pub files: Vec<(PathBuf, Position)>,
19 pub files: Vec<(PathBuf, Position)>,
20+ pub working_directory: Option<PathBuf>,
20}
21}
21
22
22impl Args {
23impl Args {
23 pub fn parse_args() -> Result<Args> {
24 pub fn parse_args() -> Result<Args> {
24 let mut args = Args::default();
25 let mut args = Args::default();
25 let mut argv = std::env::args().peekable();
26 let mut argv = std::env::args().peekable();
27+ let mut line_number = 0;
26
28
27 argv.next(); // skip the program, we don't care about that
29 argv.next(); // skip the program, we don't care about that
28
30
@@ -59,6 +61,20 @@ impl Args {
59 Some(path) => args.log_file = Some(path.into()),
61 Some(path) => args.log_file = Some(path.into()),
60 None => anyhow::bail!("--log must specify a path to write"),
62 None => anyhow::bail!("--log must specify a path to write"),
61 },
63 },
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+ },
62 arg if arg.starts_with("--") => {
78 arg if arg.starts_with("--") => {
63 anyhow::bail!("unexpected double dash argument: {}", arg)
79 anyhow::bail!("unexpected double dash argument: {}", arg)
64 }
80 }
@@ -73,6 +89,13 @@ impl Args {
73 }
89 }
74 }
90 }
75 }
91 }
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+ }
76 arg => args.files.push(parse_file(arg)),
99 arg => args.files.push(parse_file(arg)),
77 }
100 }
78 }
101 }
@@ -82,6 +105,12 @@ impl Args {
82 args.files.push(parse_file(&arg));
105 args.files.push(parse_file(&arg));
83 }
106 }
84
107
108+ if let Some(file) = args.files.first_mut() {
109+ if line_number != 0 {
110+ file.1.row = line_number;
111+ }
112+ }
113+
85 Ok(args)
114 Ok(args)
86 }
115 }
87}
116}
helix-term/src/commands.rs+545−351
@@ -32,7 +32,6 @@ use helix_core::{
32 RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
32 RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
33};
33};
34use helix_view::{
34use helix_view::{
35− clipboard::ClipboardType,
36 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
35 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
37 editor::{Action, CompleteAction},
36 editor::{Action, CompleteAction},
38 info::Info,
37 info::Info,
@@ -44,7 +43,6 @@ use helix_view::{
44};
43};
45
44
46use anyhow::{anyhow, bail, ensure, Context as _};
45use anyhow::{anyhow, bail, ensure, Context as _};
47−use fuzzy_matcher::FuzzyMatcher;
48use insert::*;
46use insert::*;
49use movement::Movement;
47use movement::Movement;
50
48
@@ -61,9 +59,14 @@ use crate::{
61};
59};
62
60
63use crate::job::{self, Jobs};
61use crate::job::{self, Jobs};
64−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
62+use futures_util::{stream::FuturesUnordered, TryStreamExt};
63+use std::{
65−use std::{collections::HashMap, fmt, future::Future};
64+ collections::{HashMap, HashSet},
65+ fmt,
66+ future::Future,
67+ io::Read,
66−use std::{collections::HashSet, num::NonZeroUsize};
68+ num::NonZeroUsize,
69+};
67
70
68use std::{
71use std::{
69 borrow::Cow,
72 borrow::Cow,
@@ -72,11 +75,11 @@ use std::{
72
75
73use once_cell::sync::Lazy;
76use once_cell::sync::Lazy;
74use serde::de::{self, Deserialize, Deserializer};
77use serde::de::{self, Deserialize, Deserializer};
78+use url::Url;
75
79
76use grep_regex::RegexMatcherBuilder;
80use grep_regex::RegexMatcherBuilder;
77use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
81use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
78use ignore::{DirEntry, WalkBuilder, WalkState};
82use ignore::{DirEntry, WalkBuilder, WalkState};
79−use tokio_stream::wrappers::UnboundedReceiverStream;
80
83
81pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
84pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
82
85
@@ -248,6 +251,8 @@ impl MappableCommand {
248 move_prev_long_word_start, "Move to start of previous long word",
251 move_prev_long_word_start, "Move to start of previous long word",
249 move_next_long_word_end, "Move to end of next long word",
252 move_next_long_word_end, "Move to end of next long word",
250 move_prev_long_word_end, "Move to end of previous long word",
253 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",
251 extend_next_word_start, "Extend to start of next word",
256 extend_next_word_start, "Extend to start of next word",
252 extend_prev_word_start, "Extend to start of previous word",
257 extend_prev_word_start, "Extend to start of previous word",
253 extend_next_word_end, "Extend to end of next word",
258 extend_next_word_end, "Extend to end of next word",
@@ -256,6 +261,8 @@ impl MappableCommand {
256 extend_prev_long_word_start, "Extend to start of previous long word",
261 extend_prev_long_word_start, "Extend to start of previous long word",
257 extend_next_long_word_end, "Extend to end of next long word",
262 extend_next_long_word_end, "Extend to end of next long word",
258 extend_prev_long_word_end, "Extend to end of prev long word",
263 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",
259 find_till_char, "Move till next occurrence of char",
266 find_till_char, "Move till next occurrence of char",
260 find_next_char, "Move to next occurrence of char",
267 find_next_char, "Move to next occurrence of char",
261 extend_till_char, "Extend till next occurrence of char",
268 extend_till_char, "Extend till next occurrence of char",
@@ -330,7 +337,7 @@ impl MappableCommand {
330 goto_implementation, "Goto implementation",
337 goto_implementation, "Goto implementation",
331 goto_file_start, "Goto line number <n> else file start",
338 goto_file_start, "Goto line number <n> else file start",
332 goto_file_end, "Goto file end",
339 goto_file_end, "Goto file end",
333− goto_file, "Goto files in selection",
340+ goto_file, "Goto files/URLs in selection",
334 goto_file_hsplit, "Goto files in selection (hsplit)",
341 goto_file_hsplit, "Goto files in selection (hsplit)",
335 goto_file_vsplit, "Goto files in selection (vsplit)",
342 goto_file_vsplit, "Goto files in selection (vsplit)",
336 goto_reference, "Goto references",
343 goto_reference, "Goto references",
@@ -362,6 +369,7 @@ impl MappableCommand {
362 extend_to_line_end, "Extend to line end",
369 extend_to_line_end, "Extend to line end",
363 extend_to_line_end_newline, "Extend to line end",
370 extend_to_line_end_newline, "Extend to line end",
364 signature_help, "Show signature help",
371 signature_help, "Show signature help",
372+ smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
365 insert_tab, "Insert tab char",
373 insert_tab, "Insert tab char",
366 insert_newline, "Insert newline char",
374 insert_newline, "Insert newline char",
367 delete_char_backward, "Delete previous char",
375 delete_char_backward, "Delete previous char",
@@ -376,6 +384,8 @@ impl MappableCommand {
376 later, "Move forward in history",
384 later, "Move forward in history",
377 commit_undo_checkpoint, "Commit changes to new checkpoint",
385 commit_undo_checkpoint, "Commit changes to new checkpoint",
378 yank, "Yank selection",
386 yank, "Yank selection",
387+ yank_to_clipboard, "Yank selections to clipboard",
388+ yank_to_primary_clipboard, "Yank selections to primary clipboard",
379 yank_joined, "Join and yank selections",
389 yank_joined, "Join and yank selections",
380 yank_joined_to_clipboard, "Join and yank selections to clipboard",
390 yank_joined_to_clipboard, "Join and yank selections to clipboard",
381 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
391 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -980,6 +990,7 @@ fn align_selections(cx: &mut Context) {
980
990
981 let transaction = Transaction::change(doc.text(), changes.into_iter());
991 let transaction = Transaction::change(doc.text(), changes.into_iter());
982 doc.apply(&transaction, view.id);
992 doc.apply(&transaction, view.id);
993+ exit_select_mode(cx);
983}
994}
984
995
985fn goto_window(cx: &mut Context, align: Align) {
996fn goto_window(cx: &mut Context, align: Align) {
@@ -1154,6 +1165,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
1154 let (view, doc) = current_ref!(cx.editor);
1165 let (view, doc) = current_ref!(cx.editor);
1155 let text = doc.text();
1166 let text = doc.text();
1156 let selections = doc.selection(view.id);
1167 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();
1157 let mut paths: Vec<_> = selections
1172 let mut paths: Vec<_> = selections
1158 .iter()
1173 .iter()
1159 .map(|r| text.slice(r.from()..r.to()).to_string())
1174 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1181,16 +1196,80 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
1181 .to_string(),
1196 .to_string(),
1182 );
1197 );
1183 }
1198 }
1199+
1184 for sel in paths {
1200 for sel in paths {
1185 let p = sel.trim();
1201 let p = sel.trim();
1186− if !p.is_empty() {
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)));
1187− if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
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) {
1188 cx.editor.set_error(format!("Open file failed: {:?}", e));
1250 cx.editor.set_error(format!("Open file failed: {:?}", e));
1189 }
1251 }
1190 }
1252 }
1191 }
1253 }
1192}
1254}
1193
1255
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+
1194fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
1273fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
1195where
1274where
1196 F: Fn(RopeSlice, Range, usize) -> Range,
1275 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1239,6 +1318,65 @@ fn extend_next_long_word_end(cx: &mut Context) {
1239 extend_word_impl(cx, movement::move_next_long_word_end)
1318 extend_word_impl(cx, movement::move_next_long_word_end)
1240}
1319}
1241
1320
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+
1242fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
1380fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
1243 // TODO: count is reset to 1 before next key so we move it into the closure here.
1381 // TODO: count is reset to 1 before next key so we move it into the closure here.
1244 // Would be nice to carry over.
1382 // Would be nice to carry over.
@@ -1252,13 +1390,9 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo
1252 KeyEvent {
1390 KeyEvent {
1253 code: KeyCode::Enter,
1391 code: KeyCode::Enter,
1254 ..
1392 ..
1255− } =>
1393+ } => {
1256− // TODO: this isn't quite correct when CRLF is involved.
1394+ find_char_line_ending(cx, count, direction, inclusive, extend);
1257− // This hack will work in most cases, since documents don't
1395+ return;
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()
1262 }
1396 }
1263
1397
1264 KeyEvent {
1398 KeyEvent {
@@ -1460,6 +1594,7 @@ where
1460 });
1594 });
1461
1595
1462 doc.apply(&transaction, view.id);
1596 doc.apply(&transaction, view.id);
1597+ exit_select_mode(cx);
1463}
1598}
1464
1599
1465fn switch_case(cx: &mut Context) {
1600fn switch_case(cx: &mut Context) {
@@ -1701,8 +1836,8 @@ fn select_regex(cx: &mut Context) {
1701 "select:".into(),
1836 "select:".into(),
1702 Some(reg),
1837 Some(reg),
1703 ui::completers::none,
1838 ui::completers::none,
1704− move |editor, regex, event| {
1839+ move |cx, regex, event| {
1705− let (view, doc) = current!(editor);
1840+ let (view, doc) = current!(cx.editor);
1706 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1841 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1707 return;
1842 return;
1708 }
1843 }
@@ -1723,8 +1858,8 @@ fn split_selection(cx: &mut Context) {
1723 "split:".into(),
1858 "split:".into(),
1724 Some(reg),
1859 Some(reg),
1725 ui::completers::none,
1860 ui::completers::none,
1726− move |editor, regex, event| {
1861+ move |cx, regex, event| {
1727− let (view, doc) = current!(editor);
1862+ let (view, doc) = current!(cx.editor);
1728 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1863 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1729 return;
1864 return;
1730 }
1865 }
@@ -1847,11 +1982,11 @@ fn search_impl(
1847
1982
1848fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1983fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1849 let mut items = reg
1984 let mut items = reg
1850− .and_then(|reg| cx.editor.registers.get(reg))
1985+ .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1851− .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1986+ .map_or(Vec::new(), |reg| reg.take(200).collect());
1852 items.sort_unstable();
1987 items.sort_unstable();
1853 items.dedup();
1988 items.dedup();
1854− items.into_iter().cloned().collect()
1989+ items.into_iter().map(|value| value.to_string()).collect()
1855}
1990}
1856
1991
1857fn search(cx: &mut Context) {
1992fn search(cx: &mut Context) {
@@ -1888,12 +2023,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
1888 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2023 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1889 .collect()
2024 .collect()
1890 },
2025 },
1891− move |editor, regex, event| {
2026+ move |cx, regex, event| {
2027+ if event == PromptEvent::Validate {
2028+ cx.editor.registers.last_search_register = reg;
1892− if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
2029+ } else if event != PromptEvent::Update {
1893 return;
2030 return;
1894 }
2031 }
1895 search_impl(
2032 search_impl(
1896− editor,
2033+ cx.editor,
1897 &contents,
2034 &contents,
1898 &regex,
2035 &regex,
1899 Movement::Move,
2036 Movement::Move,
@@ -1908,11 +2045,13 @@ fn searcher(cx: &mut Context, direction: Direction) {
1908
2045
1909fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
2046fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1910 let count = cx.count();
2047 let count = cx.count();
2048+ let register = cx
2049+ .register
2050+ .unwrap_or(cx.editor.registers.last_search_register);
1911 let config = cx.editor.config();
2051 let config = cx.editor.config();
1912 let scrolloff = config.scrolloff;
2052 let scrolloff = config.scrolloff;
1913− let (_, doc) = current!(cx.editor);
1914− let registers = &cx.editor.registers;
1915− if let Some(query) = registers.read('/').and_then(|query| query.last()) {
2053+ if let Some(query) = cx.editor.registers.first(register, cx.editor) {
2054+ let doc = doc!(cx.editor);
1916 let contents = doc.text().slice(..).to_string();
2055 let contents = doc.text().slice(..).to_string();
1917 let search_config = &config.search;
2056 let search_config = &config.search;
1918 let case_insensitive = if search_config.smart_case {
2057 let case_insensitive = if search_config.smart_case {
@@ -1921,7 +2060,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
1921 false
2060 false
1922 };
2061 };
1923 let wrap_around = search_config.wrap_around;
2062 let wrap_around = search_config.wrap_around;
1924− if let Ok(regex) = RegexBuilder::new(query)
2063+ if let Ok(regex) = RegexBuilder::new(&query)
1925 .case_insensitive(case_insensitive)
2064 .case_insensitive(case_insensitive)
1926 .multi_line(true)
2065 .multi_line(true)
1927 .build()
2066 .build()
@@ -1961,6 +2100,7 @@ fn extend_search_prev(cx: &mut Context) {
1961}
2100}
1962
2101
1963fn search_selection(cx: &mut Context) {
2102fn search_selection(cx: &mut Context) {
2103+ let register = cx.register.unwrap_or('/');
1964 let (view, doc) = current!(cx.editor);
2104 let (view, doc) = current!(cx.editor);
1965 let contents = doc.text().slice(..);
2105 let contents = doc.text().slice(..);
1966
2106
@@ -1973,13 +2113,24 @@ fn search_selection(cx: &mut Context) {
1973 .collect::<Vec<_>>()
2113 .collect::<Vec<_>>()
1974 .join("|");
2114 .join("|");
1975
2115
1976− let msg = format!("register '{}' set to '{}'", '/', &regex);
2116+ let msg = format!("register '{}' set to '{}'", register, &regex);
1977− cx.editor.registers.push('/', regex);
2117+ match cx.editor.registers.push(register, regex) {
2118+ Ok(_) => {
2119+ cx.editor.registers.last_search_register = register;
1978− cx.editor.set_status(msg);
2120+ cx.editor.set_status(msg)
2121+ }
2122+ Err(err) => cx.editor.set_error(err.to_string()),
2123+ }
1979}
2124}
1980
2125
1981fn make_search_word_bounded(cx: &mut Context) {
2126fn 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);
1982− let regex = match cx.editor.registers.last('/') {
2133+ let regex = match cx.editor.registers.first(register, cx.editor) {
1983 Some(regex) => regex,
2134 Some(regex) => regex,
1984 None => return,
2135 None => return,
1985 };
2136 };
@@ -1997,14 +2148,19 @@ fn make_search_word_bounded(cx: &mut Context) {
1997 if !start_anchored {
2148 if !start_anchored {
1998 new_regex.push_str("\\b");
2149 new_regex.push_str("\\b");
1999 }
2150 }
2000− new_regex.push_str(regex);
2151+ new_regex.push_str(&regex);
2001 if !end_anchored {
2152 if !end_anchored {
2002 new_regex.push_str("\\b");
2153 new_regex.push_str("\\b");
2003 }
2154 }
2004
2155
2005− let msg = format!("register '{}' set to '{}'", '/', &new_regex);
2156+ let msg = format!("register '{}' set to '{}'", register, &new_regex);
2006− cx.editor.registers.push('/', new_regex);
2157+ match cx.editor.registers.push(register, new_regex) {
2158+ Ok(_) => {
2159+ cx.editor.registers.last_search_register = register;
2007− cx.editor.set_status(msg);
2160+ cx.editor.set_status(msg)
2161+ }
2162+ Err(err) => cx.editor.set_error(err.to_string()),
2163+ }
2008}
2164}
2009
2165
2010fn global_search(cx: &mut Context) {
2166fn global_search(cx: &mut Context) {
@@ -2043,13 +2199,11 @@ fn global_search(cx: &mut Context) {
2043 }
2199 }
2044 }
2200 }
2045
2201
2046− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
2047 let config = cx.editor.config();
2202 let config = cx.editor.config();
2048 let smart_case = config.search.smart_case;
2203 let smart_case = config.search.smart_case;
2049 let file_picker_config = config.file_picker.clone();
2204 let file_picker_config = config.file_picker.clone();
2050
2205
2051 let reg = cx.register.unwrap_or('/');
2206 let reg = cx.register.unwrap_or('/');
2052−
2053 let completions = search_completions(cx, Some(reg));
2207 let completions = search_completions(cx, Some(reg));
2054 ui::regex_prompt(
2208 ui::regex_prompt(
2055 cx,
2209 cx,
@@ -2062,52 +2216,66 @@ fn global_search(cx: &mut Context) {
2062 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2216 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2063 .collect()
2217 .collect()
2064 },
2218 },
2065− move |editor, regex, event| {
2219+ move |cx, regex, event| {
2066 if event != PromptEvent::Validate {
2220 if event != PromptEvent::Validate {
2067 return;
2221 return;
2068 }
2222 }
2223+ cx.editor.registers.last_search_register = reg;
2069
2224
2225+ let current_path = doc_mut!(cx.editor).path().cloned();
2070− let documents: Vec<_> = editor
2226+ let documents: Vec<_> = cx
2227+ .editor
2071 .documents()
2228 .documents()
2072− .map(|doc| (doc.path(), doc.text()))
2229+ .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
2073 .collect();
2230 .collect();
2074
2231
2075 if let Ok(matcher) = RegexMatcherBuilder::new()
2232 if let Ok(matcher) = RegexMatcherBuilder::new()
2076 .case_smart(smart_case)
2233 .case_smart(smart_case)
2077 .build(regex.as_str())
2234 .build(regex.as_str())
2078 {
2235 {
2079− let searcher = SearcherBuilder::new()
2080− .binary_detection(BinaryDetection::quit(b'\x00'))
2081− .build();
2082−
2083 let search_root = helix_loader::current_working_dir();
2236 let search_root = helix_loader::current_working_dir();
2084 if !search_root.exists() {
2237 if !search_root.exists() {
2238+ cx.editor
2085− editor.set_error("Current working directory does not exist");
2239+ .set_error("Current working directory does not exist");
2086 return;
2240 return;
2087 }
2241 }
2088
2242
2243+ let (picker, injector) = Picker::stream(current_path);
2244+
2089 let dedup_symlinks = file_picker_config.deduplicate_links;
2245 let dedup_symlinks = file_picker_config.deduplicate_links;
2090 let absolute_root = search_root
2246 let absolute_root = search_root
2091 .canonicalize()
2247 .canonicalize()
2092 .unwrap_or_else(|_| search_root.clone());
2248 .unwrap_or_else(|_| search_root.clone());
2093−
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+
2094− WalkBuilder::new(search_root)
2256+ let mut walk_builder = WalkBuilder::new(search_root);
2257+
2258+ walk_builder
2095− .hidden(file_picker_config.hidden)
2259+ .hidden(file_picker_config.hidden)
2096− .parents(file_picker_config.parents)
2260+ .parents(file_picker_config.parents)
2097− .ignore(file_picker_config.ignore)
2261+ .ignore(file_picker_config.ignore)
2098− .follow_links(file_picker_config.follow_symlinks)
2262+ .follow_links(file_picker_config.follow_symlinks)
2099− .git_ignore(file_picker_config.git_ignore)
2263+ .git_ignore(file_picker_config.git_ignore)
2100− .git_global(file_picker_config.git_global)
2264+ .git_global(file_picker_config.git_global)
2101− .git_exclude(file_picker_config.git_exclude)
2265+ .git_exclude(file_picker_config.git_exclude)
2102− .max_depth(file_picker_config.max_depth)
2266+ .max_depth(file_picker_config.max_depth)
2103− .filter_entry(move |entry| {
2267+ .filter_entry(move |entry| {
2104− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2268+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2105− })
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+
2106− .build_parallel()
2275+ walk_builder.build_parallel().run(|| {
2107− .run(|| {
2108 let mut searcher = searcher.clone();
2276 let mut searcher = searcher.clone();
2109 let matcher = matcher.clone();
2277 let matcher = matcher.clone();
2110− let all_matches_sx = all_matches_sx.clone();
2278+ let injector = injector_.clone();
2111 let documents = &documents;
2279 let documents = &documents;
2112 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2280 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2113 let entry = match entry {
2281 let entry = match entry {
@@ -2121,21 +2289,24 @@ fn global_search(cx: &mut Context) {
2121 _ => return WalkState::Continue,
2289 _ => return WalkState::Continue,
2122 };
2290 };
2123
2291
2292+ let mut stop = false;
2124 let sink = sinks::UTF8(|line_num, _| {
2293 let sink = sinks::UTF8(|line_num, _| {
2125− all_matches_sx
2294+ stop = injector
2126− .send(FileResult::new(entry.path(), line_num as usize - 1))
2295+ .push(FileResult::new(entry.path(), line_num as usize - 1))
2127− .unwrap();
2296+ .is_err();
2128
2297
2129− Ok(true)
2298+ Ok(!stop)
2130 });
2299 });
2131 let doc = documents.iter().find(|&(doc_path, _)| {
2300 let doc = documents.iter().find(|&(doc_path, _)| {
2301+ doc_path
2302+ .as_ref()
2132− doc_path.map_or(false, |doc_path| doc_path == entry.path())
2303+ .map_or(false, |doc_path| doc_path == entry.path())
2133 });
2304 });
2134
2305
2135 let result = if let Some((_, doc)) = doc {
2306 let result = if let Some((_, doc)) = doc {
2136 // there is already a buffer for this file
2307 // there is already a buffer for this file
2137 // search the buffer instead of the file because it's faster
2308 // search the buffer instead of the file because it's faster
2138− // and captures new edits without requireing a save
2309+ // and captures new edits without requiring a save
2139 if searcher.multi_line_with_matcher(&matcher) {
2310 if searcher.multi_line_with_matcher(&matcher) {
2140 // in this case a continous buffer is required
2311 // in this case a continous buffer is required
2141 // convert the rope to a string
2312 // convert the rope to a string
@@ -2159,67 +2330,66 @@ fn global_search(cx: &mut Context) {
2159 err
2330 err
2160 );
2331 );
2161 }
2332 }
2333+ if stop {
2334+ WalkState::Quit
2335+ } else {
2162− WalkState::Continue
2336+ WalkState::Continue
2337+ }
2163 })
2338 })
2164 });
2339 });
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+ })
2165 } else {
2387 } else {
2166 // Otherwise do nothing
2388 // Otherwise do nothing
2167 // log::warn!("Global Search Invalid Pattern")
2389 // log::warn!("Global Search Invalid Pattern")
2168 }
2390 }
2169 },
2391 },
2170 );
2392 );
2171−
2172− let current_path = doc_mut!(cx.editor).path().cloned();
2173−
2174− let show_picker = async move {
2175− let all_matches: Vec<FileResult> =
2176− UnboundedReceiverStream::new(all_matches_rx).collect().await;
2177− let call: job::Callback = Callback::EditorCompositor(Box::new(
2178− move |editor: &mut Editor, compositor: &mut Compositor| {
2179− if all_matches.is_empty() {
2180− if !editor.is_err() {
2181− editor.set_status("No matches found");
2182− }
2183− return;
2184− }
2185−
2186− let picker = Picker::new(
2187− all_matches,
2188− current_path,
2189− move |cx, FileResult { path, line_num }, action| {
2190− match cx.editor.open(path, action) {
2191− Ok(_) => {}
2192− Err(e) => {
2193− cx.editor.set_error(format!(
2194− "Failed to open file '{}': {}",
2195− path.display(),
2196− e
2197− ));
2198− return;
2199− }
2200− }
2201−
2202− let line_num = *line_num;
2203− let (view, doc) = current!(cx.editor);
2204− let text = doc.text();
2205− if line_num >= text.len_lines() {
2206− cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2207− return;
2208− }
2209− let start = text.line_to_char(line_num);
2210− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2211−
2212− doc.set_selection(view.id, Selection::single(start, end));
2213− align_view(doc, view, Align::Center);
2214− }).with_preview(|_editor, FileResult { path, line_num }| {
2215− Some((path.clone().into(), Some((*line_num, *line_num))))
2216− });
2217− compositor.push(Box::new(overlaid(picker)));
2218− },
2219− ));
2220− Ok(call)
2221− };
2222− cx.jobs.callback(show_picker);
2223}
2393}
2224
2394
2225enum Extend {
2395enum Extend {
@@ -2367,7 +2537,10 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
2367 let text = doc.text().slice(..);
2537 let text = doc.text().slice(..);
2368 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
2538 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
2369 let reg_name = cx.register.unwrap_or('"');
2539 let reg_name = cx.register.unwrap_or('"');
2370− cx.editor.registers.write(reg_name, values);
2540+ if let Err(err) = cx.editor.registers.write(reg_name, values) {
2541+ cx.editor.set_error(err.to_string());
2542+ return;
2543+ }
2371 };
2544 };
2372
2545
2373 // then delete
2546 // then delete
@@ -2507,6 +2680,10 @@ fn insert_mode(cx: &mut Context) {
2507 .transform(|range| Range::new(range.to(), range.from()));
2680 .transform(|range| Range::new(range.to(), range.from()));
2508
2681
2509 doc.set_selection(view.id, selection);
2682 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();
2510}
2687}
2511
2688
2512// inserts at the end of each selection
2689// inserts at the end of each selection
@@ -2567,6 +2744,7 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
2567 let picker = ui::file_picker(path, &cx.editor.config());
2744 let picker = ui::file_picker(path, &cx.editor.config());
2568 cx.push_layer(Box::new(overlaid(picker)));
2745 cx.push_layer(Box::new(overlaid(picker)));
2569}
2746}
2747+
2570fn file_picker_in_current_directory(cx: &mut Context) {
2748fn file_picker_in_current_directory(cx: &mut Context) {
2571 let cwd = helix_loader::current_working_dir();
2749 let cwd = helix_loader::current_working_dir();
2572 if !cwd.exists() {
2750 if !cwd.exists() {
@@ -2723,9 +2901,11 @@ fn jumplist_picker(cx: &mut Context) {
2723 |cx, meta, action| {
2901 |cx, meta, action| {
2724 cx.editor.switch(meta.id, action);
2902 cx.editor.switch(meta.id, action);
2725 let config = cx.editor.config();
2903 let config = cx.editor.config();
2726− let (view, doc) = current!(cx.editor);
2904+ let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
2727 doc.set_selection(view.id, meta.selection.clone());
2905 doc.set_selection(view.id, meta.selection.clone());
2906+ if action.align_view(view, doc.id()) {
2728− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2907+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
2908+ }
2729 },
2909 },
2730 )
2910 )
2731 .with_preview(|editor, meta| {
2911 .with_preview(|editor, meta| {
@@ -2873,6 +3053,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
2873 let indent = indent::indent_for_newline(
3053 let indent = indent::indent_for_newline(
2874 language_config,
3054 language_config,
2875 syntax,
3055 syntax,
3056+ &doc.config.load().indent_heuristic,
2876 &doc.indent_style,
3057 &doc.indent_style,
2877 tab_width,
3058 tab_width,
2878 text,
3059 text,
@@ -2977,6 +3158,7 @@ fn open(cx: &mut Context, open: Open) {
2977 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
3158 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
2978 Open::Above => range.from(),
3159 Open::Above => range.from(),
2979 });
3160 });
3161+
2980 let new_line = match open {
3162 let new_line = match open {
2981 // adjust position to the end of the line (next line - 1)
3163 // adjust position to the end of the line (next line - 1)
2982 Open::Below => cursor_line + 1,
3164 Open::Below => cursor_line + 1,
@@ -2984,13 +3166,15 @@ fn open(cx: &mut Context, open: Open) {
2984 Open::Above => cursor_line,
3166 Open::Above => cursor_line,
2985 };
3167 };
2986
3168
3169+ let line_num = new_line.saturating_sub(1);
3170+
2987 // Index to insert newlines after, as well as the char width
3171 // Index to insert newlines after, as well as the char width
2988 // to use to compensate for those inserted newlines.
3172 // to use to compensate for those inserted newlines.
2989 let (line_end_index, line_end_offset_width) = if new_line == 0 {
3173 let (line_end_index, line_end_offset_width) = if new_line == 0 {
2990 (0, 0)
3174 (0, 0)
2991 } else {
3175 } else {
2992 (
3176 (
2993− line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
3177+ line_end_char_index(&text, line_num),
2994 doc.line_ending.len_chars(),
3178 doc.line_ending.len_chars(),
2995 )
3179 )
2996 };
3180 };
@@ -2998,13 +3182,15 @@ fn open(cx: &mut Context, open: Open) {
2998 let indent = indent::indent_for_newline(
3182 let indent = indent::indent_for_newline(
2999 doc.language_config(),
3183 doc.language_config(),
3000 doc.syntax(),
3184 doc.syntax(),
3185+ &doc.config.load().indent_heuristic,
3001 &doc.indent_style,
3186 &doc.indent_style,
3002 doc.tab_width(),
3187 doc.tab_width(),
3003 text,
3188 text,
3004− new_line.saturating_sub(1),
3189+ line_num,
3005 line_end_index,
3190 line_end_index,
3006 cursor_line,
3191 cursor_line,
3007 );
3192 );
3193+
3008 let indent_len = indent.len();
3194 let indent_len = indent.len();
3009 let mut text = String::with_capacity(1 + indent_len);
3195 let mut text = String::with_capacity(1 + indent_len);
3010 text.push_str(doc.line_ending.as_str());
3196 text.push_str(doc.line_ending.as_str());
@@ -3430,6 +3616,7 @@ pub mod insert {
3430 }
3616 }
3431
3617
3432 use helix_core::auto_pairs;
3618 use helix_core::auto_pairs;
3619+ use helix_view::editor::SmartTabConfig;
3433
3620
3434 pub fn insert_char(cx: &mut Context, c: char) {
3621 pub fn insert_char(cx: &mut Context, c: char) {
3435 let (view, doc) = current_ref!(cx.editor);
3622 let (view, doc) = current_ref!(cx.editor);
@@ -3455,6 +3642,31 @@ pub mod insert {
3455 }
3642 }
3456 }
3643 }
3457
3644
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+
3458 pub fn insert_tab(cx: &mut Context) {
3670 pub fn insert_tab(cx: &mut Context) {
3459 let (view, doc) = current!(cx.editor);
3671 let (view, doc) = current!(cx.editor);
3460 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
3672 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3510,6 +3722,7 @@ pub mod insert {
3510 let indent = indent::indent_for_newline(
3722 let indent = indent::indent_for_newline(
3511 doc.language_config(),
3723 doc.language_config(),
3512 doc.syntax(),
3724 doc.syntax(),
3725+ &doc.config.load().indent_heuristic,
3513 &doc.indent_style,
3726 &doc.indent_style,
3514 doc.tab_width(),
3727 doc.tab_width(),
3515 text,
3728 text,
@@ -3750,7 +3963,22 @@ fn commit_undo_checkpoint(cx: &mut Context) {
3750// Yank / Paste
3963// Yank / Paste
3751
3964
3752fn yank(cx: &mut Context) {
3965fn 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) {
3753− let (view, doc) = current!(cx.editor);
3981+ let (view, doc) = current!(editor);
3754 let text = doc.text().slice(..);
3982 let text = doc.text().slice(..);
3755
3983
3756 let values: Vec<String> = doc
3984 let values: Vec<String> = doc
@@ -3758,19 +3986,15 @@ fn yank(cx: &mut Context) {
3758 .fragments(text)
3986 .fragments(text)
3759 .map(Cow::into_owned)
3987 .map(Cow::into_owned)
3760 .collect();
3988 .collect();
3989+ let selections = values.len();
3761
3990
3762− let msg = format!(
3991+ match editor.registers.write(register, values) {
3992+ Ok(_) => editor.set_status(format!(
3763− "yanked {} selection(s) to register {}",
3993+ "yanked {selections} selection{} to register {register}",
3764− values.len(),
3994+ if selections == 1 { "" } else { "s" }
3765− cx.register.unwrap_or('"')
3995+ )),
3766− );
3996+ Err(err) => editor.set_error(err.to_string()),
3767−
3997+ }
3768− cx.editor
3769− .registers
3770− .write(cx.register.unwrap_or('"'), values);
3771−
3772− cx.editor.set_status(msg);
3773− exit_select_mode(cx);
3774}
3998}
3775
3999
3776fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
4000fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
@@ -3778,6 +4002,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
3778 let text = doc.text().slice(..);
4002 let text = doc.text().slice(..);
3779
4003
3780 let selection = doc.selection(view.id);
4004 let selection = doc.selection(view.id);
4005+ let selections = selection.len();
3781 let joined = selection
4006 let joined = selection
3782 .fragments(text)
4007 .fragments(text)
3783 .fold(String::new(), |mut acc, fragment| {
4008 .fold(String::new(), |mut acc, fragment| {
@@ -3788,104 +4013,52 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
3788 acc
4013 acc
3789 });
4014 });
3790
4015
3791− let msg = format!(
3792− "joined and yanked {} selection(s) to register {}",
3793− selection.len(),
3794− register,
3795− );
3796−
3797− editor.registers.write(register, vec![joined]);
4016+ match editor.registers.write(register, vec![joined]) {
3798− editor.set_status(msg);
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+ }
3799}
4023}
3800
4024
3801fn yank_joined(cx: &mut Context) {
4025fn yank_joined(cx: &mut Context) {
3802− let line_ending = doc!(cx.editor).line_ending;
4026+ let separator = doc!(cx.editor).line_ending.as_str();
3803− let register = cx.register.unwrap_or('"');
4027+ yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
3804− yank_joined_impl(cx.editor, line_ending.as_str(), register);
3805 exit_select_mode(cx);
4028 exit_select_mode(cx);
3806}
4029}
3807
4030
3808−fn yank_joined_to_clipboard_impl(
4031+fn yank_joined_to_clipboard(cx: &mut Context) {
3809− editor: &mut Editor,
4032+ let line_ending = doc!(cx.editor).line_ending;
3810− separator: &str,
4033+ yank_joined_impl(cx.editor, line_ending.as_str(), '+');
3811− clipboard_type: ClipboardType,
4034+ exit_select_mode(cx);
3812−) -> anyhow::Result<()> {
3813− let (view, doc) = current!(editor);
3814− let text = doc.text().slice(..);
3815−
3816− let values: Vec<String> = doc
3817− .selection(view.id)
3818− .fragments(text)
3819− .map(Cow::into_owned)
3820− .collect();
3821−
3822− let clipboard_text = match clipboard_type {
3823− ClipboardType::Clipboard => "system clipboard",
3824− ClipboardType::Selection => "primary clipboard",
3825− };
3826−
3827− let msg = format!(
3828− "joined and yanked {} selection(s) to {}",
3829− values.len(),
3830− clipboard_text,
3831− );
3832−
3833− let joined = values.join(separator);
3834−
3835− editor
3836− .clipboard_provider
3837− .set_contents(joined, clipboard_type)
3838− .context("Couldn't set system clipboard content")?;
3839−
3840− editor.set_status(msg);
3841−
3842− Ok(())
3843}
4035}
3844
4036
3845−fn yank_joined_to_clipboard(cx: &mut Context) {
4037+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3846 let line_ending = doc!(cx.editor).line_ending;
4038 let line_ending = doc!(cx.editor).line_ending;
3847− let _ =
3848− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
4039+ yank_joined_impl(cx.editor, line_ending.as_str(), '*');
3849 exit_select_mode(cx);
4040 exit_select_mode(cx);
3850}
4041}
3851
4042
3852−fn yank_main_selection_to_clipboard_impl(
3853− editor: &mut Editor,
4043+fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
3854− clipboard_type: ClipboardType,
3855−) -> anyhow::Result<()> {
3856 let (view, doc) = current!(editor);
4044 let (view, doc) = current!(editor);
3857 let text = doc.text().slice(..);
4045 let text = doc.text().slice(..);
3858
4046
3859− let message_text = match clipboard_type {
3860− ClipboardType::Clipboard => "yanked main selection to system clipboard",
3861− ClipboardType::Selection => "yanked main selection to primary clipboard",
3862− };
3863−
3864− let value = doc.selection(view.id).primary().fragment(text);
4047+ let selection = doc.selection(view.id).primary().fragment(text).to_string();
3865
4048
3866− if let Err(e) = editor
4049+ match editor.registers.write(register, vec![selection]) {
3867− .clipboard_provider
4050+ Ok(_) => editor.set_status(format!("yanked primary selection to register {register}",)),
3868− .set_contents(value.into_owned(), clipboard_type)
4051+ Err(err) => editor.set_error(err.to_string()),
3869− {
3870− bail!("Couldn't set system clipboard content: {}", e);
3871 }
4052 }
3872−
3873− editor.set_status(message_text);
3874− Ok(())
3875}
4053}
3876
4054
3877fn yank_main_selection_to_clipboard(cx: &mut Context) {
4055fn yank_main_selection_to_clipboard(cx: &mut Context) {
3878− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
4056+ yank_primary_selection_impl(cx.editor, '+');
3879−}
4057+ exit_select_mode(cx);
3880−
3881−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3882− let line_ending = doc!(cx.editor).line_ending;
3883− let _ =
3884− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
3885}
4058}
3886
4059
3887fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4060fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3888− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
4061+ yank_primary_selection_impl(cx.editor, '*');
3889 exit_select_mode(cx);
4062 exit_select_mode(cx);
3890}
4063}
3891
4064
@@ -3983,146 +4156,99 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
3983 };
4156 };
3984 let (view, doc) = current!(cx.editor);
4157 let (view, doc) = current!(cx.editor);
3985 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
4158 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
3986−}
4159+ exit_select_mode(cx);
3987−
3988−fn paste_clipboard_impl(
3989− editor: &mut Editor,
3990− action: Paste,
3991− clipboard_type: ClipboardType,
3992− count: usize,
3993−) -> anyhow::Result<()> {
3994− let (view, doc) = current!(editor);
3995− match editor.clipboard_provider.get_contents(clipboard_type) {
3996− Ok(contents) => {
3997− paste_impl(&[contents], doc, view, action, count, editor.mode);
3998− Ok(())
3999− }
4000− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
4001− }
4002}
4160}
4003
4161
4004fn paste_clipboard_after(cx: &mut Context) {
4162fn paste_clipboard_after(cx: &mut Context) {
4005− let _ = paste_clipboard_impl(
4006− cx.editor,
4007− Paste::After,
4163+ paste(cx.editor, '+', Paste::After, cx.count());
4008− ClipboardType::Clipboard,
4164+ exit_select_mode(cx);
4009− cx.count(),
4010− );
4011}
4165}
4012
4166
4013fn paste_clipboard_before(cx: &mut Context) {
4167fn paste_clipboard_before(cx: &mut Context) {
4014− let _ = paste_clipboard_impl(
4015− cx.editor,
4016− Paste::Before,
4168+ paste(cx.editor, '+', Paste::Before, cx.count());
4017− ClipboardType::Clipboard,
4169+ exit_select_mode(cx);
4018− cx.count(),
4019− );
4020}
4170}
4021
4171
4022fn paste_primary_clipboard_after(cx: &mut Context) {
4172fn paste_primary_clipboard_after(cx: &mut Context) {
4023− let _ = paste_clipboard_impl(
4024− cx.editor,
4025− Paste::After,
4173+ paste(cx.editor, '*', Paste::After, cx.count());
4026− ClipboardType::Selection,
4174+ exit_select_mode(cx);
4027− cx.count(),
4028− );
4029}
4175}
4030
4176
4031fn paste_primary_clipboard_before(cx: &mut Context) {
4177fn paste_primary_clipboard_before(cx: &mut Context) {
4032− let _ = paste_clipboard_impl(
4033− cx.editor,
4034− Paste::Before,
4178+ paste(cx.editor, '*', Paste::Before, cx.count());
4035− ClipboardType::Selection,
4179+ exit_select_mode(cx);
4036− cx.count(),
4037− );
4038}
4180}
4039
4181
4040fn replace_with_yanked(cx: &mut Context) {
4182fn replace_with_yanked(cx: &mut Context) {
4041− let count = cx.count();
4042− let reg_name = cx.register.unwrap_or('"');
4183+ replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
4043− let (view, doc) = current!(cx.editor);
4044− let registers = &mut cx.editor.registers;
4045−
4046− if let Some(values) = registers.read(reg_name) {
4047− if !values.is_empty() {
4048− let repeat = std::iter::repeat(
4049− values
4050− .last()
4051− .map(|value| Tendril::from(&value.repeat(count)))
4052− .unwrap(),
4053− );
4054− let mut values = values
4055− .iter()
4056− .map(|value| Tendril::from(&value.repeat(count)))
4057− .chain(repeat);
4058− let selection = doc.selection(view.id);
4059− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4060− if !range.is_empty() {
4061− (range.from(), range.to(), Some(values.next().unwrap()))
4062− } else {
4063− (range.from(), range.to(), None)
4064− }
4065− });
4066−
4067− doc.apply(&transaction, view.id);
4068− exit_select_mode(cx);
4184+ exit_select_mode(cx);
4069− }
4070− }
4071}
4185}
4072
4186
4073−fn replace_selections_with_clipboard_impl(
4187+fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) {
4074− cx: &mut Context,
4188+ let Some(values) = editor.registers
4075− clipboard_type: ClipboardType,
4189+ .read(register, editor)
4076−) -> anyhow::Result<()> {
4190+ .filter(|values| values.len() > 0) else { return };
4077− let count = cx.count();
4191+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
4078− let (view, doc) = current!(cx.editor);
4079
4192
4080− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
4193+ let (view, doc) = current!(editor);
4081− Ok(contents) => {
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);
4082− let selection = doc.selection(view.id);
4204+ let selection = doc.selection(view.id);
4083− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4205+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4084− (
4206+ if !range.is_empty() {
4085− range.from(),
4207+ (range.from(), range.to(), Some(values.next().unwrap()))
4086− range.to(),
4208+ } else {
4087− Some(contents.repeat(count).as_str().into()),
4209+ (range.from(), range.to(), None)
4088− )
4089− });
4090−
4091− doc.apply(&transaction, view.id);
4092− doc.append_changes_to_history(view);
4093 }
4210 }
4094− Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
4095− }
4211+ });
4096
4212
4097− exit_select_mode(cx);
4213+ doc.apply(&transaction, view.id);
4098− Ok(())
4099}
4214}
4100
4215
4101fn replace_selections_with_clipboard(cx: &mut Context) {
4216fn replace_selections_with_clipboard(cx: &mut Context) {
4102− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
4217+ replace_with_yanked_impl(cx.editor, '+', cx.count());
4218+ exit_select_mode(cx);
4103}
4219}
4104
4220
4105fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4221fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4106− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
4222+ replace_with_yanked_impl(cx.editor, '*', cx.count());
4223+ exit_select_mode(cx);
4107}
4224}
4108
4225
4109−fn paste(cx: &mut Context, pos: Paste) {
4226+fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
4110− let count = cx.count();
4227+ let Some(values) = editor.registers.read(register, editor) else { return };
4111− let reg_name = cx.register.unwrap_or('"');
4228+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
4112− let (view, doc) = current!(cx.editor);
4113− let registers = &mut cx.editor.registers;
4114
4229
4115− if let Some(values) = registers.read(reg_name) {
4230+ let (view, doc) = current!(editor);
4116− paste_impl(values, doc, view, pos, count, cx.editor.mode);
4231+ paste_impl(&values, doc, view, pos, count, editor.mode);
4117− }
4118}
4232}
4119
4233
4120fn paste_after(cx: &mut Context) {
4234fn paste_after(cx: &mut Context) {
4235+ paste(
4236+ cx.editor,
4237+ cx.register.unwrap_or('"'),
4121− paste(cx, Paste::After)
4238+ Paste::After,
4239+ cx.count(),
4240+ );
4241+ exit_select_mode(cx);
4122}
4242}
4123
4243
4124fn paste_before(cx: &mut Context) {
4244fn paste_before(cx: &mut Context) {
4245+ paste(
4246+ cx.editor,
4247+ cx.register.unwrap_or('"'),
4125− paste(cx, Paste::Before)
4248+ Paste::Before,
4249+ cx.count(),
4250+ );
4251+ exit_select_mode(cx);
4126}
4252}
4127
4253
4128fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
4254fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4161,6 +4287,7 @@ fn indent(cx: &mut Context) {
4161 }),
4287 }),
4162 );
4288 );
4163 doc.apply(&transaction, view.id);
4289 doc.apply(&transaction, view.id);
4290+ exit_select_mode(cx);
4164}
4291}
4165
4292
4166fn unindent(cx: &mut Context) {
4293fn unindent(cx: &mut Context) {
@@ -4200,6 +4327,7 @@ fn unindent(cx: &mut Context) {
4200 let transaction = Transaction::change(doc.text(), changes.into_iter());
4327 let transaction = Transaction::change(doc.text(), changes.into_iter());
4201
4328
4202 doc.apply(&transaction, view.id);
4329 doc.apply(&transaction, view.id);
4330+ exit_select_mode(cx);
4203}
4331}
4204
4332
4205fn format_selections(cx: &mut Context) {
4333fn format_selections(cx: &mut Context) {
@@ -4229,7 +4357,7 @@ fn format_selections(cx: &mut Context) {
4229 })
4357 })
4230 else {
4358 else {
4231 cx.editor
4359 cx.editor
4232− .set_error("No configured language server does not support range formatting");
4360+ .set_error("No configured language server supports range formatting");
4233 return;
4361 return;
4234 };
4362 };
4235
4363
@@ -4266,10 +4394,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4266 use movement::skip_while;
4394 use movement::skip_while;
4267 let (view, doc) = current!(cx.editor);
4395 let (view, doc) = current!(cx.editor);
4268 let text = doc.text();
4396 let text = doc.text();
4269− let slice = doc.text().slice(..);
4397+ let slice = text.slice(..);
4270
4398
4271 let mut changes = Vec::new();
4399 let mut changes = Vec::new();
4272− let fragment = Tendril::from(" ");
4273
4400
4274 for selection in doc.selection(view.id) {
4401 for selection in doc.selection(view.id) {
4275 let (start, mut end) = selection.line_range(slice);
4402 let (start, mut end) = selection.line_range(slice);
@@ -4285,9 +4412,13 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4285 let mut end = text.line_to_char(line + 1);
4412 let mut end = text.line_to_char(line + 1);
4286 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
4413 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
4287
4414
4288− // need to skip from start, not end
4415+ let separator = if end == line_end_char_index(&slice, line + 1) {
4289− let change = (start, end, Some(fragment.clone()));
4416+ // the joining line contains only space-characters => don't include a whitespace when joining
4417+ None
4418+ } else {
4419+ Some(Tendril::from(" "))
4420+ };
4290− changes.push(change);
4421+ changes.push((start, end, separator));
4291 }
4422 }
4292 }
4423 }
4293
4424
@@ -4299,9 +4430,6 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4299 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
4430 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
4300 changes.dedup();
4431 changes.dedup();
4301
4432
4302− // TODO: joining multiple empty lines should be replaced by a single space.
4303− // need to merge change ranges that touch
4304−
4305 // select inserted spaces
4433 // select inserted spaces
4306 let transaction = if select_space {
4434 let transaction = if select_space {
4307 let ranges: SmallVec<_> = changes
4435 let ranges: SmallVec<_> = changes
@@ -4313,9 +4441,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4313 })
4441 })
4314 .collect();
4442 .collect();
4315 let selection = Selection::new(ranges, 0);
4443 let selection = Selection::new(ranges, 0);
4316− Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
4444+ Transaction::change(text, changes.into_iter()).with_selection(selection)
4317 } else {
4445 } else {
4318− Transaction::change(doc.text(), changes.into_iter())
4446+ Transaction::change(text, changes.into_iter())
4319 };
4447 };
4320
4448
4321 doc.apply(&transaction, view.id);
4449 doc.apply(&transaction, view.id);
@@ -4329,8 +4457,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4329 if remove { "remove:" } else { "keep:" }.into(),
4457 if remove { "remove:" } else { "keep:" }.into(),
4330 Some(reg),
4458 Some(reg),
4331 ui::completers::none,
4459 ui::completers::none,
4332− move |editor, regex, event| {
4460+ move |cx, regex, event| {
4333− let (view, doc) = current!(editor);
4461+ let (view, doc) = current!(cx.editor);
4334 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
4462 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
4335 return;
4463 return;
4336 }
4464 }
@@ -4691,6 +4819,49 @@ fn select_prev_sibling(cx: &mut Context) {
4691 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
4819 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
4692}
4820}
4693
4821
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+
4694fn match_brackets(cx: &mut Context) {
4865fn match_brackets(cx: &mut Context) {
4695 let (view, doc) = current!(cx.editor);
4866 let (view, doc) = current!(cx.editor);
4696 let is_select = cx.editor.mode == Mode::Select;
4867 let is_select = cx.editor.mode == Mode::Select;
@@ -4804,17 +4975,19 @@ fn transpose_view(cx: &mut Context) {
4804 cx.editor.transpose_view()
4975 cx.editor.transpose_view()
4805}
4976}
4806
4977
4807−// split helper, clear it later
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).
4808−fn split(cx: &mut Context, action: Action) {
4981+fn split(editor: &mut Editor, action: Action) {
4809− let (view, doc) = current!(cx.editor);
4982+ let (view, doc) = current!(editor);
4810 let id = doc.id();
4983 let id = doc.id();
4811 let selection = doc.selection(view.id).clone();
4984 let selection = doc.selection(view.id).clone();
4812 let offset = view.offset;
4985 let offset = view.offset;
4813
4986
4814− cx.editor.switch(id, action);
4987+ editor.switch(id, action);
4815
4988
4816 // match the selection in the previous view
4989 // match the selection in the previous view
4817− let (view, doc) = current!(cx.editor);
4990+ let (view, doc) = current!(editor);
4818 doc.set_selection(view.id, selection);
4991 doc.set_selection(view.id, selection);
4819 // match the view scroll offset (switch doesn't handle this fully
4992 // match the view scroll offset (switch doesn't handle this fully
4820 // since the selection is only matched after the split)
4993 // since the selection is only matched after the split)
@@ -4822,7 +4995,7 @@ fn split(cx: &mut Context, action: Action) {
4822}
4995}
4823
4996
4824fn hsplit(cx: &mut Context) {
4997fn hsplit(cx: &mut Context) {
4825− split(cx, Action::HorizontalSplit);
4998+ split(cx.editor, Action::HorizontalSplit);
4826}
4999}
4827
5000
4828fn hsplit_new(cx: &mut Context) {
5001fn hsplit_new(cx: &mut Context) {
@@ -4830,7 +5003,7 @@ fn hsplit_new(cx: &mut Context) {
4830}
5003}
4831
5004
4832fn vsplit(cx: &mut Context) {
5005fn vsplit(cx: &mut Context) {
4833− split(cx, Action::VerticalSplit);
5006+ split(cx.editor, Action::VerticalSplit);
4834}
5007}
4835
5008
4836fn vsplit_new(cx: &mut Context) {
5009fn vsplit_new(cx: &mut Context) {
@@ -4879,7 +5052,12 @@ fn insert_register(cx: &mut Context) {
4879 if let Some(ch) = event.char() {
5052 if let Some(ch) = event.char() {
4880 cx.editor.autoinfo = None;
5053 cx.editor.autoinfo = None;
4881 cx.register = Some(ch);
5054 cx.register = Some(ch);
5055+ paste(
5056+ cx.editor,
5057+ cx.register.unwrap_or('"'),
4882− paste(cx, Paste::Cursor);
5058+ Paste::Cursor,
5059+ cx.count(),
5060+ );
4883 }
5061 }
4884 })
5062 })
4885}
5063}
@@ -5431,12 +5609,18 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
5431 };
5609 };
5432
5610
5433 // These `usize`s cannot underflow because selection ranges cannot overlap.
5611 // These `usize`s cannot underflow because selection ranges cannot overlap.
5434− // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5612+ let anchor = to
5435− // arithmetic to assert this.
5613+ .checked_add_signed(offset)
5436− let anchor = (to as isize + offset - deleted_len as isize) as usize;
5614+ .expect("Selection ranges cannot overlap")
5615+ .checked_sub(deleted_len)
5616+ .expect("Selection ranges cannot overlap");
5437 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
5617 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
5438 ranges.push(new_range);
5618 ranges.push(new_range);
5439− offset = offset + output_len as isize - deleted_len as isize;
5619+ offset = offset
5620+ .checked_add_unsigned(output_len)
5621+ .expect("Selection ranges cannot overlap")
5622+ .checked_sub_unsigned(deleted_len)
5623+ .expect("Selection ranges cannot overlap");
5440
5624
5441 changes.push((from, to, Some(output)));
5625 changes.push((from, to, Some(output)));
5442 }
5626 }
@@ -5575,6 +5759,7 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
5575 let transaction = Transaction::change(doc.text(), changes.into_iter());
5759 let transaction = Transaction::change(doc.text(), changes.into_iter());
5576 let transaction = transaction.with_selection(new_selection);
5760 let transaction = transaction.with_selection(new_selection);
5577 doc.apply(&transaction, view.id);
5761 doc.apply(&transaction, view.id);
5762+ exit_select_mode(cx);
5578 }
5763 }
5579}
5764}
5580
5765
@@ -5593,9 +5778,12 @@ fn record_macro(cx: &mut Context) {
5593 }
5778 }
5594 })
5779 })
5595 .collect::<String>();
5780 .collect::<String>();
5596− cx.editor.registers.write(reg, vec![s]);
5781+ match cx.editor.registers.write(reg, vec![s]) {
5782+ Ok(_) => cx
5597− cx.editor
5783+ .editor
5598− .set_status(format!("Recorded to register [{}]", reg));
5784+ .set_status(format!("Recorded to register [{}]", reg)),
5785+ Err(err) => cx.editor.set_error(err.to_string()),
5786+ }
5599 } else {
5787 } else {
5600 let reg = cx.register.take().unwrap_or('@');
5788 let reg = cx.register.take().unwrap_or('@');
5601 cx.editor.macro_recording = Some((reg, Vec::new()));
5789 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5615,8 +5803,14 @@ fn replay_macro(cx: &mut Context) {
5615 return;
5803 return;
5616 }
5804 }
5617
5805
5618− let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
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+ {
5619− match helix_view::input::parse_macro(keys_str) {
5813+ match helix_view::input::parse_macro(&keys) {
5620 Ok(keys) => keys,
5814 Ok(keys) => keys,
5621 Err(err) => {
5815 Err(err) => {
5622 cx.editor.set_error(format!("Invalid macro: {}", err));
5816 cx.editor.set_error(format!("Invalid macro: {}", err));
helix-term/src/commands/dap.rs+13−4
@@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
8use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
8use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
9use helix_dap::{self as dap, Client};
9use helix_dap::{self as dap, Client};
10use helix_lsp::block_on;
10use helix_lsp::block_on;
11−use helix_view::editor::Breakpoint;
11+use helix_view::{editor::Breakpoint, graphics::Margin};
12
12
13use serde_json::{to_value, Value};
13use serde_json::{to_value, Value};
14use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -339,8 +339,12 @@ fn debug_parameter_prompt(
339 .to_owned();
339 .to_owned();
340
340
341 let completer = match field_type {
341 let completer = match field_type {
342− "filename" => ui::completers::filename,
342+ "filename" => |editor: &Editor, input: &str| {
343− "directory" => ui::completers::directory,
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+ },
344 _ => ui::completers::none,
348 _ => ui::completers::none,
345 };
349 };
346
350
@@ -577,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
577 }
581 }
578
582
579 let contents = Text::from(tui::text::Text::from(variables));
583 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+ };
580− let popup = Popup::new("dap-variables", contents);
589+ let popup = Popup::new("dap-variables", contents).margin(margin);
581 cx.replace_or_push_layer("dap-variables", popup);
590 cx.replace_or_push_layer("dap-variables", popup);
582}
591}
583
592
helix-term/src/commands/lsp.rs+48−63
@@ -23,6 +23,7 @@ use helix_core::{
23use helix_view::{
23use helix_view::{
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
25 editor::Action,
25 editor::Action,
26+ graphics::Margin,
26 theme::Style,
27 theme::Style,
27 Document, View,
28 Document, View,
28};
29};
@@ -49,7 +50,7 @@ use std::{
49/// If there is no configured language server that supports the feature, this displays a status message.
50/// If there is no configured language server that supports the feature, this displays a status message.
50/// Using this macro in a context where the editor automatically queries the LSP
51/// Using this macro in a context where the editor automatically queries the LSP
51/// (instead of when the user explicitly does so via a keybind like `gd`)
52/// (instead of when the user explicitly does so via a keybind like `gd`)
52−/// will spam the "No configured language server supports <feature>" status message confusingly.
53+/// will spam the "No configured language server supports \<feature>" status message confusingly.
53#[macro_export]
54#[macro_export]
54macro_rules! language_server_with_feature {
55macro_rules! language_server_with_feature {
55 ($editor:expr, $doc:expr, $feature:expr) => {{
56 ($editor:expr, $doc:expr, $feature:expr) => {{
@@ -195,7 +196,6 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
195 (path.into(), line)
196 (path.into(), line)
196}
197}
197
198
198−// TODO: share with symbol picker(symbol.location)
199fn jump_to_location(
199fn jump_to_location(
200 editor: &mut Editor,
200 editor: &mut Editor,
201 location: &lsp::Location,
201 location: &lsp::Location,
@@ -213,15 +213,16 @@ fn jump_to_location(
213 return;
213 return;
214 }
214 }
215 };
215 };
216+
216− match editor.open(&path, action) {
217+ let doc = match editor.open(&path, action) {
217− Ok(_) => (),
218+ Ok(id) => doc_mut!(editor, &id),
218 Err(err) => {
219 Err(err) => {
219 let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
220 let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
220 editor.set_error(err);
221 editor.set_error(err);
221 return;
222 return;
222 }
223 }
223− }
224+ };
224− let (view, doc) = current!(editor);
225+ let view = view_mut!(editor);
225 // TODO: convert inside server
226 // TODO: convert inside server
226 let new_range =
227 let new_range =
227 if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
228 if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
@@ -233,45 +234,22 @@ fn jump_to_location(
233 // we flip the range so that the cursor sits on the start of the symbol
234 // we flip the range so that the cursor sits on the start of the symbol
234 // (for example start of the function).
235 // (for example start of the function).
235 doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
236 doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
237+ if action.align_view(view, doc.id()) {
236− align_view(doc, view, Align::Center);
238+ align_view(doc, view, Align::Center);
239+ }
237}
240}
238
241
239type SymbolPicker = Picker<SymbolInformationItem>;
242type SymbolPicker = Picker<SymbolInformationItem>;
240
243
241fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
244fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
242 // TODO: drop current_path comparison and instead use workspace: bool flag?
245 // TODO: drop current_path comparison and instead use workspace: bool flag?
243− Picker::new(symbols, current_path.clone(), move |cx, item, action| {
246+ Picker::new(symbols, current_path, move |cx, item, action| {
247+ jump_to_location(
244− let (view, doc) = current!(cx.editor);
248+ 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+ &item.symbol.location,
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)
250+ item.offset_encoding,
269− {
251+ action,
270− // we flip the range so that the cursor sits on the start of the symbol
252+ );
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− }
275 })
253 })
276 .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
254 .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
277 .truncate_start(false)
255 .truncate_start(false)
@@ -286,7 +264,7 @@ enum DiagnosticsFormat {
286fn diag_picker(
264fn diag_picker(
287 cx: &Context,
265 cx: &Context,
288 diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
266 diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
289− current_path: Option<lsp::Url>,
267+ _current_path: Option<lsp::Url>,
290 format: DiagnosticsFormat,
268 format: DiagnosticsFormat,
291) -> Picker<PickerDiagnostic> {
269) -> Picker<PickerDiagnostic> {
292 // TODO: drop current_path comparison and instead use workspace: bool flag?
270 // TODO: drop current_path comparison and instead use workspace: bool flag?
@@ -324,22 +302,12 @@ fn diag_picker(
324 offset_encoding,
302 offset_encoding,
325 },
303 },
326 action| {
304 action| {
327− if current_path.as_ref() == Some(url) {
305+ jump_to_location(
328− let (view, doc) = current!(cx.editor);
306+ cx.editor,
329− push_jump(view, doc);
307+ &lsp::Location::new(url.clone(), diag.range),
330− } else {
308+ *offset_encoding,
331− let path = url.to_file_path().unwrap();
309+ action,
332− cx.editor.open(&path, action).expect("editor.open failed");
310+ )
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− }
343 },
311 },
344 )
312 )
345 .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
313 .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
@@ -730,7 +698,8 @@ pub fn code_action(cx: &mut Context) {
730
698
731 // always present here
699 // always present here
732 let action = action.unwrap();
700 let action = action.unwrap();
733− let Some(language_server) = editor.language_server_by_id(action.language_server_id) else {
701+ let Some(language_server) = editor.language_server_by_id(action.language_server_id)
702+ else {
734 editor.set_error("Language Server disappeared");
703 editor.set_error("Language Server disappeared");
735 return;
704 return;
736 };
705 };
@@ -746,15 +715,20 @@ pub fn code_action(cx: &mut Context) {
746 // we support lsp "codeAction/resolve" for `edit` and `command` fields
715 // we support lsp "codeAction/resolve" for `edit` and `command` fields
747 let mut resolved_code_action = None;
716 let mut resolved_code_action = None;
748 if code_action.edit.is_none() || code_action.command.is_none() {
717 if code_action.edit.is_none() || code_action.command.is_none() {
718+ if let Some(future) =
749− if let Some(future) = language_server.resolve_code_action(code_action.clone()) {
719+ language_server.resolve_code_action(code_action.clone())
720+ {
750 if let Ok(response) = helix_lsp::block_on(future) {
721 if let Ok(response) = helix_lsp::block_on(future) {
722+ if let Ok(code_action) =
751− if let Ok(code_action) = serde_json::from_value::<CodeAction>(response) {
723+ serde_json::from_value::<CodeAction>(response)
724+ {
752 resolved_code_action = Some(code_action);
725 resolved_code_action = Some(code_action);
753 }
726 }
754 }
727 }
755 }
728 }
756 }
729 }
730+ let resolved_code_action =
757− let resolved_code_action = resolved_code_action.as_ref().unwrap_or(code_action);
731+ resolved_code_action.as_ref().unwrap_or(code_action);
758
732
759 if let Some(ref workspace_edit) = resolved_code_action.edit {
733 if let Some(ref workspace_edit) = resolved_code_action.edit {
760 log::debug!("edit: {:?}", workspace_edit);
734 log::debug!("edit: {:?}", workspace_edit);
@@ -771,7 +745,16 @@ pub fn code_action(cx: &mut Context) {
771 });
745 });
772 picker.move_down(); // pre-select the first item
746 picker.move_down(); // pre-select the first item
773
747
748+ let margin = if editor.menu_border() {
749+ Margin::vertical(1)
750+ } else {
751+ Margin::none()
752+ };
753+
774− let popup = Popup::new("code-action", picker).with_scrollbar(false);
754+ let popup = Popup::new("code-action", picker)
755+ .with_scrollbar(false)
756+ .margin(margin);
757+
775 compositor.replace_or_push("code-action", popup);
758 compositor.replace_or_push("code-action", popup);
776 };
759 };
777
760
@@ -1186,7 +1169,8 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1186 // Do not show the message if signature help was invoked
1169 // Do not show the message if signature help was invoked
1187 // automatically on backspace, trigger characters, etc.
1170 // automatically on backspace, trigger characters, etc.
1188 if invoked == SignatureHelpInvoked::Manual {
1171 if invoked == SignatureHelpInvoked::Manual {
1172+ cx.editor
1189− cx.editor.set_error("No configured language server supports signature-help");
1173+ .set_error("No configured language server supports signature-help");
1190 }
1174 }
1191 return;
1175 return;
1192 };
1176 };
@@ -1411,7 +1395,8 @@ pub fn rename_symbol(cx: &mut Context) {
1411 .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1395 .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1412 .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
1396 .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
1413 else {
1397 else {
1398+ cx.editor
1414− cx.editor.set_error("No configured language server supports symbol renaming");
1399+ .set_error("No configured language server supports symbol renaming");
1415 return;
1400 return;
1416 };
1401 };
1417
1402
helix-term/src/commands/typed.rs+892−698
@@ -5,7 +5,10 @@ use crate::job::Job;
5
5
6use super::*;
6use super::*;
7
7
8+use helix_core::fuzzy::fuzzy_match;
9+use helix_core::indent::MAX_INDENT;
8−use helix_core::{encoding, shellwords::Shellwords};
10+use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
11+use helix_lsp::{OffsetEncoding, Url};
9use helix_view::document::DEFAULT_LANGUAGE_NAME;
12use helix_view::document::DEFAULT_LANGUAGE_NAME;
10use helix_view::editor::{Action, CloseError, ConfigEvent};
13use helix_view::editor::{Action, CloseError, ConfigEvent};
11use serde_json::Value;
14use serde_json::Value;
@@ -329,12 +332,16 @@ fn write_impl(
329 path: Option<&Cow<str>>,
332 path: Option<&Cow<str>>,
330 force: bool,
333 force: bool,
331) -> anyhow::Result<()> {
334) -> anyhow::Result<()> {
332− let editor_auto_fmt = cx.editor.config().auto_format;
335+ let config = cx.editor.config();
333 let jobs = &mut cx.jobs;
336 let jobs = &mut cx.jobs;
334 let (view, doc) = current!(cx.editor);
337 let (view, doc) = current!(cx.editor);
335 let path = path.map(AsRef::as_ref);
338 let path = path.map(AsRef::as_ref);
336
339
337− let fmt = if editor_auto_fmt {
340+ if config.insert_final_newline {
341+ insert_final_newline(doc, view);
342+ }
343+
344+ let fmt = if config.auto_format {
338 doc.auto_format().map(|fmt| {
345 doc.auto_format().map(|fmt| {
339 let callback = make_format_callback(
346 let callback = make_format_callback(
340 doc.id(),
347 doc.id(),
@@ -358,6 +365,16 @@ fn write_impl(
358 Ok(())
365 Ok(())
359}
366}
360
367
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+
361fn write(
378fn write(
362 cx: &mut compositor::Context,
379 cx: &mut compositor::Context,
363 args: &[Cow<str>],
380 args: &[Cow<str>],
@@ -460,8 +477,7 @@ fn set_indent_style(
460 cx.editor.set_status(match style {
477 cx.editor.set_status(match style {
461 Tabs => "tabs".to_owned(),
478 Tabs => "tabs".to_owned(),
462 Spaces(1) => "1 space".to_owned(),
479 Spaces(1) => "1 space".to_owned(),
463− Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
480+ Spaces(n) => format!("{} spaces", n),
464− _ => unreachable!(), // Shouldn't happen.
465 });
481 });
466 return Ok(());
482 return Ok(());
467 }
483 }
@@ -473,7 +489,7 @@ fn set_indent_style(
473 Some(arg) => arg
489 Some(arg) => arg
474 .parse::<u8>()
490 .parse::<u8>()
475 .ok()
491 .ok()
476− .filter(|n| (1..=8).contains(n))
492+ .filter(|n| (1..=MAX_INDENT).contains(n))
477 .map(Spaces),
493 .map(Spaces),
478 _ => None,
494 _ => None,
479 };
495 };
@@ -558,7 +574,6 @@ fn set_line_ending(
558
574
559 Ok(())
575 Ok(())
560}
576}
561−
562fn earlier(
577fn earlier(
563 cx: &mut compositor::Context,
578 cx: &mut compositor::Context,
564 args: &[Cow<str>],
579 args: &[Cow<str>],
@@ -657,11 +672,10 @@ pub fn write_all_impl(
657 write_scratch: bool,
672 write_scratch: bool,
658) -> anyhow::Result<()> {
673) -> anyhow::Result<()> {
659 let mut errors: Vec<&'static str> = Vec::new();
674 let mut errors: Vec<&'static str> = Vec::new();
660− let auto_format = cx.editor.config().auto_format;
675+ let config = cx.editor.config();
661 let jobs = &mut cx.jobs;
676 let jobs = &mut cx.jobs;
662 let current_view = view!(cx.editor);
677 let current_view = view!(cx.editor);
663
678
664− // save all documents
665 let saves: Vec<_> = cx
679 let saves: Vec<_> = cx
666 .editor
680 .editor
667 .documents
681 .documents
@@ -672,7 +686,7 @@ pub fn write_all_impl(
672 }
686 }
673 if doc.path().is_none() {
687 if doc.path().is_none() {
674 if write_scratch {
688 if write_scratch {
675− errors.push("cannot write a buffer without a filename\n");
689+ errors.push("cannot write a buffer without a filename");
676 }
690 }
677 return None;
691 return None;
678 }
692 }
@@ -692,32 +706,35 @@ pub fn write_all_impl(
692 current_view.id
706 current_view.id
693 };
707 };
694
708
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,
709+ Some((doc.id(), target_view))
701− fmt,
702− Some((None, force)),
703− );
710+ })
704− jobs.add(Job::with_callback(callback).wait_before_exiting());
711+ .collect();
705− })
706− } else {
707− None
708− };
709
712
710− if fmt.is_none() {
713+ for (doc_id, target_view) in saves {
711− return Some(doc.id());
714+ let doc = doc_mut!(cx.editor, &doc_id);
715+
716+ if config.insert_final_newline {
717+ insert_final_newline(doc, view_mut!(cx.editor, target_view));
712− }
718+ }
713
719
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 {
714 None
732 None
715− })
733+ };
716− .collect();
717
734
718− // manually call save for the rest of docs that don't have a formatter
735+ if fmt.is_none() {
719− for id in saves {
720− cx.editor.save::<PathBuf>(id, None, force)?;
736+ cx.editor.save::<PathBuf>(doc_id, None, force)?;
737+ }
721 }
738 }
722
739
723 if !errors.is_empty() && !force {
740 if !errors.is_empty() && !force {
@@ -904,7 +921,8 @@ fn yank_main_selection_to_clipboard(
904 return Ok(());
921 return Ok(());
905 }
922 }
906
923
907− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
924+ yank_primary_selection_impl(cx.editor, '+');
925+ Ok(())
908}
926}
909
927
910fn yank_joined(
928fn yank_joined(
@@ -938,7 +956,8 @@ fn yank_joined_to_clipboard(
938 let doc = doc!(cx.editor);
956 let doc = doc!(cx.editor);
939 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
957 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
940 let separator = args.first().unwrap_or(&default_sep);
958 let separator = args.first().unwrap_or(&default_sep);
941− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
959+ yank_joined_impl(cx.editor, separator, '+');
960+ Ok(())
942}
961}
943
962
944fn yank_main_selection_to_primary_clipboard(
963fn yank_main_selection_to_primary_clipboard(
@@ -950,7 +969,8 @@ fn yank_main_selection_to_primary_clipboard(
950 return Ok(());
969 return Ok(());
951 }
970 }
952
971
953− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
972+ yank_primary_selection_impl(cx.editor, '*');
973+ Ok(())
954}
974}
955
975
956fn yank_joined_to_primary_clipboard(
976fn yank_joined_to_primary_clipboard(
@@ -965,7 +985,8 @@ fn yank_joined_to_primary_clipboard(
965 let doc = doc!(cx.editor);
985 let doc = doc!(cx.editor);
966 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
986 let default_sep = Cow::Borrowed(doc.line_ending.as_str());
967 let separator = args.first().unwrap_or(&default_sep);
987 let separator = args.first().unwrap_or(&default_sep);
968− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
988+ yank_joined_impl(cx.editor, separator, '*');
989+ Ok(())
969}
990}
970
991
971fn paste_clipboard_after(
992fn paste_clipboard_after(
@@ -977,7 +998,8 @@ fn paste_clipboard_after(
977 return Ok(());
998 return Ok(());
978 }
999 }
979
1000
980− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
1001+ paste(cx.editor, '+', Paste::After, 1);
1002+ Ok(())
981}
1003}
982
1004
983fn paste_clipboard_before(
1005fn paste_clipboard_before(
@@ -989,7 +1011,8 @@ fn paste_clipboard_before(
989 return Ok(());
1011 return Ok(());
990 }
1012 }
991
1013
992− paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard, 1)
1014+ paste(cx.editor, '+', Paste::Before, 1);
1015+ Ok(())
993}
1016}
994
1017
995fn paste_primary_clipboard_after(
1018fn paste_primary_clipboard_after(
@@ -1001,7 +1024,8 @@ fn paste_primary_clipboard_after(
1001 return Ok(());
1024 return Ok(());
1002 }
1025 }
1003
1026
1004− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
1027+ paste(cx.editor, '*', Paste::After, 1);
1028+ Ok(())
1005}
1029}
1006
1030
1007fn paste_primary_clipboard_before(
1031fn paste_primary_clipboard_before(
@@ -1013,30 +1037,8 @@ fn paste_primary_clipboard_before(
1013 return Ok(());
1037 return Ok(());
1014 }
1038 }
1015
1039
1016− paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection, 1)
1040+ paste(cx.editor, '*', Paste::Before, 1);
1017−}
1041+ Ok(())
1018−
1019−fn replace_selections_with_clipboard_impl(
1020− cx: &mut compositor::Context,
1021− clipboard_type: ClipboardType,
1022−) -> anyhow::Result<()> {
1023− let scrolloff = cx.editor.config().scrolloff;
1024− let (view, doc) = current!(cx.editor);
1025−
1026− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
1027− Ok(contents) => {
1028− let selection = doc.selection(view.id);
1029− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1030− (range.from(), range.to(), Some(contents.as_str().into()))
1031− });
1032−
1033− doc.apply(&transaction, view.id);
1034− doc.append_changes_to_history(view);
1035− view.ensure_cursor_in_view(doc, scrolloff);
1036− Ok(())
1037− }
1038− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
1039− }
1040}
1042}
1041
1043
1042fn replace_selections_with_clipboard(
1044fn replace_selections_with_clipboard(
@@ -1048,7 +1050,8 @@ fn replace_selections_with_clipboard(
1048 return Ok(());
1050 return Ok(());
1049 }
1051 }
1050
1052
1051− replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
1053+ replace_with_yanked_impl(cx.editor, '+', 1);
1054+ Ok(())
1052}
1055}
1053
1056
1054fn replace_selections_with_primary_clipboard(
1057fn replace_selections_with_primary_clipboard(
@@ -1060,7 +1063,8 @@ fn replace_selections_with_primary_clipboard(
1060 return Ok(());
1063 return Ok(());
1061 }
1064 }
1062
1065
1063− replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
1066+ replace_with_yanked_impl(cx.editor, '*', 1);
1067+ Ok(())
1064}
1068}
1065
1069
1066fn show_clipboard_provider(
1070fn show_clipboard_provider(
@@ -1073,7 +1077,7 @@ fn show_clipboard_provider(
1073 }
1077 }
1074
1078
1075 cx.editor
1079 cx.editor
1076− .set_status(cx.editor.clipboard_provider.name().to_string());
1080+ .set_status(cx.editor.registers.clipboard_provider_name().to_string());
1077 Ok(())
1081 Ok(())
1078}
1082}
1079
1083
@@ -1278,12 +1282,10 @@ fn reload(
1278 }
1282 }
1279
1283
1280 let scrolloff = cx.editor.config().scrolloff;
1284 let scrolloff = cx.editor.config().scrolloff;
1281− let redraw_handle = cx.editor.redraw_handle.clone();
1282 let (view, doc) = current!(cx.editor);
1285 let (view, doc) = current!(cx.editor);
1283− doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1286+ doc.reload(view, &cx.editor.diff_providers).map(|_| {
1284− .map(|_| {
1285− view.ensure_cursor_in_view(doc, scrolloff);
1287+ view.ensure_cursor_in_view(doc, scrolloff);
1286− })?;
1288+ })?;
1287 if let Some(path) = doc.path() {
1289 if let Some(path) = doc.path() {
1288 cx.editor
1290 cx.editor
1289 .language_servers
1291 .language_servers
@@ -1329,8 +1331,7 @@ fn reload_all(
1329 // Ensure that the view is synced with the document's history.
1331 // Ensure that the view is synced with the document's history.
1330 view.sync_changes(doc);
1332 view.sync_changes(doc);
1331
1333
1332− let redraw_handle = cx.editor.redraw_handle.clone();
1333− doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1334+ doc.reload(view, &cx.editor.diff_providers)?;
1334 if let Some(path) = doc.path() {
1335 if let Some(path) = doc.path() {
1335 cx.editor
1336 cx.editor
1336 .language_servers
1337 .language_servers
@@ -1385,9 +1386,8 @@ fn lsp_workspace_command(
1385 .map(|options| (ls.id(), options))
1386 .map(|options| (ls.id(), options))
1386 })
1387 })
1387 else {
1388 else {
1388− cx.editor.set_status(
1389+ cx.editor
1389− "No active language servers for this document support workspace commands",
1390+ .set_status("No active language servers for this document support workspace commands");
1390− );
1391 return Ok(());
1391 return Ok(());
1392 };
1392 };
1393
1393
@@ -1543,6 +1543,84 @@ fn tree_sitter_scopes(
1543 Ok(())
1543 Ok(())
1544}
1544}
1545
1545
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+
1546fn vsplit(
1624fn vsplit(
1547 cx: &mut compositor::Context,
1625 cx: &mut compositor::Context,
1548 args: &[Cow<str>],
1626 args: &[Cow<str>],
@@ -1552,10 +1630,8 @@ fn vsplit(
1552 return Ok(());
1630 return Ok(());
1553 }
1631 }
1554
1632
1555− let id = view!(cx.editor).doc;
1556−
1557 if args.is_empty() {
1633 if args.is_empty() {
1558− cx.editor.switch(id, Action::VerticalSplit);
1634+ split(cx.editor, Action::VerticalSplit);
1559 } else {
1635 } else {
1560 for arg in args {
1636 for arg in args {
1561 cx.editor
1637 cx.editor
@@ -1575,10 +1651,8 @@ fn hsplit(
1575 return Ok(());
1651 return Ok(());
1576 }
1652 }
1577
1653
1578− let id = view!(cx.editor).doc;
1579−
1580 if args.is_empty() {
1654 if args.is_empty() {
1581− cx.editor.switch(id, Action::HorizontalSplit);
1655+ split(cx.editor, Action::HorizontalSplit);
1582 } else {
1656 } else {
1583 for arg in args {
1657 for arg in args {
1584 cx.editor
1658 cx.editor
@@ -1693,7 +1767,7 @@ fn tutor(
1693 let path = helix_loader::runtime_file(Path::new("tutor"));
1767 let path = helix_loader::runtime_file(Path::new("tutor"));
1694 cx.editor.open(&path, Action::Replace)?;
1768 cx.editor.open(&path, Action::Replace)?;
1695 // Unset path to prevent accidentally saving to the original tutor file.
1769 // Unset path to prevent accidentally saving to the original tutor file.
1696− doc_mut!(cx.editor).set_path(None)?;
1770+ doc_mut!(cx.editor).set_path(None);
1697 Ok(())
1771 Ok(())
1698}
1772}
1699
1773
@@ -1870,14 +1944,29 @@ fn toggle_option(
1870 .to_string(),
1944 .to_string(),
1871 )
1945 )
1872 }
1946 }
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+ }
1873− Value::Null | Value::Object(_) | Value::Array(_) | Value::Number(_) => {
1962+ Value::Null | Value::Object(_) | Value::Array(_) => {
1874 anyhow::bail!("Configuration {key} does not support toggle yet")
1963 anyhow::bail!("Configuration {key} does not support toggle yet")
1875 }
1964 }
1876 };
1965 };
1877
1966
1878 let status = format!("'{key}' is now set to {value}");
1967 let status = format!("'{key}' is now set to {value}");
1879 let config = serde_json::from_value(config)
1968 let config = serde_json::from_value(config)
1880− .map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?;
1969+ .map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?;
1881
1970
1882 cx.editor
1971 cx.editor
1883 .config_events
1972 .config_events
@@ -2286,606 +2375,721 @@ fn clear_register(
2286 format!("Invalid register {}", args[0])
2375 format!("Invalid register {}", args[0])
2287 );
2376 );
2288 let register = args[0].chars().next().unwrap_or_default();
2377 let register = args[0].chars().next().unwrap_or_default();
2289− match cx.editor.registers.remove(register) {
2378+ if cx.editor.registers.remove(register) {
2290− Some(_) => cx
2291− .editor
2379+ cx.editor
2292− .set_status(format!("Register {} cleared", register)),
2380+ .set_status(format!("Register {} cleared", register));
2293− None => cx
2381+ } else {
2294− .editor
2382+ cx.editor
2295− .set_error(format!("Register {} not found", register)),
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(());
2296 }
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(());
2418+ }
2419+
2420+ ensure!(args.len() == 1, format!(":move takes one argument"));
2421+ let doc = doc!(cx.editor);
2422+
2423+ let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
2424+ let old_path = doc
2425+ .path()
2426+ .ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
2427+ .clone();
2428+ let old_path_as_url = doc.url().unwrap();
2429+ let new_path_as_url = Url::from_file_path(&new_path).unwrap();
2430+
2431+ let edits: Vec<(
2432+ helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
2433+ OffsetEncoding,
2434+ String,
2435+ )> = doc
2436+ .language_servers()
2437+ .map(|lsp| {
2438+ (
2439+ lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
2440+ lsp.offset_encoding(),
2441+ lsp.name().to_owned(),
2442+ )
2443+ })
2444+ .filter(|(f, _, _)| f.is_some())
2445+ .map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
2446+ .collect();
2447+
2448+ for (lsp_reply, encoding, name) in edits {
2449+ match lsp_reply {
2450+ Ok(edit) => {
2451+ if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
2452+ log::error!(
2453+ ":move command failed to apply edits from lsp {}: {:?}",
2454+ name,
2455+ e
2456+ );
2457+ };
2458+ }
2459+ Err(e) => {
2460+ log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
2461+ }
2462+ };
2463+ }
2464+
2465+ let doc = doc_mut!(cx.editor);
2466+
2467+ doc.set_path(Some(new_path.as_path()));
2468+ if let Err(e) = std::fs::rename(&old_path, &new_path) {
2469+ doc.set_path(Some(old_path.as_path()));
2470+ bail!("Could not move file: {}", e);
2471+ };
2472+
2473+ doc.language_servers().for_each(|lsp| {
2474+ lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
2475+ });
2476+
2477+ cx.editor
2478+ .language_servers
2479+ .file_event_handler
2480+ .file_changed(new_path);
2481+
2297 Ok(())
2482 Ok(())
2298}
2483}
2299
2484
2300pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2485pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2301− TypableCommand {
2486+ TypableCommand {
2302− name: "quit",
2487+ name: "quit",
2303− aliases: &["q"],
2488+ aliases: &["q"],
2304− doc: "Close the current view.",
2489+ doc: "Close the current view.",
2305− fun: quit,
2490+ fun: quit,
2306− signature: CommandSignature::none(),
2491+ signature: CommandSignature::none(),
2307− },
2492+ },
2308− TypableCommand {
2493+ TypableCommand {
2309− name: "quit!",
2494+ name: "quit!",
2310− aliases: &["q!"],
2495+ aliases: &["q!"],
2311− doc: "Force close the current view, ignoring unsaved changes.",
2496+ doc: "Force close the current view, ignoring unsaved changes.",
2312− fun: force_quit,
2497+ fun: force_quit,
2313− signature: CommandSignature::none(),
2498+ signature: CommandSignature::none(),
2314− },
2499+ },
2315− TypableCommand {
2500+ TypableCommand {
2316− name: "open",
2501+ name: "open",
2317− aliases: &["o"],
2502+ aliases: &["o"],
2318− doc: "Open a file from disk into the current view.",
2503+ doc: "Open a file from disk into the current view.",
2319− fun: open,
2504+ fun: open,
2320− signature: CommandSignature::all(completers::filename),
2505+ signature: CommandSignature::all(completers::filename),
2321− },
2506+ },
2322− TypableCommand {
2507+ TypableCommand {
2323− name: "buffer-close",
2508+ name: "buffer-close",
2324− aliases: &["bc", "bclose"],
2509+ aliases: &["bc", "bclose"],
2325− doc: "Close the current buffer.",
2510+ doc: "Close the current buffer.",
2326− fun: buffer_close,
2511+ fun: buffer_close,
2327− signature: CommandSignature::all(completers::buffer),
2512+ signature: CommandSignature::all(completers::buffer),
2328− },
2513+ },
2329− TypableCommand {
2514+ TypableCommand {
2330− name: "buffer-close!",
2515+ name: "buffer-close!",
2331− aliases: &["bc!", "bclose!"],
2516+ aliases: &["bc!", "bclose!"],
2332− doc: "Close the current buffer forcefully, ignoring unsaved changes.",
2517+ doc: "Close the current buffer forcefully, ignoring unsaved changes.",
2333− fun: force_buffer_close,
2518+ fun: force_buffer_close,
2334− signature: CommandSignature::all(completers::buffer)
2519+ signature: CommandSignature::all(completers::buffer)
2335− },
2520+ },
2336− TypableCommand {
2521+ TypableCommand {
2337− name: "buffer-close-others",
2522+ name: "buffer-close-others",
2338− aliases: &["bco", "bcloseother"],
2523+ aliases: &["bco", "bcloseother"],
2339− doc: "Close all buffers but the currently focused one.",
2524+ doc: "Close all buffers but the currently focused one.",
2340− fun: buffer_close_others,
2525+ fun: buffer_close_others,
2341− signature: CommandSignature::none(),
2526+ signature: CommandSignature::none(),
2342− },
2527+ },
2343− TypableCommand {
2528+ TypableCommand {
2344− name: "buffer-close-others!",
2529+ name: "buffer-close-others!",
2345− aliases: &["bco!", "bcloseother!"],
2530+ aliases: &["bco!", "bcloseother!"],
2346− doc: "Force close all buffers but the currently focused one.",
2531+ doc: "Force close all buffers but the currently focused one.",
2347− fun: force_buffer_close_others,
2532+ fun: force_buffer_close_others,
2348− signature: CommandSignature::none(),
2533+ signature: CommandSignature::none(),
2349− },
2534+ },
2350− TypableCommand {
2535+ TypableCommand {
2351− name: "buffer-close-all",
2536+ name: "buffer-close-all",
2352− aliases: &["bca", "bcloseall"],
2537+ aliases: &["bca", "bcloseall"],
2353− doc: "Close all buffers without quitting.",
2538+ doc: "Close all buffers without quitting.",
2354− fun: buffer_close_all,
2539+ fun: buffer_close_all,
2355− signature: CommandSignature::none(),
2540+ signature: CommandSignature::none(),
2356− },
2541+ },
2357− TypableCommand {
2542+ TypableCommand {
2358− name: "buffer-close-all!",
2543+ name: "buffer-close-all!",
2359− aliases: &["bca!", "bcloseall!"],
2544+ aliases: &["bca!", "bcloseall!"],
2360− doc: "Force close all buffers ignoring unsaved changes without quitting.",
2545+ doc: "Force close all buffers ignoring unsaved changes without quitting.",
2361− fun: force_buffer_close_all,
2546+ fun: force_buffer_close_all,
2362− signature: CommandSignature::none(),
2547+ signature: CommandSignature::none(),
2363− },
2548+ },
2364− TypableCommand {
2549+ TypableCommand {
2365− name: "buffer-next",
2550+ name: "buffer-next",
2366− aliases: &["bn", "bnext"],
2551+ aliases: &["bn", "bnext"],
2367− doc: "Goto next buffer.",
2552+ doc: "Goto next buffer.",
2368− fun: buffer_next,
2553+ fun: buffer_next,
2369− signature: CommandSignature::none(),
2554+ signature: CommandSignature::none(),
2370− },
2555+ },
2371− TypableCommand {
2556+ TypableCommand {
2372− name: "buffer-previous",
2557+ name: "buffer-previous",
2373− aliases: &["bp", "bprev"],
2558+ aliases: &["bp", "bprev"],
2374− doc: "Goto previous buffer.",
2559+ doc: "Goto previous buffer.",
2375− fun: buffer_previous,
2560+ fun: buffer_previous,
2376− signature: CommandSignature::none(),
2561+ signature: CommandSignature::none(),
2377− },
2562+ },
2378− TypableCommand {
2563+ TypableCommand {
2379− name: "write",
2564+ name: "write",
2380− aliases: &["w"],
2565+ aliases: &["w"],
2381− doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2566+ doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2382− fun: write,
2567+ fun: write,
2383− signature: CommandSignature::positional(&[completers::filename]),
2568+ signature: CommandSignature::positional(&[completers::filename]),
2384− },
2569+ },
2385− TypableCommand {
2570+ TypableCommand {
2386− name: "write!",
2571+ name: "write!",
2387− aliases: &["w!"],
2572+ aliases: &["w!"],
2388− doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
2573+ doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
2389− fun: force_write,
2574+ fun: force_write,
2390− signature: CommandSignature::positional(&[completers::filename]),
2575+ signature: CommandSignature::positional(&[completers::filename]),
2391− },
2576+ },
2392− TypableCommand {
2577+ TypableCommand {
2393− name: "write-buffer-close",
2578+ name: "write-buffer-close",
2394− aliases: &["wbc"],
2579+ aliases: &["wbc"],
2395− doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
2580+ doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
2396− fun: write_buffer_close,
2581+ fun: write_buffer_close,
2397− signature: CommandSignature::positional(&[completers::filename]),
2582+ signature: CommandSignature::positional(&[completers::filename]),
2398− },
2583+ },
2399− TypableCommand {
2584+ TypableCommand {
2400− name: "write-buffer-close!",
2585+ name: "write-buffer-close!",
2401− aliases: &["wbc!"],
2586+ aliases: &["wbc!"],
2402− doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
2587+ doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
2403− fun: force_write_buffer_close,
2588+ fun: force_write_buffer_close,
2404− signature: CommandSignature::positional(&[completers::filename]),
2589+ signature: CommandSignature::positional(&[completers::filename]),
2405− },
2590+ },
2406− TypableCommand {
2591+ TypableCommand {
2407− name: "new",
2592+ name: "new",
2408− aliases: &["n"],
2593+ aliases: &["n"],
2409− doc: "Create a new scratch buffer.",
2594+ doc: "Create a new scratch buffer.",
2410− fun: new_file,
2595+ fun: new_file,
2411− // TODO: This seems to complete with a filename, but doesn't use that filename to
2596+ signature: CommandSignature::none(),
2412− // set the path of the newly created buffer.
2597+ },
2413− signature: CommandSignature::positional(&[completers::filename]),
2598+ TypableCommand {
2414− },
2599+ name: "format",
2415− TypableCommand {
2600+ aliases: &["fmt"],
2416− name: "format",
2601+ doc: "Format the file using the LSP formatter.",
2417− aliases: &["fmt"],
2602+ fun: format,
2418− doc: "Format the file using the LSP formatter.",
2603+ signature: CommandSignature::none(),
2419− fun: format,
2604+ },
2420− signature: CommandSignature::none(),
2605+ TypableCommand {
2421− },
2606+ name: "indent-style",
2422− TypableCommand {
2607+ aliases: &[],
2423− name: "indent-style",
2608+ doc: "Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.)",
2424− aliases: &[],
2609+ fun: set_indent_style,
2425− doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
2610+ signature: CommandSignature::none(),
2426− fun: set_indent_style,
2611+ },
2427− signature: CommandSignature::none(),
2612+ TypableCommand {
2428− },
2613+ name: "line-ending",
2429− TypableCommand {
2614+ aliases: &[],
2430− name: "line-ending",
2615+ #[cfg(not(feature = "unicode-lines"))]
2431− aliases: &[],
2616+ doc: "Set the document's default line ending. Options: crlf, lf.",
2432− #[cfg(not(feature = "unicode-lines"))]
2617+ #[cfg(feature = "unicode-lines")]
2433− doc: "Set the document's default line ending. Options: crlf, lf.",
2618+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2434− #[cfg(feature = "unicode-lines")]
2619+ fun: set_line_ending,
2435− doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2620+ signature: CommandSignature::none(),
2436− fun: set_line_ending,
2621+ },
2437− signature: CommandSignature::none(),
2622+ TypableCommand {
2438− },
2623+ name: "earlier",
2439− TypableCommand {
2624+ aliases: &["ear"],
2440− name: "earlier",
2625+ doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2441− aliases: &["ear"],
2626+ fun: earlier,
2442− doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2627+ signature: CommandSignature::none(),
2443− fun: earlier,
2628+ },
2444− signature: CommandSignature::none(),
2629+ TypableCommand {
2445− },
2630+ name: "later",
2446− TypableCommand {
2631+ aliases: &["lat"],
2447− name: "later",
2632+ doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2448− aliases: &["lat"],
2633+ fun: later,
2449− doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2634+ signature: CommandSignature::none(),
2450− fun: later,
2635+ },
2451− signature: CommandSignature::none(),
2636+ TypableCommand {
2452− },
2637+ name: "write-quit",
2453− TypableCommand {
2638+ aliases: &["wq", "x"],
2454− name: "write-quit",
2639+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2455− aliases: &["wq", "x"],
2640+ fun: write_quit,
2456− doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2641+ signature: CommandSignature::positional(&[completers::filename]),
2457− fun: write_quit,
2642+ },
2458− signature: CommandSignature::positional(&[completers::filename]),
2643+ TypableCommand {
2459− },
2644+ name: "write-quit!",
2460− TypableCommand {
2645+ aliases: &["wq!", "x!"],
2461− name: "write-quit!",
2646+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2462− aliases: &["wq!", "x!"],
2647+ fun: force_write_quit,
2463− doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2648+ signature: CommandSignature::positional(&[completers::filename]),
2464− fun: force_write_quit,
2649+ },
2465− signature: CommandSignature::positional(&[completers::filename]),
2650+ TypableCommand {
2466− },
2651+ name: "write-all",
2467− TypableCommand {
2652+ aliases: &["wa"],
2468− name: "write-all",
2653+ doc: "Write changes from all buffers to disk.",
2469− aliases: &["wa"],
2654+ fun: write_all,
2470− doc: "Write changes from all buffers to disk.",
2655+ signature: CommandSignature::none(),
2471− fun: write_all,
2656+ },
2472− signature: CommandSignature::none(),
2657+ TypableCommand {
2473− },
2658+ name: "write-all!",
2474− TypableCommand {
2659+ aliases: &["wa!"],
2475− name: "write-all!",
2660+ doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
2476− aliases: &["wa!"],
2661+ fun: force_write_all,
2477− doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
2662+ signature: CommandSignature::none(),
2478− fun: force_write_all,
2663+ },
2479− signature: CommandSignature::none(),
2664+ TypableCommand {
2480− },
2665+ name: "write-quit-all",
2481− TypableCommand {
2666+ aliases: &["wqa", "xa"],
2482− name: "write-quit-all",
2667+ doc: "Write changes from all buffers to disk and close all views.",
2483− aliases: &["wqa", "xa"],
2668+ fun: write_all_quit,
2484− doc: "Write changes from all buffers to disk and close all views.",
2669+ signature: CommandSignature::none(),
2485− fun: write_all_quit,
2670+ },
2486− signature: CommandSignature::none(),
2671+ TypableCommand {
2487− },
2672+ name: "write-quit-all!",
2488− TypableCommand {
2673+ aliases: &["wqa!", "xa!"],
2489− name: "write-quit-all!",
2674+ doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
2490− aliases: &["wqa!", "xa!"],
2675+ fun: force_write_all_quit,
2491− doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
2676+ signature: CommandSignature::none(),
2492− fun: force_write_all_quit,
2677+ },
2493− signature: CommandSignature::none(),
2678+ TypableCommand {
2494− },
2679+ name: "quit-all",
2495− TypableCommand {
2680+ aliases: &["qa"],
2496− name: "quit-all",
2681+ doc: "Close all views.",
2497− aliases: &["qa"],
2682+ fun: quit_all,
2498− doc: "Close all views.",
2683+ signature: CommandSignature::none(),
2499− fun: quit_all,
2684+ },
2500− signature: CommandSignature::none(),
2685+ TypableCommand {
2501− },
2686+ name: "quit-all!",
2502− TypableCommand {
2687+ aliases: &["qa!"],
2503− name: "quit-all!",
2688+ doc: "Force close all views ignoring unsaved changes.",
2504− aliases: &["qa!"],
2689+ fun: force_quit_all,
2505− doc: "Force close all views ignoring unsaved changes.",
2690+ signature: CommandSignature::none(),
2506− fun: force_quit_all,
2691+ },
2507− signature: CommandSignature::none(),
2692+ TypableCommand {
2508− },
2693+ name: "cquit",
2509− TypableCommand {
2694+ aliases: &["cq"],
2510− name: "cquit",
2695+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2511− aliases: &["cq"],
2696+ fun: cquit,
2512− doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2697+ signature: CommandSignature::none(),
2513− fun: cquit,
2698+ },
2514− signature: CommandSignature::none(),
2699+ TypableCommand {
2515− },
2700+ name: "cquit!",
2516− TypableCommand {
2701+ aliases: &["cq!"],
2517− name: "cquit!",
2702+ doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
2518− aliases: &["cq!"],
2703+ fun: force_cquit,
2519− doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
2704+ signature: CommandSignature::none(),
2520− fun: force_cquit,
2705+ },
2521− signature: CommandSignature::none(),
2706+ TypableCommand {
2522− },
2707+ name: "theme",
2523− TypableCommand {
2708+ aliases: &[],
2524− name: "theme",
2709+ doc: "Change the editor theme (show current theme if no name specified).",
2525− aliases: &[],
2710+ fun: theme,
2526− doc: "Change the editor theme (show current theme if no name specified).",
2711+ signature: CommandSignature::positional(&[completers::theme]),
2527− fun: theme,
2712+ },
2528− signature: CommandSignature::positional(&[completers::theme]),
2713+ TypableCommand {
2529− },
2714+ name: "yank-join",
2530− TypableCommand {
2715+ aliases: &[],
2531− name: "yank-join",
2716+ doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
2532− aliases: &[],
2717+ fun: yank_joined,
2533− doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
2718+ signature: CommandSignature::none(),
2534− fun: yank_joined,
2719+ },
2535− signature: CommandSignature::none(),
2720+ TypableCommand {
2536− },
2721+ name: "clipboard-yank",
2537− TypableCommand {
2722+ aliases: &[],
2538− name: "clipboard-yank",
2723+ doc: "Yank main selection into system clipboard.",
2539− aliases: &[],
2724+ fun: yank_main_selection_to_clipboard,
2540− doc: "Yank main selection into system clipboard.",
2725+ signature: CommandSignature::none(),
2541− fun: yank_main_selection_to_clipboard,
2726+ },
2542− signature: CommandSignature::none(),
2727+ TypableCommand {
2543− },
2728+ name: "clipboard-yank-join",
2544− TypableCommand {
2729+ aliases: &[],
2545− name: "clipboard-yank-join",
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.
2546− aliases: &[],
2731+ fun: yank_joined_to_clipboard,
2547− 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.
2732+ signature: CommandSignature::none(),
2548− fun: yank_joined_to_clipboard,
2733+ },
2549− signature: CommandSignature::none(),
2734+ TypableCommand {
2550− },
2735+ name: "primary-clipboard-yank",
2551− TypableCommand {
2736+ aliases: &[],
2552− name: "primary-clipboard-yank",
2737+ doc: "Yank main selection into system primary clipboard.",
2553− aliases: &[],
2738+ fun: yank_main_selection_to_primary_clipboard,
2554− doc: "Yank main selection into system primary clipboard.",
2739+ signature: CommandSignature::none(),
2555− fun: yank_main_selection_to_primary_clipboard,
2740+ },
2556− signature: CommandSignature::none(),
2741+ TypableCommand {
2557− },
2742+ name: "primary-clipboard-yank-join",
2558− TypableCommand {
2743+ aliases: &[],
2559− name: "primary-clipboard-yank-join",
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.
2560− aliases: &[],
2745+ fun: yank_joined_to_primary_clipboard,
2561− 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.
2746+ signature: CommandSignature::none(),
2562− fun: yank_joined_to_primary_clipboard,
2747+ },
2563− signature: CommandSignature::none(),
2748+ TypableCommand {
2564− },
2749+ name: "clipboard-paste-after",
2565− TypableCommand {
2750+ aliases: &[],
2566− name: "clipboard-paste-after",
2751+ doc: "Paste system clipboard after selections.",
2567− aliases: &[],
2752+ fun: paste_clipboard_after,
2568− doc: "Paste system clipboard after selections.",
2753+ signature: CommandSignature::none(),
2569− fun: paste_clipboard_after,
2754+ },
2570− signature: CommandSignature::none(),
2755+ TypableCommand {
2571− },
2756+ name: "clipboard-paste-before",
2572− TypableCommand {
2757+ aliases: &[],
2573− name: "clipboard-paste-before",
2758+ doc: "Paste system clipboard before selections.",
2574− aliases: &[],
2759+ fun: paste_clipboard_before,
2575− doc: "Paste system clipboard before selections.",
2760+ signature: CommandSignature::none(),
2576− fun: paste_clipboard_before,
2761+ },
2577− signature: CommandSignature::none(),
2762+ TypableCommand {
2578− },
2763+ name: "clipboard-paste-replace",
2579− TypableCommand {
2764+ aliases: &[],
2580− name: "clipboard-paste-replace",
2765+ doc: "Replace selections with content of system clipboard.",
2581− aliases: &[],
2766+ fun: replace_selections_with_clipboard,
2582− doc: "Replace selections with content of system clipboard.",
2767+ signature: CommandSignature::none(),
2583− fun: replace_selections_with_clipboard,
2768+ },
2584− signature: CommandSignature::none(),
2769+ TypableCommand {
2585− },
2770+ name: "primary-clipboard-paste-after",
2586− TypableCommand {
2771+ aliases: &[],
2587− name: "primary-clipboard-paste-after",
2772+ doc: "Paste primary clipboard after selections.",
2588− aliases: &[],
2773+ fun: paste_primary_clipboard_after,
2589− doc: "Paste primary clipboard after selections.",
2774+ signature: CommandSignature::none(),
2590− fun: paste_primary_clipboard_after,
2775+ },
2591− signature: CommandSignature::none(),
2776+ TypableCommand {
2592− },
2777+ name: "primary-clipboard-paste-before",
2593− TypableCommand {
2778+ aliases: &[],
2594− name: "primary-clipboard-paste-before",
2779+ doc: "Paste primary clipboard before selections.",
2595− aliases: &[],
2780+ fun: paste_primary_clipboard_before,
2596− doc: "Paste primary clipboard before selections.",
2781+ signature: CommandSignature::none(),
2597− fun: paste_primary_clipboard_before,
2782+ },
2598− signature: CommandSignature::none(),
2783+ TypableCommand {
2599− },
2784+ name: "primary-clipboard-paste-replace",
2600− TypableCommand {
2785+ aliases: &[],
2601− name: "primary-clipboard-paste-replace",
2786+ doc: "Replace selections with content of system primary clipboard.",
2602− aliases: &[],
2787+ fun: replace_selections_with_primary_clipboard,
2603− doc: "Replace selections with content of system primary clipboard.",
2788+ signature: CommandSignature::none(),
2604− fun: replace_selections_with_primary_clipboard,
2789+ },
2605− signature: CommandSignature::none(),
2790+ TypableCommand {
2606− },
2791+ name: "show-clipboard-provider",
2607− TypableCommand {
2792+ aliases: &[],
2608− name: "show-clipboard-provider",
2793+ doc: "Show clipboard provider name in status bar.",
2609− aliases: &[],
2794+ fun: show_clipboard_provider,
2610− doc: "Show clipboard provider name in status bar.",
2795+ signature: CommandSignature::none(),
2611− fun: show_clipboard_provider,
2796+ },
2612− signature: CommandSignature::none(),
2797+ TypableCommand {
2613− },
2798+ name: "change-current-directory",
2614− TypableCommand {
2799+ aliases: &["cd"],
2615− name: "change-current-directory",
2800+ doc: "Change the current working directory.",
2616− aliases: &["cd"],
2801+ fun: change_current_directory,
2617− doc: "Change the current working directory.",
2802+ signature: CommandSignature::positional(&[completers::directory]),
2618− fun: change_current_directory,
2803+ },
2619− signature: CommandSignature::positional(&[completers::directory]),
2804+ TypableCommand {
2620− },
2805+ name: "show-directory",
2621− TypableCommand {
2806+ aliases: &["pwd"],
2622− name: "show-directory",
2807+ doc: "Show the current working directory.",
2623− aliases: &["pwd"],
2808+ fun: show_current_directory,
2624− doc: "Show the current working directory.",
2809+ signature: CommandSignature::none(),
2625− fun: show_current_directory,
2810+ },
2626− signature: CommandSignature::none(),
2811+ TypableCommand {
2627− },
2812+ name: "encoding",
2628− TypableCommand {
2813+ aliases: &[],
2629− name: "encoding",
2814+ doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
2630− aliases: &[],
2815+ fun: set_encoding,
2631− doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
2816+ signature: CommandSignature::none(),
2632− fun: set_encoding,
2817+ },
2633− signature: CommandSignature::none(),
2818+ TypableCommand {
2634− },
2819+ name: "character-info",
2635− TypableCommand {
2820+ aliases: &["char"],
2636− name: "character-info",
2821+ doc: "Get info about the character under the primary cursor.",
2637− aliases: &["char"],
2822+ fun: get_character_info,
2638− doc: "Get info about the character under the primary cursor.",
2823+ signature: CommandSignature::none(),
2639− fun: get_character_info,
2824+ },
2640− signature: CommandSignature::none(),
2825+ TypableCommand {
2641− },
2826+ name: "reload",
2642− TypableCommand {
2827+ aliases: &["rl"],
2643− name: "reload",
2828+ doc: "Discard changes and reload from the source file.",
2644− aliases: &["rl"],
2829+ fun: reload,
2645− doc: "Discard changes and reload from the source file.",
2830+ signature: CommandSignature::none(),
2646− fun: reload,
2831+ },
2647− signature: CommandSignature::none(),
2832+ TypableCommand {
2648− },
2833+ name: "reload-all",
2649− TypableCommand {
2834+ aliases: &["rla"],
2650− name: "reload-all",
2835+ doc: "Discard changes and reload all documents from the source files.",
2651− aliases: &["rla"],
2836+ fun: reload_all,
2652− doc: "Discard changes and reload all documents from the source files.",
2837+ signature: CommandSignature::none(),
2653− fun: reload_all,
2838+ },
2654− signature: CommandSignature::none(),
2839+ TypableCommand {
2655− },
2840+ name: "update",
2656− TypableCommand {
2841+ aliases: &["u"],
2657− name: "update",
2842+ doc: "Write changes only if the file has been modified.",
2658− aliases: &["u"],
2843+ fun: update,
2659− doc: "Write changes only if the file has been modified.",
2844+ signature: CommandSignature::none(),
2660− fun: update,
2845+ },
2661− signature: CommandSignature::none(),
2846+ TypableCommand {
2662− },
2847+ name: "lsp-workspace-command",
2663− TypableCommand {
2848+ aliases: &[],
2664− name: "lsp-workspace-command",
2849+ doc: "Open workspace command picker",
2665− aliases: &[],
2850+ fun: lsp_workspace_command,
2666− doc: "Open workspace command picker",
2851+ signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
2667− fun: lsp_workspace_command,
2852+ },
2668− signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
2853+ TypableCommand {
2669− },
2854+ name: "lsp-restart",
2670− TypableCommand {
2855+ aliases: &[],
2671− name: "lsp-restart",
2856+ doc: "Restarts the language servers used by the current doc",
2672− aliases: &[],
2857+ fun: lsp_restart,
2673− doc: "Restarts the language servers used by the current doc",
2858+ signature: CommandSignature::none(),
2674− fun: lsp_restart,
2859+ },
2675− signature: CommandSignature::none(),
2860+ TypableCommand {
2676− },
2861+ name: "lsp-stop",
2677− TypableCommand {
2862+ aliases: &[],
2678− name: "lsp-stop",
2863+ doc: "Stops the language servers that are used by the current doc",
2679− aliases: &[],
2864+ fun: lsp_stop,
2680− doc: "Stops the language servers that are used by the current doc",
2865+ signature: CommandSignature::none(),
2681− fun: lsp_stop,
2866+ },
2682− signature: CommandSignature::none(),
2867+ TypableCommand {
2683− },
2868+ name: "tree-sitter-scopes",
2684− TypableCommand {
2869+ aliases: &[],
2685− name: "tree-sitter-scopes",
2870+ doc: "Display tree sitter scopes, primarily for theming and development.",
2686− aliases: &[],
2871+ fun: tree_sitter_scopes,
2687− doc: "Display tree sitter scopes, primarily for theming and development.",
2872+ signature: CommandSignature::none(),
2688− fun: tree_sitter_scopes,
2873+ },
2689− signature: CommandSignature::none(),
2874+ TypableCommand {
2690− },
2875+ name: "tree-sitter-highlight-name",
2691− TypableCommand {
2876+ aliases: &[],
2692− name: "debug-start",
2877+ doc: "Display name of tree-sitter highlight scope under the cursor.",
2693− aliases: &["dbg"],
2878+ fun: tree_sitter_highlight_name,
2694− doc: "Start a debug session from a given template with given parameters.",
2879+ signature: CommandSignature::none(),
2695− fun: debug_start,
2880+ },
2696− signature: CommandSignature::none(),
2881+ TypableCommand {
2697− },
2882+ name: "debug-start",
2698− TypableCommand {
2883+ aliases: &["dbg"],
2699− name: "debug-remote",
2884+ doc: "Start a debug session from a given template with given parameters.",
2700− aliases: &["dbg-tcp"],
2885+ fun: debug_start,
2701− doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
2886+ signature: CommandSignature::none(),
2702− fun: debug_remote,
2887+ },
2703− signature: CommandSignature::none(),
2888+ TypableCommand {
2704− },
2889+ name: "debug-remote",
2705− TypableCommand {
2890+ aliases: &["dbg-tcp"],
2706− name: "debug-eval",
2891+ doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
2707− aliases: &[],
2892+ fun: debug_remote,
2708− doc: "Evaluate expression in current debug context.",
2893+ signature: CommandSignature::none(),
2709− fun: debug_eval,
2894+ },
2710− signature: CommandSignature::none(),
2895+ TypableCommand {
2711− },
2896+ name: "debug-eval",
2712− TypableCommand {
2897+ aliases: &[],
2713− name: "vsplit",
2898+ doc: "Evaluate expression in current debug context.",
2714− aliases: &["vs"],
2899+ fun: debug_eval,
2715− doc: "Open the file in a vertical split.",
2900+ signature: CommandSignature::none(),
2716− fun: vsplit,
2901+ },
2717− signature: CommandSignature::all(completers::filename)
2902+ TypableCommand {
2718− },
2903+ name: "vsplit",
2719− TypableCommand {
2904+ aliases: &["vs"],
2720− name: "vsplit-new",
2905+ doc: "Open the file in a vertical split.",
2721− aliases: &["vnew"],
2906+ fun: vsplit,
2722− doc: "Open a scratch buffer in a vertical split.",
2907+ signature: CommandSignature::all(completers::filename)
2723− fun: vsplit_new,
2908+ },
2724− signature: CommandSignature::none(),
2909+ TypableCommand {
2725− },
2910+ name: "vsplit-new",
2726− TypableCommand {
2911+ aliases: &["vnew"],
2727− name: "hsplit",
2912+ doc: "Open a scratch buffer in a vertical split.",
2728− aliases: &["hs", "sp"],
2913+ fun: vsplit_new,
2729− doc: "Open the file in a horizontal split.",
2914+ signature: CommandSignature::none(),
2730− fun: hsplit,
2915+ },
2731− signature: CommandSignature::all(completers::filename)
2916+ TypableCommand {
2732− },
2917+ name: "hsplit",
2733− TypableCommand {
2918+ aliases: &["hs", "sp"],
2734− name: "hsplit-new",
2919+ doc: "Open the file in a horizontal split.",
2735− aliases: &["hnew"],
2920+ fun: hsplit,
2736− doc: "Open a scratch buffer in a horizontal split.",
2921+ signature: CommandSignature::all(completers::filename)
2737− fun: hsplit_new,
2922+ },
2738− signature: CommandSignature::none(),
2923+ TypableCommand {
2739− },
2924+ name: "hsplit-new",
2740− TypableCommand {
2925+ aliases: &["hnew"],
2741− name: "tutor",
2926+ doc: "Open a scratch buffer in a horizontal split.",
2742− aliases: &[],
2927+ fun: hsplit_new,
2743− doc: "Open the tutorial.",
2928+ signature: CommandSignature::none(),
2744− fun: tutor,
2929+ },
2745− signature: CommandSignature::none(),
2930+ TypableCommand {
2746− },
2931+ name: "tutor",
2747− TypableCommand {
2932+ aliases: &[],
2748− name: "goto",
2933+ doc: "Open the tutorial.",
2749− aliases: &["g"],
2934+ fun: tutor,
2750− doc: "Goto line number.",
2935+ signature: CommandSignature::none(),
2751− fun: goto_line_number,
2936+ },
2752− signature: CommandSignature::none(),
2937+ TypableCommand {
2753− },
2938+ name: "goto",
2754− TypableCommand {
2939+ aliases: &["g"],
2755− name: "set-language",
2940+ doc: "Goto line number.",
2756− aliases: &["lang"],
2941+ fun: goto_line_number,
2757− doc: "Set the language of current buffer (show current language if no value specified).",
2942+ signature: CommandSignature::none(),
2758− fun: language,
2943+ },
2759− signature: CommandSignature::positional(&[completers::language]),
2944+ TypableCommand {
2760− },
2945+ name: "set-language",
2761− TypableCommand {
2946+ aliases: &["lang"],
2762− name: "set-option",
2947+ doc: "Set the language of current buffer (show current language if no value specified).",
2763− aliases: &["set"],
2948+ fun: language,
2764− doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
2949+ signature: CommandSignature::positional(&[completers::language]),
2765− fun: set_option,
2950+ },
2766− // TODO: Add support for completion of the options value(s), when appropriate.
2951+ TypableCommand {
2767− signature: CommandSignature::positional(&[completers::setting]),
2952+ name: "set-option",
2768− },
2953+ aliases: &["set"],
2769− TypableCommand {
2954+ doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
2770− name: "toggle-option",
2955+ fun: set_option,
2771− aliases: &["toggle"],
2956+ // TODO: Add support for completion of the options value(s), when appropriate.
2772− doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
2957+ signature: CommandSignature::positional(&[completers::setting]),
2773− fun: toggle_option,
2958+ },
2774− signature: CommandSignature::positional(&[completers::setting]),
2959+ TypableCommand {
2775− },
2960+ name: "toggle-option",
2776− TypableCommand {
2961+ aliases: &["toggle"],
2777− name: "get-option",
2962+ doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
2778− aliases: &["get"],
2963+ fun: toggle_option,
2779− doc: "Get the current value of a config option.",
2964+ signature: CommandSignature::positional(&[completers::setting]),
2780− fun: get_option,
2965+ },
2781− signature: CommandSignature::positional(&[completers::setting]),
2966+ TypableCommand {
2782− },
2967+ name: "get-option",
2783− TypableCommand {
2968+ aliases: &["get"],
2784− name: "sort",
2969+ doc: "Get the current value of a config option.",
2785− aliases: &[],
2970+ fun: get_option,
2786− doc: "Sort ranges in selection.",
2971+ signature: CommandSignature::positional(&[completers::setting]),
2787− fun: sort,
2972+ },
2788− signature: CommandSignature::none(),
2973+ TypableCommand {
2789− },
2974+ name: "sort",
2790− TypableCommand {
2975+ aliases: &[],
2791− name: "rsort",
2976+ doc: "Sort ranges in selection.",
2792− aliases: &[],
2977+ fun: sort,
2793− doc: "Sort ranges in selection in reverse order.",
2978+ signature: CommandSignature::none(),
2794− fun: sort_reverse,
2979+ },
2795− signature: CommandSignature::none(),
2980+ TypableCommand {
2796− },
2981+ name: "rsort",
2797− TypableCommand {
2982+ aliases: &[],
2798− name: "reflow",
2983+ doc: "Sort ranges in selection in reverse order.",
2799− aliases: &[],
2984+ fun: sort_reverse,
2800− doc: "Hard-wrap the current selection of lines to a given width.",
2985+ signature: CommandSignature::none(),
2801− fun: reflow,
2986+ },
2802− signature: CommandSignature::none(),
2987+ TypableCommand {
2803− },
2988+ name: "reflow",
2804− TypableCommand {
2989+ aliases: &[],
2805− name: "tree-sitter-subtree",
2990+ doc: "Hard-wrap the current selection of lines to a given width.",
2806− aliases: &["ts-subtree"],
2991+ fun: reflow,
2807− doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
2992+ signature: CommandSignature::none(),
2808− fun: tree_sitter_subtree,
2993+ },
2809− signature: CommandSignature::none(),
2994+ TypableCommand {
2810− },
2995+ name: "tree-sitter-subtree",
2811− TypableCommand {
2996+ aliases: &["ts-subtree"],
2812− name: "config-reload",
2997+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
2813− aliases: &[],
2998+ fun: tree_sitter_subtree,
2814− doc: "Refresh user config.",
2999+ signature: CommandSignature::none(),
2815− fun: refresh_config,
3000+ },
2816− signature: CommandSignature::none(),
3001+ TypableCommand {
2817− },
3002+ name: "config-reload",
2818− TypableCommand {
3003+ aliases: &[],
2819− name: "config-open",
3004+ doc: "Refresh user config.",
2820− aliases: &[],
3005+ fun: refresh_config,
2821− doc: "Open the user config.toml file.",
3006+ signature: CommandSignature::none(),
2822− fun: open_config,
3007+ },
2823− signature: CommandSignature::none(),
3008+ TypableCommand {
2824− },
3009+ name: "config-open",
2825− TypableCommand {
3010+ aliases: &[],
2826− name: "config-open-workspace",
3011+ doc: "Open the user config.toml file.",
2827− aliases: &[],
3012+ fun: open_config,
2828− doc: "Open the workspace config.toml file.",
3013+ signature: CommandSignature::none(),
2829− fun: open_workspace_config,
3014+ },
2830− signature: CommandSignature::none(),
3015+ TypableCommand {
2831− },
3016+ name: "config-open-workspace",
2832− TypableCommand {
3017+ aliases: &[],
2833− name: "log-open",
3018+ doc: "Open the workspace config.toml file.",
2834− aliases: &[],
3019+ fun: open_workspace_config,
2835− doc: "Open the helix log file.",
3020+ signature: CommandSignature::none(),
2836− fun: open_log,
3021+ },
2837− signature: CommandSignature::none(),
3022+ TypableCommand {
2838− },
3023+ name: "log-open",
2839− TypableCommand {
3024+ aliases: &[],
2840− name: "insert-output",
3025+ doc: "Open the helix log file.",
2841− aliases: &[],
3026+ fun: open_log,
2842− doc: "Run shell command, inserting output before each selection.",
3027+ signature: CommandSignature::none(),
2843− fun: insert_output,
3028+ },
2844− signature: CommandSignature::none(),
3029+ TypableCommand {
2845− },
3030+ name: "insert-output",
2846− TypableCommand {
3031+ aliases: &[],
2847− name: "append-output",
3032+ doc: "Run shell command, inserting output before each selection.",
2848− aliases: &[],
3033+ fun: insert_output,
2849− doc: "Run shell command, appending output after each selection.",
3034+ signature: CommandSignature::none(),
2850− fun: append_output,
3035+ },
2851− signature: CommandSignature::none(),
3036+ TypableCommand {
2852− },
3037+ name: "append-output",
2853− TypableCommand {
3038+ aliases: &[],
2854− name: "pipe",
3039+ doc: "Run shell command, appending output after each selection.",
2855− aliases: &[],
3040+ fun: append_output,
2856− doc: "Pipe each selection to the shell command.",
3041+ signature: CommandSignature::none(),
2857− fun: pipe,
3042+ },
2858− signature: CommandSignature::none(),
3043+ TypableCommand {
2859− },
3044+ name: "pipe",
2860− TypableCommand {
3045+ aliases: &[],
2861− name: "pipe-to",
3046+ doc: "Pipe each selection to the shell command.",
2862− aliases: &[],
3047+ fun: pipe,
2863− doc: "Pipe each selection to the shell command, ignoring output.",
3048+ signature: CommandSignature::none(),
2864− fun: pipe_to,
3049+ },
2865− signature: CommandSignature::none(),
3050+ TypableCommand {
2866− },
3051+ name: "pipe-to",
2867− TypableCommand {
3052+ aliases: &[],
2868− name: "run-shell-command",
3053+ doc: "Pipe each selection to the shell command, ignoring output.",
2869− aliases: &["sh"],
3054+ fun: pipe_to,
2870− doc: "Run a shell command",
3055+ signature: CommandSignature::none(),
2871− fun: run_shell_command,
3056+ },
2872− signature: CommandSignature::all(completers::filename)
3057+ TypableCommand {
2873− },
3058+ name: "run-shell-command",
2874− TypableCommand {
3059+ aliases: &["sh"],
2875− name: "reset-diff-change",
3060+ doc: "Run a shell command",
2876− aliases: &["diffget", "diffg"],
3061+ fun: run_shell_command,
2877− doc: "Reset the diff change at the cursor position.",
3062+ signature: CommandSignature::all(completers::filename)
2878− fun: reset_diff_change,
3063+ },
2879− signature: CommandSignature::none(),
3064+ TypableCommand {
2880− },
3065+ name: "reset-diff-change",
2881− TypableCommand {
3066+ aliases: &["diffget", "diffg"],
2882− name: "clear-register",
3067+ doc: "Reset the diff change at the cursor position.",
2883− aliases: &[],
3068+ fun: reset_diff_change,
2884− doc: "Clear given register. If no argument is provided, clear all registers.",
3069+ signature: CommandSignature::none(),
2885− fun: clear_register,
3070+ },
2886− signature: CommandSignature::none(),
3071+ TypableCommand {
2887− },
3072+ name: "clear-register",
2888− ];
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+];
2889
3093
2890pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
3094pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
2891 Lazy::new(|| {
3095 Lazy::new(|| {
@@ -2904,28 +3108,18 @@ pub(super) fn command_mode(cx: &mut Context) {
2904 ":".into(),
3108 ":".into(),
2905 Some(':'),
3109 Some(':'),
2906 |editor: &Editor, input: &str| {
3110 |editor: &Editor, input: &str| {
2907− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2908− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2909−
2910 let shellwords = Shellwords::from(input);
3111 let shellwords = Shellwords::from(input);
2911 let words = shellwords.words();
3112 let words = shellwords.words();
2912
3113
2913 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
3114 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
2914− // If the command has not been finished yet, complete commands.
3115+ fuzzy_match(
3116+ input,
2915− let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
3117+ TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
2916− .iter()
3118+ false,
2917− .filter_map(|command| {
3119+ )
2918− FUZZY_MATCHER
2919− .fuzzy_match(command.name, input)
2920− .map(|score| (command.name, score))
2921− })
2922− .collect();
2923−
2924− matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
2925− matches
2926− .into_iter()
3120+ .into_iter()
2927− .map(|(name, _)| (0.., name.into()))
3121+ .map(|(name, _)| (0.., name.into()))
2928− .collect()
3122+ .collect()
2929 } else {
3123 } else {
2930 // Otherwise, use the command's completer and the last shellword
3124 // Otherwise, use the command's completer and the last shellword
2931 // as completion input.
3125 // as completion input.
helix-term/src/compositor.rs+12
@@ -16,6 +16,7 @@ pub enum EventResult {
16}
16}
17
17
18use crate::job::Jobs;
18use crate::job::Jobs;
19+use crate::ui::picker;
19use helix_view::Editor;
20use helix_view::Editor;
20
21
21pub use helix_view::input::Event;
22pub use helix_view::input::Event;
@@ -79,6 +80,7 @@ pub struct Compositor {
79 area: Rect,
80 area: Rect,
80
81
81 pub(crate) last_picker: Option<Box<dyn Component>>,
82 pub(crate) last_picker: Option<Box<dyn Component>>,
83+ pub(crate) full_redraw: bool,
82}
84}
83
85
84impl Compositor {
86impl Compositor {
@@ -87,6 +89,7 @@ impl Compositor {
87 layers: Vec::new(),
89 layers: Vec::new(),
88 area,
90 area,
89 last_picker: None,
91 last_picker: None,
92+ full_redraw: false,
90 }
93 }
91 }
94 }
92
95
@@ -100,6 +103,11 @@ impl Compositor {
100
103
101 /// Add a layer to be rendered in front of all existing layers.
104 /// Add a layer to be rendered in front of all existing layers.
102 pub fn push(&mut self, mut layer: Box<dyn Component>) {
105 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+ }
103 let size = self.size();
111 let size = self.size();
104 // trigger required_size on init
112 // trigger required_size on init
105 layer.required_size((size.width, size.height));
113 layer.required_size((size.width, size.height));
@@ -200,6 +208,10 @@ impl Compositor {
200 .find(|component| component.id() == Some(id))
208 .find(|component| component.id() == Some(id))
201 .and_then(|component| component.as_any_mut().downcast_mut())
209 .and_then(|component| component.as_any_mut().downcast_mut())
202 }
210 }
211+
212+ pub fn need_full_redraw(&mut self) {
213+ self.full_redraw = true;
214+ }
203}
215}
204
216
205// View casting, taken straight from Cursive
217// View casting, taken straight from Cursive
helix-term/src/config.rs+1
@@ -109,6 +109,7 @@ impl Config {
109 )?,
109 )?,
110 }
110 }
111 }
111 }
112+
112 // these are just two io errors return the one for the global config
113 // these are just two io errors return the one for the global config
113 (Err(err), Err(_)) => return Err(err),
114 (Err(err), Err(_)) => return Err(err),
114 };
115 };
helix-term/src/health.rs+44−16
@@ -181,8 +181,8 @@ pub fn languages_all() -> std::io::Result<()> {
181 .language
181 .language
182 .sort_unstable_by_key(|l| l.language_id.clone());
182 .sort_unstable_by_key(|l| l.language_id.clone());
183
183
184− let check_binary = |cmd: Option<String>| match cmd {
184+ let check_binary = |cmd: Option<&str>| match cmd {
185− Some(cmd) => match which::which(&cmd) {
185+ Some(cmd) => match which::which(cmd) {
186 Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
186 Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
187 Err(_) => column(&format!("✘ {}", cmd), Color::Red),
187 Err(_) => column(&format!("✘ {}", cmd), Color::Red),
188 },
188 },
@@ -192,17 +192,15 @@ pub fn languages_all() -> std::io::Result<()> {
192 for lang in &syn_loader_conf.language {
192 for lang in &syn_loader_conf.language {
193 column(&lang.language_id, Color::Reset);
193 column(&lang.language_id, Color::Reset);
194
194
195− // TODO multiple language servers (check binary for each supported language server, not just the first)
196−
197− let lsp = lang.language_servers.first().and_then(|ls| {
195+ let mut cmds = lang.language_servers.iter().filter_map(|ls| {
198 syn_loader_conf
196 syn_loader_conf
199 .language_server
197 .language_server
200 .get(&ls.name)
198 .get(&ls.name)
201− .map(|config| config.command.clone())
199+ .map(|config| config.command.as_str())
202 });
200 });
203− check_binary(lsp);
201+ check_binary(cmds.next());
204
202
205− let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
203+ let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
206 check_binary(dap);
204 check_binary(dap);
207
205
208 for ts_feat in TsFeature::all() {
206 for ts_feat in TsFeature::all() {
@@ -213,6 +211,12 @@ pub fn languages_all() -> std::io::Result<()> {
213 }
211 }
214
212
215 writeln!(stdout)?;
213 writeln!(stdout)?;
214+
215+ for cmd in cmds {
216+ column("", Color::Reset);
217+ check_binary(Some(cmd));
218+ writeln!(stdout)?;
219+ }
216 }
220 }
217
221
218 Ok(())
222 Ok(())
@@ -268,15 +272,12 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
268 }
272 }
269 };
273 };
270
274
271− // TODO multiple language servers
275+ probe_protocols(
272− probe_protocol(
273 "language server",
276 "language server",
274− lang.language_servers.first().and_then(|ls| {
277+ lang.language_servers
278+ .iter()
275− syn_loader_conf
279+ .filter_map(|ls| syn_loader_conf.language_server.get(&ls.name))
276− .language_server
277− .get(&ls.name)
278− .map(|config| config.command.clone())
280+ .map(|config| config.command.as_str()),
279− }),
280 )?;
281 )?;
281
282
282 probe_protocol(
283 probe_protocol(
@@ -291,6 +292,33 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
291 Ok(())
292 Ok(())
292}
293}
293
294
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+
294/// Display diagnostics about LSP and DAP.
322/// Display diagnostics about LSP and DAP.
295fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::Result<()> {
323fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::Result<()> {
296 let stdout = std::io::stdout();
324 let stdout = std::io::stdout();
helix-term/src/keymap.rs+14−14
@@ -401,14 +401,14 @@ mod tests {
401 fn merge_partial_keys() {
401 fn merge_partial_keys() {
402 let keymap = hashmap! {
402 let keymap = hashmap! {
403 Mode::Normal => keymap!({ "Normal mode"
403 Mode::Normal => keymap!({ "Normal mode"
404− "i" => normal_mode,
404+ "i" => normal_mode,
405− "无" => insert_mode,
405+ "无" => insert_mode,
406− "z" => jump_backward,
406+ "z" => jump_backward,
407− "g" => { "Merge into goto mode"
407+ "g" => { "Merge into goto mode"
408− "$" => goto_line_end,
408+ "$" => goto_line_end,
409− "g" => delete_char_forward,
409+ "g" => delete_char_forward,
410− },
410+ },
411− })
411+ })
412 };
412 };
413 let mut merged_keyamp = default();
413 let mut merged_keyamp = default();
414 merge_keys(&mut merged_keyamp, keymap.clone());
414 merge_keys(&mut merged_keyamp, keymap.clone());
@@ -474,13 +474,13 @@ mod tests {
474 fn order_should_be_set() {
474 fn order_should_be_set() {
475 let keymap = hashmap! {
475 let keymap = hashmap! {
476 Mode::Normal => keymap!({ "Normal mode"
476 Mode::Normal => keymap!({ "Normal mode"
477− "space" => { ""
477+ "space" => { ""
478− "s" => { ""
478+ "s" => { ""
479− "v" => vsplit,
479+ "v" => vsplit,
480− "c" => hsplit,
480+ "c" => hsplit,
481− },
482 },
481 },
483− })
482+ },
483+ })
484 };
484 };
485 let mut merged_keyamp = default();
485 let mut merged_keyamp = default();
486 merge_keys(&mut merged_keyamp, keymap.clone());
486 merge_keys(&mut merged_keyamp, keymap.clone());
helix-term/src/keymap/default.rs+8−2
@@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
88 "A-i" | "A-down" => shrink_selection,
88 "A-i" | "A-down" => shrink_selection,
89 "A-p" | "A-left" => select_prev_sibling,
89 "A-p" | "A-left" => select_prev_sibling,
90 "A-n" | "A-right" => select_next_sibling,
90 "A-n" | "A-right" => select_next_sibling,
91+ "A-e" => move_parent_node_end,
92+ "A-b" => move_parent_node_start,
91
93
92 "%" => select_all,
94 "%" => select_all,
93 "x" => extend_line_below,
95 "x" => extend_line_below,
@@ -265,7 +267,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
265 "C-v" | "v" => vsplit_new,
267 "C-v" | "v" => vsplit_new,
266 },
268 },
267 },
269 },
268− "y" => yank_joined_to_clipboard,
270+ "y" => yank_to_clipboard,
269 "Y" => yank_main_selection_to_clipboard,
271 "Y" => yank_main_selection_to_clipboard,
270 "p" => paste_clipboard_after,
272 "p" => paste_clipboard_after,
271 "P" => paste_clipboard_before,
273 "P" => paste_clipboard_before,
@@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
336 "B" => extend_prev_long_word_start,
338 "B" => extend_prev_long_word_start,
337 "E" => extend_next_long_word_end,
339 "E" => extend_next_long_word_end,
338
340
341+ "A-e" => extend_parent_node_end,
342+ "A-b" => extend_parent_node_start,
343+
339 "n" => extend_search_next,
344 "n" => extend_search_next,
340 "N" => extend_search_prev,
345 "N" => extend_search_prev,
341
346
@@ -368,7 +373,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
368 "C-h" | "backspace" | "S-backspace" => delete_char_backward,
373 "C-h" | "backspace" | "S-backspace" => delete_char_backward,
369 "C-d" | "del" => delete_char_forward,
374 "C-d" | "del" => delete_char_forward,
370 "C-j" | "ret" => insert_newline,
375 "C-j" | "ret" => insert_newline,
376+ "tab" => smart_tab,
371− "tab" => insert_tab,
377+ "S-tab" => insert_tab,
372
378
373 "up" => move_visual_line_up,
379 "up" => move_visual_line_up,
374 "down" => move_visual_line_down,
380 "down" => move_visual_line_down,
helix-term/src/main.rs+17−1
@@ -66,6 +66,8 @@ FLAGS:
66 -V, --version Prints version information
66 -V, --version Prints version information
67 --vsplit Splits all given files vertically into different windows
67 --vsplit Splits all given files vertically into different windows
68 --hsplit Splits all given files horizontally into different windows
68 --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
69",
71",
70 env!("CARGO_PKG_NAME"),
72 env!("CARGO_PKG_NAME"),
71 VERSION_AND_GIT_HASH,
73 VERSION_AND_GIT_HASH,
@@ -74,7 +76,7 @@ FLAGS:
74 helix_loader::default_log_file().display(),
76 helix_loader::default_log_file().display(),
75 );
77 );
76
78
77− let args = Args::parse_args().context("could not parse arguments")?;
79+ let mut args = Args::parse_args().context("could not parse arguments")?;
78
80
79 helix_loader::initialize_config_file(args.config_file.clone());
81 helix_loader::initialize_config_file(args.config_file.clone());
80 helix_loader::initialize_log_file(args.log_file.clone());
82 helix_loader::initialize_log_file(args.log_file.clone());
@@ -114,6 +116,20 @@ FLAGS:
114
116
115 setup_logging(args.verbosity).context("failed to initialize logging")?;
117 setup_logging(args.verbosity).context("failed to initialize logging")?;
116
118
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+
117 let config = match Config::load_default() {
133 let config = match Config::load_default() {
118 Ok(config) => config,
134 Ok(config) => config,
119 Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
135 Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
helix-term/src/ui/completion.rs+30−4
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
2use helix_view::{
2use helix_view::{
3 document::SavePoint,
3 document::SavePoint,
4 editor::CompleteAction,
4 editor::CompleteAction,
5+ graphics::Margin,
5 theme::{Modifier, Style},
6 theme::{Modifier, Style},
6 ViewId,
7 ViewId,
7};
8};
@@ -144,7 +145,9 @@ impl Completion {
144 }
145 }
145 };
146 };
146
147
148+ let Some(range) =
147− let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
149+ util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
150+ else {
148 return Transaction::new(doc.text());
151 return Transaction::new(doc.text());
149 };
152 };
150
153
@@ -324,9 +327,18 @@ impl Completion {
324 }
327 }
325 };
328 };
326 });
329 });
330+
331+ let margin = if editor.menu_border() {
332+ Margin::vertical(1)
333+ } else {
334+ Margin::none()
335+ };
336+
327 let popup = Popup::new(Self::ID, menu)
337 let popup = Popup::new(Self::ID, menu)
328 .with_scrollbar(false)
338 .with_scrollbar(false)
329− .ignore_escape_key(true);
339+ .ignore_escape_key(true)
340+ .margin(margin);
341+
330 let mut completion = Self {
342 let mut completion = Self {
331 popup,
343 popup,
332 start_offset,
344 start_offset,
@@ -413,10 +425,18 @@ impl Completion {
413 _ => return false,
425 _ => return false,
414 };
426 };
415
427
428+ let Some(language_server) = cx
429+ .editor
416− let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
430+ .language_server_by_id(current_item.language_server_id)
431+ else {
432+ return false;
433+ };
417
434
418 // This method should not block the compositor so we handle the response asynchronously.
435 // This method should not block the compositor so we handle the response asynchronously.
419− let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
436+ let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
437+ else {
438+ return false;
439+ };
420
440
421 cx.callback(
441 cx.callback(
422 future,
442 future,
@@ -559,6 +579,12 @@ impl Component for Completion {
559 // clear area
579 // clear area
560 let background = cx.editor.theme.get("ui.popup");
580 let background = cx.editor.theme.get("ui.popup");
561 surface.clear_with(doc_area, background);
581 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+
562 markdown_doc.render(doc_area, surface, cx);
588 markdown_doc.render(doc_area, surface, cx);
563 }
589 }
564}
590}
helix-term/src/ui/document.rs+60−24
@@ -97,7 +97,8 @@ pub fn render_document(
97 doc: &Document,
97 doc: &Document,
98 offset: ViewPosition,
98 offset: ViewPosition,
99 doc_annotations: &TextAnnotations,
99 doc_annotations: &TextAnnotations,
100+ syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
100− highlight_iter: impl Iterator<Item = HighlightEvent>,
101+ overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
101 theme: &Theme,
102 theme: &Theme,
102 line_decoration: &mut [Box<dyn LineDecoration + '_>],
103 line_decoration: &mut [Box<dyn LineDecoration + '_>],
103 translated_positions: &mut [TranslatedPosition],
104 translated_positions: &mut [TranslatedPosition],
@@ -109,7 +110,8 @@ pub fn render_document(
109 offset,
110 offset,
110 &doc.text_format(viewport.width, Some(theme)),
111 &doc.text_format(viewport.width, Some(theme)),
111 doc_annotations,
112 doc_annotations,
112− highlight_iter,
113+ syntax_highlight_iter,
114+ overlay_highlight_iter,
113 theme,
115 theme,
114 line_decoration,
116 line_decoration,
115 translated_positions,
117 translated_positions,
@@ -157,7 +159,8 @@ pub fn render_text<'t>(
157 offset: ViewPosition,
159 offset: ViewPosition,
158 text_fmt: &TextFormat,
160 text_fmt: &TextFormat,
159 text_annotations: &TextAnnotations,
161 text_annotations: &TextAnnotations,
162+ syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
160− highlight_iter: impl Iterator<Item = HighlightEvent>,
163+ overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
161 theme: &Theme,
164 theme: &Theme,
162 line_decorations: &mut [Box<dyn LineDecoration + '_>],
165 line_decorations: &mut [Box<dyn LineDecoration + '_>],
163 translated_positions: &mut [TranslatedPosition],
166 translated_positions: &mut [TranslatedPosition],
@@ -178,10 +181,16 @@ pub fn render_text<'t>(
178
181
179 let (mut formatter, mut first_visible_char_idx) =
182 let (mut formatter, mut first_visible_char_idx) =
180 DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
183 DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
181− let mut styles = StyleIter {
184+ let mut syntax_styles = StyleIter {
182 text_style: renderer.text_style,
185 text_style: renderer.text_style,
183 active_highlights: Vec::with_capacity(64),
186 active_highlights: Vec::with_capacity(64),
184− highlight_iter,
187+ highlight_iter: syntax_highlight_iter,
188+ theme,
189+ };
190+ let mut overlay_styles = StyleIter {
191+ text_style: Style::default(),
192+ active_highlights: Vec::with_capacity(64),
193+ highlight_iter: overlay_highlight_iter,
185 theme,
194 theme,
186 };
195 };
187
196
@@ -193,7 +202,10 @@ pub fn render_text<'t>(
193 };
202 };
194 let mut is_in_indent_area = true;
203 let mut is_in_indent_area = true;
195 let mut last_line_indent_level = 0;
204 let mut last_line_indent_level = 0;
196− let mut style_span = styles
205+ let mut syntax_style_span = syntax_styles
206+ .next()
207+ .unwrap_or_else(|| (Style::default(), usize::MAX));
208+ let mut overlay_style_span = overlay_styles
197 .next()
209 .next()
198 .unwrap_or_else(|| (Style::default(), usize::MAX));
210 .unwrap_or_else(|| (Style::default(), usize::MAX));
199
211
@@ -221,9 +233,16 @@ pub fn render_text<'t>(
221
233
222 // skip any graphemes on visual lines before the block start
234 // skip any graphemes on visual lines before the block start
223 if pos.row < row_off {
235 if pos.row < row_off {
224− if char_pos >= style_span.1 {
236+ if char_pos >= syntax_style_span.1 {
225− style_span = if let Some(style_span) = styles.next() {
237+ syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() {
226− style_span
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
227 } else {
246 } else {
228 break;
247 break;
229 }
248 }
@@ -260,8 +279,15 @@ pub fn render_text<'t>(
260 }
279 }
261
280
262 // acquire the correct grapheme style
281 // acquire the correct grapheme style
263− if char_pos >= style_span.1 {
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()
264− style_span = styles.next().unwrap_or((Style::default(), usize::MAX));
290+ .unwrap_or((Style::default(), usize::MAX));
265 }
291 }
266 char_pos += grapheme.doc_chars();
292 char_pos += grapheme.doc_chars();
267
293
@@ -275,22 +301,25 @@ pub fn render_text<'t>(
275 pos,
301 pos,
276 );
302 );
277
303
304+ let (syntax_style, overlay_style) =
278− let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
305+ if let GraphemeSource::VirtualText { highlight } = grapheme.source {
279− let style = renderer.text_style;
306+ let mut style = renderer.text_style;
280− if let Some(highlight) = highlight {
307+ if let Some(highlight) = highlight {
281− style.patch(theme.highlight(highlight.0))
308+ style = style.patch(theme.highlight(highlight.0))
309+ }
310+ (style, Style::default())
282 } else {
311 } else {
283− style
312+ (syntax_style_span.0, overlay_style_span.0)
284− }
285− } else {
286− style_span.0
287− };
313+ };
288
314
289− let virt = grapheme.is_virtual();
315+ let is_virtual = grapheme.is_virtual();
290 renderer.draw_grapheme(
316 renderer.draw_grapheme(
291 grapheme.grapheme,
317 grapheme.grapheme,
292− grapheme_style,
318+ GraphemeStyle {
293− virt,
319+ syntax_style,
320+ overlay_style,
321+ },
322+ is_virtual,
294 &mut last_line_indent_level,
323 &mut last_line_indent_level,
295 &mut is_in_indent_area,
324 &mut is_in_indent_area,
296 pos,
325 pos,
@@ -322,6 +351,11 @@ pub struct TextRenderer<'a> {
322 pub viewport: Rect,
351 pub viewport: Rect,
323}
352}
324
353
354+pub struct GraphemeStyle {
355+ syntax_style: Style,
356+ overlay_style: Style,
357+}
358+
325impl<'a> TextRenderer<'a> {
359impl<'a> TextRenderer<'a> {
326 pub fn new(
360 pub fn new(
327 surface: &'a mut Surface,
361 surface: &'a mut Surface,
@@ -395,7 +429,7 @@ impl<'a> TextRenderer<'a> {
395 pub fn draw_grapheme(
429 pub fn draw_grapheme(
396 &mut self,
430 &mut self,
397 grapheme: Grapheme,
431 grapheme: Grapheme,
398− mut style: Style,
432+ grapheme_style: GraphemeStyle,
399 is_virtual: bool,
433 is_virtual: bool,
400 last_indent_level: &mut usize,
434 last_indent_level: &mut usize,
401 is_in_indent_area: &mut bool,
435 is_in_indent_area: &mut bool,
@@ -405,9 +439,11 @@ impl<'a> TextRenderer<'a> {
405 let is_whitespace = grapheme.is_whitespace();
439 let is_whitespace = grapheme.is_whitespace();
406
440
407 // TODO is it correct to apply the whitespace style to all unicode white spaces?
441 // TODO is it correct to apply the whitespace style to all unicode white spaces?
442+ let mut style = grapheme_style.syntax_style;
408 if is_whitespace {
443 if is_whitespace {
409 style = style.patch(self.whitespace_style);
444 style = style.patch(self.whitespace_style);
410 }
445 }
446+ style = style.patch(grapheme_style.overlay_style);
411
447
412 let width = grapheme.width();
448 let width = grapheme.width();
413 let space = if is_virtual { " " } else { &self.space };
449 let space = if is_virtual { " " } else { &self.space };
helix-term/src/ui/editor.rs+102−54
@@ -43,6 +43,8 @@ pub struct EditorView {
43 pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
43 pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
44 pub(crate) completion: Option<Completion>,
44 pub(crate) completion: Option<Completion>,
45 spinners: ProgressSpinners,
45 spinners: ProgressSpinners,
46+ /// Tracks if the terminal window is focused by reaction to terminal focus events
47+ terminal_focused: bool,
46}
48}
47
49
48#[derive(Debug, Clone)]
50#[derive(Debug, Clone)]
@@ -71,6 +73,7 @@ impl EditorView {
71 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
73 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
72 completion: None,
74 completion: None,
73 spinners: ProgressSpinners::default(),
75 spinners: ProgressSpinners::default(),
76+ terminal_focused: true,
74 }
77 }
75 }
78 }
76
79
@@ -121,16 +124,20 @@ impl EditorView {
121 line_decorations.push(Box::new(line_decoration));
124 line_decorations.push(Box::new(line_decoration));
122 }
125 }
123
126
124− let mut highlights =
127+ let syntax_highlights =
125 Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
128 Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
129+
126− let overlay_highlights = Self::overlay_syntax_highlights(
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(
127 doc,
133 doc,
128 view.offset.anchor,
134 view.offset.anchor,
129 inner.height,
135 inner.height,
130 &text_annotations,
136 &text_annotations,
131 );
137 );
132− if !overlay_highlights.is_empty() {
138+ if !overlay_syntax_highlights.is_empty() {
133− highlights = Box::new(syntax::merge(highlights, overlay_highlights));
139+ overlay_highlights =
140+ Box::new(syntax::merge(overlay_highlights, overlay_syntax_highlights));
134 }
141 }
135
142
136 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
143 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
@@ -139,39 +146,41 @@ impl EditorView {
139 if diagnostic.is_empty() {
146 if diagnostic.is_empty() {
140 continue;
147 continue;
141 }
148 }
142− highlights = Box::new(syntax::merge(highlights, diagnostic));
149+ overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic));
143 }
150 }
144
151
145− let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
152+ if is_focused {
146 let highlights = syntax::merge(
153 let highlights = syntax::merge(
147− highlights,
154+ overlay_highlights,
148 Self::doc_selection_highlights(
155 Self::doc_selection_highlights(
149 editor.mode(),
156 editor.mode(),
150 doc,
157 doc,
151 view,
158 view,
152 theme,
159 theme,
153 &config.cursor_shape,
160 &config.cursor_shape,
161+ self.terminal_focused,
154 ),
162 ),
155 );
163 );
156 let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
164 let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
157 if focused_view_elements.is_empty() {
165 if focused_view_elements.is_empty() {
158− Box::new(highlights)
166+ overlay_highlights = Box::new(highlights)
159 } else {
167 } else {
160− Box::new(syntax::merge(highlights, focused_view_elements))
168+ overlay_highlights = Box::new(syntax::merge(highlights, focused_view_elements))
161 }
169 }
162− } else {
170+ }
163− Box::new(highlights)
164− };
165
171
172+ let gutter_overflow = view.gutter_offset(doc) == 0;
173+ if !gutter_overflow {
166− Self::render_gutter(
174+ Self::render_gutter(
167− editor,
175+ editor,
168− doc,
176+ doc,
169− view,
177+ view,
170− view.area,
178+ view.area,
171− theme,
179+ theme,
172− is_focused,
180+ is_focused & self.terminal_focused,
173− &mut line_decorations,
181+ &mut line_decorations,
174− );
182+ );
183+ }
175
184
176 if is_focused {
185 if is_focused {
177 let cursor = doc
186 let cursor = doc
@@ -191,7 +200,8 @@ impl EditorView {
191 doc,
200 doc,
192 view.offset,
201 view.offset,
193 &text_annotations,
202 &text_annotations,
194− highlights,
203+ syntax_highlights,
204+ overlay_highlights,
195 theme,
205 theme,
196 &mut line_decorations,
206 &mut line_decorations,
197 &mut translated_positions,
207 &mut translated_positions,
@@ -251,27 +261,39 @@ impl EditorView {
251 .for_each(|area| surface.set_style(area, ruler_theme))
261 .for_each(|area| surface.set_style(area, ruler_theme))
252 }
262 }
253
263
254− pub fn overlay_syntax_highlights(
264+ fn viewport_byte_range(
265+ text: helix_core::RopeSlice,
266+ row: usize,
267+ height: u16,
268+ ) -> std::ops::Range<usize> {
269+ // Calculate viewport byte ranges:
270+ // Saturating subs to make it inclusive zero indexing.
271+ let last_line = text.len_lines().saturating_sub(1);
272+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
273+ let start = text.line_to_byte(row.min(last_line));
274+ let end = text.line_to_byte(last_visible_line + 1);
275+
276+ start..end
277+ }
278+
279+ pub fn empty_highlight_iter(
255 doc: &Document,
280 doc: &Document,
256 anchor: usize,
281 anchor: usize,
257 height: u16,
282 height: u16,
258− text_annotations: &TextAnnotations,
283+ ) -> Box<dyn Iterator<Item = HighlightEvent>> {
259− ) -> Vec<(usize, std::ops::Range<usize>)> {
260 let text = doc.text().slice(..);
284 let text = doc.text().slice(..);
261 let row = text.char_to_line(anchor.min(text.len_chars()));
285 let row = text.char_to_line(anchor.min(text.len_chars()));
262
286
263− let range = {
264− // Calculate viewport byte ranges:
287+ // Calculate viewport byte ranges:
265− // Saturating subs to make it inclusive zero indexing.
288+ // Saturating subs to make it inclusive zero indexing.
266− let last_line = text.len_lines().saturating_sub(1);
289+ let range = Self::viewport_byte_range(text, row, height);
267− let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
290+ Box::new(
268− let start = text.line_to_byte(row.min(last_line));
291+ [HighlightEvent::Source {
269− let end = text.line_to_byte(last_visible_line + 1);
292+ start: text.byte_to_char(range.start),
270−
293+ end: text.byte_to_char(range.end),
271− start..end
272− };
294+ }]
273−
295+ .into_iter(),
274− text_annotations.collect_overlay_highlights(range)
296+ )
275 }
297 }
276
298
277 /// Get syntax highlights for a document in a view represented by the first line
299 /// Get syntax highlights for a document in a view represented by the first line
@@ -286,16 +308,7 @@ impl EditorView {
286 let text = doc.text().slice(..);
308 let text = doc.text().slice(..);
287 let row = text.char_to_line(anchor.min(text.len_chars()));
309 let row = text.char_to_line(anchor.min(text.len_chars()));
288
310
289− let range = {
311+ let range = Self::viewport_byte_range(text, row, height);
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− };
299
312
300 match doc.syntax() {
313 match doc.syntax() {
301 Some(syntax) => {
314 Some(syntax) => {
@@ -328,6 +341,20 @@ impl EditorView {
328 }
341 }
329 }
342 }
330
343
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+
331 /// Get highlight spans for document diagnostics
358 /// Get highlight spans for document diagnostics
332 pub fn doc_diagnostics_highlights(
359 pub fn doc_diagnostics_highlights(
333 doc: &Document,
360 doc: &Document,
@@ -359,7 +386,7 @@ impl EditorView {
359 let mut warning_vec = Vec::new();
386 let mut warning_vec = Vec::new();
360 let mut error_vec = Vec::new();
387 let mut error_vec = Vec::new();
361
388
362− for diagnostic in doc.diagnostics() {
389+ for diagnostic in doc.shown_diagnostics() {
363 // Separate diagnostics into different Vecs by severity.
390 // Separate diagnostics into different Vecs by severity.
364 let (vec, scope) = match diagnostic.severity {
391 let (vec, scope) = match diagnostic.severity {
365 Some(Severity::Info) => (&mut info_vec, info),
392 Some(Severity::Info) => (&mut info_vec, info),
@@ -394,6 +421,7 @@ impl EditorView {
394 view: &View,
421 view: &View,
395 theme: &Theme,
422 theme: &Theme,
396 cursor_shape_config: &CursorShapeConfig,
423 cursor_shape_config: &CursorShapeConfig,
424+ is_terminal_focused: bool,
397 ) -> Vec<(usize, std::ops::Range<usize>)> {
425 ) -> Vec<(usize, std::ops::Range<usize>)> {
398 let text = doc.text().slice(..);
426 let text = doc.text().slice(..);
399 let selection = doc.selection(view.id);
427 let selection = doc.selection(view.id);
@@ -441,7 +469,7 @@ impl EditorView {
441
469
442 // Special-case: cursor at end of the rope.
470 // Special-case: cursor at end of the rope.
443 if range.head == range.anchor && range.head == text.len_chars() {
471 if range.head == range.anchor && range.head == text.len_chars() {
444− if !selection_is_primary || cursor_is_block {
472+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
445 // Bar and underline cursors are drawn by the terminal
473 // Bar and underline cursors are drawn by the terminal
446 // BUG: If the editor area loses focus while having a bar or
474 // BUG: If the editor area loses focus while having a bar or
447 // underline cursor (eg. when a regex prompt has focus) then
475 // underline cursor (eg. when a regex prompt has focus) then
@@ -464,13 +492,17 @@ impl EditorView {
464 cursor_start
492 cursor_start
465 };
493 };
466 spans.push((selection_scope, range.anchor..selection_end));
494 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
467− if !selection_is_primary || cursor_is_block {
497+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
468 spans.push((cursor_scope, cursor_start..range.head));
498 spans.push((cursor_scope, cursor_start..range.head));
469 }
499 }
470 } else {
500 } else {
471 // Reverse case.
501 // Reverse case.
472 let cursor_end = next_grapheme_boundary(text, range.head);
502 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
473− if !selection_is_primary || cursor_is_block {
505+ if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
474 spans.push((cursor_scope, range.head..cursor_end));
506 spans.push((cursor_scope, range.head..cursor_end));
475 }
507 }
476 // non block cursors look like they exclude the cursor
508 // non block cursors look like they exclude the cursor
@@ -1067,6 +1099,7 @@ impl EditorView {
1067 let editor = &mut cxt.editor;
1099 let editor = &mut cxt.editor;
1068
1100
1069 if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
1101 if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
1102+ let prev_view_id = view!(editor).id;
1070 let doc = doc_mut!(editor, &view!(editor, view_id).doc);
1103 let doc = doc_mut!(editor, &view!(editor, view_id).doc);
1071
1104
1072 if modifiers == KeyModifiers::ALT {
1105 if modifiers == KeyModifiers::ALT {
@@ -1076,6 +1109,10 @@ impl EditorView {
1076 doc.set_selection(view_id, Selection::point(pos));
1109 doc.set_selection(view_id, Selection::point(pos));
1077 }
1110 }
1078
1111
1112+ if view_id != prev_view_id {
1113+ self.clear_completion(editor);
1114+ }
1115+
1079 editor.focus(view_id);
1116 editor.focus(view_id);
1080 editor.ensure_cursor_in_view(view_id);
1117 editor.ensure_cursor_in_view(view_id);
1081
1118
@@ -1369,13 +1406,17 @@ impl Component for EditorView {
1369
1406
1370 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
1407 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
1371 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1408 Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
1372− Event::FocusGained => EventResult::Ignored(None),
1409+ Event::FocusGained => {
1410+ self.terminal_focused = true;
1411+ EventResult::Consumed(None)
1412+ }
1373 Event::FocusLost => {
1413 Event::FocusLost => {
1374 if context.editor.config().auto_save {
1414 if context.editor.config().auto_save {
1375 if let Err(e) = commands::typed::write_all_impl(context, false, false) {
1415 if let Err(e) = commands::typed::write_all_impl(context, false, false) {
1376 context.editor.set_error(format!("{}", e));
1416 context.editor.set_error(format!("{}", e));
1377 }
1417 }
1378 }
1418 }
1419+ self.terminal_focused = false;
1379 EventResult::Consumed(None)
1420 EventResult::Consumed(None)
1380 }
1421 }
1381 }
1422 }
@@ -1485,8 +1526,15 @@ impl Component for EditorView {
1485
1526
1486 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1527 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1487 match editor.cursor() {
1528 match editor.cursor() {
1488− // All block cursors are drawn manually
1529+ // all block cursors are drawn manually
1530+ (pos, CursorKind::Block) => {
1531+ if self.terminal_focused {
1489− (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
1532+ (pos, CursorKind::Hidden)
1533+ } else {
1534+ // use crossterm cursor when terminal loses focus
1535+ (pos, CursorKind::Underline)
1536+ }
1537+ }
1490 cursor => cursor,
1538 cursor => cursor,
1491 }
1539 }
1492 }
1540 }
helix-term/src/ui/fuzzy_match.rsdeleted−239
@@ -1,239 +0,0 @@
1−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2−use fuzzy_matcher::FuzzyMatcher;
3−
4−#[cfg(test)]
5−mod test;
6−
7−struct QueryAtom {
8− kind: QueryAtomKind,
9− atom: String,
10− ignore_case: bool,
11− inverse: bool,
12−}
13−impl QueryAtom {
14− fn new(atom: &str) -> Option<QueryAtom> {
15− let mut atom = atom.to_string();
16− let inverse = atom.starts_with('!');
17− if inverse {
18− atom.remove(0);
19− }
20−
21− let mut kind = match atom.chars().next() {
22− Some('^') => QueryAtomKind::Prefix,
23− Some('\'') => QueryAtomKind::Substring,
24− _ if inverse => QueryAtomKind::Substring,
25− _ => QueryAtomKind::Fuzzy,
26− };
27−
28− if atom.starts_with(['^', '\'']) {
29− atom.remove(0);
30− }
31−
32− if atom.is_empty() {
33− return None;
34− }
35−
36− if atom.ends_with('$') && !atom.ends_with("\\$") {
37− atom.pop();
38− kind = if kind == QueryAtomKind::Prefix {
39− QueryAtomKind::Exact
40− } else {
41− QueryAtomKind::Postfix
42− }
43− }
44−
45− Some(QueryAtom {
46− kind,
47− atom: atom.replace('\\', ""),
48− // not ideal but fuzzy_matches only knows ascii uppercase so more consistent
49− // to behave the same
50− ignore_case: kind != QueryAtomKind::Fuzzy
51− && atom.chars().all(|c| c.is_ascii_lowercase()),
52− inverse,
53− })
54− }
55−
56− fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
57− // for inverse there are no indices to return
58− // just return whether we matched
59− if self.inverse {
60− return self.matches(matcher, item);
61− }
62− let buf;
63− let item = if self.ignore_case {
64− buf = item.to_ascii_lowercase();
65− &buf
66− } else {
67− item
68− };
69− let off = match self.kind {
70− QueryAtomKind::Fuzzy => {
71− if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
72− indices.extend_from_slice(&fuzzy_indices);
73− return true;
74− } else {
75− return false;
76− }
77− }
78− QueryAtomKind::Substring => {
79− if let Some(off) = item.find(&self.atom) {
80− off
81− } else {
82− return false;
83− }
84− }
85− QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
86− QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
87− QueryAtomKind::Exact if item == self.atom => 0,
88− _ => return false,
89− };
90−
91− indices.extend(off..(off + self.atom.len()));
92− true
93− }
94−
95− fn matches(&self, matcher: &Matcher, item: &str) -> bool {
96− let buf;
97− let item = if self.ignore_case {
98− buf = item.to_ascii_lowercase();
99− &buf
100− } else {
101− item
102− };
103− let mut res = match self.kind {
104− QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
105− QueryAtomKind::Substring => item.contains(&self.atom),
106− QueryAtomKind::Prefix => item.starts_with(&self.atom),
107− QueryAtomKind::Postfix => item.ends_with(&self.atom),
108− QueryAtomKind::Exact => item == self.atom,
109− };
110− if self.inverse {
111− res = !res;
112− }
113− res
114− }
115−}
116−
117−#[derive(Debug, PartialEq, Eq, Clone, Copy)]
118−enum QueryAtomKind {
119− /// Item is a fuzzy match of this behaviour
120− ///
121− /// Usage: `foo`
122− Fuzzy,
123− /// Item contains query atom as a continuous substring
124− ///
125− /// Usage `'foo`
126− Substring,
127− /// Item starts with query atom
128− ///
129− /// Usage: `^foo`
130− Prefix,
131− /// Item ends with query atom
132− ///
133− /// Usage: `foo$`
134− Postfix,
135− /// Item is equal to query atom
136− ///
137− /// Usage `^foo$`
138− Exact,
139−}
140−
141−#[derive(Default)]
142−pub struct FuzzyQuery {
143− first_fuzzy_atom: Option<String>,
144− query_atoms: Vec<QueryAtom>,
145−}
146−
147−fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
148− let mut saw_backslash = false;
149− query.split(move |c| {
150− saw_backslash = match c {
151− ' ' if !saw_backslash => return true,
152− '\\' => true,
153− _ => false,
154− };
155− false
156− })
157−}
158−
159−impl FuzzyQuery {
160− pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
161− // TODO: we could be a lot smarter about this
162− let new_query = Self::new(query);
163− let mut is_refinement = query.starts_with(old_query);
164−
165− // if the last atom is an inverse atom adding more text to it
166− // will actually increase the number of matches and we can not refine
167− // the matches.
168− if is_refinement && !self.query_atoms.is_empty() {
169− let last_idx = self.query_atoms.len() - 1;
170− if self.query_atoms[last_idx].inverse
171− && self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
172− {
173− is_refinement = false;
174− }
175− }
176−
177− (new_query, is_refinement)
178− }
179−
180− pub fn new(query: &str) -> FuzzyQuery {
181− let mut first_fuzzy_query = None;
182− let query_atoms = query_atoms(query)
183− .filter_map(|atom| {
184− let atom = QueryAtom::new(atom)?;
185− if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
186− first_fuzzy_query = Some(atom.atom);
187− None
188− } else {
189− Some(atom)
190− }
191− })
192− .collect();
193− FuzzyQuery {
194− first_fuzzy_atom: first_fuzzy_query,
195− query_atoms,
196− }
197− }
198−
199− pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
200− // use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
201− // this behaviour matches fzf and skim
202− let score = self
203− .first_fuzzy_atom
204− .as_ref()
205− .map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
206− if self
207− .query_atoms
208− .iter()
209− .any(|atom| !atom.matches(matcher, item))
210− {
211− return None;
212− }
213− Some(score)
214− }
215−
216− pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
217− let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
218− || Some((0, Vec::new())),
219− |atom| matcher.fuzzy_indices(item, atom),
220− )?;
221−
222− // fast path for the common case of just a single atom
223− if self.query_atoms.is_empty() {
224− return Some((score, indices));
225− }
226−
227− for atom in &self.query_atoms {
228− if !atom.indices(matcher, item, &mut indices) {
229− return None;
230− }
231− }
232−
233− // deadup and remove duplicate matches
234− indices.sort_unstable();
235− indices.dedup();
236−
237− Some((score, indices))
238− }
239−}
helix-term/src/ui/fuzzy_match/test.rsdeleted−47
@@ -1,47 +0,0 @@
1−use crate::ui::fuzzy_match::FuzzyQuery;
2−use crate::ui::fuzzy_match::Matcher;
3−
4−fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
5− let query = FuzzyQuery::new(query);
6− let matcher = Matcher::default();
7− items
8− .iter()
9− .filter_map(|item| {
10− let (_, indices) = query.fuzzy_indices(item, &matcher)?;
11− let matched_string = indices
12− .iter()
13− .map(|&pos| item.chars().nth(pos).unwrap())
14− .collect();
15− Some(matched_string)
16− })
17− .collect()
18−}
19−
20−#[test]
21−fn match_single_value() {
22− let matches = run_test("foo", &["foobar", "foo", "bar"]);
23− assert_eq!(matches, &["foo", "foo"])
24−}
25−
26−#[test]
27−fn match_multiple_values() {
28− let matches = run_test(
29− "foo bar",
30− &["foo bar", "foo bar", "bar foo", "bar", "foo"],
31− );
32− assert_eq!(matches, &["foobar", "foobar", "barfoo"])
33−}
34−
35−#[test]
36−fn space_escape() {
37− let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
38− assert_eq!(matches, &["foo bar"])
39−}
40−
41−#[test]
42−fn trim() {
43− let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
44− assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
45− let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
46− assert_eq!(matches, &["bar foo"])
47−}
helix-term/src/ui/lsp.rs+3−1
@@ -92,7 +92,9 @@ impl Component for SignatureHelp {
92 Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
92 Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
93 };
93 };
94 let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
94 let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
95+ let sig_doc_area = area
95− let sig_doc_area = area.clip_top(sig_text_area.height + 2);
96+ .clip_top(sig_text_area.height + 2)
97+ .clip_bottom(u16::from(cx.editor.popup_border()));
96 let sig_doc_para = Paragraph::new(sig_doc)
98 let sig_doc_para = Paragraph::new(sig_doc)
97 .wrap(Wrap { trim: false })
99 .wrap(Wrap { trim: false })
98 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
100 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
helix-term/src/ui/markdown.rs+12−5
@@ -14,6 +14,7 @@ use helix_core::{
14};
14};
15use helix_view::{
15use helix_view::{
16 graphics::{Margin, Rect, Style},
16 graphics::{Margin, Rect, Style},
17+ theme::Modifier,
17 Theme,
18 Theme,
18};
19};
19
20
@@ -183,7 +184,9 @@ impl Markdown {
183 // Transform text in `<code>` blocks into `Event::Code`
184 // Transform text in `<code>` blocks into `Event::Code`
184 let mut in_code = false;
185 let mut in_code = false;
185 let parser = parser.filter_map(|event| match event {
186 let parser = parser.filter_map(|event| match event {
186− Event::Html(tag) if *tag == *"<code>" => {
187+ Event::Html(tag)
188+ if tag.starts_with("<code") && matches!(tag.chars().nth(5), Some(' ' | '>')) =>
189+ {
187 in_code = true;
190 in_code = true;
188 None
191 None
189 }
192 }
@@ -275,17 +278,21 @@ impl Markdown {
275 );
278 );
276 lines.extend(tui_text.lines.into_iter());
279 lines.extend(tui_text.lines.into_iter());
277 } else {
280 } else {
281+ let style = match tags.last() {
278− let style = if let Some(Tag::Heading(level, ..)) = tags.last() {
282+ Some(Tag::Heading(level, ..)) => match level {
279− match level {
280 HeadingLevel::H1 => heading_styles[0],
283 HeadingLevel::H1 => heading_styles[0],
281 HeadingLevel::H2 => heading_styles[1],
284 HeadingLevel::H2 => heading_styles[1],
282 HeadingLevel::H3 => heading_styles[2],
285 HeadingLevel::H3 => heading_styles[2],
283 HeadingLevel::H4 => heading_styles[3],
286 HeadingLevel::H4 => heading_styles[3],
284 HeadingLevel::H5 => heading_styles[4],
287 HeadingLevel::H5 => heading_styles[4],
285 HeadingLevel::H6 => heading_styles[5],
288 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)
286 }
294 }
287− } else {
288− text_style
295+ _ => text_style,
289 };
296 };
290 spans.push(Span::styled(text, style));
297 spans.push(Span::styled(text, style));
291 }
298 }
helix-term/src/ui/menu.rs+73−40
@@ -1,22 +1,29 @@
1−use std::{borrow::Cow, path::PathBuf};
1+use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
2
2
3use crate::{
3use crate::{
4 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
4 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
5 ctrl, key, shift,
5 ctrl, key, shift,
6};
6};
7+use helix_core::fuzzy::MATCHER;
8+use nucleo::pattern::{Atom, AtomKind, CaseMatching};
9+use nucleo::{Config, Utf32Str};
10+use tui::{
7−use tui::{buffer::Buffer as Surface, widgets::Table};
11+ buffer::Buffer as Surface,
12+ widgets::{Block, Borders, Table, Widget},
13+};
8
14
9pub use tui::widgets::{Cell, Row};
15pub use tui::widgets::{Cell, Row};
10
16
11−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
17+use helix_view::{
12−use fuzzy_matcher::FuzzyMatcher;
18+ editor::SmartTabConfig,
13−
14−use helix_view::{graphics::Rect, Editor};
19+ graphics::{Margin, Rect},
20+ Editor,
21+};
15use tui::layout::Constraint;
22use tui::layout::Constraint;
16
23
17−pub trait Item {
24+pub trait Item: Sync + Send + 'static {
18 /// Additional editor state that is used for label calculation.
25 /// Additional editor state that is used for label calculation.
19− type Data;
26+ type Data: Sync + Send + 'static;
20
27
21 fn format(&self, data: &Self::Data) -> Row;
28 fn format(&self, data: &Self::Data) -> Row;
22
29
@@ -51,9 +58,8 @@ pub struct Menu<T: Item> {
51
58
52 cursor: Option<usize>,
59 cursor: Option<usize>,
53
60
54− matcher: Box<Matcher>,
55 /// (index, score)
61 /// (index, score)
56− matches: Vec<(usize, i64)>,
62+ matches: Vec<(u32, u32)>,
57
63
58 widths: Vec<Constraint>,
64 widths: Vec<Constraint>,
59
65
@@ -75,11 +81,10 @@ impl<T: Item> Menu<T> {
75 editor_data: <T as Item>::Data,
81 editor_data: <T as Item>::Data,
76 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
82 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
77 ) -> Self {
83 ) -> Self {
78− let matches = (0..options.len()).map(|i| (i, 0)).collect();
84+ let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
79 Self {
85 Self {
80 options,
86 options,
81 editor_data,
87 editor_data,
82− matcher: Box::new(Matcher::default().ignore_case()),
83 matches,
88 matches,
84 cursor: None,
89 cursor: None,
85 widths: Vec::new(),
90 widths: Vec::new(),
@@ -94,20 +99,19 @@ impl<T: Item> Menu<T> {
94 pub fn score(&mut self, pattern: &str) {
99 pub fn score(&mut self, pattern: &str) {
95 // reuse the matches allocation
100 // reuse the matches allocation
96 self.matches.clear();
101 self.matches.clear();
97− self.matches.extend(
102+ let mut matcher = MATCHER.lock();
98− self.options
103+ matcher.config = Config::DEFAULT;
99− .iter()
104+ let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
100− .enumerate()
105+ let mut buf = Vec::new();
101− .filter_map(|(index, option)| {
106+ let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
102− let text = option.filter_text(&self.editor_data);
107+ let text = option.filter_text(&self.editor_data);
103− // TODO: using fuzzy_indices could give us the char idx for match highlighting
108+ pattern
104− self.matcher
109+ .score(Utf32Str::new(&text, &mut buf), &mut matcher)
105− .fuzzy_match(&text, pattern)
106− .map(|score| (index, score))
110+ .map(|score| (i as u32, score as u32))
107− }),
111+ });
108− );
112+ self.matches.extend(matches);
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);
113+ self.matches
114+ .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
111
115
112 // reset cursor position
116 // reset cursor position
113 self.cursor = None;
117 self.cursor = None;
@@ -201,7 +205,7 @@ impl<T: Item> Menu<T> {
201 self.cursor.and_then(|cursor| {
205 self.cursor.and_then(|cursor| {
202 self.matches
206 self.matches
203 .get(cursor)
207 .get(cursor)
204− .map(|(index, _score)| &self.options[*index])
208+ .map(|(index, _score)| &self.options[*index as usize])
205 })
209 })
206 }
210 }
207
211
@@ -209,7 +213,7 @@ impl<T: Item> Menu<T> {
209 self.cursor.and_then(|cursor| {
213 self.cursor.and_then(|cursor| {
210 self.matches
214 self.matches
211 .get(cursor)
215 .get(cursor)
212− .map(|(index, _score)| &mut self.options[*index])
216+ .map(|(index, _score)| &mut self.options[*index as usize])
213 })
217 })
214 }
218 }
215
219
@@ -247,6 +251,21 @@ impl<T: Item + 'static> Component for Menu<T> {
247 compositor.pop();
251 compositor.pop();
248 }));
252 }));
249
253
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+
250 match event {
269 match event {
251 // esc or ctrl-c aborts the completion and closes the menu
270 // esc or ctrl-c aborts the completion and closes the menu
252 key!(Esc) | ctrl!('c') => {
271 key!(Esc) | ctrl!('c') => {
@@ -310,6 +329,15 @@ impl<T: Item + 'static> Component for Menu<T> {
310 let selected = theme.get("ui.menu.selected");
329 let selected = theme.get("ui.menu.selected");
311 surface.clear_with(area, style);
330 surface.clear_with(area, style);
312
331
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+
313 let scroll = self.scroll;
341 let scroll = self.scroll;
314
342
315 let options: Vec<_> = self
343 let options: Vec<_> = self
@@ -317,7 +345,7 @@ impl<T: Item + 'static> Component for Menu<T> {
317 .iter()
345 .iter()
318 .map(|(index, _score)| {
346 .map(|(index, _score)| {
319 // (index, self.options.get(*index).unwrap()) // get_unchecked
347 // (index, self.options.get(*index).unwrap()) // get_unchecked
320− &self.options[*index] // get_unchecked
348+ &self.options[*index as usize] // get_unchecked
321 })
349 })
322 .collect();
350 .collect();
323
351
@@ -350,15 +378,19 @@ impl<T: Item + 'static> Component for Menu<T> {
350 false,
378 false,
351 );
379 );
352
380
381+ let render_borders = cx.editor.menu_border();
382+
383+ if !render_borders {
353− if let Some(cursor) = self.cursor {
384+ if let Some(cursor) = self.cursor {
354− let offset_from_top = cursor - scroll;
385+ let offset_from_top = cursor - scroll;
355− let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
386+ let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
356− left.set_style(selected);
387+ left.set_style(selected);
357− let right = &mut surface[(
388+ let right = &mut surface[(
358− area.right().saturating_sub(1),
389+ area.right().saturating_sub(1),
359− area.y + offset_from_top as u16,
390+ area.y + offset_from_top as u16,
360− )];
391+ )];
361− right.set_style(selected);
392+ right.set_style(selected);
393+ }
362 }
394 }
363
395
364 let fits = len <= win_height;
396 let fits = len <= win_height;
@@ -373,12 +405,13 @@ impl<T: Item + 'static> Component for Menu<T> {
373 for i in 0..win_height {
405 for i in 0..win_height {
374 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
406 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
375
407
376− cell.set_symbol("▐"); // right half block
408+ let half_block = if render_borders { "▌" } else { "▐" };
377
409
378 if scroll_line <= i && i < scroll_line + scroll_height {
410 if scroll_line <= i && i < scroll_line + scroll_height {
379 // Draw scroll thumb
411 // Draw scroll thumb
412+ cell.set_symbol(half_block);
380 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
413 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
381− } else {
414+ } else if !render_borders {
382 // Draw scroll track
415 // Draw scroll track
383 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
416 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
384 }
417 }
helix-term/src/ui/mod.rs+88−145
@@ -1,13 +1,12 @@
1mod completion;
1mod completion;
2mod document;
2mod document;
3pub(crate) mod editor;
3pub(crate) mod editor;
4−mod fuzzy_match;
5mod info;
4mod info;
6pub mod lsp;
5pub mod lsp;
7mod markdown;
6mod markdown;
8pub mod menu;
7pub mod menu;
9pub mod overlay;
8pub mod overlay;
10−mod picker;
9+pub mod picker;
11pub mod popup;
10pub mod popup;
12mod prompt;
11mod prompt;
13mod spinner;
12mod spinner;
@@ -64,7 +63,7 @@ pub fn regex_prompt(
64 prompt: std::borrow::Cow<'static, str>,
63 prompt: std::borrow::Cow<'static, str>,
65 history_register: Option<char>,
64 history_register: Option<char>,
66 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
65 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
67− fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
66+ fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
68) {
67) {
69 let (view, doc) = current!(cx.editor);
68 let (view, doc) = current!(cx.editor);
70 let doc_id = view.doc;
69 let doc_id = view.doc;
@@ -111,7 +110,7 @@ pub fn regex_prompt(
111 view.jumps.push((doc_id, snapshot.clone()));
110 view.jumps.push((doc_id, snapshot.clone()));
112 }
111 }
113
112
114− fun(cx.editor, regex, event);
113+ fun(cx, regex, event);
115
114
116 let (view, doc) = current!(cx.editor);
115 let (view, doc) = current!(cx.editor);
117 view.ensure_cursor_in_view(doc, config.scrolloff);
116 view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -174,9 +173,13 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
174 .git_ignore(config.file_picker.git_ignore)
173 .git_ignore(config.file_picker.git_ignore)
175 .git_global(config.file_picker.git_global)
174 .git_global(config.file_picker.git_global)
176 .git_exclude(config.file_picker.git_exclude)
175 .git_exclude(config.file_picker.git_exclude)
176+ .sort_by_file_name(|name1, name2| name1.cmp(name2))
177 .max_depth(config.file_picker.max_depth)
177 .max_depth(config.file_picker.max_depth)
178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
179
179
180+ walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
181+ walk_builder.add_custom_ignore_filename(".helix/ignore");
182+
180 // We want to exclude files that the editor can't handle yet
183 // We want to exclude files that the editor can't handle yet
181 let mut type_builder = TypesBuilder::new();
184 let mut type_builder = TypesBuilder::new();
182 type_builder
185 type_builder
@@ -190,32 +193,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
190 .build()
193 .build()
191 .expect("failed to build excluded_types");
194 .expect("failed to build excluded_types");
192 walk_builder.types(excluded_types);
195 walk_builder.types(excluded_types);
193−
194− // We want files along with their modification date for sorting
195− let files = walk_builder.build().filter_map(|entry| {
196+ let mut files = walk_builder.build().filter_map(|entry| {
196 let entry = entry.ok()?;
197 let entry = entry.ok()?;
197− // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
198− if entry.file_type()?.is_file() {
198+ if !entry.file_type()?.is_file() {
199− Some(entry.into_path())
200− } else {
201− None
199+ return None;
202 }
200 }
201+ Some(entry.into_path())
203 });
202 });
204−
205− // Cap the number of files if we aren't in a git project, preventing
206− // hangs when using the picker in your home directory
207− let mut files: Vec<PathBuf> = if root.join(".git").exists() {
208− files.collect()
209− } else {
210− // const MAX: usize = 8192;
211− const MAX: usize = 100_000;
212− files.take(MAX).collect()
213− };
214− files.sort();
215−
216 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
203 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
217
204
218− Picker::new(files, root, move |cx, path: &PathBuf, action| {
205+ let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
219 if let Err(e) = cx.editor.open(path, action) {
206 if let Err(e) = cx.editor.open(path, action) {
220 let err = if let Some(err) = e.source() {
207 let err = if let Some(err) = e.source() {
221 format!("{}", err)
208 format!("{}", err)
@@ -225,20 +212,41 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
225 cx.editor.set_error(err);
212 cx.editor.set_error(err);
226 }
213 }
227 })
214 })
228− .with_preview(|_editor, path| Some((path.clone().into(), None)))
215+ .with_preview(|_editor, path| Some((path.clone().into(), None)));
216+ let injector = picker.injector();
217+ let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
218+
219+ let mut hit_timeout = false;
220+ for file in &mut files {
221+ if injector.push(file).is_err() {
222+ break;
223+ }
224+ if std::time::Instant::now() >= timeout {
225+ hit_timeout = true;
226+ break;
227+ }
228+ }
229+ if hit_timeout {
230+ std::thread::spawn(move || {
231+ for file in files {
232+ if injector.push(file).is_err() {
233+ break;
234+ }
235+ }
236+ });
237+ }
238+ picker
229}
239}
230
240
231pub mod completers {
241pub mod completers {
232 use crate::ui::prompt::Completion;
242 use crate::ui::prompt::Completion;
233− use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
243+ use helix_core::fuzzy::fuzzy_match;
234− use fuzzy_matcher::FuzzyMatcher;
235 use helix_core::syntax::LanguageServerFeature;
244 use helix_core::syntax::LanguageServerFeature;
236 use helix_view::document::SCRATCH_BUFFER_NAME;
245 use helix_view::document::SCRATCH_BUFFER_NAME;
237 use helix_view::theme;
246 use helix_view::theme;
238 use helix_view::{editor::Config, Editor};
247 use helix_view::{editor::Config, Editor};
239 use once_cell::sync::Lazy;
248 use once_cell::sync::Lazy;
240 use std::borrow::Cow;
249 use std::borrow::Cow;
241− use std::cmp::Reverse;
242
250
243 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
251 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
244
252
@@ -247,31 +255,16 @@ pub mod completers {
247 }
255 }
248
256
249 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
257 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
250− let mut names: Vec<_> = editor
258+ let names = editor.documents.values().map(|doc| {
251− .documents
252− .values()
253− .map(|doc| {
254− let name = doc
255− .relative_path()
259+ doc.relative_path()
256− .map(|p| p.display().to_string())
260+ .map(|p| p.display().to_string().into())
257− .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
261+ .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
258− ((0..), Cow::from(name))
259− })
260− .collect();
261−
262− let matcher = Matcher::default();
263−
264− let mut matches: Vec<_> = names
265− .into_iter()
266− .filter_map(|(_range, name)| {
267− matcher.fuzzy_match(&name, input).map(|score| (name, score))
268− })
262+ });
269− .collect();
270−
271− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
272− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
273
263
274− names
264+ fuzzy_match(input, names, true)
265+ .into_iter()
266+ .map(|(name, _)| ((0..), name))
267+ .collect()
275 }
268 }
276
269
277 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
270 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@@ -284,26 +277,10 @@ pub mod completers {
284 names.sort();
277 names.sort();
285 names.dedup();
278 names.dedup();
286
279
287− let mut names: Vec<_> = names
280+ fuzzy_match(input, names, false)
288 .into_iter()
281 .into_iter()
289− .map(|name| ((0..), Cow::from(name)))
282+ .map(|(name, _)| ((0..), name.into()))
290− .collect();
283+ .collect()
291−
292− let matcher = Matcher::default();
293−
294− let mut matches: Vec<_> = names
295− .into_iter()
296− .filter_map(|(_range, name)| {
297− matcher.fuzzy_match(&name, input).map(|score| (name, score))
298− })
299− .collect();
300−
301− matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
302− (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
303− });
304− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
305−
306− names
307 }
284 }
308
285
309 /// Recursive function to get all keys from this value and add them to vec
286 /// Recursive function to get all keys from this value and add them to vec
@@ -330,22 +307,22 @@ pub mod completers {
330 keys
307 keys
331 });
308 });
332
309
333− let matcher = Matcher::default();
310+ fuzzy_match(input, &*KEYS, false)
334−
335− let mut matches: Vec<_> = KEYS
336− .iter()
337− .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
338− .collect();
339−
340− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
341− matches
342 .into_iter()
311 .into_iter()
343 .map(|(name, _)| ((0..), name.into()))
312 .map(|(name, _)| ((0..), name.into()))
344 .collect()
313 .collect()
345 }
314 }
346
315
347 pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> {
316 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> {
348− filename_impl(editor, input, |entry| {
325+ filename_impl(editor, input, git_ignore, |entry| {
349 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
326 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
350
327
351 if is_dir {
328 if is_dir {
@@ -357,8 +334,6 @@ pub mod completers {
357 }
334 }
358
335
359 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
336 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
360− let matcher = Matcher::default();
361−
362 let text: String = "text".into();
337 let text: String = "text".into();
363
338
364 let language_ids = editor
339 let language_ids = editor
@@ -367,27 +342,13 @@ pub mod completers {
367 .map(|config| &config.language_id)
342 .map(|config| &config.language_id)
368 .chain(std::iter::once(&text));
343 .chain(std::iter::once(&text));
369
344
370− let mut matches: Vec<_> = language_ids
371− .filter_map(|language_id| {
372− matcher
373− .fuzzy_match(language_id, input)
345+ fuzzy_match(input, language_ids, false)
374− .map(|score| (language_id, score))
375− })
376− .collect();
377−
378− matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
379− (Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
380− });
381−
382− matches
383 .into_iter()
346 .into_iter()
384− .map(|(language, _score)| ((0..), language.clone().into()))
347+ .map(|(name, _)| ((0..), name.to_owned().into()))
385 .collect()
348 .collect()
386 }
349 }
387
350
388 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
351 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
389− let matcher = Matcher::default();
390−
391 let Some(options) = doc!(editor)
352 let Some(options) = doc!(editor)
392 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
353 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
393 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
354 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@@ -395,28 +356,22 @@ pub mod completers {
395 return vec![];
356 return vec![];
396 };
357 };
397
358
398− let mut matches: Vec<_> = options
399− .commands
400− .iter()
401− .filter_map(|command| {
402− matcher
403− .fuzzy_match(command, input)
359+ fuzzy_match(input, &options.commands, false)
404− .map(|score| (command, score))
405− })
406− .collect();
407−
408− matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
409− (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
410− });
411−
412− matches
413 .into_iter()
360 .into_iter()
414− .map(|(command, _score)| ((0..), command.clone().into()))
361+ .map(|(name, _)| ((0..), name.to_owned().into()))
415 .collect()
362 .collect()
416 }
363 }
417
364
418 pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
365 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> {
419− filename_impl(editor, input, |entry| {
374+ filename_impl(editor, input, git_ignore, |entry| {
420 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
375 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
421
376
422 if is_dir {
377 if is_dir {
@@ -439,7 +394,12 @@ pub mod completers {
439 }
394 }
440
395
441 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
396 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
442− fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion>
397+ fn filename_impl<F>(
398+ _editor: &Editor,
399+ input: &str,
400+ git_ignore: bool,
401+ filter_fn: F,
402+ ) -> Vec<Completion>
443 where
403 where
444 F: Fn(&ignore::DirEntry) -> FileMatch,
404 F: Fn(&ignore::DirEntry) -> FileMatch,
445 {
405 {
@@ -479,9 +439,10 @@ pub mod completers {
479
439
480 let end = input.len()..;
440 let end = input.len()..;
481
441
482− let mut files: Vec<_> = WalkBuilder::new(&dir)
442+ let files = WalkBuilder::new(&dir)
483 .hidden(false)
443 .hidden(false)
484 .follow_links(false) // We're scanning over depth 1
444 .follow_links(false) // We're scanning over depth 1
445+ .git_ignore(git_ignore)
485 .max_depth(Some(1))
446 .max_depth(Some(1))
486 .build()
447 .build()
487 .filter_map(|file| {
448 .filter_map(|file| {
@@ -510,43 +471,25 @@ pub mod completers {
510 path.push("");
471 path.push("");
511 }
472 }
512
473
513− let path = path.to_str()?.to_owned();
474+ let path = path.into_os_string().into_string().ok()?;
514− Some((end.clone(), Cow::from(path)))
475+ Some(Cow::from(path))
515 })
476 })
516 }) // TODO: unwrap or skip
477 }) // TODO: unwrap or skip
517− .filter(|(_, path)| !path.is_empty()) // TODO
478+ .filter(|path| !path.is_empty());
518− .collect();
519
479
520 // if empty, return a list of dirs and files in current dir
480 // if empty, return a list of dirs and files in current dir
521 if let Some(file_name) = file_name {
481 if let Some(file_name) = file_name {
522− let matcher = Matcher::default();
523−
524− // inefficient, but we need to calculate the scores, filter out None, then sort.
525− let mut matches: Vec<_> = files
526− .into_iter()
527− .filter_map(|(_range, file)| {
528− matcher
529− .fuzzy_match(&file, &file_name)
530− .map(|score| (file, score))
531− })
532− .collect();
533−
534 let range = (input.len().saturating_sub(file_name.len()))..;
482 let range = (input.len().saturating_sub(file_name.len()))..;
535−
483+ fuzzy_match(&file_name, files, true)
536− matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
537− (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
538− });
539−
540− files = matches
541 .into_iter()
484 .into_iter()
542− .map(|(file, _)| (range.clone(), file))
485+ .map(|(name, _)| (range.clone(), name))
543− .collect();
486+ .collect()
544
487
545 // TODO: complete to longest common match
488 // TODO: complete to longest common match
546 } else {
489 } else {
490+ let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
547 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
491 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
492+ files
548 }
493 }
549−
550− files
551 }
494 }
552}
495}
helix-term/src/ui/picker.rs+359−322
@@ -7,11 +7,12 @@ use crate::{
7 ui::{
7 ui::{
8 self,
8 self,
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
11 EditorView,
10 EditorView,
12 },
11 },
13};
12};
14use futures_util::{future::BoxFuture, FutureExt};
13use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
15use tui::{
16use tui::{
16 buffer::Buffer as Surface,
17 buffer::Buffer as Surface,
17 layout::Constraint,
18 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
19 widgets::{Block, BorderType, Borders, Cell, Table},
20 widgets::{Block, BorderType, Borders, Cell, Table},
20};
21};
21
22
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
23use tui::widgets::Widget;
23use tui::widgets::Widget;
24
24
25−use std::cmp::{self, Ordering};
25+use std::{
26−use std::{collections::HashMap, io::Read, path::PathBuf};
26+ collections::HashMap,
27+ io::Read,
28+ path::PathBuf,
29+ sync::{
30+ atomic::{self, AtomicBool},
31+ Arc,
32+ },
33+};
27
34
28use crate::ui::{Prompt, PromptEvent};
35use crate::ui::{Prompt, PromptEvent};
29use helix_core::{
36use helix_core::{
30− movement::Direction, text_annotations::TextAnnotations,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39+ Syntax,
32};
40};
33use helix_view::{
41use helix_view::{
34 editor::Action,
42 editor::Action,
@@ -38,6 +46,7 @@ use helix_view::{
38 Document, DocumentId, Editor,
46 Document, DocumentId, Editor,
39};
47};
40
48
49+pub const ID: &str = "picker";
41use super::{menu::Item, overlay::Overlay};
50use super::{menu::Item, overlay::Overlay};
42
51
43pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
52pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
@@ -51,12 +60,12 @@ pub enum PathOrId {
51}
60}
52
61
53impl PathOrId {
62impl PathOrId {
54− fn get_canonicalized(self) -> std::io::Result<Self> {
63+ fn get_canonicalized(self) -> Self {
55 use PathOrId::*;
64 use PathOrId::*;
56− Ok(match self {
65+ match self {
57− Path(path) => Path(helix_core::path::get_canonicalized_path(&path)?),
66+ Path(path) => Path(helix_core::path::get_canonicalized_path(&path)),
58 Id(id) => Id(id),
67 Id(id) => Id(id),
59− })
68+ }
60 }
69 }
61}
70}
62
71
@@ -103,9 +112,9 @@ impl Preview<'_, '_> {
103 /// Alternate text to show for the preview.
112 /// Alternate text to show for the preview.
104 fn placeholder(&self) -> &str {
113 fn placeholder(&self) -> &str {
105 match *self {
114 match *self {
106− Self::EditorDocument(_) => "<File preview>",
115+ Self::EditorDocument(_) => "<Invalid file location>",
107 Self::Cached(preview) => match preview {
116 Self::Cached(preview) => match preview {
108− CachedPreview::Document(_) => "<File preview>",
117+ CachedPreview::Document(_) => "<Invalid file location>",
109 CachedPreview::Binary => "<Binary file>",
118 CachedPreview::Binary => "<Binary file>",
110 CachedPreview::LargeFile => "<File too large to preview>",
119 CachedPreview::LargeFile => "<File too large to preview>",
111 CachedPreview::NotFound => "<File not found>",
120 CachedPreview::NotFound => "<File not found>",
@@ -114,20 +123,71 @@ impl Preview<'_, '_> {
114 }
123 }
115}
124}
116
125
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+
117pub struct Picker<T: Item> {
179pub struct Picker<T: Item> {
118− options: Vec<T>,
119− editor_data: T::Data,
180+ editor_data: Arc<T::Data>,
120− // filter: String,
181+ shutdown: Arc<AtomicBool>,
121− matcher: Box<Matcher>,
182+ matcher: Nucleo<T>,
122− matches: Vec<PickerMatch>,
123
183
124 /// Current height of the completions box
184 /// Current height of the completions box
125 completion_height: u16,
185 completion_height: u16,
126
186
127− cursor: usize,
187+ cursor: u32,
128− // pattern: String,
129 prompt: Prompt,
188 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
189+ previous_pattern: String,
190+
131 /// Whether to show the preview panel (default true)
191 /// Whether to show the preview panel (default true)
132 show_preview: bool,
192 show_preview: bool,
133 /// Constraints for tabular formatting
193 /// Constraints for tabular formatting
@@ -144,10 +204,59 @@ pub struct Picker<T: Item> {
144}
204}
145
205
146impl<T: Item + 'static> Picker<T> {
206impl<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+
147 pub fn new(
222 pub fn new(
148 options: Vec<T>,
223 options: Vec<T>,
149 editor_data: T::Data,
224 editor_data: T::Data,
150 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
225 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,
151 ) -> Self {
260 ) -> Self {
152 let prompt = Prompt::new(
261 let prompt = Prompt::new(
153 "".into(),
262 "".into(),
@@ -156,14 +265,13 @@ impl<T: Item + 'static> Picker<T> {
156 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
265 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
157 );
266 );
158
267
159− let mut picker = Self {
268+ Self {
160− options,
269+ matcher,
161 editor_data,
270 editor_data,
162− matcher: Box::default(),
271+ shutdown,
163− matches: Vec::new(),
164 cursor: 0,
272 cursor: 0,
165 prompt,
273 prompt,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
274+ previous_pattern: String::new(),
167 truncate_start: true,
275 truncate_start: true,
168 show_preview: true,
276 show_preview: true,
169 callback_fn: Box::new(callback_fn),
277 callback_fn: Box::new(callback_fn),
@@ -172,24 +280,15 @@ impl<T: Item + 'static> Picker<T> {
172 preview_cache: HashMap::new(),
280 preview_cache: HashMap::new(),
173 read_buffer: Vec::with_capacity(1024),
281 read_buffer: Vec::with_capacity(1024),
174 file_fn: None,
282 file_fn: None,
175− };
283+ }
176−
177− picker.calculate_column_widths();
178−
179− // scoring on empty input
180− // TODO: just reuse score()
181− picker
182− .matches
183− .extend(picker.options.iter().enumerate().map(|(index, option)| {
184− let text = option.filter_text(&picker.editor_data);
185− PickerMatch {
186− index,
187− score: 0,
188− len: text.chars().count(),
189− }
190− }));
284+ }
191
285
192− picker
286+ pub fn injector(&self) -> Injector<T> {
287+ Injector {
288+ dst: self.matcher.injector(),
289+ editor_data: self.editor_data.clone(),
290+ shutown: self.shutdown.clone(),
291+ }
193 }
292 }
194
293
195 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
294 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +301,25 @@ impl<T: Item + 'static> Picker<T> {
202 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
301 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203 ) -> Self {
302 ) -> Self {
204 self.file_fn = Some(Box::new(preview_fn));
303 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());
205 self
307 self
206 }
308 }
207
309
208 pub fn set_options(&mut self, new_options: Vec<T>) {
310 pub fn set_options(&mut self, new_options: Vec<T>) {
311+ self.matcher.restart(false);
312+ let injector = self.matcher.injector();
209− self.options = new_options;
313+ for item in new_options {
210− self.cursor = 0;
314+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
211− self.force_score();
315+ injector.push(item, |dst| dst[0] = matcher_text);
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− }
231 }
316 }
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;
245 }
317 }
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();
316 }
318 }
317
319
318 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
320 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319− pub fn move_by(&mut self, amount: usize, direction: Direction) {
321+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
320− let len = self.matches.len();
322+ let len = self.matcher.snapshot().matched_item_count();
321
323
322 if len == 0 {
324 if len == 0 {
323 // No results, can't move.
325 // No results, can't move.
@@ -336,12 +338,12 @@ impl<T: Item + 'static> Picker<T> {
336
338
337 /// Move the cursor down by exactly one page. After the last page comes the first page.
339 /// Move the cursor down by exactly one page. After the last page comes the first page.
338 pub fn page_up(&mut self) {
340 pub fn page_up(&mut self) {
339− self.move_by(self.completion_height as usize, Direction::Backward);
341+ self.move_by(self.completion_height as u32, Direction::Backward);
340 }
342 }
341
343
342 /// Move the cursor up by exactly one page. After the first page comes the last page.
344 /// Move the cursor up by exactly one page. After the first page comes the last page.
343 pub fn page_down(&mut self) {
345 pub fn page_down(&mut self) {
344− self.move_by(self.completion_height as usize, Direction::Forward);
346+ self.move_by(self.completion_height as u32, Direction::Forward);
345 }
347 }
346
348
347 /// Move the cursor to the first entry
349 /// Move the cursor to the first entry
@@ -351,13 +353,18 @@ impl<T: Item + 'static> Picker<T> {
351
353
352 /// Move the cursor to the last entry
354 /// Move the cursor to the last entry
353 pub fn to_end(&mut self) {
355 pub fn to_end(&mut self) {
356+ self.cursor = self
357+ .matcher
358+ .snapshot()
359+ .matched_item_count()
354− self.cursor = self.matches.len().saturating_sub(1);
360+ .saturating_sub(1);
355 }
361 }
356
362
357 pub fn selection(&self) -> Option<&T> {
363 pub fn selection(&self) -> Option<&T> {
358− self.matches
364+ self.matcher
365+ .snapshot()
359− .get(self.cursor)
366+ .get_matched_item(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
367+ .map(|item| item.data)
361 }
368 }
362
369
363 pub fn toggle_preview(&mut self) {
370 pub fn toggle_preview(&mut self) {
@@ -366,8 +373,17 @@ impl<T: Item + 'static> Picker<T> {
366
373
367 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
374 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
375 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
376+ let pattern = self.prompt.line();
369− // TODO: recalculate only if pattern changed
377+ // TODO: better track how the pattern has changed
370− self.score();
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+ }
371 }
387 }
372 EventResult::Consumed(None)
388 EventResult::Consumed(None)
373 }
389 }
@@ -375,7 +391,7 @@ impl<T: Item + 'static> Picker<T> {
375 fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
391 fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
376 self.selection()
392 self.selection()
377 .and_then(|current| (self.file_fn.as_ref()?)(editor, current))
393 .and_then(|current| (self.file_fn.as_ref()?)(editor, current))
378− .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
394+ .map(|(path_or_id, line)| (path_or_id.get_canonicalized(), line))
379 }
395 }
380
396
381 /// Get (cached) preview for a given path. If a document corresponding
397 /// Get (cached) preview for a given path. If a document corresponding
@@ -411,12 +427,9 @@ impl<T: Item + 'static> Picker<T> {
411 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
427 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412 CachedPreview::LargeFile
428 CachedPreview::LargeFile
413 }
429 }
414− _ => {
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
430+ _ => Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
431+ .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
432+ .unwrap_or(CachedPreview::NotFound),
419− }
420 },
433 },
421 )
434 )
422 .unwrap_or(CachedPreview::NotFound);
435 .unwrap_or(CachedPreview::NotFound);
@@ -432,7 +445,7 @@ impl<T: Item + 'static> Picker<T> {
432
445
433 fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
446 fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
434 let Some((current_file, _)) = self.current_file(cx.editor) else {
447 let Some((current_file, _)) = self.current_file(cx.editor) else {
435− return EventResult::Consumed(None)
448+ return EventResult::Consumed(None);
436 };
449 };
437
450
438 // Try to find a document in the cache
451 // Try to find a document in the cache
@@ -459,11 +472,18 @@ impl<T: Item + 'static> Picker<T> {
459 let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
472 let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
460 let Some(syntax) = syntax else {
473 let Some(syntax) = syntax else {
461 log::info!("highlighting picker item failed");
474 log::info!("highlighting picker item failed");
462− return
475+ return;
463 };
476 };
464− let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
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),
482+ };
483+ let Some(picker) = picker
484+ else {
465 log::info!("picker closed before syntax highlighting finished");
485 log::info!("picker closed before syntax highlighting finished");
466− return
486+ return;
467 };
487 };
468 // Try to find a document in the cache
488 // Try to find a document in the cache
469 let doc = match current_file {
489 let doc = match current_file {
@@ -492,6 +512,14 @@ impl<T: Item + 'static> Picker<T> {
492 }
512 }
493
513
494 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
514 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+
495 let text_style = cx.editor.theme.get("ui.text");
523 let text_style = cx.editor.theme.get("ui.text");
496 let selected = cx.editor.theme.get("ui.text.focus");
524 let selected = cx.editor.theme.get("ui.text.focus");
497 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
525 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -512,8 +540,15 @@ impl<T: Item + 'static> Picker<T> {
512 // -- Render the input bar:
540 // -- Render the input bar:
513
541
514 let area = inner.clip_left(1).with_height(1);
542 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);
515
545
516− let count = format!("{}/{}", self.matches.len(), self.options.len());
546+ let count = format!(
547+ "{}{}/{}",
548+ if status.running { "(running) " } else { "" },
549+ snapshot.matched_item_count(),
550+ snapshot.item_count(),
551+ );
517 surface.set_stringn(
552 surface.set_stringn(
518 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
553 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
519 area.y,
554 area.y,
@@ -522,8 +557,6 @@ impl<T: Item + 'static> Picker<T> {
522 text_style,
557 text_style,
523 );
558 );
524
559
525− self.prompt.render(area, surface, cx);
526−
527 // -- Separator
560 // -- Separator
528 let sep_style = cx.editor.theme.get("ui.background.separator");
561 let sep_style = cx.editor.theme.get("ui.background.separator");
529 let borders = BorderType::line_symbols(BorderType::Plain);
562 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -536,106 +569,89 @@ impl<T: Item + 'static> Picker<T> {
536 // -- Render the contents:
569 // -- Render the contents:
537 // subtract area of prompt from top
570 // subtract area of prompt from top
538 let inner = inner.clip_top(2);
571 let inner = inner.clip_top(2);
539−
540− let rows = inner.height;
572+ let rows = inner.height as u32;
541− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
573+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
542 let cursor = self.cursor.saturating_sub(offset);
574 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+ }
543
584
544− let options = self
585+ let options = snapshot.matched_items(offset..end).map(|item| {
545− .matches
586+ snapshot.pattern().column_pattern(0).indices(
546− .iter()
587+ item.matcher_columns[0].slice(..),
547− .skip(offset)
588+ &mut matcher,
548− .take(rows as usize)
589+ &mut indices,
549− .map(|pmatch| &self.options[pmatch.index])
590+ );
591+ indices.sort_unstable();
592+ indices.dedup();
550− .map(|option| option.format(&self.editor_data))
593+ let mut row = item.data.format(&self.editor_data);
551− .map(|mut row| {
594+
552− const TEMP_CELL_SEP: &str = " ";
595+ let mut grapheme_idx = 0u32;
553−
596+ let mut indices = indices.drain(..);
554− let line = row.cell_text().fold(String::new(), |mut s, frag| {
597+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
555− s.push_str(&frag);
598+ if self.widths.len() < row.cells.len() {
556− s.push_str(TEMP_CELL_SEP);
599+ self.widths.resize(row.cells.len(), Constraint::Length(0));
557− s
600+ }
558− });
601+ let mut widths = self.widths.iter_mut();
559−
602+ for cell in &mut row.cells {
560− // Items are filtered by using the text returned by menu::Item::filter_text
603+ let Some(Constraint::Length(max_width)) = widths.next() else {
561− // but we do highlighting here using the text in Row and therefore there
604+ unreachable!();
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− };
605+ };
588
606
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();
589− let mut cell_len = 0;
611+ let mut width = 0;
590−
612+
591− let graphemes_with_style: Vec<_> = spans
613+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
592− .0
614+ for span in spans {
593− .iter()
615+ // this looks like a bug on first glance, we are iterating
594− .flat_map(|span| {
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
595− span.content
620+ for grapheme in span.content.graphemes(true) {
596− .grapheme_indices(true)
621+ let style = if grapheme_idx == next_highlight_idx {
597− .zip(std::iter::repeat(span.style))
622+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
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))
623+ span.style.patch(highlight_style)
611− } else {
612− (grapheme, style)
613− }
614− })
615− .collect();
616−
617− let mut span_list: Vec<(String, Style)> = Vec::new();
618− for (grapheme, style) in graphemes_with_style {
619− if span_list.last().map(|(_, sty)| sty) == Some(&style) {
620− let (string, _) = span_list.last_mut().unwrap();
621− string.push_str(grapheme);
622 } else {
624 } else {
623− span_list.push((String::from(grapheme), style))
625+ span.style
626+ };
627+ if style != current_style {
628+ if !current_span.is_empty() {
629+ span_list.push(Span::styled(current_span, current_style))
630+ }
631+ current_span = String::new();
632+ current_style = style;
624 }
633 }
634+ current_span.push_str(grapheme);
635+ grapheme_idx += 1;
625 }
636 }
637+ width += span.width();
638+ }
626
639
627− let spans: Vec<Span> = span_list
640+ span_list.push(Span::styled(current_span, current_style));
628− .into_iter()
641+ if width as u16 > *max_width {
629− .map(|(string, style)| Span::styled(string, style))
642+ *max_width = width as u16;
630− .collect();
643+ }
631− let spans: Spans = spans.into();
632− *cell = Cell::from(spans);
644+ *cell = Cell::from(Spans::from(span_list));
633
645
634− cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
646+ // spacer
647+ if grapheme_idx == next_highlight_idx {
648+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
635 }
649 }
650+ grapheme_idx += 1;
651+ }
636
652
637− row
653+ row
638− });
654+ });
639
655
640 let table = Table::new(options)
656 let table = Table::new(options)
641 .style(text_style)
657 .style(text_style)
@@ -651,7 +667,7 @@ impl<T: Item + 'static> Picker<T> {
651 surface,
667 surface,
652 &mut TableState {
668 &mut TableState {
653 offset: 0,
669 offset: 0,
654− selected: Some(cursor),
670+ selected: Some(cursor as usize),
655 },
671 },
656 self.truncate_start,
672 self.truncate_start,
657 );
673 );
@@ -677,8 +693,14 @@ impl<T: Item + 'static> Picker<T> {
677 if let Some((path, range)) = self.current_file(cx.editor) {
693 if let Some((path, range)) = self.current_file(cx.editor) {
678 let preview = self.get_preview(path, cx.editor);
694 let preview = self.get_preview(path, cx.editor);
679 let doc = match preview.document() {
695 let doc = match preview.document() {
680− Some(doc) => doc,
696+ Some(doc)
681− None => {
697+ if range.map_or(true, |(start, end)| {
698+ start <= end && end <= doc.text().len_lines()
699+ }) =>
700+ {
701+ doc
702+ }
703+ _ => {
682 let alt_text = preview.placeholder();
704 let alt_text = preview.placeholder();
683 let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
705 let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
684 let y = inner.y + inner.height / 2;
706 let y = inner.y + inner.height / 2;
@@ -687,32 +709,47 @@ impl<T: Item + 'static> Picker<T> {
687 }
709 }
688 };
710 };
689
711
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,
690− // align to middle
724+ // align to middle
691− let first_line = range
725+ -(inner.height as isize / 2),
692− .map(|(start, end)| {
726+ 0,
693− let height = end.saturating_sub(start) + 1;
727+ &text_fmt,
694− let middle = start + (height.saturating_sub(1) / 2);
728+ &annotations,
695− middle.saturating_sub(inner.height as usize / 2).min(start)
729+ );
696− })
730+ if start < offset.anchor {
697− .unwrap_or(0);
731+ offset.anchor = start;
698−
732+ offset.vertical_offset = 0;
699− let offset = ViewPosition {
733+ }
700− anchor: doc.text().line_to_char(first_line),
734+ } else {
701− horizontal_offset: 0,
735+ offset.anchor = start;
702− vertical_offset: 0,
736+ }
703− };
737+ }
704
738
705− let mut highlights = EditorView::doc_syntax_highlights(
739+ let syntax_highlights = EditorView::doc_syntax_highlights(
706 doc,
740 doc,
707 offset.anchor,
741 offset.anchor,
708 area.height,
742 area.height,
709 &cx.editor.theme,
743 &cx.editor.theme,
710 );
744 );
745+
746+ let mut overlay_highlights =
747+ EditorView::empty_highlight_iter(doc, offset.anchor, area.height);
711 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
748 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
712 if spans.is_empty() {
749 if spans.is_empty() {
713 continue;
750 continue;
714 }
751 }
715− highlights = Box::new(helix_core::syntax::merge(highlights, spans));
752+ overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
716 }
753 }
717 let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
754 let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
718
755
@@ -743,7 +780,8 @@ impl<T: Item + 'static> Picker<T> {
743 offset,
780 offset,
744 // TODO: compute text annotations asynchronously here (like inlay hints)
781 // TODO: compute text annotations asynchronously here (like inlay hints)
745 &TextAnnotations::default(),
782 &TextAnnotations::default(),
746− highlights,
783+ syntax_highlights,
784+ overlay_highlights,
747 &cx.editor.theme,
785 &cx.editor.theme,
748 &mut decorations,
786 &mut decorations,
749 &mut [],
787 &mut [],
@@ -752,7 +790,7 @@ impl<T: Item + 'static> Picker<T> {
752 }
790 }
753}
791}
754
792
755−impl<T: Item + 'static> Component for Picker<T> {
793+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
756 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
794 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
757 // +---------+ +---------+
795 // +---------+ +---------+
758 // |prompt | |preview |
796 // |prompt | |preview |
@@ -761,7 +799,8 @@ impl<T: Item + 'static> Component for Picker<T> {
761 // | | | |
799 // | | | |
762 // +---------+ +---------+
800 // +---------+ +---------+
763
801
802+ let render_preview =
764− let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
803+ self.show_preview && self.file_fn.is_some() && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
765
804
766 let picker_width = if render_preview {
805 let picker_width = if render_preview {
767 area.width / 2
806 area.width / 2
@@ -791,11 +830,28 @@ impl<T: Item + 'static> Component for Picker<T> {
791 _ => return EventResult::Ignored(None),
830 _ => return EventResult::Ignored(None),
792 };
831 };
793
832
794− let close_fn =
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+ };
795− EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
853+ EventResult::Consumed(Some(callback))
796− // remove the layer
797− compositor.last_picker = compositor.pop();
798− })));
854+ };
799
855
800 // So that idle timeout retriggers
856 // So that idle timeout retriggers
801 ctx.editor.reset_idle_timer();
857 ctx.editor.reset_idle_timer();
@@ -819,9 +875,7 @@ impl<T: Item + 'static> Component for Picker<T> {
819 key!(End) => {
875 key!(End) => {
820 self.to_end();
876 self.to_end();
821 }
877 }
822− key!(Esc) | ctrl!('c') => {
823− return close_fn;
878+ key!(Esc) | ctrl!('c') => return close_fn(self),
824− }
825 alt!(Enter) => {
879 alt!(Enter) => {
826 if let Some(option) = self.selection() {
880 if let Some(option) = self.selection() {
827 (self.callback_fn)(ctx, option, Action::Load);
881 (self.callback_fn)(ctx, option, Action::Load);
@@ -831,19 +885,19 @@ impl<T: Item + 'static> Component for Picker<T> {
831 if let Some(option) = self.selection() {
885 if let Some(option) = self.selection() {
832 (self.callback_fn)(ctx, option, Action::Replace);
886 (self.callback_fn)(ctx, option, Action::Replace);
833 }
887 }
834− return close_fn;
888+ return close_fn(self);
835 }
889 }
836 ctrl!('s') => {
890 ctrl!('s') => {
837 if let Some(option) = self.selection() {
891 if let Some(option) = self.selection() {
838 (self.callback_fn)(ctx, option, Action::HorizontalSplit);
892 (self.callback_fn)(ctx, option, Action::HorizontalSplit);
839 }
893 }
840− return close_fn;
894+ return close_fn(self);
841 }
895 }
842 ctrl!('v') => {
896 ctrl!('v') => {
843 if let Some(option) = self.selection() {
897 if let Some(option) = self.selection() {
844 (self.callback_fn)(ctx, option, Action::VerticalSplit);
898 (self.callback_fn)(ctx, option, Action::VerticalSplit);
845 }
899 }
846− return close_fn;
900+ return close_fn(self);
847 }
901 }
848 ctrl!('t') => {
902 ctrl!('t') => {
849 self.toggle_preview();
903 self.toggle_preview();
@@ -871,30 +925,15 @@ impl<T: Item + 'static> Component for Picker<T> {
871 self.completion_height = height.saturating_sub(4);
925 self.completion_height = height.saturating_sub(4);
872 Some((width, height))
926 Some((width, height))
873 }
927 }
874−}
875−
876−#[derive(PartialEq, Eq, Debug)]
877−struct PickerMatch {
878− score: i64,
879− index: usize,
880− len: usize,
881−}
882−
883−impl PickerMatch {
884− fn key(&self) -> impl Ord {
885− (cmp::Reverse(self.score), self.len, self.index)
886− }
887−}
888
928
889−impl PartialOrd for PickerMatch {
929+ fn id(&self) -> Option<&'static str> {
890− fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
930+ Some(ID)
891− Some(self.cmp(other))
892 }
931 }
893}
932}
894−
933+impl<T: Item> Drop for Picker<T> {
895−impl Ord for PickerMatch {
934+ fn drop(&mut self) {
896− fn cmp(&self, other: &Self) -> Ordering {
935+ // ensure we cancel any ongoing background threads streaming into the picker
897− self.key().cmp(&other.key())
936+ self.shutdown.store(true, atomic::Ordering::Relaxed)
898 }
937 }
899}
938}
900
939
@@ -907,15 +946,13 @@ pub type DynQueryCallback<T> =
907
946
908/// A picker that updates its contents via a callback whenever the
947/// A picker that updates its contents via a callback whenever the
909/// query string changes. Useful for live grep, workspace symbols, etc.
948/// query string changes. Useful for live grep, workspace symbols, etc.
910−pub struct DynamicPicker<T: ui::menu::Item + Send> {
949+pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
911 file_picker: Picker<T>,
950 file_picker: Picker<T>,
912 query_callback: DynQueryCallback<T>,
951 query_callback: DynQueryCallback<T>,
913 query: String,
952 query: String,
914}
953}
915
954
916−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
955+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
917− pub const ID: &'static str = "dynamic-picker";
918−
919 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
956 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
920 Self {
957 Self {
921 file_picker,
958 file_picker,
@@ -925,7 +962,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
925 }
962 }
926}
963}
927
964
928−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
965+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
929 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
966 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
930 self.file_picker.render(area, surface, cx);
967 self.file_picker.render(area, surface, cx);
931 }
968 }
@@ -947,7 +984,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
947 let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
984 let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
948 // Wrapping of pickers in overlay is done outside the picker code,
985 // Wrapping of pickers in overlay is done outside the picker code,
949 // so this is fragile and will break if wrapped in some other widget.
986 // so this is fragile and will break if wrapped in some other widget.
950− let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
987+ let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
951 Some(overlay) => &mut overlay.content.file_picker,
988 Some(overlay) => &mut overlay.content.file_picker,
952 None => return,
989 None => return,
953 };
990 };
@@ -968,6 +1005,6 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
968 }
1005 }
969
1006
970 fn id(&self) -> Option<&'static str> {
1007 fn id(&self) -> Option<&'static str> {
971− Some(Self::ID)
1008+ Some(ID)
972 }
1009 }
973}
1010}
helix-term/src/ui/popup.rs+27−7
@@ -3,7 +3,10 @@ use crate::{
3 compositor::{Callback, Component, Context, Event, EventResult},
3 compositor::{Callback, Component, Context, Event, EventResult},
4 ctrl, key,
4 ctrl, key,
5};
5};
6+use tui::{
6−use tui::buffer::Buffer as Surface;
7+ buffer::Buffer as Surface,
8+ widgets::{Block, Borders, Widget},
9+};
7
10
8use helix_core::Position;
11use helix_core::Position;
9use helix_view::{
12use helix_view::{
@@ -252,13 +255,29 @@ impl<T: Component> Component for Popup<T> {
252 let background = cx.editor.theme.get("ui.popup");
255 let background = cx.editor.theme.get("ui.popup");
253 surface.clear_with(area, background);
256 surface.clear_with(area, background);
254
257
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 {
255− let inner = area.inner(&self.margin);
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+
256 self.contents.render(inner, surface, cx);
275 self.contents.render(inner, surface, cx);
257
276
258 // render scrollbar if contents do not fit
277 // render scrollbar if contents do not fit
259 if self.has_scrollbar {
278 if self.has_scrollbar {
260− let win_height = inner.height as usize;
279+ let win_height = (inner.height as usize).saturating_sub(2 * border);
261− let len = self.child_size.1 as usize;
280+ let len = (self.child_size.1 as usize).saturating_sub(2 * border);
262 let fits = len <= win_height;
281 let fits = len <= win_height;
263 let scroll = self.scroll;
282 let scroll = self.scroll;
264 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
283 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -274,14 +293,15 @@ impl<T: Component> Component for Popup<T> {
274
293
275 let mut cell;
294 let mut cell;
276 for i in 0..win_height {
295 for i in 0..win_height {
277− cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
296+ cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
278
297
279− cell.set_symbol("▐"); // right half block
298+ let half_block = if render_borders { "▌" } else { "▐" };
280
299
281 if scroll_line <= i && i < scroll_line + scroll_height {
300 if scroll_line <= i && i < scroll_line + scroll_height {
282 // Draw scroll thumb
301 // Draw scroll thumb
302+ cell.set_symbol(half_block);
283 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
303 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
284− } else {
304+ } else if !render_borders {
285 // Draw scroll track
305 // Draw scroll track
286 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
306 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
287 }
307 }
helix-term/src/ui/prompt.rs+27−29
@@ -306,8 +306,8 @@ impl Prompt {
306 direction: CompletionDirection,
306 direction: CompletionDirection,
307 ) {
307 ) {
308 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
308 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
309− let values = match cx.editor.registers.read(register) {
309+ let mut values = match cx.editor.registers.read(register, cx.editor) {
310− Some(values) if !values.is_empty() => values,
310+ Some(values) if values.len() > 0 => values.rev(),
311 _ => return,
311 _ => return,
312 };
312 };
313
313
@@ -315,13 +315,16 @@ impl Prompt {
315
315
316 let index = match direction {
316 let index = match direction {
317 CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
317 CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
318− CompletionDirection::Backward => {
318+ CompletionDirection::Backward => self
319+ .history_pos
320+ .unwrap_or_else(|| values.len())
319− self.history_pos.unwrap_or(values.len()).saturating_sub(1)
321+ .saturating_sub(1),
320− }
321 }
322 }
322 .min(end);
323 .min(end);
323
324
324− self.line = values[index].clone();
325+ self.line = values.nth(index).unwrap().to_string();
326+ // Appease the borrow checker.
327+ drop(values);
325
328
326 self.history_pos = Some(index);
329 self.history_pos = Some(index);
327
330
@@ -470,7 +473,7 @@ impl Prompt {
470 // Show the most recently entered value as a suggestion.
473 // Show the most recently entered value as a suggestion.
471 if let Some(suggestion) = self
474 if let Some(suggestion) = self
472 .history_register
475 .history_register
473− .and_then(|reg| cx.editor.registers.last(reg))
476+ .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
474 {
477 {
475 surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
478 surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
476 }
479 }
@@ -567,25 +570,29 @@ impl Component for Prompt {
567 } else {
570 } else {
568 let last_item = self
571 let last_item = self
569 .history_register
572 .history_register
570− .and_then(|reg| cx.editor.registers.last(reg).cloned())
573+ .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
571− .map(|entry| entry.into())
574+ .map(|entry| entry.to_string())
572− .unwrap_or_else(|| Cow::from(""));
575+ .unwrap_or_else(|| String::from(""));
573
576
574 // handle executing with last command in history if nothing entered
577 // handle executing with last command in history if nothing entered
575− let input: Cow<str> = if self.line.is_empty() {
578+ let input = if self.line.is_empty() {
576− last_item
579+ &last_item
577 } else {
580 } else {
578 if last_item != self.line {
581 if last_item != self.line {
579 // store in history
582 // store in history
580 if let Some(register) = self.history_register {
583 if let Some(register) = self.history_register {
584+ if let Err(err) =
581− cx.editor.registers.push(register, self.line.clone());
585+ cx.editor.registers.push(register, self.line.clone())
586+ {
587+ cx.editor.set_error(err.to_string());
588+ }
582 };
589 };
583 }
590 }
584
591
585− self.line.as_str().into()
592+ &self.line
586 };
593 };
587
594
588− (self.callback_fn)(cx, &input, PromptEvent::Validate);
595+ (self.callback_fn)(cx, input, PromptEvent::Validate);
589
596
590 return close_fn;
597 return close_fn;
591 }
598 }
@@ -617,25 +624,16 @@ impl Component for Prompt {
617 self.completion = cx
624 self.completion = cx
618 .editor
625 .editor
619 .registers
626 .registers
620− .inner()
627+ .iter_preview()
621− .iter()
622− .map(|(ch, reg)| {
623− let content = reg
624− .read()
625− .get(0)
626− .and_then(|s| s.lines().next().to_owned())
627− .unwrap_or_default();
628− (0.., format!("{} {}", ch, &content).into())
628+ .map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
629− })
630 .collect();
629 .collect();
631 self.next_char_handler = Some(Box::new(|prompt, c, context| {
630 self.next_char_handler = Some(Box::new(|prompt, c, context| {
632 prompt.insert_str(
631 prompt.insert_str(
633− context
632+ &context
634 .editor
633 .editor
635 .registers
634 .registers
636− .read(c)
635+ .first(c, context.editor)
637− .and_then(|r| r.first())
636+ .unwrap_or_default(),
638− .map_or("", |r| r.as_str()),
639 context.editor,
637 context.editor,
640 );
638 );
641 }));
639 }));
helix-term/src/ui/statusline.rs+14
@@ -145,6 +145,7 @@ where
145 helix_view::editor::StatusLineElement::FileModificationIndicator => {
145 helix_view::editor::StatusLineElement::FileModificationIndicator => {
146 render_file_modification_indicator
146 render_file_modification_indicator
147 }
147 }
148+ helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator,
148 helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
149 helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
149 helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
150 helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
150 helix_view::editor::StatusLineElement::FileType => render_file_type,
151 helix_view::editor::StatusLineElement::FileType => render_file_type,
@@ -442,6 +443,19 @@ where
442 write(context, title, None);
443 write(context, title, None);
443}
444}
444
445
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+
445fn render_file_base_name<F>(context: &mut RenderContext, write: F)
459fn render_file_base_name<F>(context: &mut RenderContext, write: F)
446where
460where
447 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
461 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
helix-term/tests/integration.rs+1
@@ -18,6 +18,7 @@ mod test {
18 mod auto_indent;
18 mod auto_indent;
19 mod auto_pairs;
19 mod auto_pairs;
20 mod commands;
20 mod commands;
21+ mod languages;
21 mod movement;
22 mod movement;
22 mod prompt;
23 mod prompt;
23 mod splits;
24 mod splits;
helix-term/tests/test/commands.rs+47
@@ -2,6 +2,7 @@ use helix_term::application::Application;
2
2
3use super::*;
3use super::*;
4
4
5+mod movement;
5mod write;
6mod write;
6
7
7#[tokio::test(flavor = "multi_thread")]
8#[tokio::test(flavor = "multi_thread")]
@@ -479,3 +480,49 @@ fn bar() {#(\n|)#\
479
480
480 Ok(())
481 Ok(())
481}
482}
483+
484+#[tokio::test(flavor = "multi_thread")]
485+async fn test_join_selections() -> anyhow::Result<()> {
486+ // normal join
487+ test((
488+ platform_line(indoc! {"\
489+ #[a|]#bc
490+ def
491+ "}),
492+ "J",
493+ platform_line(indoc! {"\
494+ #[a|]#bc def
495+ "}),
496+ ))
497+ .await?;
498+
499+ // join with empty line
500+ test((
501+ platform_line(indoc! {"\
502+ #[a|]#bc
503+
504+ def
505+ "}),
506+ "JJ",
507+ platform_line(indoc! {"\
508+ #[a|]#bc def
509+ "}),
510+ ))
511+ .await?;
512+
513+ // join with additional space in non-empty line
514+ test((
515+ platform_line(indoc! {"\
516+ #[a|]#bc
517+
518+ def
519+ "}),
520+ "JJ",
521+ platform_line(indoc! {"\
522+ #[a|]#bc def
523+ "}),
524+ ))
525+ .await?;
526+
527+ Ok(())
528+}
helix-term/tests/test/commands/movement.rsnew file+452
@@ -0,0 +1,452 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn test_move_parent_node_end() -> anyhow::Result<()> {
5+ let tests = vec![
6+ // single cursor stays single cursor, first goes to end of current
7+ // node, then parent
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ fn foo() {
11+ let result = if true {
12+ "yes"
13+ } else {
14+ "no#["|]#
15+ }
16+ }
17+ "##}),
18+ "<A-e>",
19+ helpers::platform_line(indoc! {"\
20+ fn foo() {
21+ let result = if true {
22+ \"yes\"
23+ } else {
24+ \"no\"#[\n|]#
25+ }
26+ }
27+ "}),
28+ ),
29+ (
30+ helpers::platform_line(indoc! {"\
31+ fn foo() {
32+ let result = if true {
33+ \"yes\"
34+ } else {
35+ \"no\"#[\n|]#
36+ }
37+ }
38+ "}),
39+ "<A-e>",
40+ helpers::platform_line(indoc! {"\
41+ fn foo() {
42+ let result = if true {
43+ \"yes\"
44+ } else {
45+ \"no\"
46+ }#[\n|]#
47+ }
48+ "}),
49+ ),
50+ // select mode extends
51+ (
52+ helpers::platform_line(indoc! {r##"
53+ fn foo() {
54+ let result = if true {
55+ "yes"
56+ } else {
57+ #["no"|]#
58+ }
59+ }
60+ "##}),
61+ "v<A-e><A-e>",
62+ helpers::platform_line(indoc! {"\
63+ fn foo() {
64+ let result = if true {
65+ \"yes\"
66+ } else {
67+ #[\"no\"
68+ }\n|]#
69+ }
70+ "}),
71+ ),
72+ ];
73+
74+ for test in tests {
75+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
76+ }
77+
78+ Ok(())
79+}
80+
81+#[tokio::test(flavor = "multi_thread")]
82+async fn test_move_parent_node_start() -> anyhow::Result<()> {
83+ let tests = vec![
84+ // single cursor stays single cursor, first goes to end of current
85+ // node, then parent
86+ (
87+ helpers::platform_line(indoc! {r##"
88+ fn foo() {
89+ let result = if true {
90+ "yes"
91+ } else {
92+ "no#["|]#
93+ }
94+ }
95+ "##}),
96+ "<A-b>",
97+ helpers::platform_line(indoc! {"\
98+ fn foo() {
99+ let result = if true {
100+ \"yes\"
101+ } else {
102+ #[\"|]#no\"
103+ }
104+ }
105+ "}),
106+ ),
107+ (
108+ helpers::platform_line(indoc! {"\
109+ fn foo() {
110+ let result = if true {
111+ \"yes\"
112+ } else {
113+ \"no\"#[\n|]#
114+ }
115+ }
116+ "}),
117+ "<A-b>",
118+ helpers::platform_line(indoc! {"\
119+ fn foo() {
120+ let result = if true {
121+ \"yes\"
122+ } else #[{|]#
123+ \"no\"
124+ }
125+ }
126+ "}),
127+ ),
128+ (
129+ helpers::platform_line(indoc! {"\
130+ fn foo() {
131+ let result = if true {
132+ \"yes\"
133+ } else #[{|]#
134+ \"no\"
135+ }
136+ }
137+ "}),
138+ "<A-b>",
139+ helpers::platform_line(indoc! {"\
140+ fn foo() {
141+ let result = if true {
142+ \"yes\"
143+ } #[e|]#lse {
144+ \"no\"
145+ }
146+ }
147+ "}),
148+ ),
149+ // select mode extends
150+ (
151+ helpers::platform_line(indoc! {r##"
152+ fn foo() {
153+ let result = if true {
154+ "yes"
155+ } else {
156+ #["no"|]#
157+ }
158+ }
159+ "##}),
160+ "v<A-b><A-b>",
161+ helpers::platform_line(indoc! {"\
162+ fn foo() {
163+ let result = if true {
164+ \"yes\"
165+ } else #[|{
166+ ]#\"no\"
167+ }
168+ }
169+ "}),
170+ ),
171+ (
172+ helpers::platform_line(indoc! {r##"
173+ fn foo() {
174+ let result = if true {
175+ "yes"
176+ } else {
177+ #["no"|]#
178+ }
179+ }
180+ "##}),
181+ "v<A-b><A-b><A-b>",
182+ helpers::platform_line(indoc! {"\
183+ fn foo() {
184+ let result = if true {
185+ \"yes\"
186+ } #[|else {
187+ ]#\"no\"
188+ }
189+ }
190+ "}),
191+ ),
192+ ];
193+
194+ for test in tests {
195+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
196+ }
197+
198+ Ok(())
199+}
200+
201+#[tokio::test(flavor = "multi_thread")]
202+async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
203+ let tests = vec![
204+ // single cursor stays single cursor, first goes to end of current
205+ // node, then parent
206+ (
207+ helpers::platform_line(indoc! {r##"
208+ fn foo() {
209+ let result = if true {
210+ "yes"
211+ } else {
212+ "no#["|]#
213+ }
214+ }
215+ "##}),
216+ "i<tab>",
217+ helpers::platform_line(indoc! {"\
218+ fn foo() {
219+ let result = if true {
220+ \"yes\"
221+ } else {
222+ \"no\"#[|\n]#
223+ }
224+ }
225+ "}),
226+ ),
227+ (
228+ helpers::platform_line(indoc! {"\
229+ fn foo() {
230+ let result = if true {
231+ \"yes\"
232+ } else {
233+ \"no\"#[\n|]#
234+ }
235+ }
236+ "}),
237+ "i<tab>",
238+ helpers::platform_line(indoc! {"\
239+ fn foo() {
240+ let result = if true {
241+ \"yes\"
242+ } else {
243+ \"no\"
244+ }#[|\n]#
245+ }
246+ "}),
247+ ),
248+ // appending to the end of a line should still look at the current
249+ // line, not the next one
250+ (
251+ helpers::platform_line(indoc! {"\
252+ fn foo() {
253+ let result = if true {
254+ \"yes\"
255+ } else {
256+ \"no#[\"|]#
257+ }
258+ }
259+ "}),
260+ "a<tab>",
261+ helpers::platform_line(indoc! {"\
262+ fn foo() {
263+ let result = if true {
264+ \"yes\"
265+ } else {
266+ \"no\"
267+ }#[\n|]#
268+ }
269+ "}),
270+ ),
271+ // before cursor is all whitespace, so insert tab
272+ (
273+ helpers::platform_line(indoc! {"\
274+ fn foo() {
275+ let result = if true {
276+ \"yes\"
277+ } else {
278+ #[\"no\"|]#
279+ }
280+ }
281+ "}),
282+ "i<tab>",
283+ helpers::platform_line(indoc! {"\
284+ fn foo() {
285+ let result = if true {
286+ \"yes\"
287+ } else {
288+ #[|\"no\"]#
289+ }
290+ }
291+ "}),
292+ ),
293+ // if selection spans multiple lines, it should still only look at the
294+ // line on which the head is
295+ (
296+ helpers::platform_line(indoc! {"\
297+ fn foo() {
298+ let result = if true {
299+ #[\"yes\"
300+ } else {
301+ \"no\"|]#
302+ }
303+ }
304+ "}),
305+ "a<tab>",
306+ helpers::platform_line(indoc! {"\
307+ fn foo() {
308+ let result = if true {
309+ \"yes\"
310+ } else {
311+ \"no\"
312+ }#[\n|]#
313+ }
314+ "}),
315+ ),
316+ (
317+ helpers::platform_line(indoc! {"\
318+ fn foo() {
319+ let result = if true {
320+ #[\"yes\"
321+ } else {
322+ \"no\"|]#
323+ }
324+ }
325+ "}),
326+ "i<tab>",
327+ helpers::platform_line(indoc! {"\
328+ fn foo() {
329+ let result = if true {
330+ #[|\"yes\"
331+ } else {
332+ \"no\"]#
333+ }
334+ }
335+ "}),
336+ ),
337+ (
338+ helpers::platform_line(indoc! {"\
339+ fn foo() {
340+ #[l|]#et result = if true {
341+ #(\"yes\"
342+ } else {
343+ \"no\"|)#
344+ }
345+ }
346+ "}),
347+ "i<tab>",
348+ helpers::platform_line(indoc! {"\
349+ fn foo() {
350+ #[|l]#et result = if true {
351+ #(|\"yes\"
352+ } else {
353+ \"no\")#
354+ }
355+ }
356+ "}),
357+ ),
358+ (
359+ helpers::platform_line(indoc! {"\
360+ fn foo() {
361+ let result = if true {
362+ \"yes\"#[\n|]#
363+ } else {
364+ \"no\"#(\n|)#
365+ }
366+ }
367+ "}),
368+ "i<tab>",
369+ helpers::platform_line(indoc! {"\
370+ fn foo() {
371+ let result = if true {
372+ \"yes\"
373+ }#[| ]#else {
374+ \"no\"
375+ }#(|\n)#
376+ }
377+ "}),
378+ ),
379+ (
380+ helpers::platform_line(indoc! {"\
381+ fn foo() {
382+ let result = if true {
383+ #[\"yes\"|]#
384+ } else {
385+ #(\"no\"|)#
386+ }
387+ }
388+ "}),
389+ "i<tab>",
390+ helpers::platform_line(indoc! {"\
391+ fn foo() {
392+ let result = if true {
393+ #[|\"yes\"]#
394+ } else {
395+ #(|\"no\")#
396+ }
397+ }
398+ "}),
399+ ),
400+ // if any cursors are not preceded by all whitespace, then do the
401+ // smart_tab action
402+ (
403+ helpers::platform_line(indoc! {"\
404+ fn foo() {
405+ let result = if true {
406+ #[\"yes\"\n|]#
407+ } else {
408+ \"no#(\"\n|)#
409+ }
410+ }
411+ "}),
412+ "i<tab>",
413+ helpers::platform_line(indoc! {"\
414+ fn foo() {
415+ let result = if true {
416+ \"yes\"
417+ }#[| ]#else {
418+ \"no\"
419+ }#(|\n)#
420+ }
421+ "}),
422+ ),
423+ // Ctrl-tab always inserts a tab
424+ (
425+ helpers::platform_line(indoc! {"\
426+ fn foo() {
427+ let result = if true {
428+ #[\"yes\"\n|]#
429+ } else {
430+ \"no#(\"\n|)#
431+ }
432+ }
433+ "}),
434+ "i<S-tab>",
435+ helpers::platform_line(indoc! {"\
436+ fn foo() {
437+ let result = if true {
438+ #[|\"yes\"\n]#
439+ } else {
440+ \"no #(|\"\n)#
441+ }
442+ }
443+ "}),
444+ ),
445+ ];
446+
447+ for test in tests {
448+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
449+ }
450+
451+ Ok(())
452+}
helix-term/tests/test/commands/write.rs+122−3
@@ -93,7 +93,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
93 )
93 )
94 .await?;
94 .await?;
95
95
96− helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
96+ helpers::assert_file_has_content(file.as_file_mut(), &platform_line(&RANGE.end().to_string()))?;
97
97
98 Ok(())
98 Ok(())
99}
99}
@@ -209,7 +209,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
209
209
210 let mut file_content = String::new();
210 let mut file_content = String::new();
211 file.as_file_mut().read_to_string(&mut file_content)?;
211 file.as_file_mut().read_to_string(&mut file_content)?;
212− assert_eq!(RANGE.end().to_string(), file_content);
212+ assert_eq!(platform_line(&RANGE.end().to_string()), file_content);
213
213
214 Ok(())
214 Ok(())
215}
215}
@@ -424,13 +424,132 @@ async fn test_write_utf_bom_file() -> anyhow::Result<()> {
424 Ok(())
424 Ok(())
425}
425}
426
426
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+
427async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
538async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
428 let mut file = tempfile::NamedTempFile::new()?;
539 let mut file = tempfile::NamedTempFile::new()?;
429
540
430 file.as_file_mut().write_all(&file_content)?;
541 file.as_file_mut().write_all(&file_content)?;
431
542
432 helpers::test_key_sequence(
543 helpers::test_key_sequence(
433− &mut helpers::AppBuilder::new().build()?,
544+ &mut helpers::AppBuilder::new()
545+ .with_config(Config {
546+ editor: helix_view::editor::Config {
547+ insert_final_newline: false,
548+ ..Default::default()
549+ },
550+ ..Default::default()
551+ })
552+ .build()?,
434 Some(&format!(":o {}<ret>:x<ret>", file.path().to_string_lossy())),
553 Some(&format!(":o {}<ret>:x<ret>", file.path().to_string_lossy())),
435 None,
554 None,
436 true,
555 true,
helix-term/tests/test/helpers.rs+9−1
@@ -320,6 +320,14 @@ impl AppBuilder {
320 }
320 }
321
321
322 pub fn build(self) -> anyhow::Result<Application> {
322 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+
323 let mut app = Application::new(self.args, self.config, self.syn_conf)?;
331 let mut app = Application::new(self.args, self.config, self.syn_conf)?;
324
332
325 if let Some((text, selection)) = self.input {
333 if let Some((text, selection)) = self.input {
@@ -350,7 +358,7 @@ pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result
350
358
351 let mut file_content = String::new();
359 let mut file_content = String::new();
352 file.read_to_string(&mut file_content)?;
360 file.read_to_string(&mut file_content)?;
353− assert_eq!(content, file_content);
361+ assert_eq!(file_content, content);
354
362
355 Ok(())
363 Ok(())
356}
364}
helix-term/tests/test/languages/go.rsnew file+41
@@ -0,0 +1,41 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn auto_indent() -> anyhow::Result<()> {
5+ let app = || AppBuilder::new().with_file("foo.go", None);
6+
7+ let enter_tests = [
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ type Test struct {#[}|]#
11+ "##}),
12+ "i<ret>",
13+ helpers::platform_line(indoc! {"\
14+ type Test struct {
15+ \t#[|\n]#
16+ }
17+ "}),
18+ ),
19+ (
20+ helpers::platform_line(indoc! {"\
21+ func main() {
22+ \tswitch nil {#[}|]#
23+ }
24+ "}),
25+ "i<ret>",
26+ helpers::platform_line(indoc! {"\
27+ func main() {
28+ \tswitch nil {
29+ \t\t#[|\n]#
30+ \t}
31+ }
32+ "}),
33+ ),
34+ ];
35+
36+ for test in enter_tests {
37+ test_with_config(app(), test).await?;
38+ }
39+
40+ Ok(())
41+}
helix-term/tests/test/languages/mod.rsnew file+4
@@ -0,0 +1,4 @@
1+use super::*;
2+
3+mod go;
4+mod yaml;
helix-term/tests/test/languages/yaml.rsnew file+819
@@ -0,0 +1,819 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn auto_indent() -> anyhow::Result<()> {
5+ let app = || AppBuilder::new().with_file("foo.yaml", None);
6+
7+ let below_tests = [
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ #[t|]#op:
11+ baz: foo
12+ bazi:
13+ more: yes
14+ why: because
15+ quux:
16+ - 1
17+ - 2
18+ bax: foox
19+ fook:
20+ "##}),
21+ "o",
22+ helpers::platform_line(indoc! {"\
23+ top:
24+ #[\n|]#
25+ baz: foo
26+ bazi:
27+ more: yes
28+ why: because
29+ quux:
30+ - 1
31+ - 2
32+ bax: foox
33+ fook:
34+ "}),
35+ ),
36+ (
37+ helpers::platform_line(indoc! {r##"
38+ top:
39+ b#[a|]#z: foo
40+ bazi:
41+ more: yes
42+ why: because
43+ quux:
44+ - 1
45+ - 2
46+ bax: foox
47+ fook:
48+ "##}),
49+ "o",
50+ helpers::platform_line(indoc! {"\
51+ top:
52+ baz: foo
53+ #[\n|]#
54+ bazi:
55+ more: yes
56+ why: because
57+ quux:
58+ - 1
59+ - 2
60+ bax: foox
61+ fook:
62+ "}),
63+ ),
64+ (
65+ helpers::platform_line(indoc! {r##"
66+ top:
67+ baz: foo
68+ bazi#[:|]#
69+ more: yes
70+ why: because
71+ quux:
72+ - 1
73+ - 2
74+ bax: foox
75+ fook:
76+ "##}),
77+ "o",
78+ helpers::platform_line(indoc! {"\
79+ top:
80+ baz: foo
81+ bazi:
82+ #[\n|]#
83+ more: yes
84+ why: because
85+ quux:
86+ - 1
87+ - 2
88+ bax: foox
89+ fook:
90+ "}),
91+ ),
92+ (
93+ helpers::platform_line(indoc! {r##"
94+ top:
95+ baz: foo
96+ bazi:
97+ more: #[yes|]#
98+ why: because
99+ quux:
100+ - 1
101+ - 2
102+ bax: foox
103+ fook:
104+ "##}),
105+ "o",
106+ helpers::platform_line(indoc! {"\
107+ top:
108+ baz: foo
109+ bazi:
110+ more: yes
111+ #[\n|]#
112+ why: because
113+ quux:
114+ - 1
115+ - 2
116+ bax: foox
117+ fook:
118+ "}),
119+ ),
120+ (
121+ helpers::platform_line(indoc! {r##"
122+ top:
123+ baz: foo
124+ bazi:
125+ more: yes
126+ why: becaus#[e|]#
127+ quux:
128+ - 1
129+ - 2
130+ bax: foox
131+ fook:
132+ "##}),
133+ "o",
134+ helpers::platform_line(indoc! {"\
135+ top:
136+ baz: foo
137+ bazi:
138+ more: yes
139+ why: because
140+ #[\n|]#
141+ quux:
142+ - 1
143+ - 2
144+ bax: foox
145+ fook:
146+ "}),
147+ ),
148+ (
149+ helpers::platform_line(indoc! {"\
150+ top:
151+ baz: foo
152+ bazi:
153+ more: yes
154+ why: because
155+ quux:#[\n|]#
156+ - 1
157+ - 2
158+ bax: foox
159+ fook:
160+ "}),
161+ "o",
162+ helpers::platform_line(indoc! {"\
163+ top:
164+ baz: foo
165+ bazi:
166+ more: yes
167+ why: because
168+ quux:
169+ #[\n|]#
170+ - 1
171+ - 2
172+ bax: foox
173+ fook:
174+ "}),
175+ ),
176+ (
177+ helpers::platform_line(indoc! {"\
178+ top:
179+ baz: foo
180+ bazi:
181+ more: yes
182+ why: because
183+ quux:
184+ - 1#[\n|]#
185+ - 2
186+ bax: foox
187+ fook:
188+ "}),
189+ "o",
190+ helpers::platform_line(indoc! {"\
191+ top:
192+ baz: foo
193+ bazi:
194+ more: yes
195+ why: because
196+ quux:
197+ - 1
198+ #[\n|]#
199+ - 2
200+ bax: foox
201+ fook:
202+ "}),
203+ ),
204+ (
205+ helpers::platform_line(indoc! {"\
206+ top:
207+ baz: foo
208+ bazi:
209+ more: yes
210+ why: because
211+ quux:
212+ - 1
213+ - 2
214+ bax: foox
215+ fook:#[\n|]#
216+ "}),
217+ "o",
218+ helpers::platform_line(indoc! {"\
219+ top:
220+ baz: foo
221+ bazi:
222+ more: yes
223+ why: because
224+ quux:
225+ - 1
226+ - 2
227+ bax: foox
228+ fook:
229+ #[\n|]#
230+ "}),
231+ ),
232+ (
233+ helpers::platform_line(indoc! {"\
234+ top:
235+ baz: foo
236+ bax: |
237+ some
238+ multi
239+ line
240+ string#[\n|]#
241+ fook:
242+ "}),
243+ "o",
244+ helpers::platform_line(indoc! {"\
245+ top:
246+ baz: foo
247+ bax: |
248+ some
249+ multi
250+ line
251+ string
252+ #[\n|]#
253+ fook:
254+ "}),
255+ ),
256+ (
257+ helpers::platform_line(indoc! {"\
258+ top:
259+ baz: foo
260+ bax: >
261+ some
262+ multi
263+ line#[\n|]#
264+ string
265+ fook:
266+ "}),
267+ "o",
268+ helpers::platform_line(indoc! {"\
269+ top:
270+ baz: foo
271+ bax: >
272+ some
273+ multi
274+ line
275+ #[\n|]#
276+ string
277+ fook:
278+ "}),
279+ ),
280+ (
281+ helpers::platform_line(indoc! {"\
282+ top:
283+ baz: foo
284+ bax: >#[\n|]#
285+ fook:
286+ "}),
287+ "o",
288+ helpers::platform_line(indoc! {"\
289+ top:
290+ baz: foo
291+ bax: >
292+ #[\n|]#
293+ fook:
294+ "}),
295+ ),
296+ (
297+ helpers::platform_line(indoc! {"\
298+ - top:#[\n|]#
299+ baz: foo
300+ bax: foox
301+ fook:
302+ "}),
303+ "o",
304+ helpers::platform_line(indoc! {"\
305+ - top:
306+ #[\n|]#
307+ baz: foo
308+ bax: foox
309+ fook:
310+ "}),
311+ ),
312+ (
313+ helpers::platform_line(indoc! {"\
314+ - top:
315+ baz: foo#[\n|]#
316+ bax: foox
317+ fook:
318+ "}),
319+ "o",
320+ helpers::platform_line(indoc! {"\
321+ - top:
322+ baz: foo
323+ #[\n|]#
324+ bax: foox
325+ fook:
326+ "}),
327+ ),
328+ (
329+ helpers::platform_line(indoc! {"\
330+ - top:
331+ baz: foo
332+ bax: foox#[\n|]#
333+ fook:
334+ "}),
335+ "o",
336+ helpers::platform_line(indoc! {"\
337+ - top:
338+ baz: foo
339+ bax: foox
340+ #[\n|]#
341+ fook:
342+ "}),
343+ ),
344+ (
345+ helpers::platform_line(indoc! {"\
346+ top:
347+ baz:
348+ - one: two#[\n|]#
349+ three: four
350+ - top:
351+ baz: foo
352+ bax: foox
353+ "}),
354+ "o",
355+ helpers::platform_line(indoc! {"\
356+ top:
357+ baz:
358+ - one: two
359+ #[\n|]#
360+ three: four
361+ - top:
362+ baz: foo
363+ bax: foox
364+ "}),
365+ ),
366+ // yaml map without a key
367+ (
368+ helpers::platform_line(indoc! {"\
369+ top:#[\n|]#
370+ "}),
371+ "o",
372+ helpers::platform_line(indoc! {"\
373+ top:
374+ #[\n|]#
375+ "}),
376+ ),
377+ (
378+ helpers::platform_line(indoc! {"\
379+ top#[:|]#
380+ bottom: withvalue
381+ "}),
382+ "o",
383+ helpers::platform_line(indoc! {"\
384+ top:
385+ #[\n|]#
386+ bottom: withvalue
387+ "}),
388+ ),
389+ (
390+ helpers::platform_line(indoc! {"\
391+ bottom: withvalue
392+ top#[:|]#
393+ "}),
394+ "o",
395+ helpers::platform_line(indoc! {"\
396+ bottom: withvalue
397+ top:
398+ #[\n|]#
399+ "}),
400+ ),
401+ ];
402+
403+ for test in below_tests {
404+ test_with_config(app(), test).await?;
405+ }
406+
407+ let above_tests = [
408+ (
409+ helpers::platform_line(indoc! {r##"
410+ #[t|]#op:
411+ baz: foo
412+ bazi:
413+ more: yes
414+ why: because
415+ quux:
416+ - 1
417+ - 2
418+ bax: foox
419+ fook:
420+ "##}),
421+ "O",
422+ helpers::platform_line(indoc! {"\
423+ #[\n|]#
424+ top:
425+ baz: foo
426+ bazi:
427+ more: yes
428+ why: because
429+ quux:
430+ - 1
431+ - 2
432+ bax: foox
433+ fook:
434+ "}),
435+ ),
436+ (
437+ helpers::platform_line(indoc! {r##"
438+ top:
439+ b#[a|]#z: foo
440+ bazi:
441+ more: yes
442+ why: because
443+ quux:
444+ - 1
445+ - 2
446+ bax: foox
447+ fook:
448+ "##}),
449+ "O",
450+ helpers::platform_line(indoc! {"\
451+ top:
452+ #[\n|]#
453+ baz: foo
454+ bazi:
455+ more: yes
456+ why: because
457+ quux:
458+ - 1
459+ - 2
460+ bax: foox
461+ fook:
462+ "}),
463+ ),
464+ (
465+ helpers::platform_line(indoc! {r##"
466+ top:
467+ baz: foo
468+ bazi#[:|]#
469+ more: yes
470+ why: because
471+ quux:
472+ - 1
473+ - 2
474+ bax: foox
475+ fook:
476+ "##}),
477+ "O",
478+ helpers::platform_line(indoc! {"\
479+ top:
480+ baz: foo
481+ #[\n|]#
482+ bazi:
483+ more: yes
484+ why: because
485+ quux:
486+ - 1
487+ - 2
488+ bax: foox
489+ fook:
490+ "}),
491+ ),
492+ (
493+ helpers::platform_line(indoc! {r##"
494+ top:
495+ baz: foo
496+ bazi:
497+ more: #[yes|]#
498+ why: because
499+ quux:
500+ - 1
501+ - 2
502+ bax: foox
503+ fook:
504+ "##}),
505+ "O",
506+ helpers::platform_line(indoc! {"\
507+ top:
508+ baz: foo
509+ bazi:
510+ #[\n|]#
511+ more: yes
512+ why: because
513+ quux:
514+ - 1
515+ - 2
516+ bax: foox
517+ fook:
518+ "}),
519+ ),
520+ (
521+ helpers::platform_line(indoc! {r##"
522+ top:
523+ baz: foo
524+ bazi:
525+ more: yes
526+ why: becaus#[e|]#
527+ quux:
528+ - 1
529+ - 2
530+ bax: foox
531+ fook:
532+ "##}),
533+ "O",
534+ helpers::platform_line(indoc! {"\
535+ top:
536+ baz: foo
537+ bazi:
538+ more: yes
539+ #[\n|]#
540+ why: because
541+ quux:
542+ - 1
543+ - 2
544+ bax: foox
545+ fook:
546+ "}),
547+ ),
548+ (
549+ helpers::platform_line(indoc! {"\
550+ top:
551+ baz: foo
552+ bazi:
553+ more: yes
554+ why: because
555+ quux:#[\n|]#
556+ - 1
557+ - 2
558+ bax: foox
559+ fook:
560+ "}),
561+ "O",
562+ helpers::platform_line(indoc! {"\
563+ top:
564+ baz: foo
565+ bazi:
566+ more: yes
567+ why: because
568+ #[\n|]#
569+ quux:
570+ - 1
571+ - 2
572+ bax: foox
573+ fook:
574+ "}),
575+ ),
576+ (
577+ helpers::platform_line(indoc! {"\
578+ top:
579+ baz: foo
580+ bazi:
581+ more: yes
582+ why: because
583+ quux:
584+ - 1#[\n|]#
585+ - 2
586+ bax: foox
587+ fook:
588+ "}),
589+ "O",
590+ helpers::platform_line(indoc! {"\
591+ top:
592+ baz: foo
593+ bazi:
594+ more: yes
595+ why: because
596+ quux:
597+ #[\n|]#
598+ - 1
599+ - 2
600+ bax: foox
601+ fook:
602+ "}),
603+ ),
604+ (
605+ helpers::platform_line(indoc! {"\
606+ top:
607+ baz: foo
608+ bazi:
609+ more: yes
610+ why: because
611+ quux:
612+ - 1
613+ - 2
614+ bax: foox
615+ fook:#[\n|]#
616+ "}),
617+ "O",
618+ helpers::platform_line(indoc! {"\
619+ top:
620+ baz: foo
621+ bazi:
622+ more: yes
623+ why: because
624+ quux:
625+ - 1
626+ - 2
627+ bax: foox
628+ #[\n|]#
629+ fook:
630+ "}),
631+ ),
632+ (
633+ helpers::platform_line(indoc! {"\
634+ top:
635+ baz: foo
636+ bax: |
637+ some
638+ multi
639+ line
640+ string#[\n|]#
641+ fook:
642+ "}),
643+ "O",
644+ helpers::platform_line(indoc! {"\
645+ top:
646+ baz: foo
647+ bax: |
648+ some
649+ multi
650+ line
651+ #[\n|]#
652+ string
653+ fook:
654+ "}),
655+ ),
656+ (
657+ helpers::platform_line(indoc! {"\
658+ top:
659+ baz: foo
660+ bax: >
661+ some#[\n|]#
662+ multi
663+ line
664+ string
665+ fook:
666+ "}),
667+ "O",
668+ helpers::platform_line(indoc! {"\
669+ top:
670+ baz: foo
671+ bax: >
672+ #[\n|]#
673+ some
674+ multi
675+ line
676+ string
677+ fook:
678+ "}),
679+ ),
680+ (
681+ helpers::platform_line(indoc! {"\
682+ top:
683+ baz: foo
684+ bax: >
685+ fook:#[\n|]#
686+ "}),
687+ "O",
688+ helpers::platform_line(indoc! {"\
689+ top:
690+ baz: foo
691+ bax: >
692+ #[\n|]#
693+ fook:
694+ "}),
695+ ),
696+ (
697+ helpers::platform_line(indoc! {"\
698+ - top:
699+ baz: foo#[\n|]#
700+ bax: foox
701+ fook:
702+ "}),
703+ "O",
704+ helpers::platform_line(indoc! {"\
705+ - top:
706+ #[\n|]#
707+ baz: foo
708+ bax: foox
709+ fook:
710+ "}),
711+ ),
712+ (
713+ helpers::platform_line(indoc! {"\
714+ - top:
715+ baz: foo
716+ bax: foox
717+ fook:#[\n|]#
718+ "}),
719+ "O",
720+ helpers::platform_line(indoc! {"\
721+ - top:
722+ baz: foo
723+ bax: foox
724+ #[\n|]#
725+ fook:
726+ "}),
727+ ),
728+ (
729+ helpers::platform_line(indoc! {"\
730+ top:
731+ baz:
732+ - one: two#[\n|]#
733+ three: four
734+ - top:
735+ baz: foo
736+ bax: foox
737+ "}),
738+ "O",
739+ helpers::platform_line(indoc! {"\
740+ top:
741+ baz:
742+ #[\n|]#
743+ - one: two
744+ three: four
745+ - top:
746+ baz: foo
747+ bax: foox
748+ "}),
749+ ),
750+ // yaml map without a key
751+ (
752+ helpers::platform_line(indoc! {"\
753+ top:#[\n|]#
754+ "}),
755+ "O",
756+ helpers::platform_line(indoc! {"\
757+ #[\n|]#
758+ top:
759+ "}),
760+ ),
761+ (
762+ helpers::platform_line(indoc! {"\
763+ bottom: withvalue
764+ top#[:|]#
765+ "}),
766+ "O",
767+ helpers::platform_line(indoc! {"\
768+ bottom: withvalue
769+ #[\n|]#
770+ top:
771+ "}),
772+ ),
773+ (
774+ helpers::platform_line(indoc! {"\
775+ top:
776+ bottom:#[ |]#withvalue
777+ "}),
778+ "O",
779+ helpers::platform_line(indoc! {"\
780+ top:
781+ #[\n|]#
782+ bottom: withvalue
783+ "}),
784+ ),
785+ ];
786+
787+ for test in above_tests {
788+ test_with_config(app(), test).await?;
789+ }
790+
791+ let enter_tests = [
792+ (
793+ helpers::platform_line(indoc! {r##"
794+ foo: #[b|]#ar
795+ "##}),
796+ "i<ret>",
797+ helpers::platform_line(indoc! {"\
798+ foo:
799+ #[|b]#ar
800+ "}),
801+ ),
802+ (
803+ helpers::platform_line(indoc! {"\
804+ foo:#[\n|]#
805+ "}),
806+ "i<ret>",
807+ helpers::platform_line(indoc! {"\
808+ foo:
809+ #[|\n]#
810+ "}),
811+ ),
812+ ];
813+
814+ for test in enter_tests {
815+ test_with_config(app(), test).await?;
816+ }
817+
818+ Ok(())
819+}
helix-term/tests/test/movement.rs+39
@@ -513,3 +513,42 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
513
513
514 Ok(())
514 Ok(())
515}
515}
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<()> {
62 )
62 )
63 .await?;
63 .await?;
64
64
65− helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
65+ helpers::assert_file_has_content(file1.as_file_mut(), &platform_line("hello1"))?;
66− helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
66+ helpers::assert_file_has_content(file2.as_file_mut(), &platform_line("hello2"))?;
67− helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
67+ helpers::assert_file_has_content(file3.as_file_mut(), &platform_line("hello3"))?;
68
68
69 Ok(())
69 Ok(())
70}
70}
helix-tui/Cargo.toml+15−15
@@ -1,28 +1,28 @@
1[package]
1[package]
2name = "helix-tui"
2name = "helix-tui"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−description = """
6−A library to build rich terminal user interfaces or dashboards
3+description = """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"
13include = ["src/**/*", "README.md"]
4include = ["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
14
13
15[features]
14[features]
16default = ["crossterm"]
15default = ["crossterm"]
17
16
18[dependencies]
17[dependencies]
18+helix-view = { path = "../helix-view", features = ["term"] }
19+helix-core = { path = "../helix-core" }
20+
19−bitflags = "2.3"
21+bitflags = "2.4"
20cassowary = "0.3"
22cassowary = "0.3"
21unicode-segmentation = "1.10"
23unicode-segmentation = "1.10"
22−crossterm = { version = "0.26", optional = true }
24+crossterm = { version = "0.27", optional = true }
23termini = "1.0"
25termini = "1.0"
24serde = { version = "1", "optional" = true, features = ["derive"]}
26serde = { version = "1", "optional" = true, features = ["derive"]}
25−once_cell = "1.18"
27+once_cell = "1.19"
26log = "~0.4"
28log = "~0.4"
27−helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
28−helix-core = { version = "0.6", path = "../helix-core" }
helix-tui/src/backend/crossterm.rs+51−33
@@ -32,10 +32,21 @@ fn vte_version() -> Option<usize> {
32}
32}
33
33
34/// Describes terminal capabilities like extended underline, truecolor, etc.
34/// Describes terminal capabilities like extended underline, truecolor, etc.
35−#[derive(Copy, Clone, Debug, Default)]
35+#[derive(Clone, Debug)]
36struct Capabilities {
36struct Capabilities {
37 /// Support for undercurled, underdashed, etc.
37 /// Support for undercurled, underdashed, etc.
38 has_extended_underlines: bool,
38 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+ }
39}
50}
40
51
41impl Capabilities {
52impl Capabilities {
@@ -54,6 +65,10 @@ impl Capabilities {
54 || t.extended_cap("Su").is_some()
65 || t.extended_cap("Su").is_some()
55 || vte_version() >= Some(5102)
66 || vte_version() >= Some(5102)
56 || matches!(term_program().as_deref(), Some("WezTerm")),
67 || 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(),
57 },
72 },
58 }
73 }
59 }
74 }
@@ -154,7 +169,8 @@ where
154
169
155 fn restore(&mut self, config: Config) -> io::Result<()> {
170 fn restore(&mut self, config: Config) -> io::Result<()> {
156 // reset cursor shape
171 // reset cursor shape
157− write!(self.buffer, "\x1B[0 q")?;
172+ self.buffer
173+ .write_all(self.capabilities.reset_cursor_command.as_bytes())?;
158 if config.enable_mouse_capture {
174 if config.enable_mouse_capture {
159 execute!(self.buffer, DisableMouseCapture)?;
175 execute!(self.buffer, DisableMouseCapture)?;
160 }
176 }
@@ -201,7 +217,7 @@ where
201 for (x, y, cell) in content {
217 for (x, y, cell) in content {
202 // Move the cursor if the previous location was not (x - 1, y)
218 // Move the cursor if the previous location was not (x - 1, y)
203 if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
219 if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
204− map_error(queue!(self.buffer, MoveTo(x, y)))?;
220+ queue!(self.buffer, MoveTo(x, y))?;
205 }
221 }
206 last_pos = Some((x, y));
222 last_pos = Some((x, y));
207 if cell.modifier != modifier {
223 if cell.modifier != modifier {
@@ -214,12 +230,12 @@ where
214 }
230 }
215 if cell.fg != fg {
231 if cell.fg != fg {
216 let color = CColor::from(cell.fg);
232 let color = CColor::from(cell.fg);
217− map_error(queue!(self.buffer, SetForegroundColor(color)))?;
233+ queue!(self.buffer, SetForegroundColor(color))?;
218 fg = cell.fg;
234 fg = cell.fg;
219 }
235 }
220 if cell.bg != bg {
236 if cell.bg != bg {
221 let color = CColor::from(cell.bg);
237 let color = CColor::from(cell.bg);
222− map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
238+ queue!(self.buffer, SetBackgroundColor(color))?;
223 bg = cell.bg;
239 bg = cell.bg;
224 }
240 }
225
241
@@ -227,7 +243,7 @@ where
227 if self.capabilities.has_extended_underlines {
243 if self.capabilities.has_extended_underlines {
228 if cell.underline_color != underline_color {
244 if cell.underline_color != underline_color {
229 let color = CColor::from(cell.underline_color);
245 let color = CColor::from(cell.underline_color);
230− map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
246+ queue!(self.buffer, SetUnderlineColor(color))?;
231 underline_color = cell.underline_color;
247 underline_color = cell.underline_color;
232 }
248 }
233 } else {
249 } else {
@@ -239,24 +255,24 @@ where
239
255
240 if new_underline_style != underline_style {
256 if new_underline_style != underline_style {
241 let attr = CAttribute::from(new_underline_style);
257 let attr = CAttribute::from(new_underline_style);
242− map_error(queue!(self.buffer, SetAttribute(attr)))?;
258+ queue!(self.buffer, SetAttribute(attr))?;
243 underline_style = new_underline_style;
259 underline_style = new_underline_style;
244 }
260 }
245
261
246− map_error(queue!(self.buffer, Print(&cell.symbol)))?;
262+ queue!(self.buffer, Print(&cell.symbol))?;
247 }
263 }
248
264
249− map_error(queue!(
265+ queue!(
250 self.buffer,
266 self.buffer,
251 SetUnderlineColor(CColor::Reset),
267 SetUnderlineColor(CColor::Reset),
252 SetForegroundColor(CColor::Reset),
268 SetForegroundColor(CColor::Reset),
253 SetBackgroundColor(CColor::Reset),
269 SetBackgroundColor(CColor::Reset),
254 SetAttribute(CAttribute::Reset)
270 SetAttribute(CAttribute::Reset)
255− ))
271+ )
256 }
272 }
257
273
258 fn hide_cursor(&mut self) -> io::Result<()> {
274 fn hide_cursor(&mut self) -> io::Result<()> {
259− map_error(execute!(self.buffer, Hide))
275+ execute!(self.buffer, Hide)
260 }
276 }
261
277
262 fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
278 fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
@@ -266,7 +282,7 @@ where
266 CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
282 CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
267 CursorKind::Hidden => unreachable!(),
283 CursorKind::Hidden => unreachable!(),
268 };
284 };
269− map_error(execute!(self.buffer, Show, shape))
285+ execute!(self.buffer, Show, shape)
270 }
286 }
271
287
272 fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
288 fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -275,11 +291,11 @@ where
275 }
291 }
276
292
277 fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
293 fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
278− map_error(execute!(self.buffer, MoveTo(x, y)))
294+ execute!(self.buffer, MoveTo(x, y))
279 }
295 }
280
296
281 fn clear(&mut self) -> io::Result<()> {
297 fn clear(&mut self) -> io::Result<()> {
282− map_error(execute!(self.buffer, Clear(ClearType::All)))
298+ execute!(self.buffer, Clear(ClearType::All))
283 }
299 }
284
300
285 fn size(&self) -> io::Result<Rect> {
301 fn size(&self) -> io::Result<Rect> {
@@ -294,10 +310,6 @@ where
294 }
310 }
295}
311}
296
312
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−
301#[derive(Debug)]
313#[derive(Debug)]
302struct ModifierDiff {
314struct ModifierDiff {
303 pub from: Modifier,
315 pub from: Modifier,
@@ -312,48 +324,54 @@ impl ModifierDiff {
312 //use crossterm::Attribute;
324 //use crossterm::Attribute;
313 let removed = self.from - self.to;
325 let removed = self.from - self.to;
314 if removed.contains(Modifier::REVERSED) {
326 if removed.contains(Modifier::REVERSED) {
315− map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
327+ queue!(w, SetAttribute(CAttribute::NoReverse))?;
316 }
328 }
317 if removed.contains(Modifier::BOLD) {
329 if removed.contains(Modifier::BOLD) {
318− map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
330+ queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
319 if self.to.contains(Modifier::DIM) {
331 if self.to.contains(Modifier::DIM) {
320− map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
332+ queue!(w, SetAttribute(CAttribute::Dim))?;
321 }
333 }
322 }
334 }
323 if removed.contains(Modifier::ITALIC) {
335 if removed.contains(Modifier::ITALIC) {
324− map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
336+ queue!(w, SetAttribute(CAttribute::NoItalic))?;
325 }
337 }
326 if removed.contains(Modifier::DIM) {
338 if removed.contains(Modifier::DIM) {
327− map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
339+ queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
328 }
340 }
329 if removed.contains(Modifier::CROSSED_OUT) {
341 if removed.contains(Modifier::CROSSED_OUT) {
330− map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
342+ queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
331 }
343 }
332 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
344 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
333− map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
345+ queue!(w, SetAttribute(CAttribute::NoBlink))?;
346+ }
347+ if removed.contains(Modifier::HIDDEN) {
348+ queue!(w, SetAttribute(CAttribute::NoHidden))?;
334 }
349 }
335
350
336 let added = self.to - self.from;
351 let added = self.to - self.from;
337 if added.contains(Modifier::REVERSED) {
352 if added.contains(Modifier::REVERSED) {
338− map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
353+ queue!(w, SetAttribute(CAttribute::Reverse))?;
339 }
354 }
340 if added.contains(Modifier::BOLD) {
355 if added.contains(Modifier::BOLD) {
341− map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
356+ queue!(w, SetAttribute(CAttribute::Bold))?;
342 }
357 }
343 if added.contains(Modifier::ITALIC) {
358 if added.contains(Modifier::ITALIC) {
344− map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
359+ queue!(w, SetAttribute(CAttribute::Italic))?;
345 }
360 }
346 if added.contains(Modifier::DIM) {
361 if added.contains(Modifier::DIM) {
347− map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
362+ queue!(w, SetAttribute(CAttribute::Dim))?;
348 }
363 }
349 if added.contains(Modifier::CROSSED_OUT) {
364 if added.contains(Modifier::CROSSED_OUT) {
350− map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
365+ queue!(w, SetAttribute(CAttribute::CrossedOut))?;
351 }
366 }
352 if added.contains(Modifier::SLOW_BLINK) {
367 if added.contains(Modifier::SLOW_BLINK) {
353− map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
368+ queue!(w, SetAttribute(CAttribute::SlowBlink))?;
354 }
369 }
355 if added.contains(Modifier::RAPID_BLINK) {
370 if added.contains(Modifier::RAPID_BLINK) {
356− map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
371+ queue!(w, SetAttribute(CAttribute::RapidBlink))?;
372+ }
373+ if added.contains(Modifier::HIDDEN) {
374+ queue!(w, SetAttribute(CAttribute::Hidden))?;
357 }
375 }
358
376
359 Ok(())
377 Ok(())
@@ -407,7 +425,7 @@ impl Command for SetUnderlineColor {
407 }
425 }
408
426
409 #[cfg(windows)]
427 #[cfg(windows)]
410− fn execute_winapi(&self) -> crossterm::Result<()> {
428+ fn execute_winapi(&self) -> io::Result<()> {
411 Err(std::io::Error::new(
429 Err(std::io::Error::new(
412 std::io::ErrorKind::Other,
430 std::io::ErrorKind::Other,
413 "SetUnderlineColor not supported by winapi.",
431 "SetUnderlineColor not supported by winapi.",
helix-tui/src/buffer.rs+2−2
@@ -213,7 +213,7 @@ impl Buffer {
213 && y < self.area.bottom()
213 && y < self.area.bottom()
214 }
214 }
215
215
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.
217 ///
217 ///
218 /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
218 /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
219 ///
219 ///
@@ -242,7 +242,7 @@ impl Buffer {
242 ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize)
242 ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize)
243 }
243 }
244
244
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,
246 /// or `None` if the coordinates are outside the buffer's area.
246 /// or `None` if the coordinates are outside the buffer's area.
247 fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
247 fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
248 if self.in_bounds(x, y) {
248 if self.in_bounds(x, y) {
helix-tui/src/terminal.rs−14
@@ -65,20 +65,6 @@ where
65 viewport: Viewport,
65 viewport: Viewport,
66}
66}
67
67
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−
82impl<B> Terminal<B>
68impl<B> Terminal<B>
83where
69where
84 B: Backend,
70 B: Backend,
helix-vcs/Cargo.toml+12−10
@@ -1,23 +1,25 @@
1[package]
1[package]
2name = "helix-vcs"
2name = "helix-vcs"
3−version = "0.6.0"
3+version.workspace = true
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4+authors.workspace = true
5−edition = "2021"
5+edition.workspace = true
6−license = "MPL-2.0"
6+license.workspace = true
7+rust-version.workspace = true
7−categories = ["editor"]
8+categories.workspace = true
8−repository = "https://github.com/helix-editor/helix"
9+repository.workspace = true
9−homepage = "https://helix-editor.com"
10+homepage.workspace = true
10
11
11# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12
13
13[dependencies]
14[dependencies]
14−helix-core = { version = "0.6", path = "../helix-core" }
15+helix-core = { path = "../helix-core" }
16+helix-event = { path = "../helix-event" }
15
17
16tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
18tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
17parking_lot = "0.12"
19parking_lot = "0.12"
18arc-swap = { version = "1.6.0" }
20arc-swap = { version = "1.6.0" }
19
21
20−gix = { version = "0.48.0", default-features = false , optional = true }
22+gix = { version = "0.57.0", default-features = false , optional = true }
21imara-diff = "0.1.5"
23imara-diff = "0.1.5"
22anyhow = "1"
24anyhow = "1"
23
25
@@ -27,4 +29,4 @@ log = "0.4"
27git = ["gix"]
29git = ["gix"]
28
30
29[dev-dependencies]
31[dev-dependencies]
30−tempfile = "3.7"
32+tempfile = "3.8"
helix-vcs/src/diff.rs+6−19
@@ -2,10 +2,10 @@ use std::ops::Range;
2use std::sync::Arc;
2use std::sync::Arc;
3
3
4use helix_core::Rope;
4use helix_core::Rope;
5+use helix_event::RenderLockGuard;
5use imara_diff::Algorithm;
6use imara_diff::Algorithm;
6use parking_lot::{Mutex, MutexGuard};
7use parking_lot::{Mutex, MutexGuard};
7use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8−use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
9use tokio::task::JoinHandle;
9use tokio::task::JoinHandle;
10use tokio::time::Instant;
10use tokio::time::Instant;
11
11
@@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker;
14mod line_cache;
14mod line_cache;
15mod worker;
15mod worker;
16
16
17−type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18−
19/// A rendering lock passed to the differ the prevents redraws from occurring
17/// A rendering lock passed to the differ the prevents redraws from occurring
20struct RenderLock {
18struct RenderLock {
21− pub lock: OwnedRwLockReadGuard<()>,
19+ pub lock: RenderLockGuard,
22 pub timeout: Option<Instant>,
20 pub timeout: Option<Instant>,
23}
21}
24
22
@@ -38,28 +36,22 @@ struct DiffInner {
38#[derive(Clone, Debug)]
36#[derive(Clone, Debug)]
39pub struct DiffHandle {
37pub struct DiffHandle {
40 channel: UnboundedSender<Event>,
38 channel: UnboundedSender<Event>,
41− render_lock: Arc<RwLock<()>>,
42 diff: Arc<Mutex<DiffInner>>,
39 diff: Arc<Mutex<DiffInner>>,
43 inverted: bool,
40 inverted: bool,
44}
41}
45
42
46impl DiffHandle {
43impl DiffHandle {
47− pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
44+ pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
48− DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
45+ DiffHandle::new_with_handle(diff_base, doc).0
49 }
46 }
50
47
51− fn new_with_handle(
52− diff_base: Rope,
53− doc: Rope,
54− redraw_handle: RedrawHandle,
55− ) -> (DiffHandle, JoinHandle<()>) {
48+ fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) {
56 let (sender, receiver) = unbounded_channel();
49 let (sender, receiver) = unbounded_channel();
57 let diff: Arc<Mutex<DiffInner>> = Arc::default();
50 let diff: Arc<Mutex<DiffInner>> = Arc::default();
58 let worker = DiffWorker {
51 let worker = DiffWorker {
59 channel: receiver,
52 channel: receiver,
60 diff: diff.clone(),
53 diff: diff.clone(),
61 new_hunks: Vec::default(),
54 new_hunks: Vec::default(),
62− redraw_notify: redraw_handle.0,
63 diff_finished_notify: Arc::default(),
55 diff_finished_notify: Arc::default(),
64 };
56 };
65 let handle = tokio::spawn(worker.run(diff_base, doc));
57 let handle = tokio::spawn(worker.run(diff_base, doc));
@@ -67,7 +59,6 @@ impl DiffHandle {
67 channel: sender,
59 channel: sender,
68 diff,
60 diff,
69 inverted: false,
61 inverted: false,
70− render_lock: redraw_handle.1,
71 };
62 };
72 (differ, handle)
63 (differ, handle)
73 }
64 }
@@ -87,11 +78,7 @@ impl DiffHandle {
87 /// This function is only intended to be called from within the rendering loop
78 /// This function is only intended to be called from within the rendering loop
88 /// if called from elsewhere it may fail to acquire the render lock and panic
79 /// if called from elsewhere it may fail to acquire the render lock and panic
89 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
80 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
90− // unwrap is ok here because the rendering lock is
81+ let lock = helix_event::lock_frame();
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();
95 let timeout = if block {
82 let timeout = if block {
96 None
83 None
97 } else {
84 } else {
helix-vcs/src/diff/worker.rs+3−9
@@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
23 pub channel: UnboundedReceiver<Event>,
23 pub channel: UnboundedReceiver<Event>,
24 pub diff: Arc<Mutex<DiffInner>>,
24 pub diff: Arc<Mutex<DiffInner>>,
25 pub new_hunks: Vec<Hunk>,
25 pub new_hunks: Vec<Hunk>,
26− pub redraw_notify: Arc<Notify>,
27 pub diff_finished_notify: Arc<Notify>,
26 pub diff_finished_notify: Arc<Notify>,
28}
27}
29
28
@@ -32,11 +31,7 @@ impl DiffWorker {
32 let mut accumulator = EventAccumulator::new();
31 let mut accumulator = EventAccumulator::new();
33 accumulator.handle_event(event).await;
32 accumulator.handle_event(event).await;
34 accumulator
33 accumulator
35− .accumulate_debounced_events(
36− &mut self.channel,
37− self.redraw_notify.clone(),
38− self.diff_finished_notify.clone(),
34+ .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
39− )
40 .await;
35 .await;
41 (accumulator.doc, accumulator.diff_base)
36 (accumulator.doc, accumulator.diff_base)
42 }
37 }
@@ -137,7 +132,6 @@ impl<'a> EventAccumulator {
137 async fn accumulate_debounced_events(
132 async fn accumulate_debounced_events(
138 &mut self,
133 &mut self,
139 channel: &mut UnboundedReceiver<Event>,
134 channel: &mut UnboundedReceiver<Event>,
140− redraw_notify: Arc<Notify>,
141 diff_finished_notify: Arc<Notify>,
135 diff_finished_notify: Arc<Notify>,
142 ) {
136 ) {
143 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
137 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@@ -164,7 +158,7 @@ impl<'a> EventAccumulator {
164 None => {
158 None => {
165 tokio::spawn(async move {
159 tokio::spawn(async move {
166 diff_finished_notify.notified().await;
160 diff_finished_notify.notified().await;
167− redraw_notify.notify_one();
161+ helix_event::request_redraw();
168 });
162 });
169 }
163 }
170 // diff is performed inside the rendering loop
164 // diff is performed inside the rendering loop
@@ -190,7 +184,7 @@ impl<'a> EventAccumulator {
190 // and wait until the diff occurs to trigger an async redraw
184 // and wait until the diff occurs to trigger an async redraw
191 log::info!("Diff computation timed out, update of diffs might appear delayed");
185 log::info!("Diff computation timed out, update of diffs might appear delayed");
192 diff_finished_notify.notified().await;
186 diff_finished_notify.notified().await;
193− redraw_notify.notify_one();
187+ helix_event::request_redraw()
194 });
188 });
195 }
189 }
196 // a blocking diff is performed inside the rendering loop
190 // a blocking diff is performed inside the rendering loop
helix-vcs/src/diff/worker/test.rs+1−5
@@ -5,11 +5,7 @@ use crate::diff::{DiffHandle, Hunk};
5
5
6impl DiffHandle {
6impl DiffHandle {
7 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
7 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8− DiffHandle::new_with_handle(
8+ DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
9− Rope::from_str(diff_base),
10− Rope::from_str(doc),
11− Default::default(),
12− )
13 }
9 }
14 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
10 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
15 let diff = self.diff;
11 let diff = self.diff;
helix-vcs/src/git.rs+5−5
@@ -3,7 +3,7 @@ use arc_swap::ArcSwap;
3use std::path::Path;
3use std::path::Path;
4use std::sync::Arc;
4use std::sync::Arc;
5
5
6−use gix::objs::tree::EntryMode;
6+use gix::objs::tree::EntryKind;
7use gix::sec::trust::DefaultForLevel;
7use gix::sec::trust::DefaultForLevel;
8use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
8use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
9
9
@@ -126,14 +126,14 @@ fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Resul
126 let rel_path = file.strip_prefix(repo_dir)?;
126 let rel_path = file.strip_prefix(repo_dir)?;
127 let tree = commit.tree()?;
127 let tree = commit.tree()?;
128 let tree_entry = tree
128 let tree_entry = tree
129− .lookup_entry_by_path(rel_path)?
129+ .lookup_entry_by_path(rel_path, &mut Vec::new())?
130 .context("file is untracked")?;
130 .context("file is untracked")?;
131− match tree_entry.mode() {
131+ match tree_entry.mode().kind() {
132 // not a file, everything is new, do not show diff
132 // not a file, everything is new, do not show diff
133− mode @ (EntryMode::Tree | EntryMode::Commit | EntryMode::Link) => {
133+ mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
134 bail!("entry at {} is not a file but a {mode:?}", file.display())
134 bail!("entry at {} is not a file but a {mode:?}", file.display())
135 }
135 }
136 // found a file
136 // found a file
137− EntryMode::Blob | EntryMode::BlobExecutable => Ok(tree_entry.object_id()),
137+ EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
138 }
138 }
139}
139}
helix-view/Cargo.toml+22−18
@@ -1,31 +1,34 @@
1[package]
1[package]
2name = "helix-view"
2name = "helix-view"
3−version = "0.6.0"
4−authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2021"
6−license = "MPL-2.0"
7description = "UI abstractions for use in backends"
3description = "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
8−categories = ["editor"]
9+categories.workspace = true
9−repository = "https://github.com/helix-editor/helix"
10+repository.workspace = true
10−homepage = "https://helix-editor.com"
11+homepage.workspace = true
11
12
12[features]
13[features]
13default = []
14default = []
14term = ["crossterm"]
15term = ["crossterm"]
15
16
16[dependencies]
17[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+
17−bitflags = "2.3"
25+bitflags = "2.4"
18anyhow = "1"
26anyhow = "1"
19−helix-core = { version = "0.6", path = "../helix-core" }
20−helix-loader = { version = "0.6", path = "../helix-loader" }
21−helix-lsp = { version = "0.6", path = "../helix-lsp" }
22−helix-dap = { version = "0.6", path = "../helix-dap" }
23−crossterm = { version = "0.26", optional = true }
27+crossterm = { version = "0.27", optional = true }
24−helix-vcs = { version = "0.6", path = "../helix-vcs" }
25
28
26# Conversion traits
29# Conversion traits
27−once_cell = "1.18"
30+once_cell = "1.19"
28−url = "2"
31+url = "2.5.0"
29
32
30arc-swap = { version = "1.6.0" }
33arc-swap = { version = "1.6.0" }
31
34
@@ -42,15 +45,16 @@ serde_json = "1.0"
42toml = "0.7"
45toml = "0.7"
43log = "~0.4"
46log = "~0.4"
44
47
45−which = "4.4"
48+which = "5.0.0"
46parking_lot = "0.12.1"
49parking_lot = "0.12.1"
47
50
48
51
49[target.'cfg(windows)'.dependencies]
52[target.'cfg(windows)'.dependencies]
50−clipboard-win = { version = "4.5", features = ["std"] }
53+clipboard-win = { version = "5.0", features = ["std"] }
51
54
52[target.'cfg(unix)'.dependencies]
55[target.'cfg(unix)'.dependencies]
53libc = "0.2"
56libc = "0.2"
57+rustix = { version = "0.38", features = ["fs"] }
54
58
55[dev-dependencies]
59[dev-dependencies]
56helix-tui = { path = "../helix-tui" }
60helix-tui = { path = "../helix-tui" }
helix-view/src/clipboard.rs+7−2
@@ -73,9 +73,14 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
73
73
74#[cfg(target_os = "macos")]
74#[cfg(target_os = "macos")]
75pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
75pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
76− use crate::env::binary_exists;
76+ use crate::env::{binary_exists, env_var_is_set};
77
77
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+ }
78− if binary_exists("pbcopy") && binary_exists("pbpaste") {
83+ } else if binary_exists("pbcopy") && binary_exists("pbpaste") {
79 command_provider! {
84 command_provider! {
80 paste => "pbpaste";
85 paste => "pbpaste";
81 copy => "pbcopy";
86 copy => "pbcopy";
helix-view/src/document.rs+111−39
@@ -33,7 +33,7 @@ use helix_core::{
33 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
33 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
34};
34};
35
35
36−use crate::editor::{Config, RedrawHandle};
36+use crate::editor::Config;
37use crate::{DocumentId, Editor, Theme, View, ViewId};
37use crate::{DocumentId, Editor, Theme, View, ViewId};
38
38
39/// 8kB of buffer space for encoding and decoding `Rope`s.
39/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -185,6 +185,8 @@ pub struct Document {
185
185
186 // when document was used for most-recent-used buffer picker
186 // when document was used for most-recent-used buffer picker
187 pub focused_at: std::time::Instant,
187 pub focused_at: std::time::Instant,
188+
189+ pub readonly: bool,
188}
190}
189
191
190/// Inlay hints for a single `(Document, View)` combo.
192/// Inlay hints for a single `(Document, View)` combo.
@@ -673,6 +675,7 @@ impl Document {
673 config,
675 config,
674 version_control_head: None,
676 version_control_head: None,
675 focused_at: std::time::Instant::now(),
677 focused_at: std::time::Instant::now(),
678+ readonly: false,
676 }
679 }
677 }
680 }
678
681
@@ -705,7 +708,7 @@ impl Document {
705 let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
708 let mut doc = Self::from(rope, Some((encoding, has_bom)), config);
706
709
707 // set the path and try detecting the language
710 // set the path and try detecting the language
708− doc.set_path(Some(path))?;
711+ doc.set_path(Some(path));
709 if let Some(loader) = config_loader {
712 if let Some(loader) = config_loader {
710 doc.detect_language(loader);
713 doc.detect_language(loader);
711 }
714 }
@@ -730,16 +733,16 @@ impl Document {
730 // We can't use anyhow::Result here since the output of the future has to be
733 // We can't use anyhow::Result here since the output of the future has to be
731 // clonable to be used as shared future. So use a custom error type.
734 // clonable to be used as shared future. So use a custom error type.
732 pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
735 pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
733− if let Some(formatter) = self
736+ if let Some((fmt_cmd, fmt_args)) = self
734 .language_config()
737 .language_config()
735− .and_then(|c| c.formatter.clone())
738+ .and_then(|c| c.formatter.as_ref())
736− .filter(|formatter| which::which(&formatter.command).is_ok())
739+ .and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args)))
737 {
740 {
738 use std::process::Stdio;
741 use std::process::Stdio;
739 let text = self.text().clone();
742 let text = self.text().clone();
740− let mut process = tokio::process::Command::new(&formatter.command);
743+ let mut process = tokio::process::Command::new(&fmt_cmd);
741 process
744 process
742− .args(&formatter.args)
745+ .args(fmt_args)
743 .stdin(Stdio::piped())
746 .stdin(Stdio::piped())
744 .stdout(Stdio::piped())
747 .stdout(Stdio::piped())
745 .stderr(Stdio::piped());
748 .stderr(Stdio::piped());
@@ -748,7 +751,7 @@ impl Document {
748 let mut process = process
751 let mut process = process
749 .spawn()
752 .spawn()
750 .map_err(|e| FormatterError::SpawningFailed {
753 .map_err(|e| FormatterError::SpawningFailed {
751− command: formatter.command.clone(),
754+ command: fmt_cmd.to_string_lossy().into(),
752 error: e.kind(),
755 error: e.kind(),
753 })?;
756 })?;
754 {
757 {
@@ -850,7 +853,7 @@ impl Document {
850 let text = self.text().clone();
853 let text = self.text().clone();
851
854
852 let path = match path {
855 let path = match path {
853− Some(path) => helix_core::path::get_canonicalized_path(&path)?,
856+ Some(path) => helix_core::path::get_canonicalized_path(&path),
854 None => {
857 None => {
855 if self.path.is_none() {
858 if self.path.is_none() {
856 bail!("Can't save with no path set!");
859 bail!("Can't save with no path set!");
@@ -955,12 +958,43 @@ impl Document {
955 }
958 }
956 }
959 }
957
960
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+
958 /// Reload the document from its path.
993 /// Reload the document from its path.
959 pub fn reload(
994 pub fn reload(
960 &mut self,
995 &mut self,
961 view: &mut View,
996 view: &mut View,
962 provider_registry: &DiffProviderRegistry,
997 provider_registry: &DiffProviderRegistry,
963− redraw_handle: RedrawHandle,
964 ) -> Result<(), Error> {
998 ) -> Result<(), Error> {
965 let encoding = self.encoding;
999 let encoding = self.encoding;
966 let path = self
1000 let path = self
@@ -969,6 +1003,9 @@ impl Document {
969 .ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
1003 .ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
970 .to_owned();
1004 .to_owned();
971
1005
1006+ // Once we have a valid path we check if its readonly status has changed
1007+ self.detect_readonly();
1008+
972 let mut file = std::fs::File::open(&path)?;
1009 let mut file = std::fs::File::open(&path)?;
973 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
1010 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
974
1011
@@ -985,7 +1022,7 @@ impl Document {
985 self.detect_indent_and_line_ending();
1022 self.detect_indent_and_line_ending();
986
1023
987 match provider_registry.get_diff_base(&path) {
1024 match provider_registry.get_diff_base(&path) {
988− Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
1025+ Some(diff_base) => self.set_diff_base(diff_base),
989 None => self.diff_handle = None,
1026 None => self.diff_handle = None,
990 }
1027 }
991
1028
@@ -1009,16 +1046,14 @@ impl Document {
1009 self.encoding
1046 self.encoding
1010 }
1047 }
1011
1048
1012− pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
1049+ pub fn set_path(&mut self, path: Option<&Path>) {
1013− let path = path
1014− .map(helix_core::path::get_canonicalized_path)
1050+ let path = path.map(helix_core::path::get_canonicalized_path);
1015− .transpose()?;
1016
1051
1017 // if parent doesn't exist we still want to open the document
1052 // if parent doesn't exist we still want to open the document
1018 // and error out when document is saved
1053 // and error out when document is saved
1019 self.path = path;
1054 self.path = path;
1020
1055
1021− Ok(())
1056+ self.detect_readonly();
1022 }
1057 }
1023
1058
1024 /// Set the programming language for the file and load associated data (e.g. highlighting)
1059 /// Set the programming language for the file and load associated data (e.g. highlighting)
@@ -1171,30 +1206,47 @@ impl Document {
1171 transaction.changes(),
1206 transaction.changes(),
1172 );
1207 );
1173 if res.is_err() {
1208 if res.is_err() {
1174− log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}");
1209+ log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
1175 self.syntax = None;
1210 self.syntax = None;
1176 }
1211 }
1177 }
1212 }
1178
1213
1179 let changes = transaction.changes();
1214 let changes = transaction.changes();
1180
1215
1181− changes.update_positions(
1182− self.diagnostics
1183− .iter_mut()
1184− .map(|diagnostic| (&mut diagnostic.range.start, Assoc::After)),
1185− );
1186− changes.update_positions(
1187− self.diagnostics
1188− .iter_mut()
1189− .map(|diagnostic| (&mut diagnostic.range.end, Assoc::After)),
1190− );
1191− // map state.diagnostics over changes::map_pos too
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+ }));
1192− for diagnostic in &mut self.diagnostics {
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+ }
1193 diagnostic.line = self.text.char_to_line(diagnostic.range.start);
1239 diagnostic.line = self.text.char_to_line(diagnostic.range.start);
1240+ true
1194− }
1241+ });
1195
1242
1196− self.diagnostics
1197− .sort_unstable_by_key(|diagnostic| diagnostic.range);
1243+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
1244+ (
1245+ diagnostic.range,
1246+ diagnostic.severity,
1247+ diagnostic.language_server_id,
1248+ )
1249+ });
1198
1250
1199 // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
1251 // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
1200 let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
1252 let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
@@ -1547,13 +1599,13 @@ impl Document {
1547 }
1599 }
1548
1600
1549 /// Intialize/updates the differ for this document with a new base.
1601 /// Intialize/updates the differ for this document with a new base.
1550− pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
1602+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
1551 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
1603 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
1552 if let Some(differ) = &self.diff_handle {
1604 if let Some(differ) = &self.diff_handle {
1553 differ.update_diff_base(diff_base);
1605 differ.update_diff_base(diff_base);
1554 return;
1606 return;
1555 }
1607 }
1556− self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
1608+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone()))
1557 } else {
1609 } else {
1558 self.diff_handle = None;
1610 self.diff_handle = None;
1559 }
1611 }
@@ -1671,13 +1723,33 @@ impl Document {
1671
1723
1672 pub fn replace_diagnostics(
1724 pub fn replace_diagnostics(
1673 &mut self,
1725 &mut self,
1674− mut diagnostics: Vec<Diagnostic>,
1726+ diagnostics: impl IntoIterator<Item = Diagnostic>,
1727+ unchanged_sources: &[String],
1675 language_server_id: usize,
1728 language_server_id: usize,
1676 ) {
1729 ) {
1730+ if unchanged_sources.is_empty() {
1677− self.clear_diagnostics(language_server_id);
1731+ self.clear_diagnostics(language_server_id);
1732+ } else {
1678− self.diagnostics.append(&mut diagnostics);
1733+ self.diagnostics.retain(|d| {
1679− self.diagnostics
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);
1680− .sort_unstable_by_key(|diagnostic| diagnostic.range);
1746+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
1747+ (
1748+ diagnostic.range,
1749+ diagnostic.severity,
1750+ diagnostic.language_server_id,
1751+ )
1752+ });
1681 }
1753 }
1682
1754
1683 pub fn clear_diagnostics(&mut self, language_server_id: usize) {
1755 pub fn clear_diagnostics(&mut self, language_server_id: usize) {
helix-view/src/editor.rs+134−76
@@ -1,10 +1,10 @@
1use crate::{
1use crate::{
2 align_view,
2 align_view,
3− clipboard::{get_clipboard_provider, ClipboardProvider},
4 document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
3 document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
5 graphics::{CursorKind, Rect},
4 graphics::{CursorKind, Rect},
6 info::Info,
5 info::Info,
7 input::KeyEvent,
6 input::KeyEvent,
7+ register::Registers,
8 theme::{self, Theme},
8 theme::{self, Theme},
9 tree::{self, Tree},
9 tree::{self, Tree},
10 view::ViewPosition,
10 view::ViewPosition,
@@ -32,7 +32,7 @@ use std::{
32use tokio::{
32use tokio::{
33 sync::{
33 sync::{
34 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
34 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
35− oneshot, Notify, RwLock,
35+ oneshot,
36 },
36 },
37 time::{sleep, Duration, Instant, Sleep},
37 time::{sleep, Duration, Instant, Sleep},
38};
38};
@@ -40,13 +40,11 @@ use tokio::{
40use anyhow::{anyhow, bail, Error};
40use anyhow::{anyhow, bail, Error};
41
41
42pub use helix_core::diagnostic::Severity;
42pub use helix_core::diagnostic::Severity;
43−pub use helix_core::register::Registers;
44use helix_core::{
43use helix_core::{
45 auto_pairs::AutoPairs,
44 auto_pairs::AutoPairs,
46− syntax::{self, AutoPairConfig, SoftWrap},
45+ syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
47− Change, LineEnding, NATIVE_LINE_ENDING,
46+ Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
48};
47};
49−use helix_core::{Position, Selection};
50use helix_dap as dap;
48use helix_dap as dap;
51use helix_lsp::lsp;
49use helix_lsp::lsp;
52
50
@@ -245,7 +243,7 @@ pub struct Config {
245 /// Set a global text_width
243 /// Set a global text_width
246 pub text_width: usize,
244 pub text_width: usize,
247 /// Time in milliseconds since last keypress before idle timers trigger.
245 /// Time in milliseconds since last keypress before idle timers trigger.
248− /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
246+ /// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
249 #[serde(
247 #[serde(
250 serialize_with = "serialize_duration_millis",
248 serialize_with = "serialize_duration_millis",
251 deserialize_with = "deserialize_duration_millis"
249 deserialize_with = "deserialize_duration_millis"
@@ -288,6 +286,31 @@ pub struct Config {
288 pub workspace_lsp_roots: Vec<PathBuf>,
286 pub workspace_lsp_roots: Vec<PathBuf>,
289 /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
287 /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
290 pub default_line_ending: LineEndingConfig,
288 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+ }
291}
314}
292
315
293#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
316#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -404,6 +427,7 @@ impl Default for StatusLineConfig {
404 E::Mode,
427 E::Mode,
405 E::Spinner,
428 E::Spinner,
406 E::FileName,
429 E::FileName,
430+ E::ReadOnlyIndicator,
407 E::FileModificationIndicator,
431 E::FileModificationIndicator,
408 ],
432 ],
409 center: vec![],
433 center: vec![],
@@ -447,15 +471,18 @@ pub enum StatusLineElement {
447 /// The LSP activity spinner
471 /// The LSP activity spinner
448 Spinner,
472 Spinner,
449
473
450− /// The base file name, including a dirty flag if it's unsaved
474+ /// The file basename (the leaf of the open file's path)
451 FileBaseName,
475 FileBaseName,
452
476
453− /// The relative file path, including a dirty flag if it's unsaved
477+ /// The relative file path
454 FileName,
478 FileName,
455
479
456 // The file modification indicator
480 // The file modification indicator
457 FileModificationIndicator,
481 FileModificationIndicator,
458
482
483+ /// An indicator that shows `"[readonly]"` when a file cannot be written
484+ ReadOnlyIndicator,
485+
459 /// The file encoding
486 /// The file encoding
460 FileEncoding,
487 FileEncoding,
461
488
@@ -704,7 +731,7 @@ pub struct WhitespaceCharacters {
704impl Default for WhitespaceCharacters {
731impl Default for WhitespaceCharacters {
705 fn default() -> Self {
732 fn default() -> Self {
706 Self {
733 Self {
707− space: '·', // U+00B7
734+ space: '·', // U+00B7
708 nbsp: '⍽', // U+237D
735 nbsp: '⍽', // U+237D
709 tab: '→', // U+2192
736 tab: '→', // U+2192
710 newline: '⏎', // U+23CE
737 newline: '⏎', // U+23CE
@@ -732,12 +759,13 @@ impl Default for IndentGuidesConfig {
732}
759}
733
760
734/// Line ending configuration.
761/// Line ending configuration.
735−#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
762+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
736#[serde(rename_all = "lowercase")]
763#[serde(rename_all = "lowercase")]
737pub enum LineEndingConfig {
764pub enum LineEndingConfig {
738 /// The platform's native line ending.
765 /// The platform's native line ending.
739 ///
766 ///
740 /// `crlf` on Windows, otherwise `lf`.
767 /// `crlf` on Windows, otherwise `lf`.
768+ #[default]
741 Native,
769 Native,
742 /// Line feed.
770 /// Line feed.
743 LF,
771 LF,
@@ -754,12 +782,6 @@ pub enum LineEndingConfig {
754 Nel,
782 Nel,
755}
783}
756
784
757−impl Default for LineEndingConfig {
758− fn default() -> Self {
759− LineEndingConfig::Native
760− }
761−}
762−
763impl From<LineEndingConfig> for LineEnding {
785impl From<LineEndingConfig> for LineEnding {
764 fn from(line_ending: LineEndingConfig) -> Self {
786 fn from(line_ending: LineEndingConfig) -> Self {
765 match line_ending {
787 match line_ending {
@@ -776,6 +798,15 @@ impl From<LineEndingConfig> for LineEnding {
776 }
798 }
777}
799}
778
800
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+
779impl Default for Config {
810impl Default for Config {
780 fn default() -> Self {
811 fn default() -> Self {
781 Self {
812 Self {
@@ -796,7 +827,7 @@ impl Default for Config {
796 auto_completion: true,
827 auto_completion: true,
797 auto_format: true,
828 auto_format: true,
798 auto_save: false,
829 auto_save: false,
799− idle_timeout: Duration::from_millis(400),
830+ idle_timeout: Duration::from_millis(250),
800 preview_completion_insert: true,
831 preview_completion_insert: true,
801 completion_trigger_len: 2,
832 completion_trigger_len: 2,
802 auto_info: true,
833 auto_info: true,
@@ -821,6 +852,10 @@ impl Default for Config {
821 completion_replace: false,
852 completion_replace: false,
822 workspace_lsp_roots: Vec::new(),
853 workspace_lsp_roots: Vec::new(),
823 default_line_ending: LineEndingConfig::default(),
854 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(),
824 }
859 }
825 }
860 }
826}
861}
@@ -875,8 +910,6 @@ pub struct Editor {
875 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
910 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
876 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
911 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
877
912
878− pub clipboard_provider: Box<dyn ClipboardProvider>,
879−
880 pub syn_loader: Arc<syntax::Loader>,
913 pub syn_loader: Arc<syntax::Loader>,
881 pub theme_loader: Arc<theme::Loader>,
914 pub theme_loader: Arc<theme::Loader>,
882 /// last_theme is used for theme previews. We store the current theme here,
915 /// last_theme is used for theme previews. We store the current theme here,
@@ -898,16 +931,13 @@ pub struct Editor {
898 pub auto_pairs: Option<AutoPairs>,
931 pub auto_pairs: Option<AutoPairs>,
899
932
900 pub idle_timer: Pin<Box<Sleep>>,
933 pub idle_timer: Pin<Box<Sleep>>,
934+ redraw_timer: Pin<Box<Sleep>>,
901 last_motion: Option<Motion>,
935 last_motion: Option<Motion>,
902 pub last_completion: Option<CompleteAction>,
936 pub last_completion: Option<CompleteAction>,
903
937
904 pub exit_code: i32,
938 pub exit_code: i32,
905
939
906 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
940 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,
911 pub needs_redraw: bool,
941 pub needs_redraw: bool,
912 /// Cached position of the cursor calculated during rendering.
942 /// Cached position of the cursor calculated during rendering.
913 /// The content of `cursor_cache` is returned by `Editor::cursor` if
943 /// The content of `cursor_cache` is returned by `Editor::cursor` if
@@ -934,8 +964,6 @@ pub struct Editor {
934
964
935pub type Motion = Box<dyn Fn(&mut Editor)>;
965pub type Motion = Box<dyn Fn(&mut Editor)>;
936
966
937−pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
938−
939#[derive(Debug)]
967#[derive(Debug)]
940pub enum EditorEvent {
968pub enum EditorEvent {
941 DocumentSaved(DocumentSavedEventResult),
969 DocumentSaved(DocumentSavedEventResult),
@@ -943,6 +971,7 @@ pub enum EditorEvent {
943 LanguageServerMessage((usize, Call)),
971 LanguageServerMessage((usize, Call)),
944 DebuggerEvent(dap::Payload),
972 DebuggerEvent(dap::Payload),
945 IdleTimer,
973 IdleTimer,
974+ Redraw,
946}
975}
947
976
948#[derive(Debug, Clone)]
977#[derive(Debug, Clone)]
@@ -975,6 +1004,13 @@ pub enum Action {
975 VerticalSplit,
1004 VerticalSplit,
976}
1005}
977
1006
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+
978/// Error thrown on failed document closed
1014/// Error thrown on failed document closed
979pub enum CloseError {
1015pub enum CloseError {
980 /// Document doesn't exist
1016 /// Document doesn't exist
@@ -1023,23 +1059,32 @@ impl Editor {
1023 last_theme: None,
1059 last_theme: None,
1024 last_selection: None,
1060 last_selection: None,
1025 registers: Registers::default(),
1061 registers: Registers::default(),
1026− clipboard_provider: get_clipboard_provider(),
1027 status_msg: None,
1062 status_msg: None,
1028 autoinfo: None,
1063 autoinfo: None,
1029 idle_timer: Box::pin(sleep(conf.idle_timeout)),
1064 idle_timer: Box::pin(sleep(conf.idle_timeout)),
1065+ redraw_timer: Box::pin(sleep(Duration::MAX)),
1030 last_motion: None,
1066 last_motion: None,
1031 last_completion: None,
1067 last_completion: None,
1032 config,
1068 config,
1033 auto_pairs,
1069 auto_pairs,
1034 exit_code: 0,
1070 exit_code: 0,
1035 config_events: unbounded_channel(),
1071 config_events: unbounded_channel(),
1036− redraw_handle: Default::default(),
1037 needs_redraw: false,
1072 needs_redraw: false,
1038 cursor_cache: Cell::new(None),
1073 cursor_cache: Cell::new(None),
1039 completion_request_handle: None,
1074 completion_request_handle: None,
1040 }
1075 }
1041 }
1076 }
1042
1077
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+
1043 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
1088 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
1044 motion(self);
1089 motion(self);
1045 self.last_motion = Some(Box::new(motion));
1090 self.last_motion = Some(Box::new(motion));
@@ -1163,71 +1208,80 @@ impl Editor {
1163 }
1208 }
1164
1209
1165 /// Refreshes the language server for a given document
1210 /// Refreshes the language server for a given document
1166− pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1211+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
1167 self.launch_language_servers(doc_id)
1212 self.launch_language_servers(doc_id)
1168 }
1213 }
1169
1214
1170 /// Launch a language server for a given document
1215 /// Launch a language server for a given document
1171− fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1216+ fn launch_language_servers(&mut self, doc_id: DocumentId) {
1172 if !self.config().lsp.enable {
1217 if !self.config().lsp.enable {
1173− return None;
1218+ return;
1174 }
1219 }
1175 // if doc doesn't have a URL it's a scratch buffer, ignore it
1220 // if doc doesn't have a URL it's a scratch buffer, ignore it
1176− let doc = self.documents.get_mut(&doc_id)?;
1221+ let Some(doc) = self.documents.get_mut(&doc_id) else {
1222+ return;
1223+ };
1177− let doc_url = doc.url()?;
1224+ let Some(doc_url) = doc.url() else {
1225+ return;
1226+ };
1178 let (lang, path) = (doc.language.clone(), doc.path().cloned());
1227 let (lang, path) = (doc.language.clone(), doc.path().cloned());
1179 let config = doc.config.load();
1228 let config = doc.config.load();
1180 let root_dirs = &config.workspace_lsp_roots;
1229 let root_dirs = &config.workspace_lsp_roots;
1181
1230
1182− // try to find language servers based on the language name
1231+ // store only successfully started language servers
1183− let language_servers = lang.as_ref().and_then(|language| {
1232+ let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| {
1184 self.language_servers
1233 self.language_servers
1185 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1234 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1186− .map_err(|e| {
1235+ .filter_map(|(lang, client)| match client {
1236+ Ok(client) => Some((lang, client)),
1237+ Err(err) => {
1187− log::error!(
1238+ log::error!(
1188− "Failed to initialize the language servers for `{}` {{ {} }}",
1239+ "Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
1189− language.scope(),
1240+ language.scope(),
1190− e
1241+ lang,
1191− )
1242+ err
1243+ );
1244+ None
1245+ }
1192 })
1246 })
1193− .ok()
1247+ .collect::<HashMap<_, _>>()
1194 });
1248 });
1195
1249
1196− if let Some(language_servers) = language_servers {
1250+ if language_servers.is_empty() {
1197− let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1251+ return;
1198−
1199− // only spawn new language servers if the servers aren't the same
1200−
1201− let doc_language_servers_not_in_registry =
1202− doc.language_servers.iter().filter(|(name, doc_ls)| {
1203− language_servers
1204− .get(*name)
1205− .map_or(true, |ls| ls.id() != doc_ls.id())
1206− });
1252+ }
1207
1253
1208− for (_, language_server) in doc_language_servers_not_in_registry {
1254+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1209− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1210− }
1211
1255
1256+ // only spawn new language servers if the servers aren't the same
1257+ let doc_language_servers_not_in_registry =
1212− let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1258+ doc.language_servers.iter().filter(|(name, doc_ls)| {
1213− doc.language_servers
1259+ language_servers
1214 .get(*name)
1260 .get(*name)
1215− .map_or(true, |doc_ls| ls.id() != doc_ls.id())
1261+ .map_or(true, |ls| ls.id() != doc_ls.id())
1216 });
1262 });
1217
1263
1218− for (_, language_server) in language_servers_not_in_doc {
1264+ for (_, language_server) in doc_language_servers_not_in_registry {
1219− // TODO: this now races with on_init code if the init happens too quickly
1220− tokio::spawn(language_server.text_document_did_open(
1265+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1221− doc_url.clone(),
1266+ }
1222− doc.version(),
1267+
1223− doc.text(),
1268+ let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1224− language_id.clone(),
1269+ doc.language_servers
1225− ));
1270+ .get(*name)
1271+ .map_or(true, |doc_ls| ls.id() != doc_ls.id())
1226− }
1272+ });
1227
1273
1228− doc.language_servers = language_servers;
1274+ for (_, language_server) in language_servers_not_in_doc {
1275+ // TODO: this now races with on_init code if the init happens too quickly
1276+ tokio::spawn(language_server.text_document_did_open(
1277+ doc_url.clone(),
1278+ doc.version(),
1279+ doc.text(),
1280+ language_id.clone(),
1281+ ));
1229 }
1282 }
1230− Some(())
1283+
1284+ doc.language_servers = language_servers;
1231 }
1285 }
1232
1286
1233 fn _refresh(&mut self) {
1287 fn _refresh(&mut self) {
@@ -1410,7 +1464,7 @@ impl Editor {
1410
1464
1411 // ??? possible use for integration tests
1465 // ??? possible use for integration tests
1412 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
1466 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
1413− let path = helix_core::path::get_canonicalized_path(path)?;
1467+ let path = helix_core::path::get_canonicalized_path(path);
1414 let id = self.document_by_path(&path).map(|doc| doc.id);
1468 let id = self.document_by_path(&path).map(|doc| doc.id);
1415
1469
1416 let id = if let Some(id) = id {
1470 let id = if let Some(id) = id {
@@ -1424,12 +1478,12 @@ impl Editor {
1424 )?;
1478 )?;
1425
1479
1426 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1480 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1427− doc.set_diff_base(diff_base, self.redraw_handle.clone());
1481+ doc.set_diff_base(diff_base);
1428 }
1482 }
1429 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1483 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1430
1484
1431 let id = self.new_document(doc);
1485 let id = self.new_document(doc);
1432− let _ = self.launch_language_servers(id);
1486+ self.launch_language_servers(id);
1433
1487
1434 id
1488 id
1435 };
1489 };
@@ -1723,16 +1777,20 @@ impl Editor {
1723 return EditorEvent::DebuggerEvent(event)
1777 return EditorEvent::DebuggerEvent(event)
1724 }
1778 }
1725
1779
1726− _ = self.redraw_handle.0.notified() => {
1780+ _ = helix_event::redraw_requested() => {
1727 if !self.needs_redraw{
1781 if !self.needs_redraw{
1728 self.needs_redraw = true;
1782 self.needs_redraw = true;
1729 let timeout = Instant::now() + Duration::from_millis(33);
1783 let timeout = Instant::now() + Duration::from_millis(33);
1730− if timeout < self.idle_timer.deadline(){
1784+ if timeout < self.idle_timer.deadline() && timeout < self.redraw_timer.deadline(){
1731− self.idle_timer.as_mut().reset(timeout)
1785+ self.redraw_timer.as_mut().reset(timeout)
1732 }
1786 }
1733 }
1787 }
1734 }
1788 }
1735
1789
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+ }
1736 _ = &mut self.idle_timer => {
1794 _ = &mut self.idle_timer => {
1737 return EditorEvent::IdleTimer
1795 return EditorEvent::IdleTimer
1738 }
1796 }
helix-view/src/gutter.rs+4−5
@@ -9,8 +9,7 @@ use crate::{
9};
9};
10
10
11fn count_digits(n: usize) -> usize {
11fn count_digits(n: usize) -> usize {
12− // TODO: use checked_log10 when MSRV reaches 1.67
12+ (usize::checked_ilog10(n).unwrap_or(0) + 1) as usize
13− std::iter::successors(Some(n), |&n| (n >= 10).then_some(n / 10)).count()
14}
13}
15
14
16pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
15pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
@@ -94,9 +93,9 @@ pub fn diff<'doc>(
94 theme: &Theme,
93 theme: &Theme,
95 _is_focused: bool,
94 _is_focused: bool,
96) -> GutterFn<'doc> {
95) -> GutterFn<'doc> {
97− let added = theme.get("diff.plus");
96+ let added = theme.get("diff.plus.gutter");
98− let deleted = theme.get("diff.minus");
97+ let deleted = theme.get("diff.minus.gutter");
99− let modified = theme.get("diff.delta");
98+ let modified = theme.get("diff.delta.gutter");
100 if let Some(diff_handle) = doc.diff_handle() {
99 if let Some(diff_handle) = doc.diff_handle() {
101 let hunks = diff_handle.load();
100 let hunks = diff_handle.load();
102 let mut hunk_i = 0;
101 let mut hunk_i = 0;
helix-view/src/handlers/dap.rs+1−3
@@ -369,9 +369,7 @@ impl Editor {
369 {
369 {
370 Ok(process) => process,
370 Ok(process) => process,
371 Err(err) => {
371 Err(err) => {
372− // TODO replace the pretty print {:?} with a regular format {}
373− // when the MSRV is raised to 1.60.0
374− self.set_error(format!("Error starting external terminal: {:?}", err));
372+ self.set_error(format!("Error starting external terminal: {}", err));
375 return true;
373 return true;
376 }
374 }
377 };
375 };
helix-view/src/info.rs+4−11
@@ -1,4 +1,5 @@
1+use crate::register::Registers;
1−use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
2+use helix_core::unicode::width::UnicodeWidthStr;
2use std::fmt::Write;
3use std::fmt::Write;
3
4
4#[derive(Debug)]
5#[derive(Debug)]
@@ -56,16 +57,8 @@ impl Info {
56
57
57 pub fn from_registers(registers: &Registers) -> Self {
58 pub fn from_registers(registers: &Registers) -> Self {
58 let body: Vec<_> = registers
59 let body: Vec<_> = registers
59− .inner()
60+ .iter_preview()
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)
61+ .map(|(ch, preview)| (ch.to_string(), preview))
68− })
69 .collect();
62 .collect();
70
63
71 let mut infobox = Self::new("Registers", &body);
64 let mut infobox = Self::new("Registers", &body);
helix-view/src/input.rs+65−1
@@ -43,6 +43,10 @@ pub enum MouseEventKind {
43 ScrollDown,
43 ScrollDown,
44 /// Scrolled mouse wheel upwards (away from the user).
44 /// Scrolled mouse wheel upwards (away from the user).
45 ScrollUp,
45 ScrollUp,
46+ /// Scrolled mouse wheel leftwards.
47+ ScrollLeft,
48+ /// Scrolled mouse wheel rightwards.
49+ ScrollRight,
46}
50}
47
51
48/// Represents a mouse button.
52/// Represents a mouse button.
@@ -458,6 +462,8 @@ impl From<crossterm::event::MouseEventKind> for MouseEventKind {
458 crossterm::event::MouseEventKind::Moved => Self::Moved,
462 crossterm::event::MouseEventKind::Moved => Self::Moved,
459 crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
463 crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
460 crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
464 crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
465+ crossterm::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
466+ crossterm::event::MouseEventKind::ScrollRight => Self::ScrollRight,
461 }
467 }
462 }
468 }
463}
469}
@@ -543,7 +549,7 @@ pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
543 if c == ">" {
549 if c == ">" {
544 keys_res = Err(anyhow!("Unmatched '>'"));
550 keys_res = Err(anyhow!("Unmatched '>'"));
545 } else if c != "<" {
551 } else if c != "<" {
546− keys.push(c);
552+ keys.push(if c == "-" { keys::MINUS } else { c });
547 i += end_i;
553 i += end_i;
548 } else {
554 } else {
549 match s.find('>').context("'>' expected") {
555 match s.find('>').context("'>' expected") {
@@ -807,6 +813,64 @@ mod test {
807 },
813 },
808 ])
814 ])
809 );
815 );
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+ );
810 }
874 }
811
875
812 #[test]
876 #[test]
helix-view/src/lib.rs+1
@@ -15,6 +15,7 @@ pub mod base64;
15pub mod info;
15pub mod info;
16pub mod input;
16pub mod input;
17pub mod keyboard;
17pub mod keyboard;
18+pub mod register;
18pub mod theme;
19pub mod theme;
19pub mod tree;
20pub mod tree;
20pub mod view;
21pub mod view;
helix-view/src/register.rsnew file+333
@@ -0,0 +1,333 @@
1+use std::{borrow::Cow, collections::HashMap, iter};
2+
3+use anyhow::Result;
4+use helix_core::NATIVE_LINE_ENDING;
5+
6+use crate::{
7+ clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
8+ document::SCRATCH_BUFFER_NAME,
9+ Editor,
10+};
11+
12+/// A key-value store for saving sets of values.
13+///
14+/// Each register corresponds to a `char`. Most chars can be used to store any set of
15+/// values but a few chars are "special registers". Special registers have unique
16+/// behaviors when read or written to:
17+///
18+/// * Black hole (`_`): all values read and written are discarded
19+/// * Selection indices (`#`): index number of each selection starting at 1
20+/// * Selection contents (`.`)
21+/// * Document path (`%`): filename of the current buffer
22+/// * System clipboard (`*`)
23+/// * Primary clipboard (`+`)
24+#[derive(Debug)]
25+pub struct Registers {
26+ /// The mapping of register to values.
27+ /// Values are stored in reverse order when inserted with `Registers::write`.
28+ /// The order is reversed again in `Registers::read`. This allows us to
29+ /// efficiently prepend new values in `Registers::push`.
30+ inner: HashMap<char, Vec<String>>,
31+ clipboard_provider: Box<dyn ClipboardProvider>,
32+ pub last_search_register: char,
33+}
34+
35+impl Default for Registers {
36+ fn default() -> Self {
37+ Self {
38+ inner: Default::default(),
39+ clipboard_provider: get_clipboard_provider(),
40+ last_search_register: '/',
41+ }
42+ }
43+}
44+
45+impl Registers {
46+ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
47+ match name {
48+ '_' => Some(RegisterValues::new(iter::empty())),
49+ '#' => {
50+ let (view, doc) = current_ref!(editor);
51+ let selections = doc.selection(view.id).len();
52+ // ExactSizeIterator is implemented for Range<usize> but
53+ // not RangeInclusive<usize>.
54+ Some(RegisterValues::new(
55+ (0..selections).map(|i| (i + 1).to_string().into()),
56+ ))
57+ }
58+ '.' => {
59+ let (view, doc) = current_ref!(editor);
60+ let text = doc.text().slice(..);
61+ Some(RegisterValues::new(doc.selection(view.id).fragments(text)))
62+ }
63+ '%' => {
64+ let doc = doc!(editor);
65+
66+ let path = doc
67+ .path()
68+ .as_ref()
69+ .map(|p| p.to_string_lossy())
70+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
71+
72+ Some(RegisterValues::new(iter::once(path)))
73+ }
74+ '*' | '+' => Some(read_from_clipboard(
75+ self.clipboard_provider.as_ref(),
76+ self.inner.get(&name),
77+ match name {
78+ '+' => ClipboardType::Clipboard,
79+ '*' => ClipboardType::Selection,
80+ _ => unreachable!(),
81+ },
82+ )),
83+ _ => self
84+ .inner
85+ .get(&name)
86+ .map(|values| RegisterValues::new(values.iter().map(Cow::from).rev())),
87+ }
88+ }
89+
90+ pub fn write(&mut self, name: char, mut values: Vec<String>) -> Result<()> {
91+ match name {
92+ '_' => Ok(()),
93+ '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
94+ '*' | '+' => {
95+ self.clipboard_provider.set_contents(
96+ values.join(NATIVE_LINE_ENDING.as_str()),
97+ match name {
98+ '+' => ClipboardType::Clipboard,
99+ '*' => ClipboardType::Selection,
100+ _ => unreachable!(),
101+ },
102+ )?;
103+ values.reverse();
104+ self.inner.insert(name, values);
105+ Ok(())
106+ }
107+ _ => {
108+ values.reverse();
109+ self.inner.insert(name, values);
110+ Ok(())
111+ }
112+ }
113+ }
114+
115+ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
116+ match name {
117+ '_' => Ok(()),
118+ '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")),
119+ '*' | '+' => {
120+ let clipboard_type = match name {
121+ '+' => ClipboardType::Clipboard,
122+ '*' => ClipboardType::Selection,
123+ _ => unreachable!(),
124+ };
125+ let contents = self.clipboard_provider.get_contents(clipboard_type)?;
126+ let saved_values = self.inner.entry(name).or_insert_with(Vec::new);
127+
128+ if !contents_are_saved(saved_values, &contents) {
129+ anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents");
130+ }
131+
132+ saved_values.push(value.clone());
133+ if !contents.is_empty() {
134+ value.push_str(NATIVE_LINE_ENDING.as_str());
135+ }
136+ value.push_str(&contents);
137+ self.clipboard_provider
138+ .set_contents(value, clipboard_type)?;
139+
140+ Ok(())
141+ }
142+ _ => {
143+ self.inner.entry(name).or_insert_with(Vec::new).push(value);
144+ Ok(())
145+ }
146+ }
147+ }
148+
149+ pub fn first<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> {
150+ self.read(name, editor).and_then(|mut values| values.next())
151+ }
152+
153+ pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> {
154+ self.read(name, editor).and_then(|values| values.last())
155+ }
156+
157+ pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
158+ self.inner
159+ .iter()
160+ .filter(|(name, _)| !matches!(name, '*' | '+'))
161+ .map(|(name, values)| {
162+ let preview = values
163+ .last()
164+ .and_then(|s| s.lines().next())
165+ .unwrap_or("<empty>");
166+
167+ (*name, preview)
168+ })
169+ .chain(
170+ [
171+ ('_', "<empty>"),
172+ ('#', "<selection indices>"),
173+ ('.', "<selection contents>"),
174+ ('%', "<document path>"),
175+ ('+', "<system clipboard>"),
176+ ('*', "<primary clipboard>"),
177+ ]
178+ .iter()
179+ .copied(),
180+ )
181+ }
182+
183+ pub fn clear(&mut self) {
184+ self.clear_clipboard(ClipboardType::Clipboard);
185+ self.clear_clipboard(ClipboardType::Selection);
186+ self.inner.clear()
187+ }
188+
189+ pub fn remove(&mut self, name: char) -> bool {
190+ match name {
191+ '*' | '+' => {
192+ self.clear_clipboard(match name {
193+ '+' => ClipboardType::Clipboard,
194+ '*' => ClipboardType::Selection,
195+ _ => unreachable!(),
196+ });
197+ self.inner.remove(&name);
198+
199+ true
200+ }
201+ '_' | '#' | '.' | '%' => false,
202+ _ => self.inner.remove(&name).is_some(),
203+ }
204+ }
205+
206+ fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
207+ if let Err(err) = self
208+ .clipboard_provider
209+ .set_contents("".into(), clipboard_type)
210+ {
211+ log::error!(
212+ "Failed to clear {} clipboard: {err}",
213+ match clipboard_type {
214+ ClipboardType::Clipboard => "system",
215+ ClipboardType::Selection => "primary",
216+ }
217+ )
218+ }
219+ }
220+
221+ pub fn clipboard_provider_name(&self) -> Cow<str> {
222+ self.clipboard_provider.name()
223+ }
224+}
225+
226+fn read_from_clipboard<'a>(
227+ provider: &dyn ClipboardProvider,
228+ saved_values: Option<&'a Vec<String>>,
229+ clipboard_type: ClipboardType,
230+) -> RegisterValues<'a> {
231+ match provider.get_contents(clipboard_type) {
232+ Ok(contents) => {
233+ // If we're pasting the same values that we just yanked, re-use
234+ // the saved values. This allows pasting multiple selections
235+ // even when yanked to a clipboard.
236+ let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) };
237+
238+ if contents_are_saved(values, &contents) {
239+ RegisterValues::new(values.iter().map(Cow::from).rev())
240+ } else {
241+ RegisterValues::new(iter::once(contents.into()))
242+ }
243+ }
244+ Err(err) => {
245+ log::error!(
246+ "Failed to read {} clipboard: {err}",
247+ match clipboard_type {
248+ ClipboardType::Clipboard => "system",
249+ ClipboardType::Selection => "primary",
250+ }
251+ );
252+
253+ RegisterValues::new(iter::empty())
254+ }
255+ }
256+}
257+
258+fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool {
259+ let line_ending = NATIVE_LINE_ENDING.as_str();
260+ let mut values = saved_values.iter().rev();
261+
262+ match values.next() {
263+ Some(first) if contents.starts_with(first) => {
264+ contents = &contents[first.len()..];
265+ }
266+ None if contents.is_empty() => return true,
267+ _ => return false,
268+ }
269+
270+ for value in values {
271+ if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) {
272+ contents = &contents[line_ending.len() + value.len()..];
273+ } else {
274+ return false;
275+ }
276+ }
277+
278+ true
279+}
280+
281+// This is a wrapper of an iterator that is both double ended and exact size,
282+// and can return either owned or borrowed values. Regular registers can
283+// return borrowed values while some special registers need to return owned
284+// values.
285+pub struct RegisterValues<'a> {
286+ iter: Box<dyn DoubleEndedExactSizeIterator<Item = Cow<'a, str>> + 'a>,
287+}
288+
289+impl<'a> RegisterValues<'a> {
290+ fn new(
291+ iter: impl DoubleEndedIterator<Item = Cow<'a, str>>
292+ + ExactSizeIterator<Item = Cow<'a, str>>
293+ + 'a,
294+ ) -> Self {
295+ Self {
296+ iter: Box::new(iter),
297+ }
298+ }
299+}
300+
301+impl<'a> Iterator for RegisterValues<'a> {
302+ type Item = Cow<'a, str>;
303+
304+ fn next(&mut self) -> Option<Self::Item> {
305+ self.iter.next()
306+ }
307+
308+ fn size_hint(&self) -> (usize, Option<usize>) {
309+ self.iter.size_hint()
310+ }
311+}
312+
313+impl<'a> DoubleEndedIterator for RegisterValues<'a> {
314+ fn next_back(&mut self) -> Option<Self::Item> {
315+ self.iter.next_back()
316+ }
317+}
318+
319+impl<'a> ExactSizeIterator for RegisterValues<'a> {
320+ fn len(&self) -> usize {
321+ self.iter.len()
322+ }
323+}
324+
325+// Each RegisterValues iterator is both double ended and exact size. We can't
326+// type RegisterValues as `Box<dyn DoubleEndedIterator + ExactSizeIterator>`
327+// because only one non-auto trait is allowed in trait objects. So we need to
328+// create a new trait that covers both. `RegisterValues` wraps that type so that
329+// trait only needs to live in this module and not be imported for all register
330+// callsites.
331+trait DoubleEndedExactSizeIterator: DoubleEndedIterator + ExactSizeIterator {}
332+
333+impl<I: DoubleEndedIterator + ExactSizeIterator> DoubleEndedExactSizeIterator for I {}
helix-view/src/theme.rs+6
@@ -297,6 +297,11 @@ impl Theme {
297 self.highlights[index]
297 self.highlights[index]
298 }
298 }
299
299
300+ #[inline]
301+ pub fn scope(&self, index: usize) -> &str {
302+ &self.scopes[index]
303+ }
304+
300 pub fn name(&self) -> &str {
305 pub fn name(&self) -> &str {
301 &self.name
306 &self.name
302 }
307 }
@@ -359,6 +364,7 @@ impl Default for ThemePalette {
359 fn default() -> Self {
364 fn default() -> Self {
360 Self {
365 Self {
361 palette: hashmap! {
366 palette: hashmap! {
367+ "default".to_string() => Color::Reset,
362 "black".to_string() => Color::Black,
368 "black".to_string() => Color::Black,
363 "red".to_string() => Color::Red,
369 "red".to_string() => Color::Red,
364 "green".to_string() => Color::Green,
370 "green".to_string() => Color::Green,
helix-view/src/view.rs+8−2
@@ -187,11 +187,17 @@ impl View {
187 }
187 }
188
188
189 pub fn gutter_offset(&self, doc: &Document) -> u16 {
189 pub fn gutter_offset(&self, doc: &Document) -> u16 {
190+ let total_width = self
190− self.gutters
191+ .gutters
191 .layout
192 .layout
192 .iter()
193 .iter()
193 .map(|gutter| gutter.width(self, doc) as u16)
194 .map(|gutter| gutter.width(self, doc) as u16)
194− .sum()
195+ .sum();
196+ if total_width < self.area.width {
197+ total_width
198+ } else {
199+ 0
200+ }
195 }
201 }
196
202
197 //
203 //
languages.toml+514−206
@@ -3,10 +3,12 @@
3
3
4[language-server]
4[language-server]
5
5
6+als = { command = "als" }
6awk-language-server = { command = "awk-language-server" }
7awk-language-server = { command = "awk-language-server" }
7bash-language-server = { command = "bash-language-server", args = ["start"] }
8bash-language-server = { command = "bash-language-server", args = ["start"] }
8bass = { command = "bass", args = ["--lsp"] }
9bass = { command = "bass", args = ["--lsp"] }
9bicep-langserver = { command = "bicep-langserver" }
10bicep-langserver = { command = "bicep-langserver" }
11+bufls = { command = "bufls", args = ["serve"] }
10cairo-language-server = { command = "cairo-language-server", args = [] }
12cairo-language-server = { command = "cairo-language-server", args = [] }
11cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
13cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
12clangd = { command = "clangd" }
14clangd = { command = "clangd" }
@@ -14,6 +16,7 @@ clojure-lsp = { command = "clojure-lsp" }
14cmake-language-server = { command = "cmake-language-server" }
16cmake-language-server = { command = "cmake-language-server" }
15crystalline = { command = "crystalline", args = ["--stdio"] }
17crystalline = { command = "crystalline", args = ["--stdio"] }
16cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
18cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
19+csharp-ls = { command = "csharp-ls" }
17cuelsp = { command = "cuelsp" }
20cuelsp = { command = "cuelsp" }
18dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
21dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
19dhall-lsp-server = { command = "dhall-lsp-server" }
22dhall-lsp-server = { command = "dhall-lsp-server" }
@@ -28,6 +31,7 @@ forth-lsp = { command = "forth-lsp" }
28fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
31fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
29fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } }
32fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } }
30gleam = { command = "gleam", args = ["lsp"] }
33gleam = { command = "gleam", args = ["lsp"] }
34+graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] }
31haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
35haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
32idris2-lsp = { command = "idris2-lsp" }
36idris2-lsp = { command = "idris2-lsp" }
33intelephense = { command = "intelephense", args = ["--stdio"] }
37intelephense = { command = "intelephense", args = ["--stdio"] }
@@ -36,6 +40,7 @@ jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "-
36julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] }
40julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] }
37kotlin-language-server = { command = "kotlin-language-server" }
41kotlin-language-server = { command = "kotlin-language-server" }
38lean = { command = "lean", args = [ "--server" ] }
42lean = { command = "lean", args = [ "--server" ] }
43+ltex-ls = { command = "ltex-ls" }
39markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
44markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
40marksman = { command = "marksman", args = ["server"] }
45marksman = { command = "marksman", args = ["server"] }
41metals = { command = "metals", config = { "isHttpEnabled" = true } }
46metals = { command = "metals", config = { "isHttpEnabled" = true } }
@@ -43,15 +48,19 @@ mint = { command = "mint", args = ["ls"] }
43nil = { command = "nil" }
48nil = { command = "nil" }
44nimlangserver = { command = "nimlangserver" }
49nimlangserver = { command = "nimlangserver" }
45nls = { command = "nls" }
50nls = { command = "nls" }
51+nu-lsp = { command = "nu", args = [ "--lsp" ] }
46ocamllsp = { command = "ocamllsp" }
52ocamllsp = { command = "ocamllsp" }
47ols = { command = "ols", args = [] }
53ols = { command = "ols", args = [] }
48omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
54omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
49openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
55openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
50pasls = { command = "pasls", args = [] }
56pasls = { command = "pasls", args = [] }
57+pbkit = { command = "pb", args = [ "lsp" ] }
51perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
58perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
52prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
59prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
53purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
60purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
54pylsp = { command = "pylsp" }
61pylsp = { command = "pylsp" }
62+pyright = { command = "pyright-langserver", args = ["--stdio"], config = {} }
63+pylyzer = { command = "pylyzer", args = ["--server"] }
55qmlls = { command = "qmlls" }
64qmlls = { command = "qmlls" }
56r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
65r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
57racket = { command = "racket", args = ["-l", "racket-langserver"] }
66racket = { command = "racket", args = ["-l", "racket-langserver"] }
@@ -65,24 +74,30 @@ solc = { command = "solc", args = ["--lsp"] }
65sourcekit-lsp = { command = "sourcekit-lsp" }
74sourcekit-lsp = { command = "sourcekit-lsp" }
66svlangserver = { command = "svlangserver", args = [] }
75svlangserver = { command = "svlangserver", args = [] }
67swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
76swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
77+tailwindcss-ls = { command = "tailwindcss-language-server", args = ["--stdio"] }
68taplo = { command = "taplo", args = ["lsp", "stdio"] }
78taplo = { command = "taplo", args = ["lsp", "stdio"] }
79+templ = { command = "templ", args = ["lsp"] }
69terraform-ls = { command = "terraform-ls", args = ["serve"] }
80terraform-ls = { command = "terraform-ls", args = ["serve"] }
70texlab = { command = "texlab" }
81texlab = { command = "texlab" }
71vala-language-server = { command = "vala-language-server" }
82vala-language-server = { command = "vala-language-server" }
72vhdl_ls = { command = "vhdl_ls", args = [] }
83vhdl_ls = { command = "vhdl_ls", args = [] }
73−vlang-language-server = { command = "v", args = ["ls"] }
84+vlang-language-server = { command = "v-analyzer" }
74−vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { "provideFormatter" = true }}
85+vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { provideFormatter = true, css = { validate = { enable = true } } } }
75vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
86vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
76−vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } }
87+vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true, json = { validate = { enable = true } } } }
77vuels = { command = "vue-language-server", args = ["--stdio"], config = { typescript = { tsdk = "node_modules/typescript/lib/" } } }
88vuels = { command = "vue-language-server", args = ["--stdio"], config = { typescript = { tsdk = "node_modules/typescript/lib/" } } }
78wgsl_analyzer = { command = "wgsl_analyzer" }
89wgsl_analyzer = { command = "wgsl_analyzer" }
79yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
90yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
80zls = { command = "zls" }
91zls = { command = "zls" }
81blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] }
92blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] }
93+typst-lsp = { command = "typst-lsp" }
82
94
95+[language-server.ansible-language-server]
96+command = "ansible-language-server"
97+args = ["--stdio"]
83
98
84[language-server.lua-language-server]
99[language-server.lua-language-server]
85−command = "lua-language-server"
100+command = "lua-language-server"
86
101
87[language-server.lua-language-server.config.Lua.hint]
102[language-server.lua-language-server.config.Lua.hint]
88enable = true
103enable = true
@@ -104,6 +119,12 @@ functionTypeParameters = true
104parameterNames = true
119parameterNames = true
105rangeVariableTypes = true
120rangeVariableTypes = true
106
121
122+[language-server.golangci-lint-lsp]
123+command = "golangci-lint-langserver"
124+
125+[language-server.golangci-lint-lsp.config]
126+command = ["golangci-lint", "run", "--out-format", "json", "--issues-exit-code=1"]
127+
107
128
108[language-server.rust-analyzer]
129[language-server.rust-analyzer]
109command = "rust-analyzer"
130command = "rust-analyzer"
@@ -170,6 +191,7 @@ auto-format = true
170comment-token = "//"
191comment-token = "//"
171language-servers = [ "rust-analyzer" ]
192language-servers = [ "rust-analyzer" ]
172indent = { tab-width = 4, unit = " " }
193indent = { tab-width = 4, unit = " " }
194+persistent-diagnostic-sources = ["rustc", "clippy"]
173
195
174[language.auto-pairs]
196[language.auto-pairs]
175'(' = ')'
197'(' = ')'
@@ -230,7 +252,6 @@ name = "toml"
230scope = "source.toml"
252scope = "source.toml"
231injection-regex = "toml"
253injection-regex = "toml"
232file-types = ["toml", "poetry.lock", "Cargo.lock"]
254file-types = ["toml", "poetry.lock", "Cargo.lock"]
233−roots = []
234comment-token = "#"
255comment-token = "#"
235language-servers = [ "taplo" ]
256language-servers = [ "taplo" ]
236indent = { tab-width = 2, unit = " " }
257indent = { tab-width = 2, unit = " " }
@@ -244,7 +265,6 @@ name = "awk"
244scope = "source.awk"
265scope = "source.awk"
245injection-regex = "awk"
266injection-regex = "awk"
246file-types = ["awk", "gawk", "nawk", "mawk"]
267file-types = ["awk", "gawk", "nawk", "mawk"]
247−roots = []
248comment-token = "#"
268comment-token = "#"
249language-servers = [ "awk-language-server" ]
269language-servers = [ "awk-language-server" ]
250indent = { tab-width = 2, unit = " " }
270indent = { tab-width = 2, unit = " " }
@@ -258,7 +278,7 @@ name = "protobuf"
258scope = "source.proto"
278scope = "source.proto"
259injection-regex = "protobuf"
279injection-regex = "protobuf"
260file-types = ["proto"]
280file-types = ["proto"]
261−roots = []
281+language-servers = [ "bufls", "pbkit" ]
262comment-token = "//"
282comment-token = "//"
263indent = { tab-width = 2, unit = " " }
283indent = { tab-width = 2, unit = " " }
264
284
@@ -287,7 +307,6 @@ scope = "source.fish"
287injection-regex = "fish"
307injection-regex = "fish"
288file-types = ["fish"]
308file-types = ["fish"]
289shebangs = ["fish"]
309shebangs = ["fish"]
290−roots = []
291comment-token = "#"
310comment-token = "#"
292indent = { tab-width = 4, unit = " " }
311indent = { tab-width = 4, unit = " " }
293
312
@@ -301,7 +320,6 @@ scope = "source.mint"
301injection-regex = "mint"
320injection-regex = "mint"
302file-types = ["mint"]
321file-types = ["mint"]
303shebangs = []
322shebangs = []
304−roots = []
305comment-token = "//"
323comment-token = "//"
306language-servers = [ "mint" ]
324language-servers = [ "mint" ]
307indent = { tab-width = 2, unit = " " }
325indent = { tab-width = 2, unit = " " }
@@ -310,8 +328,28 @@ indent = { tab-width = 2, unit = " " }
310name = "json"
328name = "json"
311scope = "source.json"
329scope = "source.json"
312injection-regex = "json"
330injection-regex = "json"
313−file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
331+file-types = [
314−roots = []
332+ "json",
333+ "jsonc",
334+ "arb",
335+ "ipynb",
336+ "geojson",
337+ "gltf",
338+ "webmanifest",
339+ "flake.lock",
340+ ".babelrc",
341+ ".bowerrc",
342+ ".jscrc",
343+ "js.map",
344+ "ts.map",
345+ "css.map",
346+ ".jslintrc",
347+ "jsonld",
348+ ".vuerc",
349+ "composer.lock",
350+ ".watchmanconfig",
351+ "avsc"
352+]
315language-servers = [ "vscode-json-language-server" ]
353language-servers = [ "vscode-json-language-server" ]
316auto-format = true
354auto-format = true
317indent = { tab-width = 2, unit = " " }
355indent = { tab-width = 2, unit = " " }
@@ -320,12 +358,26 @@ indent = { tab-width = 2, unit = " " }
320name = "json"
358name = "json"
321source = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "73076754005a460947cafe8e03a8cf5fa4fa2938" }
359source = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "73076754005a460947cafe8e03a8cf5fa4fa2938" }
322
360
361+
362+[[language]]
363+name = "json5"
364+scope = "source.json5"
365+injection-regex = "json5"
366+file-types = ["json5"]
367+language-servers = []
368+comment-token = "//"
369+indent = { tab-width = 4, unit = " " }
370+# https://json5.org
371+
372+[[grammar]]
373+name = "json5"
374+source = { git = "https://github.com/Joakker/tree-sitter-json5", rev = "c23f7a9b1ee7d45f516496b1e0e4be067264fa0d" }
375+
323[[language]]
376[[language]]
324name = "c"
377name = "c"
325scope = "source.c"
378scope = "source.c"
326injection-regex = "c"
379injection-regex = "c"
327file-types = ["c"] # TODO: ["h"]
380file-types = ["c"] # TODO: ["h"]
328−roots = []
329comment-token = "//"
381comment-token = "//"
330language-servers = [ "clangd" ]
382language-servers = [ "clangd" ]
331indent = { tab-width = 2, unit = " " }
383indent = { tab-width = 2, unit = " " }
@@ -361,8 +413,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
361name = "cpp"
413name = "cpp"
362scope = "source.cpp"
414scope = "source.cpp"
363injection-regex = "cpp"
415injection-regex = "cpp"
364−file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm"]
416+file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh", "cppm", "h++", "ii", "inl", { suffix = ".hpp.in" }, { suffix = ".h.in" }]
365−roots = []
366comment-token = "//"
417comment-token = "//"
367language-servers = [ "clangd" ]
418language-servers = [ "clangd" ]
368indent = { tab-width = 2, unit = " " }
419indent = { tab-width = 2, unit = " " }
@@ -408,7 +459,7 @@ language-servers = [ "crystalline" ]
408name = "c-sharp"
459name = "c-sharp"
409scope = "source.csharp"
460scope = "source.csharp"
410injection-regex = "c-?sharp"
461injection-regex = "c-?sharp"
411−file-types = ["cs"]
462+file-types = ["cs", "csx", "cake"]
412roots = ["sln", "csproj"]
463roots = ["sln", "csproj"]
413comment-token = "//"
464comment-token = "//"
414indent = { tab-width = 4, unit = "\t" }
465indent = { tab-width = 4, unit = "\t" }
@@ -445,7 +496,7 @@ file-types = ["go"]
445roots = ["go.work", "go.mod"]
496roots = ["go.work", "go.mod"]
446auto-format = true
497auto-format = true
447comment-token = "//"
498comment-token = "//"
448−language-servers = [ "gopls" ]
499+language-servers = [ "gopls", "golangci-lint-lsp" ]
449# TODO: gopls needs utf-8 offsets?
500# TODO: gopls needs utf-8 offsets?
450indent = { tab-width = 4, unit = "\t" }
501indent = { tab-width = 4, unit = "\t" }
451
502
@@ -495,7 +546,6 @@ name = "gomod"
495scope = "source.gomod"
546scope = "source.gomod"
496injection-regex = "gomod"
547injection-regex = "gomod"
497file-types = ["go.mod"]
548file-types = ["go.mod"]
498−roots = []
499auto-format = true
549auto-format = true
500comment-token = "//"
550comment-token = "//"
501language-servers = [ "gopls" ]
551language-servers = [ "gopls" ]
@@ -510,7 +560,6 @@ name = "gotmpl"
510scope = "source.gotmpl"
560scope = "source.gotmpl"
511injection-regex = "gotmpl"
561injection-regex = "gotmpl"
512file-types = ["gotmpl"]
562file-types = ["gotmpl"]
513−roots = []
514comment-token = "//"
563comment-token = "//"
515language-servers = [ "gopls" ]
564language-servers = [ "gopls" ]
516indent = { tab-width = 2, unit = " " }
565indent = { tab-width = 2, unit = " " }
@@ -524,7 +573,6 @@ name = "gowork"
524scope = "source.gowork"
573scope = "source.gowork"
525injection-regex = "gowork"
574injection-regex = "gowork"
526file-types = ["go.work"]
575file-types = ["go.work"]
527−roots = []
528auto-format = true
576auto-format = true
529comment-token = "//"
577comment-token = "//"
530language-servers = [ "gopls" ]
578language-servers = [ "gopls" ]
@@ -539,9 +587,8 @@ name = "javascript"
539scope = "source.js"
587scope = "source.js"
540injection-regex = "(js|javascript)"
588injection-regex = "(js|javascript)"
541language-id = "javascript"
589language-id = "javascript"
542−file-types = ["js", "mjs", "cjs"]
590+file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", "jakefile"]
543shebangs = ["node"]
591shebangs = ["node"]
544−roots = []
545comment-token = "//"
592comment-token = "//"
546language-servers = [ "typescript-language-server" ]
593language-servers = [ "typescript-language-server" ]
547indent = { tab-width = 2, unit = " " }
594indent = { tab-width = 2, unit = " " }
@@ -560,7 +607,7 @@ args = { program = "{0}" }
560
607
561[[grammar]]
608[[grammar]]
562name = "javascript"
609name = "javascript"
563−source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev = "4a95461c4761c624f2263725aca79eeaefd36cad" }
610+source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev = "f772967f7b7bc7c28f845be2420a38472b16a8ee" }
564
611
565[[language]]
612[[language]]
566name = "jsx"
613name = "jsx"
@@ -568,7 +615,6 @@ scope = "source.jsx"
568injection-regex = "jsx"
615injection-regex = "jsx"
569language-id = "javascriptreact"
616language-id = "javascriptreact"
570file-types = ["jsx"]
617file-types = ["jsx"]
571−roots = []
572comment-token = "//"
618comment-token = "//"
573language-servers = [ "typescript-language-server" ]
619language-servers = [ "typescript-language-server" ]
574indent = { tab-width = 2, unit = " " }
620indent = { tab-width = 2, unit = " " }
@@ -580,14 +626,13 @@ scope = "source.ts"
580injection-regex = "(ts|typescript)"
626injection-regex = "(ts|typescript)"
581file-types = ["ts", "mts", "cts"]
627file-types = ["ts", "mts", "cts"]
582language-id = "typescript"
628language-id = "typescript"
583−shebangs = []
629+shebangs = ["deno", "ts-node"]
584−roots = []
585language-servers = [ "typescript-language-server" ]
630language-servers = [ "typescript-language-server" ]
586indent = { tab-width = 2, unit = " " }
631indent = { tab-width = 2, unit = " " }
587
632
588[[grammar]]
633[[grammar]]
589name = "typescript"
634name = "typescript"
590−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", subpath = "typescript" }
635+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "b1bf4825d9eaa0f3bdeb1e52f099533328acfbdf", subpath = "typescript" }
591
636
592[[language]]
637[[language]]
593name = "tsx"
638name = "tsx"
@@ -595,20 +640,18 @@ scope = "source.tsx"
595injection-regex = "(tsx)" # |typescript
640injection-regex = "(tsx)" # |typescript
596language-id = "typescriptreact"
641language-id = "typescriptreact"
597file-types = ["tsx"]
642file-types = ["tsx"]
598−roots = []
599language-servers = [ "typescript-language-server" ]
643language-servers = [ "typescript-language-server" ]
600indent = { tab-width = 2, unit = " " }
644indent = { tab-width = 2, unit = " " }
601
645
602[[grammar]]
646[[grammar]]
603name = "tsx"
647name = "tsx"
604−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", subpath = "tsx" }
648+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "b1bf4825d9eaa0f3bdeb1e52f099533328acfbdf", subpath = "tsx" }
605
649
606[[language]]
650[[language]]
607name = "css"
651name = "css"
608scope = "source.css"
652scope = "source.css"
609injection-regex = "css"
653injection-regex = "css"
610file-types = ["css", "scss"]
654file-types = ["css", "scss"]
611−roots = []
612language-servers = [ "vscode-css-language-server" ]
655language-servers = [ "vscode-css-language-server" ]
613auto-format = true
656auto-format = true
614indent = { tab-width = 2, unit = " " }
657indent = { tab-width = 2, unit = " " }
@@ -622,7 +665,6 @@ name = "scss"
622scope = "source.scss"
665scope = "source.scss"
623injection-regex = "scss"
666injection-regex = "scss"
624file-types = ["scss"]
667file-types = ["scss"]
625−roots = []
626language-servers = [ "vscode-css-language-server" ]
668language-servers = [ "vscode-css-language-server" ]
627auto-format = true
669auto-format = true
628indent = { tab-width = 2, unit = " " }
670indent = { tab-width = 2, unit = " " }
@@ -635,8 +677,7 @@ source = { git = "https://github.com/serenadeai/tree-sitter-scss", rev = "c478c6
635name = "html"
677name = "html"
636scope = "text.html.basic"
678scope = "text.html.basic"
637injection-regex = "html"
679injection-regex = "html"
638−file-types = ["html"]
680+file-types = ["html", "htm", "shtml", "xhtml", "xht", "jsp", "asp", "aspx", "jshtm", "volt", "rhtml"]
639−roots = []
640language-servers = [ "vscode-html-language-server" ]
681language-servers = [ "vscode-html-language-server" ]
641auto-format = true
682auto-format = true
642indent = { tab-width = 2, unit = " " }
683indent = { tab-width = 2, unit = " " }
@@ -649,9 +690,9 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53
649name = "python"
690name = "python"
650scope = "source.python"
691scope = "source.python"
651injection-regex = "python"
692injection-regex = "python"
652−file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct"]
693+file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstruct", "rpy", "cpy", "ipy", "pyt", "SConscript"]
653shebangs = ["python"]
694shebangs = ["python"]
654−roots = []
695+roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"]
655comment-token = "#"
696comment-token = "#"
656language-servers = [ "pylsp" ]
697language-servers = [ "pylsp" ]
657# TODO: pyls needs utf-8 offsets
698# TODO: pyls needs utf-8 offsets
@@ -659,7 +700,7 @@ indent = { tab-width = 4, unit = " " }
659
700
660[[grammar]]
701[[grammar]]
661name = "python"
702name = "python"
662−source = { git = "https://github.com/tree-sitter/tree-sitter-python", rev = "de221eccf9a221f5b85474a553474a69b4b5784d" }
703+source = { git = "https://github.com/tree-sitter/tree-sitter-python", rev = "4bfdd9033a2225cc95032ce77066b7aeca9e2efc" }
663
704
664[[language]]
705[[language]]
665name = "nickel"
706name = "nickel"
@@ -667,7 +708,6 @@ scope = "source.nickel"
667injection-regex = "nickel"
708injection-regex = "nickel"
668file-types = ["ncl"]
709file-types = ["ncl"]
669shebangs = []
710shebangs = []
670−roots = []
671comment-token = "#"
711comment-token = "#"
672language-servers = [ "nls" ]
712language-servers = [ "nls" ]
673indent = { tab-width = 2, unit = " " }
713indent = { tab-width = 2, unit = " " }
@@ -688,7 +728,6 @@ scope = "source.nix"
688injection-regex = "nix"
728injection-regex = "nix"
689file-types = ["nix"]
729file-types = ["nix"]
690shebangs = []
730shebangs = []
691−roots = []
692comment-token = "#"
731comment-token = "#"
693language-servers = [ "nil" ]
732language-servers = [ "nil" ]
694indent = { tab-width = 2, unit = " " }
733indent = { tab-width = 2, unit = " " }
@@ -701,9 +740,43 @@ source = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "1b69
701name = "ruby"
740name = "ruby"
702scope = "source.ruby"
741scope = "source.ruby"
703injection-regex = "ruby"
742injection-regex = "ruby"
704−file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile", "Gemfile", "rabl", "jbuilder", "jb", "Podfile", "podspec", "Vagrantfile", "Brewfile"]
743+file-types = [
744+ "rb",
745+ "rake",
746+ "rakefile",
747+ "irb",
748+ "gemfile",
749+ "gemspec",
750+ "Rakefile",
751+ "Gemfile",
752+ "rabl",
753+ "jbuilder",
754+ "jb",
755+ "Podfile",
756+ "podspec",
757+ "Vagrantfile",
758+ "Brewfile",
759+ "rjs",
760+ "rbi",
761+ "Guardfile",
762+ "Capfile",
763+ "Cheffile",
764+ "Hobofile",
765+ "Appraisals",
766+ "Rantfile",
767+ "Berksfile",
768+ "Berksfile.lock",
769+ "Thorfile",
770+ "Puppetfile",
771+ "Fastfile",
772+ "Appfile",
773+ "Deliverfile",
774+ "Matchfile",
775+ "Scanfile",
776+ "Snapfile",
777+ "Gymfile"
778+]
705shebangs = ["ruby"]
779shebangs = ["ruby"]
706−roots = []
707comment-token = "#"
780comment-token = "#"
708language-servers = [ "solargraph" ]
781language-servers = [ "solargraph" ]
709indent = { tab-width = 2, unit = " " }
782indent = { tab-width = 2, unit = " " }
@@ -716,9 +789,48 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "206c7
716name = "bash"
789name = "bash"
717scope = "source.bash"
790scope = "source.bash"
718injection-regex = "(shell|bash|zsh|sh)"
791injection-regex = "(shell|bash|zsh|sh)"
719−file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".profile", ".zshenv", ".zlogin", ".zlogout", ".zprofile", ".zshrc", ".zimrc", "APKBUILD", "PKGBUILD", "eclass", "ebuild", "bazelrc", ".bash_aliases", "Renviron", ".Renviron"]
792+file-types = [
793+ "sh",
794+ "bash",
795+ "zsh",
796+ ".bash_login",
797+ ".bash_logout",
798+ ".bash_profile",
799+ ".bashrc",
800+ ".profile",
801+ ".zshenv",
802+ "zshenv",
803+ ".zlogin",
804+ "zlogin",
805+ ".zlogout",
806+ "zlogout",
807+ ".zprofile",
808+ "zprofile",
809+ ".zshrc",
810+ "zshrc",
811+ ".zimrc",
812+ "APKBUILD",
813+ "PKGBUILD",
814+ "eclass",
815+ "ebuild",
816+ "bazelrc",
817+ ".bash_aliases",
818+ "Renviron",
819+ ".Renviron",
820+ ".xprofile",
821+ ".xsession",
822+ ".xsessionrc",
823+ "zsh-theme",
824+ "ksh",
825+ "cshrc",
826+ "tcshrc",
827+ ".yashrc",
828+ ".yash_profile",
829+ ".hushlogin",
830+ "bashrc_Apple_Terminal",
831+ "zshrc_Apple_Terminal"
832+]
720shebangs = ["sh", "bash", "dash", "zsh"]
833shebangs = ["sh", "bash", "dash", "zsh"]
721−roots = []
722comment-token = "#"
834comment-token = "#"
723language-servers = [ "bash-language-server" ]
835language-servers = [ "bash-language-server" ]
724indent = { tab-width = 2, unit = " " }
836indent = { tab-width = 2, unit = " " }
@@ -731,7 +843,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "275ef
731name = "php"
843name = "php"
732scope = "source.php"
844scope = "source.php"
733injection-regex = "php"
845injection-regex = "php"
734−file-types = ["php", "inc"]
846+file-types = ["php", "inc", "php4", "php5", "phtml", "ctp"]
735shebangs = ["php"]
847shebangs = ["php"]
736roots = ["composer.json", "index.php"]
848roots = ["composer.json", "index.php"]
737language-servers = [ "intelephense" ]
849language-servers = [ "intelephense" ]
@@ -746,7 +858,6 @@ name = "twig"
746scope = "source.twig"
858scope = "source.twig"
747injection-regex = "twig"
859injection-regex = "twig"
748file-types = ["twig"]
860file-types = ["twig"]
749−roots = []
750indent = { tab-width = 2, unit = " " }
861indent = { tab-width = 2, unit = " " }
751
862
752[[grammar]]
863[[grammar]]
@@ -757,8 +868,7 @@ source = { git = "https://github.com/gbprod/tree-sitter-twig", rev = "807b293fec
757name = "latex"
868name = "latex"
758scope = "source.tex"
869scope = "source.tex"
759injection-regex = "tex"
870injection-regex = "tex"
760−file-types = ["tex", "sty", "cls", "Rd"]
871+file-types = ["tex", "sty", "cls", "Rd", "bbx", "cbx"]
761−roots = []
762comment-token = "%"
872comment-token = "%"
763language-servers = [ "texlab" ]
873language-servers = [ "texlab" ]
764indent = { tab-width = 4, unit = "\t" }
874indent = { tab-width = 4, unit = "\t" }
@@ -772,7 +882,6 @@ name = "bibtex"
772scope = "source.bib"
882scope = "source.bib"
773injection-regex = "bib"
883injection-regex = "bib"
774file-types = ["bib"]
884file-types = ["bib"]
775−roots = []
776comment-token = "%"
885comment-token = "%"
777language-servers = [ "texlab" ]
886language-servers = [ "texlab" ]
778indent = { tab-width = 4, unit = "\t" }
887indent = { tab-width = 4, unit = "\t" }
@@ -809,11 +918,23 @@ indent = { tab-width = 2, unit = " " }
809name = "lean"
918name = "lean"
810source = { git = "https://github.com/Julian/tree-sitter-lean", rev = "d98426109258b266e1e92358c5f11716d2e8f638" }
919source = { git = "https://github.com/Julian/tree-sitter-lean", rev = "d98426109258b266e1e92358c5f11716d2e8f638" }
811
920
921+
922+[[language]]
923+name = "lpf"
924+comment-token = "#"
925+scope = "source.lpf"
926+file-types = ["lpf"]
927+
928+[[grammar]]
929+name = "lpf"
930+source = { git = "https://gitlab.com/TheZoq2/tree-sitter-lpf", rev = "db7372e60c722ca7f12ab359e57e6bf7611ab126" }
931+
812[[language]]
932[[language]]
813name = "julia"
933name = "julia"
814scope = "source.julia"
934scope = "source.julia"
815injection-regex = "julia"
935injection-regex = "julia"
816file-types = ["jl"]
936file-types = ["jl"]
937+shebangs = ["julia"]
817roots = ["Manifest.toml", "Project.toml"]
938roots = ["Manifest.toml", "Project.toml"]
818comment-token = "#"
939comment-token = "#"
819language-servers = [ "julia" ]
940language-servers = [ "julia" ]
@@ -827,21 +948,33 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-julia", rev = "8fb3
827name = "java"
948name = "java"
828scope = "source.java"
949scope = "source.java"
829injection-regex = "java"
950injection-regex = "java"
830−file-types = ["java"]
951+file-types = ["java", "jav"]
831roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
952roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
832language-servers = [ "jdtls" ]
953language-servers = [ "jdtls" ]
833−indent = { tab-width = 4, unit = " " }
954+indent = { tab-width = 2, unit = " " }
834
955
835[[grammar]]
956[[grammar]]
836name = "java"
957name = "java"
837source = { git = "https://github.com/tree-sitter/tree-sitter-java", rev = "09d650def6cdf7f479f4b78f595e9ef5b58ce31e" }
958source = { git = "https://github.com/tree-sitter/tree-sitter-java", rev = "09d650def6cdf7f479f4b78f595e9ef5b58ce31e" }
838
959
960+[[language]]
961+name = "smali"
962+scope = "source.smali"
963+injection-regex = "smali"
964+file-types = ["smali"]
965+comment-token = "#"
966+roots = []
967+indent = { tab-width = 4, unit = " " }
968+
969+[[grammar]]
970+name = "smali"
971+source = { git = "https://github.com/amaanq/tree-sitter-smali", rev = "5ae51e15c4d1ac93cba6127caf3d1f0a072c140c" }
972+
839[[language]]
973[[language]]
840name = "ledger"
974name = "ledger"
841scope = "source.ledger"
975scope = "source.ledger"
842injection-regex = "ledger"
976injection-regex = "ledger"
843file-types = ["ldg", "ledger", "journal"]
977file-types = ["ldg", "ledger", "journal"]
844−roots = []
845comment-token = ";"
978comment-token = ";"
846indent = { tab-width = 4, unit = " " }
979indent = { tab-width = 4, unit = " " }
847
980
@@ -854,21 +987,19 @@ name = "beancount"
854scope = "source.beancount"
987scope = "source.beancount"
855injection-regex = "beancount"
988injection-regex = "beancount"
856file-types = ["beancount", "bean"]
989file-types = ["beancount", "bean"]
857−roots = []
858comment-token = ";"
990comment-token = ";"
859indent = { tab-width = 2, unit = " " }
991indent = { tab-width = 2, unit = " " }
860
992
861[[grammar]]
993[[grammar]]
862name = "beancount"
994name = "beancount"
863−source = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "4cbd1f09cd07c1f1fabf867c2cf354f9da53cc4c" }
995+source = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "f3741a3a68ade59ec894ed84a64673494d2ba8f3" }
864
996
865[[language]]
997[[language]]
866name = "ocaml"
998name = "ocaml"
867scope = "source.ocaml"
999scope = "source.ocaml"
868injection-regex = "ocaml"
1000injection-regex = "ocaml"
869file-types = ["ml"]
1001file-types = ["ml"]
870−shebangs = []
1002+shebangs = ["ocaml", "ocamlrun", "ocamlscript"]
871−roots = []
872comment-token = "(**)"
1003comment-token = "(**)"
873language-servers = [ "ocamllsp" ]
1004language-servers = [ "ocamllsp" ]
874indent = { tab-width = 2, unit = " " }
1005indent = { tab-width = 2, unit = " " }
@@ -878,7 +1009,6 @@ indent = { tab-width = 2, unit = " " }
878'{' = '}'
1009'{' = '}'
879'[' = ']'
1010'[' = ']'
880'"' = '"'
1011'"' = '"'
881−'`' = '`'
882
1012
883[[grammar]]
1013[[grammar]]
884name = "ocaml"
1014name = "ocaml"
@@ -889,7 +1019,6 @@ name = "ocaml-interface"
889scope = "source.ocaml.interface"
1019scope = "source.ocaml.interface"
890file-types = ["mli"]
1020file-types = ["mli"]
891shebangs = []
1021shebangs = []
892−roots = []
893comment-token = "(**)"
1022comment-token = "(**)"
894language-servers = [ "ocamllsp" ]
1023language-servers = [ "ocamllsp" ]
895indent = { tab-width = 2, unit = " " }
1024indent = { tab-width = 2, unit = " " }
@@ -899,7 +1028,6 @@ indent = { tab-width = 2, unit = " " }
899'{' = '}'
1028'{' = '}'
900'[' = ']'
1029'[' = ']'
901'"' = '"'
1030'"' = '"'
902−'`' = '`'
903
1031
904[[grammar]]
1032[[grammar]]
905name = "ocaml-interface"
1033name = "ocaml-interface"
@@ -925,7 +1053,6 @@ name = "svelte"
925scope = "source.svelte"
1053scope = "source.svelte"
926injection-regex = "svelte"
1054injection-regex = "svelte"
927file-types = ["svelte"]
1055file-types = ["svelte"]
928−roots = []
929indent = { tab-width = 2, unit = " " }
1056indent = { tab-width = 2, unit = " " }
930language-servers = [ "svelteserver" ]
1057language-servers = [ "svelteserver" ]
931
1058
@@ -950,10 +1077,9 @@ source = { git = "https://github.com/ikatyang/tree-sitter-vue", rev = "91fe27547
950name = "yaml"
1077name = "yaml"
951scope = "source.yaml"
1078scope = "source.yaml"
952file-types = ["yml", "yaml"]
1079file-types = ["yml", "yaml"]
953−roots = []
954comment-token = "#"
1080comment-token = "#"
955indent = { tab-width = 2, unit = " " }
1081indent = { tab-width = 2, unit = " " }
956−language-servers = [ "yaml-language-server" ]
1082+language-servers = [ "yaml-language-server", "ansible-language-server" ]
957injection-regex = "yml|yaml"
1083injection-regex = "yml|yaml"
958
1084
959[[grammar]]
1085[[grammar]]
@@ -972,13 +1098,12 @@ indent = { tab-width = 2, unit = " " }
972
1098
973[[grammar]]
1099[[grammar]]
974name = "haskell"
1100name = "haskell"
975−source = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "98fc7f59049aeb713ab9b72a8ff25dcaaef81087" }
1101+source = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "d7ac98f49e3ed7e17541256fe3881a967d7ffdd3" }
976
1102
977[[language]]
1103[[language]]
978name = "haskell-persistent"
1104name = "haskell-persistent"
979scope = "source.persistentmodels"
1105scope = "source.persistentmodels"
980file-types = ["persistentmodels"]
1106file-types = ["persistentmodels"]
981−roots = []
982comment-token = "--"
1107comment-token = "--"
983indent = { tab-width = 2, unit = " " }
1108indent = { tab-width = 2, unit = " " }
984
1109
@@ -997,7 +1122,10 @@ language-servers = [ "purescript-language-server" ]
997indent = { tab-width = 2, unit = " " }
1122indent = { tab-width = 2, unit = " " }
998auto-format = true
1123auto-format = true
999formatter = { command = "purs-tidy", args = ["format"] }
1124formatter = { command = "purs-tidy", args = ["format"] }
1125+
1000−grammar = "haskell"
1126+[[grammar]]
1127+name = "purescript"
1128+source = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "5ef5592674ea42de75fc2792972e4ea0b6e3da6c" }
1001
1129
1002[[language]]
1130[[language]]
1003name = "zig"
1131name = "zig"
@@ -1036,12 +1164,11 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
1036
1164
1037[[grammar]]
1165[[grammar]]
1038name = "zig"
1166name = "zig"
1039−source = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "8d3224c3bd0890fe08358886ebf54fca2ed448a6" }
1167+source = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
1040
1168
1041[[language]]
1169[[language]]
1042name = "prolog"
1170name = "prolog"
1043scope = "source.prolog"
1171scope = "source.prolog"
1044−roots = []
1045file-types = ["pl", "prolog"]
1172file-types = ["pl", "prolog"]
1046shebangs = ["swipl"]
1173shebangs = ["swipl"]
1047comment-token = "%"
1174comment-token = "%"
@@ -1050,8 +1177,7 @@ language-servers = [ "swipl" ]
1050[[language]]
1177[[language]]
1051name = "tsq"
1178name = "tsq"
1052scope = "source.tsq"
1179scope = "source.tsq"
1053−file-types = ["scm"]
1180+file-types = ["tsq"]
1054−roots = []
1055comment-token = ";"
1181comment-token = ";"
1056injection-regex = "tsq"
1182injection-regex = "tsq"
1057indent = { tab-width = 2, unit = " " }
1183indent = { tab-width = 2, unit = " " }
@@ -1064,7 +1190,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-tsq", rev = "48b5
1064name = "cmake"
1190name = "cmake"
1065scope = "source.cmake"
1191scope = "source.cmake"
1066file-types = ["cmake", "CMakeLists.txt"]
1192file-types = ["cmake", "CMakeLists.txt"]
1067−roots = []
1068comment-token = "#"
1193comment-token = "#"
1069indent = { tab-width = 2, unit = " " }
1194indent = { tab-width = 2, unit = " " }
1070language-servers = [ "cmake-language-server" ]
1195language-servers = [ "cmake-language-server" ]
@@ -1077,9 +1202,9 @@ source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "6e51463ef30
1077[[language]]
1202[[language]]
1078name = "make"
1203name = "make"
1079scope = "source.make"
1204scope = "source.make"
1080−file-types = ["Makefile", "makefile", "make", "mk"]
1205+file-types = ["Makefile", "makefile", "make", "mk", "mak", "GNUmakefile", "OCamlMakefile"]
1206+shebangs = ["make", "gmake"]
1081injection-regex = "(make|makefile|Makefile|mk)"
1207injection-regex = "(make|makefile|Makefile|mk)"
1082−roots = []
1083comment-token = "#"
1208comment-token = "#"
1084indent = { tab-width = 4, unit = "\t" }
1209indent = { tab-width = 4, unit = "\t" }
1085
1210
@@ -1091,7 +1216,6 @@ source = { git = "https://github.com/alemuller/tree-sitter-make", rev = "a4b9187
1091name = "glsl"
1216name = "glsl"
1092scope = "source.glsl"
1217scope = "source.glsl"
1093file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
1218file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
1094−roots = []
1095comment-token = "//"
1219comment-token = "//"
1096indent = { tab-width = 4, unit = " " }
1220indent = { tab-width = 4, unit = " " }
1097injection-regex = "glsl"
1221injection-regex = "glsl"
@@ -1103,31 +1227,39 @@ source = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "88408ff
1103[[language]]
1227[[language]]
1104name = "perl"
1228name = "perl"
1105scope = "source.perl"
1229scope = "source.perl"
1106−file-types = ["pl", "pm", "t"]
1230+file-types = ["pl", "pm", "t", "psgi", "raku", "rakumod", "rakutest", "rakudoc", "nqp", "p6", "pl6", "pm6"]
1107shebangs = ["perl"]
1231shebangs = ["perl"]
1108−roots = []
1109comment-token = "#"
1232comment-token = "#"
1110language-servers = [ "perlnavigator" ]
1233language-servers = [ "perlnavigator" ]
1111indent = { tab-width = 2, unit = " " }
1234indent = { tab-width = 2, unit = " " }
1112
1235
1113[[grammar]]
1236[[grammar]]
1114name = "perl"
1237name = "perl"
1115−source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "ed21ecbcc128a6688770ebafd3ef68a1c9bc1ea9" }
1238+source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "e99bb5283805db4cb86c964722d709df21b0ac16" }
1239+
1240+[[language]]
1241+name = "pod"
1242+scope = "source.pod"
1243+injection-regex = "pod"
1244+file-types = ["pod"]
1245+
1246+[[grammar]]
1247+name = "pod"
1248+source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "d466b84009a63986834498073ec05d58d727d55f" }
1116
1249
1117[[language]]
1250[[language]]
1118name = "racket"
1251name = "racket"
1119scope = "source.racket"
1252scope = "source.racket"
1120−roots = []
1121file-types = ["rkt", "rktd", "rktl", "scrbl"]
1253file-types = ["rkt", "rktd", "rktl", "scrbl"]
1122shebangs = ["racket"]
1254shebangs = ["racket"]
1123comment-token = ";"
1255comment-token = ";"
1256+indent = { tab-width = 2, unit = " " }
1124language-servers = [ "racket" ]
1257language-servers = [ "racket" ]
1125grammar = "scheme"
1258grammar = "scheme"
1126
1259
1127[[language]]
1260[[language]]
1128name = "common-lisp"
1261name = "common-lisp"
1129scope = "source.lisp"
1262scope = "source.lisp"
1130−roots = []
1131file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
1263file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
1132shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1264shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1133comment-token = ";"
1265comment-token = ";"
@@ -1144,19 +1276,17 @@ grammar = "scheme"
1144[[language]]
1276[[language]]
1145name = "comment"
1277name = "comment"
1146scope = "scope.comment"
1278scope = "scope.comment"
1147−roots = []
1148file-types = []
1279file-types = []
1149injection-regex = "comment"
1280injection-regex = "comment"
1150
1281
1151[[grammar]]
1282[[grammar]]
1152name = "comment"
1283name = "comment"
1153−source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "5dd3c62f1bbe378b220fe16b317b85247898639e" }
1284+source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "a37ca370310ac6f89b6e0ebf2b86b2219780494e" }
1154
1285
1155[[language]]
1286[[language]]
1156name = "wgsl"
1287name = "wgsl"
1157scope = "source.wgsl"
1288scope = "source.wgsl"
1158file-types = ["wgsl"]
1289file-types = ["wgsl"]
1159−roots = []
1160comment-token = "//"
1290comment-token = "//"
1161language-servers = [ "wgsl_analyzer" ]
1291language-servers = [ "wgsl_analyzer" ]
1162indent = { tab-width = 4, unit = " " }
1292indent = { tab-width = 4, unit = " " }
@@ -1168,7 +1298,6 @@ source = { git = "https://github.com/szebniok/tree-sitter-wgsl", rev = "272e89ef
1168[[language]]
1298[[language]]
1169name = "llvm"
1299name = "llvm"
1170scope = "source.llvm"
1300scope = "source.llvm"
1171−roots = []
1172file-types = ["ll"]
1301file-types = ["ll"]
1173comment-token = ";"
1302comment-token = ";"
1174indent = { tab-width = 2, unit = " " }
1303indent = { tab-width = 2, unit = " " }
@@ -1176,12 +1305,11 @@ injection-regex = "llvm"
1176
1305
1177[[grammar]]
1306[[grammar]]
1178name = "llvm"
1307name = "llvm"
1179−source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "3b213925b9c4f42c1acfe2e10bfbb438d9c6834d" }
1308+source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "e9948edc41e9e5869af99dddb2b5ff5cc5581af6" }
1180
1309
1181[[language]]
1310[[language]]
1182name = "llvm-mir"
1311name = "llvm-mir"
1183scope = "source.llvm_mir"
1312scope = "source.llvm_mir"
1184−roots = []
1185file-types = []
1313file-types = []
1186comment-token = ";"
1314comment-token = ";"
1187indent = { tab-width = 2, unit = " " }
1315indent = { tab-width = 2, unit = " " }
@@ -1197,7 +1325,6 @@ name = "llvm-mir-yaml"
1197#
1325#
1198# grammar = "yaml"
1326# grammar = "yaml"
1199scope = "source.yaml"
1327scope = "source.yaml"
1200−roots = []
1201file-types = ["mir"]
1328file-types = ["mir"]
1202comment-token = "#"
1329comment-token = "#"
1203indent = { tab-width = 2, unit = " " }
1330indent = { tab-width = 2, unit = " " }
@@ -1205,7 +1332,6 @@ indent = { tab-width = 2, unit = " " }
1205[[language]]
1332[[language]]
1206name = "tablegen"
1333name = "tablegen"
1207scope = "source.tablegen"
1334scope = "source.tablegen"
1208−roots = []
1209file-types = ["td"]
1335file-types = ["td"]
1210comment-token = "//"
1336comment-token = "//"
1211indent = { tab-width = 2, unit = " " }
1337indent = { tab-width = 2, unit = " " }
@@ -1219,26 +1345,25 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd
1219name = "markdown"
1345name = "markdown"
1220scope = "source.md"
1346scope = "source.md"
1221injection-regex = "md|markdown"
1347injection-regex = "md|markdown"
1222−file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1348+file-types = ["md", "markdown", "PULLREQ_EDITMSG", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook"]
1223roots = [".marksman.toml"]
1349roots = [".marksman.toml"]
1224language-servers = [ "marksman" ]
1350language-servers = [ "marksman" ]
1225indent = { tab-width = 2, unit = " " }
1351indent = { tab-width = 2, unit = " " }
1226
1352
1227[[grammar]]
1353[[grammar]]
1228name = "markdown"
1354name = "markdown"
1229−source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "fa6bfd51727e4bef99f7eec5f43947f73d64ea7d", subpath = "tree-sitter-markdown" }
1355+source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown" }
1230
1356
1231[[language]]
1357[[language]]
1232name = "markdown.inline"
1358name = "markdown.inline"
1233scope = "source.markdown.inline"
1359scope = "source.markdown.inline"
1234injection-regex = "markdown\\.inline"
1360injection-regex = "markdown\\.inline"
1235file-types = []
1361file-types = []
1236−roots = []
1237grammar = "markdown_inline"
1362grammar = "markdown_inline"
1238
1363
1239[[grammar]]
1364[[grammar]]
1240name = "markdown_inline"
1365name = "markdown_inline"
1241−source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "fa6bfd51727e4bef99f7eec5f43947f73d64ea7d", subpath = "tree-sitter-markdown-inline" }
1366+source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown-inline" }
1242
1367
1243[[language]]
1368[[language]]
1244name = "dart"
1369name = "dart"
@@ -1284,7 +1409,6 @@ source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev =
1284[[language]]
1409[[language]]
1285name = "git-commit"
1410name = "git-commit"
1286scope = "git.commitmsg"
1411scope = "git.commitmsg"
1287−roots = []
1288file-types = ["COMMIT_EDITMSG"]
1412file-types = ["COMMIT_EDITMSG"]
1289comment-token = "#"
1413comment-token = "#"
1290indent = { tab-width = 2, unit = " " }
1414indent = { tab-width = 2, unit = " " }
@@ -1293,13 +1417,12 @@ text-width = 72
1293
1417
1294[[grammar]]
1418[[grammar]]
1295name = "git-commit"
1419name = "git-commit"
1296−source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "db88cffa3952dd2328b741af5d0fc69bdb76704f" }
1420+source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "6f193a66e9aa872760823dff020960c6cedc37b3" }
1297
1421
1298[[language]]
1422[[language]]
1299name = "diff"
1423name = "diff"
1300scope = "source.diff"
1424scope = "source.diff"
1301−roots = []
1302−file-types = ["diff", "patch"]
1425+file-types = ["diff", "patch", "rej"]
1303injection-regex = "diff"
1426injection-regex = "diff"
1304comment-token = "#"
1427comment-token = "#"
1305indent = { tab-width = 2, unit = " " }
1428indent = { tab-width = 2, unit = " " }
@@ -1311,7 +1434,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-diff", rev = "fd7
1311[[language]]
1434[[language]]
1312name = "git-rebase"
1435name = "git-rebase"
1313scope = "source.gitrebase"
1436scope = "source.gitrebase"
1314−roots = []
1315file-types = ["git-rebase-todo"]
1437file-types = ["git-rebase-todo"]
1316injection-regex = "git-rebase"
1438injection-regex = "git-rebase"
1317comment-token = "#"
1439comment-token = "#"
@@ -1326,7 +1448,6 @@ name = "regex"
1326scope = "source.regex"
1448scope = "source.regex"
1327injection-regex = "regex"
1449injection-regex = "regex"
1328file-types = ["regex", ".Rbuildignore"]
1450file-types = ["regex", ".Rbuildignore"]
1329−roots = []
1330
1451
1331[[grammar]]
1452[[grammar]]
1332name = "regex"
1453name = "regex"
@@ -1335,7 +1456,6 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf
1335[[language]]
1456[[language]]
1336name = "git-config"
1457name = "git-config"
1337scope = "source.gitconfig"
1458scope = "source.gitconfig"
1338−roots = []
1339file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }]
1459file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }]
1340injection-regex = "git-config"
1460injection-regex = "git-config"
1341comment-token = "#"
1461comment-token = "#"
@@ -1348,7 +1468,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev
1348[[language]]
1468[[language]]
1349name = "git-attributes"
1469name = "git-attributes"
1350scope = "source.gitattributes"
1470scope = "source.gitattributes"
1351−roots = []
1352file-types = [".gitattributes"]
1471file-types = [".gitattributes"]
1353injection-regex = "git-attributes"
1472injection-regex = "git-attributes"
1354comment-token = "#"
1473comment-token = "#"
@@ -1361,8 +1480,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
1361[[language]]
1480[[language]]
1362name = "git-ignore"
1481name = "git-ignore"
1363scope = "source.gitignore"
1482scope = "source.gitignore"
1364−roots = []
1365−file-types = [".gitignore", ".gitignore_global"]
1483+file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"]
1366injection-regex = "git-ignore"
1484injection-regex = "git-ignore"
1367comment-token = "#"
1485comment-token = "#"
1368grammar = "gitignore"
1486grammar = "gitignore"
@@ -1376,7 +1494,7 @@ name = "graphql"
1376scope = "source.graphql"
1494scope = "source.graphql"
1377injection-regex = "graphql"
1495injection-regex = "graphql"
1378file-types = ["gql", "graphql", "graphqls"]
1496file-types = ["gql", "graphql", "graphqls"]
1379−roots = []
1497+language-servers = [ "graphql-language-service" ]
1380indent = { tab-width = 2, unit = " " }
1498indent = { tab-width = 2, unit = " " }
1381
1499
1382[[grammar]]
1500[[grammar]]
@@ -1403,7 +1521,6 @@ name = "iex"
1403scope = "source.iex"
1521scope = "source.iex"
1404injection-regex = "iex"
1522injection-regex = "iex"
1405file-types = ["iex"]
1523file-types = ["iex"]
1406−roots = []
1407
1524
1408[[grammar]]
1525[[grammar]]
1409name = "iex"
1526name = "iex"
@@ -1422,7 +1539,7 @@ indent = { tab-width = 2, unit = " " }
1422
1539
1423[[grammar]]
1540[[grammar]]
1424name = "rescript"
1541name = "rescript"
1425−source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "65609807c628477f3b94052e7ef895885ac51c3c" }
1542+source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "467dcf99f68c47823d7b378779a6b282d7ef9782" }
1426
1543
1427[[language]]
1544[[language]]
1428name = "erlang"
1545name = "erlang"
@@ -1466,7 +1583,6 @@ scope = "source.hcl"
1466injection-regex = "(hcl|tf|nomad)"
1583injection-regex = "(hcl|tf|nomad)"
1467language-id = "terraform"
1584language-id = "terraform"
1468file-types = ["hcl", "tf", "nomad"]
1585file-types = ["hcl", "tf", "nomad"]
1469−roots = []
1470comment-token = "#"
1586comment-token = "#"
1471indent = { tab-width = 2, unit = " " }
1587indent = { tab-width = 2, unit = " " }
1472language-servers = [ "terraform-ls" ]
1588language-servers = [ "terraform-ls" ]
@@ -1481,7 +1597,6 @@ name = "tfvars"
1481scope = "source.tfvars"
1597scope = "source.tfvars"
1482language-id = "terraform-vars"
1598language-id = "terraform-vars"
1483file-types = ["tfvars"]
1599file-types = ["tfvars"]
1484−roots = []
1485comment-token = "#"
1600comment-token = "#"
1486indent = { tab-width = 2, unit = " " }
1601indent = { tab-width = 2, unit = " " }
1487language-servers = [ "terraform-ls" ]
1602language-servers = [ "terraform-ls" ]
@@ -1493,7 +1608,6 @@ name = "org"
1493scope = "source.org"
1608scope = "source.org"
1494injection-regex = "org"
1609injection-regex = "org"
1495file-types = ["org"]
1610file-types = ["org"]
1496−roots = []
1497indent = { tab-width = 2, unit = " " }
1611indent = { tab-width = 2, unit = " " }
1498
1612
1499[[grammar]]
1613[[grammar]]
@@ -1505,7 +1619,6 @@ name = "solidity"
1505scope = "source.sol"
1619scope = "source.sol"
1506injection-regex = "(sol|solidity)"
1620injection-regex = "(sol|solidity)"
1507file-types = ["sol"]
1621file-types = ["sol"]
1508−roots = []
1509comment-token = "//"
1622comment-token = "//"
1510indent = { tab-width = 4, unit = " " }
1623indent = { tab-width = 4, unit = " " }
1511language-servers = [ "solc" ]
1624language-servers = [ "solc" ]
@@ -1523,20 +1636,23 @@ roots = ["gleam.toml"]
1523comment-token = "//"
1636comment-token = "//"
1524indent = { tab-width = 2, unit = " " }
1637indent = { tab-width = 2, unit = " " }
1525language-servers = [ "gleam" ]
1638language-servers = [ "gleam" ]
1639+auto-format = true
1526
1640
1527[[grammar]]
1641[[grammar]]
1528name = "gleam"
1642name = "gleam"
1529−source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "ae79782c00656945db69641378e688cdb78d52c1" }
1643+source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "b2afa4fd6bb41a7bf912b034c653c90af7ae5122" }
1530
1644
1531[[language]]
1645[[language]]
1532name = "ron"
1646name = "ron"
1533scope = "source.ron"
1647scope = "source.ron"
1534injection-regex = "ron"
1648injection-regex = "ron"
1535file-types = ["ron"]
1649file-types = ["ron"]
1536−roots = []
1537comment-token = "//"
1650comment-token = "//"
1538indent = { tab-width = 4, unit = " " }
1651indent = { tab-width = 4, unit = " " }
1652+
1539−grammar = "rust"
1653+[[grammar]]
1654+name = "ron"
1655+source = { git = "https://github.com/zee-editor/tree-sitter-ron", rev = "7762d709a0f7c1f9e269d0125a2e8a7a69006146" }
1540
1656
1541[[language]]
1657[[language]]
1542name = "robot"
1658name = "robot"
@@ -1544,21 +1660,19 @@ scope = "source.robot"
1544injection-regex = "robot"
1660injection-regex = "robot"
1545file-types = ["robot", "resource"]
1661file-types = ["robot", "resource"]
1546comment-token = "#"
1662comment-token = "#"
1547−roots = []
1548indent = { tab-width = 4, unit = " " }
1663indent = { tab-width = 4, unit = " " }
1549language-servers = [ "robotframework_ls" ]
1664language-servers = [ "robotframework_ls" ]
1550
1665
1551[[grammar]]
1666[[grammar]]
1552name = "robot"
1667name = "robot"
1553−source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "f1142bfaa6acfce95e25d2c6d18d218f4f533927" }
1668+source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "322e4cc65754d2b3fdef4f2f8a71e0762e3d13af" }
1554
1669
1555[[language]]
1670[[language]]
1556name = "r"
1671name = "r"
1557scope = "source.r"
1672scope = "source.r"
1558injection-regex = "(r|R)"
1673injection-regex = "(r|R)"
1559−file-types = ["r", "R", ".Rprofile", "Rprofile.site"]
1674+file-types = ["r", "R", ".Rprofile", "Rprofile.site", ".RHistory"]
1560shebangs = ["r", "R"]
1675shebangs = ["r", "R"]
1561−roots = []
1562comment-token = "#"
1676comment-token = "#"
1563indent = { tab-width = 2, unit = " " }
1677indent = { tab-width = 2, unit = " " }
1564language-servers = [ "r" ]
1678language-servers = [ "r" ]
@@ -1570,9 +1684,9 @@ source = { git = "https://github.com/r-lib/tree-sitter-r", rev = "cc04302e1bff76
1570[[language]]
1684[[language]]
1571name = "rmarkdown"
1685name = "rmarkdown"
1572scope = "source.rmd"
1686scope = "source.rmd"
1687+language-id = "rmd"
1573injection-regex = "(r|R)md"
1688injection-regex = "(r|R)md"
1574file-types = ["rmd", "Rmd"]
1689file-types = ["rmd", "Rmd"]
1575−roots = []
1576indent = { tab-width = 2, unit = " " }
1690indent = { tab-width = 2, unit = " " }
1577grammar = "markdown"
1691grammar = "markdown"
1578language-servers = [ "r" ]
1692language-servers = [ "r" ]
@@ -1596,7 +1710,6 @@ name = "erb"
1596scope = "text.html.erb"
1710scope = "text.html.erb"
1597injection-regex = "erb"
1711injection-regex = "erb"
1598file-types = ["erb"]
1712file-types = ["erb"]
1599−roots = []
1600indent = { tab-width = 2, unit = " " }
1713indent = { tab-width = 2, unit = " " }
1601grammar = "embedded-template"
1714grammar = "embedded-template"
1602
1715
@@ -1605,7 +1718,6 @@ name = "ejs"
1605scope = "text.html.ejs"
1718scope = "text.html.ejs"
1606injection-regex = "ejs"
1719injection-regex = "ejs"
1607file-types = ["ejs"]
1720file-types = ["ejs"]
1608−roots = []
1609indent = { tab-width = 2, unit = " " }
1721indent = { tab-width = 2, unit = " " }
1610grammar = "embedded-template"
1722grammar = "embedded-template"
1611
1723
@@ -1641,15 +1753,14 @@ source = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "
1641[[language]]
1753[[language]]
1642name = "sql"
1754name = "sql"
1643scope = "source.sql"
1755scope = "source.sql"
1644−file-types = ["sql"]
1756+file-types = ["sql", "dsql"]
1645−roots = []
1646comment-token = "--"
1757comment-token = "--"
1647indent = { tab-width = 4, unit = " " }
1758indent = { tab-width = 4, unit = " " }
1648injection-regex = "sql"
1759injection-regex = "sql"
1649
1760
1650[[grammar]]
1761[[grammar]]
1651name = "sql"
1762name = "sql"
1652−source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "7cbac0472e5b8f8486ce64ffbcf1982d5cd5fc8d" }
1763+source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "25be0b8f17e9189ad9e1b875869d025c5aec1286" }
1653
1764
1654[[language]]
1765[[language]]
1655name = "gdscript"
1766name = "gdscript"
@@ -1688,20 +1799,19 @@ scope = "source.nu"
1688injection-regex = "nu"
1799injection-regex = "nu"
1689file-types = ["nu"]
1800file-types = ["nu"]
1690shebangs = ["nu"]
1801shebangs = ["nu"]
1691−roots = []
1692comment-token = "#"
1802comment-token = "#"
1693indent = { tab-width = 2, unit = " " }
1803indent = { tab-width = 2, unit = " " }
1804+language-servers = [ "nu-lsp" ]
1694
1805
1695[[grammar]]
1806[[grammar]]
1696name = "nu"
1807name = "nu"
1697−source = { git = "https://github.com/LhKipp/tree-sitter-nu", rev = "eb95bdac3abd73ef47e53f19c63e74a31405ebd2" }
1808+source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "98c11c491e3405c75affa1cf004097692da3dda2" }
1698
1809
1699[[language]]
1810[[language]]
1700name = "vala"
1811name = "vala"
1701scope = "source.vala"
1812scope = "source.vala"
1702injection-regex = "vala"
1813injection-regex = "vala"
1703file-types = ["vala", "vapi"]
1814file-types = ["vala", "vapi"]
1704−roots = []
1705comment-token = "//"
1815comment-token = "//"
1706indent = { tab-width = 2, unit = " " }
1816indent = { tab-width = 2, unit = " " }
1707language-servers = [ "vala-language-server" ]
1817language-servers = [ "vala-language-server" ]
@@ -1715,20 +1825,18 @@ name = "hare"
1715scope = "source.hare"
1825scope = "source.hare"
1716injection-regex = "hare"
1826injection-regex = "hare"
1717file-types = ["ha"]
1827file-types = ["ha"]
1718−roots = []
1719comment-token = "//"
1828comment-token = "//"
1720indent = { tab-width = 8, unit = "\t" }
1829indent = { tab-width = 8, unit = "\t" }
1721
1830
1722[[grammar]]
1831[[grammar]]
1723name = "hare"
1832name = "hare"
1724−source = { git = "https://git.sr.ht/~ecmma/tree-sitter-hare", rev = "bc26a6a949f2e0d98b7bfc437d459b250900a165" }
1833+source = { git = "https://git.sr.ht/~ecmma/tree-sitter-hare", rev = "2495958aaf3f93581c87ec020164255e80655331" }
1725
1834
1726[[language]]
1835[[language]]
1727name = "devicetree"
1836name = "devicetree"
1728scope = "source.devicetree"
1837scope = "source.devicetree"
1729injection-regex = "(dtsi?|devicetree|fdt)"
1838injection-regex = "(dtsi?|devicetree|fdt)"
1730file-types = ["dts", "dtsi"]
1839file-types = ["dts", "dtsi"]
1731−roots = []
1732comment-token = "//"
1840comment-token = "//"
1733indent = { tab-width = 4, unit = "\t" }
1841indent = { tab-width = 4, unit = "\t" }
1734
1842
@@ -1741,7 +1849,6 @@ name = "cairo"
1741scope = "source.cairo"
1849scope = "source.cairo"
1742injection-regex = "cairo"
1850injection-regex = "cairo"
1743file-types = ["cairo"]
1851file-types = ["cairo"]
1744−roots = []
1745comment-token = "//"
1852comment-token = "//"
1746indent = { tab-width = 4, unit = " " }
1853indent = { tab-width = 4, unit = " " }
1747# auto-format = true
1854# auto-format = true
@@ -1753,7 +1860,6 @@ name = "cpon"
1753scope = "scope.cpon"
1860scope = "scope.cpon"
1754injection-regex = "cpon"
1861injection-regex = "cpon"
1755file-types = ["cpon", "cp"]
1862file-types = ["cpon", "cp"]
1756−roots = []
1757auto-format = true
1863auto-format = true
1758comment-token = "//"
1864comment-token = "//"
1759indent = { tab-width = 2, unit = " " }
1865indent = { tab-width = 2, unit = " " }
@@ -1780,8 +1886,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4
1780name = "meson"
1886name = "meson"
1781scope = "source.meson"
1887scope = "source.meson"
1782injection-regex = "meson"
1888injection-regex = "meson"
1783−file-types = ["meson.build"]
1889+file-types = ["meson.build", "meson_options.txt"]
1784−roots = []
1785comment-token = "#"
1890comment-token = "#"
1786indent = { tab-width = 2, unit = " " }
1891indent = { tab-width = 2, unit = " " }
1787
1892
@@ -1793,7 +1898,6 @@ source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8
1793name = "sshclientconfig"
1898name = "sshclientconfig"
1794scope = "source.sshclientconfig"
1899scope = "source.sshclientconfig"
1795file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }]
1900file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }]
1796−roots = []
1797comment-token = "#"
1901comment-token = "#"
1798
1902
1799[[grammar]]
1903[[grammar]]
@@ -1804,14 +1908,14 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev =
1804name = "scheme"
1908name = "scheme"
1805scope = "source.scheme"
1909scope = "source.scheme"
1806injection-regex = "scheme"
1910injection-regex = "scheme"
1807−file-types = ["ss"] # "scm",
1911+file-types = ["ss", "scm"]
1808−roots = []
1912+shebangs = ["scheme", "guile", "chicken"]
1809comment-token = ";"
1913comment-token = ";"
1810indent = { tab-width = 2, unit = " " }
1914indent = { tab-width = 2, unit = " " }
1811
1915
1812[[grammar]]
1916[[grammar]]
1813name = "scheme"
1917name = "scheme"
1814−source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "c0741320bfca6b7b5b7a13b5171275951e96a842" }
1918+source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af3af6c9356b936f8a515a1e449c32e804c2b1a8" }
1815
1919
1816[[language]]
1920[[language]]
1817name = "v"
1921name = "v"
@@ -1826,13 +1930,12 @@ indent = { tab-width = 4, unit = "\t" }
1826
1930
1827[[grammar]]
1931[[grammar]]
1828name = "v"
1932name = "v"
1829−source = { git = "https://github.com/vlang/vls", subpath = "tree_sitter_v", rev = "66cf9d3086fb5ecc827cb32c64c5d812ab17d2c6" }
1933+source = {git = "https://github.com/v-analyzer/v-analyzer", subpath = "tree_sitter_v", rev = "e14fdf6e661b10edccc744102e4ccf0b187aa8ad"}
1830
1934
1831[[language]]
1935[[language]]
1832name = "verilog"
1936name = "verilog"
1833scope = "source.verilog"
1937scope = "source.verilog"
1834file-types = ["v", "vh", "sv", "svh"]
1938file-types = ["v", "vh", "sv", "svh"]
1835−roots = []
1836comment-token = "//"
1939comment-token = "//"
1837language-servers = [ "svlangserver" ]
1940language-servers = [ "svlangserver" ]
1838indent = { tab-width = 2, unit = " " }
1941indent = { tab-width = 2, unit = " " }
@@ -1847,7 +1950,6 @@ name = "edoc"
1847scope = "source.edoc"
1950scope = "source.edoc"
1848file-types = ["edoc", "edoc.in"]
1951file-types = ["edoc", "edoc.in"]
1849injection-regex = "edoc"
1952injection-regex = "edoc"
1850−roots = []
1851indent = { tab-width = 4, unit = " " }
1953indent = { tab-width = 4, unit = " " }
1852
1954
1853[[grammar]]
1955[[grammar]]
@@ -1859,7 +1961,6 @@ name = "jsdoc"
1859scope = "source.jsdoc"
1961scope = "source.jsdoc"
1860injection-regex = "jsdoc"
1962injection-regex = "jsdoc"
1861file-types = ["jsdoc"]
1963file-types = ["jsdoc"]
1862−roots = []
1863indent = { tab-width = 2, unit = " " }
1964indent = { tab-width = 2, unit = " " }
1864
1965
1865[[grammar]]
1966[[grammar]]
@@ -1871,7 +1972,6 @@ name = "openscad"
1871scope = "source.openscad"
1972scope = "source.openscad"
1872injection-regex = "openscad"
1973injection-regex = "openscad"
1873file-types = ["scad"]
1974file-types = ["scad"]
1874−roots = []
1875comment-token = "//"
1975comment-token = "//"
1876language-servers = [ "openscad-lsp" ]
1976language-servers = [ "openscad-lsp" ]
1877indent = { tab-width = 2, unit = "\t" }
1977indent = { tab-width = 2, unit = "\t" }
@@ -1912,8 +2012,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569
1912name = "starlark"
2012name = "starlark"
1913scope = "source.starlark"
2013scope = "source.starlark"
1914injection-regex = "(starlark|bzl|bazel)"
2014injection-regex = "(starlark|bzl|bazel)"
1915−file-types = ["bzl", "bazel", "BUILD"]
2015+file-types = ["bzl", "bazel", "BUILD", "star"]
1916−roots = []
1917comment-token = "#"
2016comment-token = "#"
1918indent = { tab-width = 4, unit = " " }
2017indent = { tab-width = 4, unit = " " }
1919grammar = "python"
2018grammar = "python"
@@ -1922,7 +2021,6 @@ grammar = "python"
1922name = "elvish"
2021name = "elvish"
1923scope = "source.elvish"
2022scope = "source.elvish"
1924file-types = ["elv"]
2023file-types = ["elv"]
1925−roots = []
1926comment-token = "#"
2024comment-token = "#"
1927indent = { tab-width = 2, unit = " " }
2025indent = { tab-width = 2, unit = " " }
1928language-servers = [ "elvish" ]
2026language-servers = [ "elvish" ]
@@ -1938,7 +2036,6 @@ scope = "source.idr"
1938injection-regex = "idr"
2036injection-regex = "idr"
1939file-types = ["idr"]
2037file-types = ["idr"]
1940shebangs = []
2038shebangs = []
1941−roots = []
1942comment-token = "--"
2039comment-token = "--"
1943indent = { tab-width = 2, unit = " " }
2040indent = { tab-width = 2, unit = " " }
1944language-servers = [ "idris2-lsp" ]
2041language-servers = [ "idris2-lsp" ]
@@ -1962,20 +2059,18 @@ name = "ungrammar"
1962scope = "source.ungrammar"
2059scope = "source.ungrammar"
1963injection-regex = "ungrammar"
2060injection-regex = "ungrammar"
1964file-types = ["ungram", "ungrammar"]
2061file-types = ["ungram", "ungrammar"]
1965−roots = []
1966comment-token = "//"
2062comment-token = "//"
1967indent = { tab-width = 2, unit = " " }
2063indent = { tab-width = 2, unit = " " }
1968
2064
1969[[grammar]]
2065[[grammar]]
1970name = "ungrammar"
2066name = "ungrammar"
1971−source = { git = "https://github.com/Philipp-M/tree-sitter-ungrammar", rev = "0113de880a58ea14f2a75802e9b99fcc25003d9c" }
2067+source = { git = "https://github.com/Philipp-M/tree-sitter-ungrammar", rev = "a7e104629cff5a8b7367187610631e8f5eb7c6ea" }
1972
2068
1973[[language]]
2069[[language]]
1974name = "dot"
2070name = "dot"
1975scope = "source.dot"
2071scope = "source.dot"
1976injection-regex = "dot"
2072injection-regex = "dot"
1977file-types = ["dot"]
2073file-types = ["dot"]
1978−roots = []
1979comment-token = "//"
2074comment-token = "//"
1980indent = { tab-width = 4, unit = " " }
2075indent = { tab-width = 4, unit = " " }
1981language-servers = [ "dot-language-server" ]
2076language-servers = [ "dot-language-server" ]
@@ -2005,21 +2100,19 @@ name = "slint"
2005scope = "source.slint"
2100scope = "source.slint"
2006injection-regex = "slint"
2101injection-regex = "slint"
2007file-types = ["slint"]
2102file-types = ["slint"]
2008−roots = []
2009comment-token = "//"
2103comment-token = "//"
2010indent = { tab-width = 4, unit = " " }
2104indent = { tab-width = 4, unit = " " }
2011language-servers = [ "slint-lsp" ]
2105language-servers = [ "slint-lsp" ]
2012
2106
2013[[grammar]]
2107[[grammar]]
2014name = "slint"
2108name = "slint"
2015−source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "0d4dda94f96623302dfc234e06be62a5717f47da" }
2109+source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "00c8a2d3645766f68c0d0460086c0a994e5b0d85" }
2016
2110
2017[[language]]
2111[[language]]
2018name = "task"
2112name = "task"
2019scope = "source.task"
2113scope = "source.task"
2020injection-regex = "task"
2114injection-regex = "task"
2021file-types = ["task"]
2115file-types = ["task"]
2022−roots = []
2023comment-token = "#"
2116comment-token = "#"
2024indent = { tab-width = 2, unit = " " }
2117indent = { tab-width = 2, unit = " " }
2025
2118
@@ -2032,7 +2125,6 @@ name = "xit"
2032scope = "source.xit"
2125scope = "source.xit"
2033injection-regex = "xit"
2126injection-regex = "xit"
2034file-types = ["xit"]
2127file-types = ["xit"]
2035−roots = []
2036indent = { tab-width = 4, unit = " " }
2128indent = { tab-width = 4, unit = " " }
2037
2129
2038[[grammar]]
2130[[grammar]]
@@ -2050,14 +2142,13 @@ roots = ["edgedb.toml"]
2050
2142
2051[[grammar]]
2143[[grammar]]
2052name ="esdl"
2144name ="esdl"
2053−source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "b840c8a8028127e0a7c6e6c45141adade2bd75cf" }
2145+source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "df83acc8cacd0cfb139eecee0e718dc32c4f92e2" }
2054
2146
2055[[language]]
2147[[language]]
2056name = "pascal"
2148name = "pascal"
2057scope = "source.pascal"
2149scope = "source.pascal"
2058injection-regex = "pascal"
2150injection-regex = "pascal"
2059file-types = ["pas", "pp", "inc", "lpr", "lfm"]
2151file-types = ["pas", "pp", "inc", "lpr", "lfm"]
2060−roots = []
2061comment-token = "//"
2152comment-token = "//"
2062indent = { tab-width = 2, unit = " " }
2153indent = { tab-width = 2, unit = " " }
2063language-servers = [ "pasls" ]
2154language-servers = [ "pasls" ]
@@ -2072,7 +2163,6 @@ scope = "source.sml"
2072injection-regex = "sml"
2163injection-regex = "sml"
2073file-types = ["sml"]
2164file-types = ["sml"]
2074comment-token = "(*"
2165comment-token = "(*"
2075−roots = []
2076
2166
2077[[grammar]]
2167[[grammar]]
2078name = "sml"
2168name = "sml"
@@ -2096,19 +2186,17 @@ name = "astro"
2096scope = "source.astro"
2186scope = "source.astro"
2097injection-regex = "astro"
2187injection-regex = "astro"
2098file-types = ["astro"]
2188file-types = ["astro"]
2099−roots = []
2100indent = { tab-width = 2, unit = " " }
2189indent = { tab-width = 2, unit = " " }
2101
2190
2102[[grammar]]
2191[[grammar]]
2103name = "astro"
2192name = "astro"
2104−source = { git = "https://github.com/virchau13/tree-sitter-astro", rev = "5f5c3e73c45967df9aa42f861fad2d77cd4e0900" }
2193+source = { git = "https://github.com/virchau13/tree-sitter-astro", rev = "947e93089e60c66e681eba22283f4037841451e7" }
2105
2194
2106[[language]]
2195[[language]]
2107name = "bass"
2196name = "bass"
2108scope = "source.bass"
2197scope = "source.bass"
2109injection-regex = "bass"
2198injection-regex = "bass"
2110file-types = ["bass"]
2199file-types = ["bass"]
2111−roots = []
2112comment-token = ";"
2200comment-token = ";"
2113indent = { tab-width = 2, unit = " " }
2201indent = { tab-width = 2, unit = " " }
2114language-servers = [ "bass" ]
2202language-servers = [ "bass" ]
@@ -2122,7 +2210,6 @@ name = "wat"
2122scope = "source.wat"
2210scope = "source.wat"
2123comment-token = ";;"
2211comment-token = ";;"
2124file-types = ["wat"]
2212file-types = ["wat"]
2125−roots = []
2126
2213
2127[[grammar]]
2214[[grammar]]
2128name = "wat"
2215name = "wat"
@@ -2133,7 +2220,6 @@ name = "wast"
2133scope = "source.wast"
2220scope = "source.wast"
2134comment-token = ";;"
2221comment-token = ";;"
2135file-types = ["wast"]
2222file-types = ["wast"]
2136−roots = []
2137
2223
2138[[grammar]]
2224[[grammar]]
2139name = "wast"
2225name = "wast"
@@ -2143,7 +2229,6 @@ source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f
2143name = "d"
2229name = "d"
2144scope = "source.d"
2230scope = "source.d"
2145file-types = [ "d", "dd" ]
2231file-types = [ "d", "dd" ]
2146−roots = []
2147comment-token = "//"
2232comment-token = "//"
2148injection-regex = "d"
2233injection-regex = "d"
2149indent = { tab-width = 4, unit = " "}
2234indent = { tab-width = 4, unit = " "}
@@ -2152,13 +2237,12 @@ formatter = { command = "dfmt" }
2152
2237
2153[[grammar]]
2238[[grammar]]
2154name = "d"
2239name = "d"
2155−source = { git = "https://github.com/gdamore/tree-sitter-d", rev="601c4a1e8310fb2f3c43fa8a923d0d27497f3c04" }
2240+source = { git = "https://github.com/gdamore/tree-sitter-d", rev = "5566f8ce8fc24186fad06170bbb3c8d97c935d74" }
2156
2241
2157[[language]]
2242[[language]]
2158name = "vhs"
2243name = "vhs"
2159scope = "source.vhs"
2244scope = "source.vhs"
2160file-types = ["tape"]
2245file-types = ["tape"]
2161−roots = []
2162comment-token = "#"
2246comment-token = "#"
2163indent = { tab-width = 2, unit = " " }
2247indent = { tab-width = 2, unit = " " }
2164grammar = "vhs"
2248grammar = "vhs"
@@ -2171,21 +2255,76 @@ source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d8
2171name = "kdl"
2255name = "kdl"
2172scope = "source.kdl"
2256scope = "source.kdl"
2173file-types = ["kdl"]
2257file-types = ["kdl"]
2174−roots = []
2175comment-token = "//"
2258comment-token = "//"
2176injection-regex = "kdl"
2259injection-regex = "kdl"
2177
2260
2178[[grammar]]
2261[[grammar]]
2179name = "kdl"
2262name = "kdl"
2180−source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d15df6610484e1d4b5c987ecad52373" }
2263+source = { git = "https://github.com/amaanq/tree-sitter-kdl", rev = "3ca569b9f9af43593c24f9e7a21f02f43a13bb88" }
2181
2264
2182[[language]]
2265[[language]]
2183name = "xml"
2266name = "xml"
2184scope = "source.xml"
2267scope = "source.xml"
2185injection-regex = "xml"
2268injection-regex = "xml"
2186−file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd", "gml", "xaml", "gir"]
2269+file-types = [
2270+ "xml",
2271+ "mobileconfig",
2272+ "plist",
2273+ "xib",
2274+ "storyboard",
2275+ "svg",
2276+ "xsd",
2277+ "gml",
2278+ "xaml",
2279+ "gir",
2280+ "rss",
2281+ "atom",
2282+ "opml",
2283+ "policy",
2284+ "ascx",
2285+ "axml",
2286+ "axaml",
2287+ "bpmn",
2288+ "cpt",
2289+ "csl",
2290+ "csproj.user",
2291+ "dita",
2292+ "ditamap",
2293+ "dtml",
2294+ "fxml",
2295+ "iml",
2296+ "isml",
2297+ "jmx",
2298+ "launch",
2299+ "menu",
2300+ "mxml",
2301+ "nuspec",
2302+ "pt",
2303+ "publishsettings",
2304+ "pubxml",
2305+ "pubxml.user",
2306+ "rbxlx",
2307+ "rbxmx",
2308+ "rng",
2309+ "shproj",
2310+ "tld",
2311+ "tmx",
2312+ "vbproj.user",
2313+ "vcxproj",
2314+ "vcxproj.filters",
2315+ "wsdl",
2316+ "wxi",
2317+ "wxs",
2318+ "xbl",
2319+ "xlf",
2320+ "xliff",
2321+ "xpdl",
2322+ "xul",
2323+ "xoml",
2324+ "musicxml",
2325+ "glif"
2326+]
2187indent = { tab-width = 2, unit = " " }
2327indent = { tab-width = 2, unit = " " }
2188−roots = []
2189
2328
2190[language.auto-pairs]
2329[language.auto-pairs]
2191'(' = ')'
2330'(' = ')'
@@ -2206,7 +2345,6 @@ scope = "source.dtd"
2206injection-regex = "dtd"
2345injection-regex = "dtd"
2207file-types = ["dtd", "ent"]
2346file-types = ["dtd", "ent"]
2208indent = {tab-width = 2, unit = " "}
2347indent = {tab-width = 2, unit = " "}
2209−roots = []
2210
2348
2211[language.auto-pairs]
2349[language.auto-pairs]
2212'(' = ')'
2350'(' = ')'
@@ -2224,7 +2362,6 @@ name = "wit"
2224scope = "source.wit"
2362scope = "source.wit"
2225injection-regex = "wit"
2363injection-regex = "wit"
2226file-types = ["wit"]
2364file-types = ["wit"]
2227−roots = []
2228comment-token = "//"
2365comment-token = "//"
2229indent = { tab-width = 2, unit = " " }
2366indent = { tab-width = 2, unit = " " }
2230
2367
@@ -2243,11 +2380,10 @@ source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9a
2243[[language]]
2380[[language]]
2244name = "env"
2381name = "env"
2245scope = "source.env"
2382scope = "source.env"
2246−file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc"]
2383+file-types = [".env", ".env.local", ".env.development", ".env.production", ".env.dist", ".envrc", ".envrc.local", ".envrc.private"]
2247injection-regex = "env"
2384injection-regex = "env"
2248comment-token = "#"
2385comment-token = "#"
2249indent = { tab-width = 4, unit = "\t" }
2386indent = { tab-width = 4, unit = "\t" }
2250−roots = []
2251grammar = "bash"
2387grammar = "bash"
2252
2388
2253[[language]]
2389[[language]]
@@ -2271,12 +2407,15 @@ file-types = [
2271 "container",
2407 "container",
2272 "volume",
2408 "volume",
2273 "kube",
2409 "kube",
2274− "network"
2410+ "network",
2411+ ".editorconfig",
2412+ "properties",
2413+ "cfg",
2414+ "directory"
2275]
2415]
2276injection-regex = "ini"
2416injection-regex = "ini"
2277comment-token = "#"
2417comment-token = "#"
2278indent = { tab-width = 4, unit = "\t" }
2418indent = { tab-width = 4, unit = "\t" }
2279−roots = []
2280
2419
2281[[grammar]]
2420[[grammar]]
2282name = "ini"
2421name = "ini"
@@ -2286,7 +2425,6 @@ source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb87
2286name = "bicep"
2425name = "bicep"
2287scope = "source.bicep"
2426scope = "source.bicep"
2288file-types = ["bicep"]
2427file-types = ["bicep"]
2289−roots = []
2290auto-format = true
2428auto-format = true
2291comment-token = "//"
2429comment-token = "//"
2292indent = { tab-width = 2, unit = " "}
2430indent = { tab-width = 2, unit = " "}
@@ -2300,7 +2438,6 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-bicep", rev = "d8
2300name = "qml"
2438name = "qml"
2301scope = "source.qml"
2439scope = "source.qml"
2302file-types = ["qml"]
2440file-types = ["qml"]
2303−roots = []
2304language-servers = [ "qmlls" ]
2441language-servers = [ "qmlls" ]
2305indent = { tab-width = 4, unit = " " }
2442indent = { tab-width = 4, unit = " " }
2306grammar = "qmljs"
2443grammar = "qmljs"
@@ -2314,7 +2451,6 @@ name = "mermaid"
2314scope = "source.mermaid"
2451scope = "source.mermaid"
2315injection-regex = "mermaid"
2452injection-regex = "mermaid"
2316file-types = ["mermaid"]
2453file-types = ["mermaid"]
2317−roots = []
2318comment-token = "%%"
2454comment-token = "%%"
2319indent = { tab-width = 4, unit = " " }
2455indent = { tab-width = 4, unit = " " }
2320
2456
@@ -2328,12 +2464,11 @@ scope = "source.m"
2328file-types = ["m"]
2464file-types = ["m"]
2329comment-token = "%"
2465comment-token = "%"
2330shebangs = ["octave-cli", "matlab"]
2466shebangs = ["octave-cli", "matlab"]
2331−roots = []
2332indent = { tab-width = 2, unit = " " }
2467indent = { tab-width = 2, unit = " " }
2333
2468
2334[[grammar]]
2469[[grammar]]
2335name = "matlab"
2470name = "matlab"
2336−source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "676117eafa64afedc8380a921a77cd9f2244bc6b" }
2471+source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "6071891a8c39600203eba20513666cf93b4d650a" }
2337
2472
2338[[language]]
2473[[language]]
2339name = "ponylang"
2474name = "ponylang"
@@ -2353,7 +2488,6 @@ name = "dhall"
2353scope = "source.dhall"
2488scope = "source.dhall"
2354injection-regex = "dhall"
2489injection-regex = "dhall"
2355file-types = ["dhall"]
2490file-types = ["dhall"]
2356−roots = []
2357comment-token = "--"
2491comment-token = "--"
2358indent = { tab-width = 2, unit = " " }
2492indent = { tab-width = 2, unit = " " }
2359language-servers = [ "dhall-lsp-server" ]
2493language-servers = [ "dhall-lsp-server" ]
@@ -2368,7 +2502,6 @@ name = "sage"
2368scope = "source.sage"
2502scope = "source.sage"
2369file-types = ["sage"]
2503file-types = ["sage"]
2370injection-regex = "sage"
2504injection-regex = "sage"
2371−roots = []
2372comment-token = "#"
2505comment-token = "#"
2373indent = { tab-width = 4, unit = " " }
2506indent = { tab-width = 4, unit = " " }
2374grammar = "python"
2507grammar = "python"
@@ -2379,7 +2512,6 @@ scope = "source.msbuild"
2379injection-regex = "msbuild"
2512injection-regex = "msbuild"
2380file-types = ["proj", "vbproj", "csproj", "fsproj", "targets", "props"]
2513file-types = ["proj", "vbproj", "csproj", "fsproj", "targets", "props"]
2381indent = { tab-width = 2, unit = " " }
2514indent = { tab-width = 2, unit = " " }
2382−roots = []
2383grammar = "xml"
2515grammar = "xml"
2384
2516
2385[language.auto-pairs]
2517[language.auto-pairs]
@@ -2395,7 +2527,6 @@ name = "pem"
2395scope = "source.pem"
2527scope = "source.pem"
2396file-types = ["pem", "cert", "crt"]
2528file-types = ["pem", "cert", "crt"]
2397injection-regex = "pem"
2529injection-regex = "pem"
2398−roots = []
2399grammar = "pem"
2530grammar = "pem"
2400
2531
2401[[grammar]]
2532[[grammar]]
@@ -2406,7 +2537,6 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-pem", rev = "be67a433
2406name = "passwd"
2537name = "passwd"
2407scope = "source.passwd"
2538scope = "source.passwd"
2408file-types = ["passwd"]
2539file-types = ["passwd"]
2409−roots = []
2410
2540
2411[[grammar]]
2541[[grammar]]
2412name = "passwd"
2542name = "passwd"
@@ -2416,7 +2546,6 @@ source = { git = "https://github.com/ath3/tree-sitter-passwd", rev = "20239395ea
2416name = "hosts"
2546name = "hosts"
2417scope = "source.hosts"
2547scope = "source.hosts"
2418file-types = ["hosts"]
2548file-types = ["hosts"]
2419−roots = []
2420comment-token = "#"
2549comment-token = "#"
2421
2550
2422[[grammar]]
2551[[grammar]]
@@ -2428,20 +2557,18 @@ name = "uxntal"
2428scope = "source.tal"
2557scope = "source.tal"
2429injection-regex = "tal"
2558injection-regex = "tal"
2430file-types = ["tal"]
2559file-types = ["tal"]
2431−roots = []
2432auto-format = false
2560auto-format = false
2433comment-token = "("
2561comment-token = "("
2434
2562
2435[[grammar]]
2563[[grammar]]
2436name = "uxntal"
2564name = "uxntal"
2437−source = { git = "https://github.com/Jummit/tree-sitter-uxntal", rev = "9297e95ef74380b0ad84c4fd98f91e9f6e4319e6" }
2565+source = { git = "https://github.com/Jummit/tree-sitter-uxntal", rev = "d68406066648cd6db4c6a2f11ec305af02079884" }
2438
2566
2439[[language]]
2567[[language]]
2440name = "yuck"
2568name = "yuck"
2441scope = "source.yuck"
2569scope = "source.yuck"
2442injection-regex = "yuck"
2570injection-regex = "yuck"
2443file-types = ["yuck"]
2571file-types = ["yuck"]
2444−roots = []
2445comment-token = ";"
2572comment-token = ";"
2446indent = { tab-width = 2, unit = " " }
2573indent = { tab-width = 2, unit = " " }
2447
2574
@@ -2454,19 +2581,17 @@ name = "prql"
2454scope = "source.prql"
2581scope = "source.prql"
2455injection-regex = "prql"
2582injection-regex = "prql"
2456file-types = ["prql"]
2583file-types = ["prql"]
2457−roots = []
2458comment-token = "#"
2584comment-token = "#"
2459indent = { tab-width = 4, unit = " " }
2585indent = { tab-width = 4, unit = " " }
2460
2586
2461[[grammar]]
2587[[grammar]]
2462name = "prql"
2588name = "prql"
2463−source = { git = "https://github.com/PRQL/tree-sitter-prql", rev = "3f27cac466f030ee7d985d91eba5470e01dd21ea" }
2589+source = { git = "https://github.com/PRQL/tree-sitter-prql", rev = "09e158cd3650581c0af4c49c2e5b10c4834c8646" }
2464
2590
2465[[language]]
2591[[language]]
2466name = "po"
2592name = "po"
2467scope = "source.po"
2593scope = "source.po"
2468file-types = ["po", "pot"]
2594file-types = ["po", "pot"]
2469−roots = []
2470comment-token = "#"
2595comment-token = "#"
2471
2596
2472[[grammar]]
2597[[grammar]]
@@ -2476,9 +2601,8 @@ source = { git = "https://github.com/erasin/tree-sitter-po", rev = "417cee9abb20
2476[[language]]
2601[[language]]
2477name = "nasm"
2602name = "nasm"
2478scope = "source.nasm"
2603scope = "source.nasm"
2479−file-types = ["asm", "s", "S", "nasm"]
2604+file-types = ["asm", "S", "nasm"]
2480injection-regex = "n?asm"
2605injection-regex = "n?asm"
2481−roots = []
2482comment-token = ";"
2606comment-token = ";"
2483indent = { tab-width = 8, unit = " " }
2607indent = { tab-width = 8, unit = " " }
2484
2608
@@ -2486,12 +2610,23 @@ indent = { tab-width = 8, unit = " " }
2486name = "nasm"
2610name = "nasm"
2487source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "a0db15db6fcfb1bf2cc8702500e55e558825c48b" }
2611source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "a0db15db6fcfb1bf2cc8702500e55e558825c48b" }
2488
2612
2613+[[language]]
2614+name = "gas"
2615+scope = "source.gas"
2616+file-types = ["s"]
2617+injection-regex = "gas"
2618+comment-token = "#"
2619+indent = { tab-width = 8, unit = " " }
2620+
2621+[[grammar]]
2622+name = "gas"
2623+source = { git = "https://github.com/sirius94/tree-sitter-gas", rev = "60f443646b20edee3b7bf18f3a4fb91dc214259a" }
2624+
2489[[language]]
2625[[language]]
2490name = "rst"
2626name = "rst"
2491scope = "source.rst"
2627scope = "source.rst"
2492comment-token = ".."
2628comment-token = ".."
2493file-types = ["rst"]
2629file-types = ["rst"]
2494−roots = []
2495
2630
2496[[grammar]]
2631[[grammar]]
2497name = "rst"
2632name = "rst"
@@ -2502,7 +2637,6 @@ name = "capnp"
2502scope = "source.capnp"
2637scope = "source.capnp"
2503injection-regex = "capnp"
2638injection-regex = "capnp"
2504file-types = ["capnp"]
2639file-types = ["capnp"]
2505−roots = []
2506comment-token = "#"
2640comment-token = "#"
2507indent = { tab-width = 2, unit = " " }
2641indent = { tab-width = 2, unit = " " }
2508
2642
@@ -2528,7 +2662,6 @@ source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "c
2528name = "vhdl"
2662name = "vhdl"
2529scope = "source.vhdl"
2663scope = "source.vhdl"
2530file-types = ["vhd", "vhdl"]
2664file-types = ["vhd", "vhdl"]
2531−roots = []
2532comment-token = "--"
2665comment-token = "--"
2533language-servers = [ "vhdl_ls" ]
2666language-servers = [ "vhdl_ls" ]
2534indent = { tab-width = 2, unit = " " }
2667indent = { tab-width = 2, unit = " " }
@@ -2540,7 +2673,6 @@ source = { git = "https://github.com/teburd/tree-sitter-vhdl", rev = "c57313adee
2540
2673
2541[[language]]
2674[[language]]
2542name = "rego"
2675name = "rego"
2543−roots = []
2544scope = "source.rego"
2676scope = "source.rego"
2545injection-regex = "rego"
2677injection-regex = "rego"
2546file-types = ["rego"]
2678file-types = ["rego"]
@@ -2551,7 +2683,7 @@ grammar = "rego"
2551
2683
2552[[grammar]]
2684[[grammar]]
2553name = "rego"
2685name = "rego"
2554−source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "b2667c975f07b33be3ceb83bea5cfbad88095866" }
2686+source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "9ac75e71b2d791e0aadeef68098319d86a2a14cf" }
2555
2687
2556[[language]]
2688[[language]]
2557name = "nim"
2689name = "nim"
@@ -2559,7 +2691,6 @@ scope = "source.nim"
2559injection-regex = "nim"
2691injection-regex = "nim"
2560file-types = ["nim", "nims", "nimble"]
2692file-types = ["nim", "nims", "nimble"]
2561shebangs = []
2693shebangs = []
2562−roots = []
2563comment-token = "#"
2694comment-token = "#"
2564indent = { tab-width = 2, unit = " " }
2695indent = { tab-width = 2, unit = " " }
2565language-servers = [ "nimlangserver" ]
2696language-servers = [ "nimlangserver" ]
@@ -2583,13 +2714,13 @@ file-types = [ "cabal" ]
2583roots = ["cabal.project", "Setup.hs"]
2714roots = ["cabal.project", "Setup.hs"]
2584indent = { tab-width = 2, unit = " " }
2715indent = { tab-width = 2, unit = " " }
2585comment-token = "--"
2716comment-token = "--"
2717+language-servers = [ "haskell-language-server" ]
2586
2718
2587[[language]]
2719[[language]]
2588name = "hurl"
2720name = "hurl"
2589scope = "source.hurl"
2721scope = "source.hurl"
2590injection-regex = "hurl"
2722injection-regex = "hurl"
2591file-types = ["hurl"]
2723file-types = ["hurl"]
2592−roots = []
2593comment-token = "#"
2724comment-token = "#"
2594indent = { tab-width = 2, unit = " " }
2725indent = { tab-width = 2, unit = " " }
2595
2726
@@ -2600,7 +2731,6 @@ source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c4206
2600[[language]]
2731[[language]]
2601name = "markdoc"
2732name = "markdoc"
2602scope = "text.markdoc"
2733scope = "text.markdoc"
2603−roots = []
2604file-types = ["mdoc"]
2734file-types = ["mdoc"]
2605language-servers = [ "markdoc-ls" ]
2735language-servers = [ "markdoc-ls" ]
2606
2736
@@ -2613,7 +2743,6 @@ name = "opencl"
2613scope = "source.cl"
2743scope = "source.cl"
2614injection-regex = "(cl|opencl)"
2744injection-regex = "(cl|opencl)"
2615file-types = ["cl"]
2745file-types = ["cl"]
2616−roots = []
2617comment-token = "//"
2746comment-token = "//"
2618language-servers = [ "clangd" ]
2747language-servers = [ "clangd" ]
2619
2748
@@ -2626,7 +2755,6 @@ name = "just"
2626scope = "source.just"
2755scope = "source.just"
2627file-types = ["justfile", "Justfile", ".justfile", ".Justfile"]
2756file-types = ["justfile", "Justfile", ".justfile", ".Justfile"]
2628injection-regex = "just"
2757injection-regex = "just"
2629−roots = []
2630comment-token = "#"
2758comment-token = "#"
2631indent = { tab-width = 4, unit = "\t" }
2759indent = { tab-width = 4, unit = "\t" }
2632
2760
@@ -2634,40 +2762,52 @@ indent = { tab-width = 4, unit = "\t" }
2634name = "just"
2762name = "just"
2635source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "8af0aab79854aaf25b620a52c39485849922f766" }
2763source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "8af0aab79854aaf25b620a52c39485849922f766" }
2636
2764
2765+[[language]]
2766+name = "gn"
2767+scope = "source.gn"
2768+injection-regex = "gn"
2769+file-types = ["gn", "gni"]
2770+roots = []
2771+comment-token = "#"
2772+indent = { tab-width = 2, unit = " " }
2773+formatter = { command = "gn", args = ["format", "--stdin"] }
2774+
2775+[[grammar]]
2776+name = "gn"
2777+source = { git = "https://github.com/willcassella/tree-sitter-gn", rev = "e18d6e36a79b20dafb58f19d407bd38b0e60260e" }
2778+
2637[[language]]
2779[[language]]
2638name = "blueprint"
2780name = "blueprint"
2639scope = "source.blueprint"
2781scope = "source.blueprint"
2640injection-regex = "blueprint"
2782injection-regex = "blueprint"
2641file-types = ["blp"]
2783file-types = ["blp"]
2642−roots = []
2643comment-token = "//"
2784comment-token = "//"
2644language-servers = [ "blueprint-compiler" ]
2785language-servers = [ "blueprint-compiler" ]
2645indent = { tab-width = 4, unit = " " }
2786indent = { tab-width = 4, unit = " " }
2646
2787
2647[[grammar]]
2788[[grammar]]
2648name = "blueprint"
2789name = "blueprint"
2649−source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "7f1a5df44861291d6951b6b2146a9fef4c226e14" }
2790+source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "863cea9f83ad5637300478e0559262f1e791684b" }
2650
2791
2651[[language]]
2792[[language]]
2652name = "forth"
2793name = "forth"
2653scope = "source.forth"
2794scope = "source.forth"
2654injection-regex = "forth"
2795injection-regex = "forth"
2655file-types = ["fs", "forth", "fth", "4th"]
2796file-types = ["fs", "forth", "fth", "4th"]
2656−roots = []
2657comment-token = "\\"
2797comment-token = "\\"
2658language-servers = [ "forth-lsp" ]
2798language-servers = [ "forth-lsp" ]
2659indent = { tab-width = 3, unit = " " }
2799indent = { tab-width = 3, unit = " " }
2660
2800
2661[[grammar]]
2801[[grammar]]
2662name = "forth"
2802name = "forth"
2663−source = { git = "https://github.com/alexanderbrevig/tree-sitter-forth", rev = "304ed77beb113e37af38b20ff14e3c37bf350d10" }
2803+source = { git = "https://github.com/alexanderbrevig/tree-sitter-forth", rev = "90189238385cf636b9ee99ce548b9e5b5e569d48" }
2664
2804
2665[[language]]
2805[[language]]
2666name = "fsharp"
2806name = "fsharp"
2667scope = "source.fs"
2807scope = "source.fs"
2668roots = ["sln", "fsproj"]
2808roots = ["sln", "fsproj"]
2669injection-regex = "fsharp"
2809injection-regex = "fsharp"
2670−file-types = ["fs", "fsx"]
2810+file-types = ["fs", "fsx", "fsi", "fsscript"]
2671comment-token = "//"
2811comment-token = "//"
2672indent = { tab-width = 4, unit = " " }
2812indent = { tab-width = 4, unit = " " }
2673auto-format = true
2813auto-format = true
@@ -2675,26 +2815,194 @@ language-servers = ["fsharp-ls"]
2675
2815
2676[[grammar]]
2816[[grammar]]
2677name = "fsharp"
2817name = "fsharp"
2678−source = { git = "https://github.com/kaashyapan/tree-sitter-fsharp", rev = "1b5a315e003024db6181744f1d25b8f525732665" }
2818+source = { git = "https://github.com/kaashyapan/tree-sitter-fsharp", rev = "18da392fd9bd5e79f357abcce13f61f3a15e3951" }
2679
2819
2680[[language]]
2820[[language]]
2681name = "t32"
2821name = "t32"
2682scope = "source.t32"
2822scope = "source.t32"
2683injection-regex = "t32"
2823injection-regex = "t32"
2684file-types = ["cmm", "t32"]
2824file-types = ["cmm", "t32"]
2685−roots = []
2686comment-token = ";"
2825comment-token = ";"
2687indent = { tab-width = 2, unit = " " }
2826indent = { tab-width = 2, unit = " " }
2688
2827
2689[[grammar]]
2828[[grammar]]
2690name = "t32"
2829name = "t32"
2691−source = { git = "https://codeberg.org/xasc/tree-sitter-t32", rev = "1dd98248b01e4a3933c1b85b58bab0875e2ba437" }
2830+source = { git = "https://gitlab.com/xasc/tree-sitter-t32", rev = "6da5e3cbabd376b566d04282005e52ffe67ef74a" }
2692
2831
2693[[language]]
2832[[language]]
2694name = "webc"
2833name = "webc"
2695scope = "text.html.webc"
2834scope = "text.html.webc"
2696injection-regex = "webc"
2835injection-regex = "webc"
2697file-types = ["webc"]
2836file-types = ["webc"]
2698−roots = []
2699indent = { tab-width = 2, unit = " " }
2837indent = { tab-width = 2, unit = " " }
2700grammar = "html"
2838grammar = "html"
2839+
2840+[[language]]
2841+name = "typst"
2842+scope = "source.typst"
2843+injection-regex = "typst"
2844+file-types = ["typst", "typ"]
2845+comment-token = "//"
2846+language-servers = ["typst-lsp"]
2847+indent = { tab-width = 2, unit = " " }
2848+
2849+[language.auto-pairs]
2850+'(' = ')'
2851+'{' = '}'
2852+'[' = ']'
2853+'$' = '$'
2854+'"' = '"'
2855+
2856+[[grammar]]
2857+name = "typst"
2858+source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "e35aa22395fdde82bbc4b5700c324ce346dfc9e5" }
2859+
2860+[[language]]
2861+name = "nunjucks"
2862+scope = "text.html.nunjucks"
2863+injection-regex = "nunjucks"
2864+file-types = ["njk"]
2865+indent = { tab-width = 2, unit = " " }
2866+grammar = "jinja2"
2867+
2868+[[language]]
2869+name = "jinja"
2870+scope = "text.html.jinja"
2871+injection-regex = "jinja"
2872+file-types = ["jinja", "jinja2", "j2"]
2873+indent = { tab-width = 2, unit = " " }
2874+grammar = "jinja2"
2875+
2876+[[grammar]]
2877+name = "jinja2"
2878+source = { git = "https://github.com/varpeti/tree-sitter-jinja2", rev = "a533cd3c33aea6acb0f9bf9a56f35dcfe6a8eb53" }
2879+
2880+[[grammar]]
2881+name = "wren"
2882+source = { git = "https://git.sr.ht/~jummit/tree-sitter-wren", rev = "793d58266924e6efcc40e411663393e9d72bec87"}
2883+
2884+[[language]]
2885+name = "wren"
2886+scope = "source.wren"
2887+injection-regex = "wren"
2888+file-types = ["wren"]
2889+indent = { tab-width = 2, unit = " "}
2890+
2891+[[language]]
2892+name = "unison"
2893+scope = "source.unison"
2894+injection-regex = "unison"
2895+file-types = ["u"]
2896+shebangs = []
2897+auto-format = false
2898+comment-token = "--"
2899+indent = { tab-width = 4, unit = " " }
2900+
2901+[language.auto-pairs]
2902+'(' = ')'
2903+'{' = '}'
2904+'[' = ']'
2905+'"' = '"'
2906+'`' = '`'
2907+
2908+[[grammar]]
2909+name = "unison"
2910+source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "98c4e8bc5c9f5989814a720457cf36963cf4043d" }
2911+
2912+[[language]]
2913+name = "todotxt"
2914+scope = "text.todotxt"
2915+file-types = [{ suffix = ".todo.txt" }, "todotxt"]
2916+formatter = { command = "sort" }
2917+auto-format = true
2918+
2919+[[grammar]]
2920+name = "todotxt"
2921+source = { git = "https://github.com/arnarg/tree-sitter-todotxt", rev = "0207f6a4ab6aeafc4b091914d31d8235049a2578" }
2922+
2923+[[language]]
2924+name = "strace"
2925+scope = "source.strace"
2926+file-types = ["strace"]
2927+
2928+[[grammar]]
2929+name = "strace"
2930+source = { git = "https://github.com/sigmaSd/tree-sitter-strace", rev = "2b18fdf9a01e7ec292cc6006724942c81beb7fd5" }
2931+
2932+[[language]]
2933+name = "gemini"
2934+scope = "source.gmi"
2935+file-types = ["gmi"]
2936+
2937+[[grammar]]
2938+name = "gemini"
2939+source = { git = "https://git.sr.ht/~nbsp/tree-sitter-gemini", rev = "3cc5e4bdf572d5df4277fc2e54d6299bd59a54b3" }
2940+
2941+[[language]]
2942+name = "agda"
2943+scope = "source.agda"
2944+injection-regex = "agda"
2945+file-types = ["agda"]
2946+roots = []
2947+comment-token = "--"
2948+# language-servers = [ "als" ]
2949+# the agda language server is of questionable functionality.
2950+auto-format = false
2951+indent = { tab-width = 2, unit = " " }
2952+
2953+[language.auto-pairs]
2954+'"' = '"'
2955+"'" = "'"
2956+'{' = '}'
2957+'(' = ')'
2958+'[' = ']'
2959+
2960+# [language.debugger]
2961+# ?? can this be used for proof assistant support? explore
2962+
2963+[[grammar]]
2964+name = "agda"
2965+source = { git = "https://github.com/tree-sitter/tree-sitter-agda", rev = "c21c3a0f996363ed17b8ac99d827fe5a4821f217" }
2966+
2967+[[language]]
2968+name = "templ"
2969+scope = "source.templ"
2970+file-types = ["templ"]
2971+roots = ["go.work", "go.mod"]
2972+comment-token = "//"
2973+indent = { tab-width = 2, unit = " " }
2974+language-servers = [ "templ" ]
2975+
2976+[[grammar]]
2977+name = "templ"
2978+source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "ea56ac0655243490a4929a988f4eaa91dfccc995" }
2979+
2980+[[language]]
2981+name = "dbml"
2982+scope = "source.dbml"
2983+injection-regex = "dbml"
2984+file-types = ["dbml"]
2985+comment-token = ";"
2986+indent = { tab-width = 2, unit = " " }
2987+
2988+[[grammar]]
2989+name = "dbml"
2990+source = { git = "https://github.com/dynamotn/tree-sitter-dbml", rev = "2e2fa5640268c33c3d3f27f7e676f631a9c68fd9" }
2991+
2992+[[language]]
2993+name = "log"
2994+scope = "source.log"
2995+file-types = ["log"]
2996+
2997+[[grammar]]
2998+name = "log"
2999+source = { git = "https://github.com/Tudyx/tree-sitter-log", rev = "62cfe307e942af3417171243b599cc7deac5eab9" }
3000+
3001+[[language]]
3002+name = "janet"
3003+scope = "source.janet"
3004+injection-regex = "janet"
3005+file-types = ["janet"]
3006+comment-token = "#"
3007+indent = { tab-width = 2, unit = " " }
3008+grammar = "clojure"
logo.svg+1−1
@@ -1 +1 @@
1−<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"> <g transform="matrix(1,0,0,1,-31352.7,-1817.25)"> <g transform="matrix(1,0,0,1,31062.7,-20.8972)"> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z" style="fill:#706bc8;"></path> </g> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z" style="fill:#55c5e4;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z" style="fill:#84ddea;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z" style="fill:#997bc8;"></path></g></g></g> </svg>
1+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"><g transform="matrix(1,0,0,1,-31352.7,-1817.25)"><g transform="matrix(1,0,0,1,31062.7,-20.8972)"><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#706bc8;" d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z"></path></g><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#55c5e4;" d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#84ddea;" d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#997bc8;" d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z"></path></g></g></g></svg>
runtime/queries/_jsx/highlights.scm+3−15
@@ -4,9 +4,6 @@
4(jsx_opening_element ((identifier) @constructor
4(jsx_opening_element ((identifier) @constructor
5 (#match? @constructor "^[A-Z]")))
5 (#match? @constructor "^[A-Z]")))
6
6
7−; Handle the dot operator effectively - <My.Component>
8−(jsx_opening_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
9−
10(jsx_opening_element (identifier) @tag)
7(jsx_opening_element (identifier) @tag)
11
8
12; Closing elements
9; Closing elements
@@ -15,9 +12,6 @@
15(jsx_closing_element ((identifier) @constructor
12(jsx_closing_element ((identifier) @constructor
16 (#match? @constructor "^[A-Z]")))
13 (#match? @constructor "^[A-Z]")))
17
14
18−; Handle the dot operator effectively - </My.Component>
19−(jsx_closing_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
20−
21(jsx_closing_element (identifier) @tag)
15(jsx_closing_element (identifier) @tag)
22
16
23; Self-closing elements
17; Self-closing elements
@@ -26,9 +20,6 @@
26(jsx_self_closing_element ((identifier) @constructor
20(jsx_self_closing_element ((identifier) @constructor
27 (#match? @constructor "^[A-Z]")))
21 (#match? @constructor "^[A-Z]")))
28
22
29−; Handle the dot operator effectively - <My.Component />
30−(jsx_self_closing_element ((nested_identifier (identifier) @tag (identifier) @constructor)))
31−
32(jsx_self_closing_element (identifier) @tag)
23(jsx_self_closing_element (identifier) @tag)
33
24
34; Attributes
25; Attributes
@@ -39,17 +30,14 @@
39; Punctuation
30; Punctuation
40; -----------
31; -----------
41
32
42−; Handle attribute delimiter
33+; Handle attribute delimiter (<Component color="red"/>)
43(jsx_attribute "=" @punctuation.delimiter)
34(jsx_attribute "=" @punctuation.delimiter)
44
35
45; <Component>
36; <Component>
46(jsx_opening_element ["<" ">"] @punctuation.bracket)
37(jsx_opening_element ["<" ">"] @punctuation.bracket)
47
38
48; </Component>
39; </Component>
49−(jsx_closing_element ["<" "/" ">"] @punctuation.bracket)
40+(jsx_closing_element ["</" ">"] @punctuation.bracket)
50
41
51; <Component />
42; <Component />
52−(jsx_self_closing_element ["<" "/" ">"] @punctuation.bracket)
43+(jsx_self_closing_element ["<" "/>"] @punctuation.braket)
53−
54−; <> ... </>
55−(jsx_fragment ["<" "/" ">"] @punctuation.bracket)
runtime/queries/_jsx/indents.scm−1
@@ -1,5 +1,4 @@
1[
1[
2− (jsx_fragment)
3 (jsx_element)
2 (jsx_element)
4 (jsx_self_closing_element)
3 (jsx_self_closing_element)
5] @indent
4] @indent
runtime/queries/_typescript/highlights.scm+5−3
@@ -39,11 +39,11 @@
39 (identifier) @variable.parameter))
39 (identifier) @variable.parameter))
40
40
41; (p?: t)
41; (p?: t)
42−; (p?: t = 1) // Invalid but still posible to hihglight.
42+; (p?: t = 1) // Invalid but still possible to highlight.
43(optional_parameter
43(optional_parameter
44 (identifier) @variable.parameter)
44 (identifier) @variable.parameter)
45
45
46−; (...p?: t) // Invalid but still posible to hihglight.
46+; (...p?: t) // Invalid but still possible to highlight.
47(optional_parameter
47(optional_parameter
48 (rest_pattern
48 (rest_pattern
49 (identifier) @variable.parameter))
49 (identifier) @variable.parameter))
@@ -59,7 +59,7 @@
59 (pair_pattern
59 (pair_pattern
60 value: (identifier) @variable.parameter)))
60 value: (identifier) @variable.parameter)))
61
61
62−; ([ p ]?: t[]) // Invalid but still posible to hihglight.
62+; ([ p ]?: t[]) // Invalid but still possible to highlight.
63(optional_parameter
63(optional_parameter
64 (array_pattern
64 (array_pattern
65 (identifier) @variable.parameter))
65 (identifier) @variable.parameter))
@@ -107,6 +107,8 @@
107; Types
107; Types
108; -----
108; -----
109
109
110+(type_parameter
111+ name: (type_identifier) @type.parameter)
110(type_identifier) @type
112(type_identifier) @type
111(predefined_type) @type.builtin
113(predefined_type) @type.builtin
112
114
runtime/queries/_typescript/locals.scm+17
@@ -1,6 +1,18 @@
1+; Scopes
2+;-------
3+
4+[
5+ (type_alias_declaration)
6+ (class_declaration)
7+ (interface_declaration)
8+] @local.scope
9+
1; Definitions
10; Definitions
2;------------
11;------------
3
12
13+(type_parameter
14+ name: (type_identifier) @local.definition)
15+
4; Javascript and Typescript Treesitter grammars deviate when defining the
16; Javascript and Typescript Treesitter grammars deviate when defining the
5; tree structure for parameters, so we need to address them in each specific
17; tree structure for parameters, so we need to address them in each specific
6; language instead of ecma.
18; language instead of ecma.
@@ -14,3 +26,8 @@
14; (i?: t = 1) // Invalid but still posible to hihglight.
26; (i?: t = 1) // Invalid but still posible to hihglight.
15(optional_parameter
27(optional_parameter
16 (identifier) @local.definition)
28 (identifier) @local.definition)
29+
30+; References
31+;-----------
32+
33+(type_identifier) @local.reference
runtime/queries/agda/highlights.scmnew file+124
@@ -0,0 +1,124 @@
1+;; Punctuation
2+[ "." ";" ":"] @punctuation.delimiter
3+[ "(" ")" "{" "}" ] @punctuation.bracket
4+
5+;; Constants
6+(integer) @constant.numeric.integer
7+; (float) @constant.numeric.float
8+(literal) @string
9+
10+;; Pragmas and comments
11+(comment) @comment
12+(pragma) @attribute
13+(macro) @function.macro
14+
15+;; Imports
16+(module_name) @namespace
17+(import_directive (id) @namespace)
18+[(module) (import) (open)] @keyword.control.import
19+
20+;; Types
21+(typed_binding (expr) @type)
22+(record (expr) @type)
23+(data (expr) @type)
24+(signature (expr) @type)
25+(function (rhs (expr) @type))
26+; todo: these are too general. ideally, any nested (atom)
27+; https://github.com/tree-sitter/tree-sitter/issues/880
28+
29+;; Variables
30+(untyped_binding (atom) @variable)
31+(typed_binding (atom) @variable)
32+(field_name) @variable.other.member
33+
34+;; Functions
35+(function_name) @function
36+;(function (lhs
37+; . (atom) @function
38+; (atom) @variable.parameter))
39+; todo: currently fails to parse, upstream tree-sitter bug
40+
41+;; Data
42+[(data_name) (record_name)] @constructor
43+((atom) @constant.builtin.boolean
44+ (#any-of? @constant.builtin.boolean "true" "false" "True" "False"))
45+
46+"Set" @type.builtin
47+
48+; postulate
49+; type_signature
50+; pattern
51+; id
52+; bid
53+; typed_binding
54+; primitive
55+; private
56+; record_signature
57+; record_assignments
58+; field_assignment
59+; module_assignment
60+; renaming
61+; import_directive
62+; lambda
63+; let
64+; instance
65+; generalize
66+; record
67+; fields
68+; syntax
69+; hole_name
70+; data_signature
71+
72+;; Keywords
73+[
74+ "where"
75+ "data"
76+ "rewrite"
77+ "postulate"
78+ "public"
79+ "private"
80+ "tactic"
81+ "Prop"
82+ "quote"
83+ "renaming"
84+ "in"
85+ "hiding"
86+ "constructor"
87+ "abstract"
88+ "let"
89+ "field"
90+ "mutual"
91+ "infix"
92+ "infixl"
93+ "infixr"
94+ "record"
95+ "overlap"
96+ "instance"
97+ "do"
98+] @keyword
99+
100+[
101+ "="
102+] @operator
103+
104+; = | -> : ? \ .. ... λ ∀ →
105+; (_LAMBDA) (_FORALL) (_ARROW)
106+; "coinductive"
107+; "eta-equality"
108+; "field"
109+; "inductive"
110+; "interleaved"
111+; "macro"
112+; "no-eta-equality"
113+; "pattern"
114+; "primitive"
115+; "quoteTerm"
116+; "rewrite"
117+; "syntax"
118+; "unquote"
119+; "unquoteDecl"
120+; "unquoteDef"
121+; "using"
122+; "variable"
123+; "with"
124+
runtime/queries/bash/indents.scm+1
@@ -2,6 +2,7 @@
2 (function_definition)
2 (function_definition)
3 (if_statement)
3 (if_statement)
4 (for_statement)
4 (for_statement)
5+ (while_statement)
5 (case_statement)
6 (case_statement)
6 (pipeline)
7 (pipeline)
7] @indent
8] @indent
runtime/queries/bash/textobjects.scmnew file+9
@@ -0,0 +1,9 @@
1+(function_definition
2+ body: (_) @function.inside) @function.around
3+
4+(command
5+ argument: (_) @parameter.inside)
6+
7+(comment) @comment.inside
8+
9+(comment)+ @comment.around
runtime/queries/blueprint/highlights.scm+5
@@ -15,6 +15,11 @@
15(decorator) @attribute
15(decorator) @attribute
16
16
17(property_definition (property_name) @variable.other.member)
17(property_definition (property_name) @variable.other.member)
18+(property_definition
19+ (property_binding
20+ "bind" @keyword
21+ (property_name) @variable.other.member
22+ ["no-sync-create" "bidirectional" "inverted"]* @keyword))
18
23
19(object) @type
24(object) @type
20
25
runtime/queries/c/indents.scm+18−1
@@ -5,7 +5,6 @@
5 (enumerator_list)
5 (enumerator_list)
6 (parameter_list)
6 (parameter_list)
7 (init_declarator)
7 (init_declarator)
8− (case_statement)
9 (expression_statement)
8 (expression_statement)
10] @indent
9] @indent
11
10
@@ -13,6 +12,7 @@
13 "case"
12 "case"
14 "}"
13 "}"
15 "]"
14 "]"
15+ ")"
16] @outdent
16] @outdent
17
17
18(if_statement
18(if_statement
@@ -32,3 +32,20 @@
32 (_) @indent
32 (_) @indent
33 (#not-kind-eq? @indent "compound_statement")
33 (#not-kind-eq? @indent "compound_statement")
34 (#set! "scope" "all"))
34 (#set! "scope" "all"))
35+
36+(parameter_list
37+ . (parameter_declaration) @anchor
38+ (#set! "scope" "tail")) @align
39+(argument_list
40+ . (_) @anchor
41+ (#set! "scope" "tail")) @align
42+; These are a bit opinionated since some people just indent binary/ternary expressions spanning multiple lines.
43+; Since they are only triggered when a newline is inserted into an already complete binary/ternary expression,
44+; this should happen rarely, so it is not a big deal either way.
45+; Additionally, adding these queries has the advantage of preventing such continuation lines from being used
46+; as the baseline when the `hybrid` indent heuristic is used (which is desirable since their indentation is so inconsistent).
47+(binary_expression
48+ (#set! "scope" "tail")) @anchor @align
49+(conditional_expression
50+ "?" @anchor
51+ (#set! "scope" "tail")) @align
runtime/queries/comment/highlights.scm+8−8
@@ -7,31 +7,31 @@
7
7
8; Hint level tags
8; Hint level tags
9((tag (name) @hint)
9((tag (name) @hint)
10− (#match? @hint "^(HINT|MARK)$"))
10+ (#match? @hint "^(HINT|MARK|PASSED|STUB|MOCK)$"))
11
11
12("text" @hint
12("text" @hint
13− (#match? @hint "^(HINT|MARK)$"))
13+ (#match? @hint "^(HINT|MARK|PASSED|STUB|MOCK)$"))
14
14
15; Info level tags
15; Info level tags
16((tag (name) @info)
16((tag (name) @info)
17− (#match? @info "^(INFO|NOTE|TODO)$"))
17+ (#match? @info "^(INFO|NOTE|TODO|PERF|OPTIMIZE|PERFORMANCE|QUESTION|ASK)$"))
18
18
19("text" @info
19("text" @info
20− (#match? @info "^(INFO|NOTE|TODO)$"))
20+ (#match? @info "^(INFO|NOTE|TODO|PERF|OPTIMIZE|PERFORMANCE|QUESTION|ASK)$"))
21
21
22; Warning level tags
22; Warning level tags
23((tag (name) @warning)
23((tag (name) @warning)
24− (#match? @warning "^(HACK|WARN|WARNING)$"))
24+ (#match? @warning "^(HACK|WARN|WARNING|TEST|TEMP)$"))
25
25
26("text" @warning
26("text" @warning
27− (#match? @warning "^(HACK|WARN|WARNING)$"))
27+ (#match? @warning "^(HACK|WARN|WARNING|TEST|TEMP)$"))
28
28
29; Error level tags
29; Error level tags
30((tag (name) @error)
30((tag (name) @error)
31− (#match? @error "^(BUG|FIXME|ISSUE|XXX)$"))
31+ (#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG)$"))
32
32
33("text" @error
33("text" @error
34− (#match? @error "^(BUG|FIXME|ISSUE|XXX)$"))
34+ (#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG)$"))
35
35
36(tag
36(tag
37 (name) @ui.text
37 (name) @ui.text
runtime/queries/common-lisp/indents.scmnew file+1
@@ -0,0 +1 @@
1+; inherits: scheme
runtime/queries/dart/highlights.scm+3−1
@@ -200,6 +200,7 @@
200 "async"
200 "async"
201 "async*"
201 "async*"
202 "await"
202 "await"
203+ "base"
203 "class"
204 "class"
204 "covariant"
205 "covariant"
205 "deferred"
206 "deferred"
@@ -219,6 +220,7 @@
219 "operator"
220 "operator"
220 "part"
221 "part"
221 "required"
222 "required"
223+ "sealed"
222 "set"
224 "set"
223 "show"
225 "show"
224 "static"
226 "static"
@@ -230,7 +232,7 @@
230
232
231; when used as an identifier:
233; when used as an identifier:
232((identifier) @variable.builtin
234((identifier) @variable.builtin
233− (#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$"))
235+ (#match? @variable.builtin "^(abstract|as|base|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|sealed|set|static|typedef)$"))
234
236
235; Error
237; Error
236(ERROR) @error
238(ERROR) @error
runtime/queries/dbml/highlights.scmnew file+44
@@ -0,0 +1,44 @@
1+
2+; comments highlighting
3+(comment) @comment
4+
5+; keyword highlighting
6+(keyword_def) @keyword
7+(keyword_enum) @keyword
8+(keyword_ref) @keyword
9+
10+; identify blocks and definitions
11+(definition) @function
12+
13+; for identifiers
14+(identifier) @variable
15+(type) @keyword
16+
17+; Highlight special types for database/data types
18+("Project" ) @type
19+("Table" ) @type
20+("TableGroup" ) @type
21+("database_type" ) @variable
22+
23+; string and number constants
24+("'''") @constant.character.escape
25+(string) @string
26+(number) @constant.numeric
27+
28+; brackets
29+[
30+ "("
31+ ")"
32+ "{"
33+ "}"
34+ "["
35+ "]"
36+] @punctuation.bracket
37+
38+; brackets
39+[
40+ ":"
41+ "."
42+ ","
43+] @punctuation.delimiter
44+
runtime/queries/ecma/highlights.scm+1−1
@@ -109,7 +109,7 @@
109
109
110[
110[
111 ";"
111 ";"
112− "?."
112+ (optional_chain) ; ?.
113 "."
113 "."
114 ","
114 ","
115] @punctuation.delimiter
115] @punctuation.delimiter
runtime/queries/fsharp/locals.scmnew file+25
@@ -0,0 +1,25 @@
1+; Scopes
2+;-------
3+
4+[
5+ (ce_expression)
6+ (module_defn)
7+ (for_expression)
8+ (do_expression)
9+ (fun_expression)
10+ (function_expression)
11+ (try_expression)
12+ (match_expression)
13+ (elif_expression)
14+ (if_expression)
15+] @local.scope
16+
17+; Definitions
18+;------------
19+
20+(function_or_value_defn) @local.definition
21+
22+; References
23+;-----------
24+
25+(identifier) @local.reference
runtime/queries/gas/highlights.scmnew file+21
@@ -0,0 +1,21 @@
1+(comment) @comment
2+(number) @constant.numeric
3+(directive_name) @keyword.directive
4+(symbol) @variable
5+(label) @function
6+(label)
7+(instruction_prefix) @keyword
8+(instruction_name) @function.special
9+(register) @constant.builtin
10+(string) @string
11+(char) @constant.character
12+(type) @type
13+(constant "$" @constant)
14+(operand_modifier) @attribute
15+
16+(expression
17+ ["-" "+" "*" "/" "="] @operator)
18+
19+["(" ")"] @punctuation.bracket
20+
21+["," ":"] @punctuation.delimiter
runtime/queries/gas/injections.scmnew file+2
@@ -0,0 +1,2 @@
1+((comment) @injection.content
2+ (#set! injection.language "comment"))
runtime/queries/gas/textobjects.scmnew file+2
@@ -0,0 +1,2 @@
1+(comment) @comment.inside
2+(comment)+ @comment.around
runtime/queries/gemini/highlights.scmnew file+26
@@ -0,0 +1,26 @@
1+(link) @punctuation.bracket
2+(link
3+ label: (text) @markup.link.label)
4+(link
5+ uri: (uri) @markup.link.url)
6+
7+[
8+ (start_pre)
9+ (pre)
10+ (end_pre)
11+] @markup.raw.block
12+(start_pre
13+ alt: (text) @label)
14+
15+(heading1
16+ (text) @markup.heading.1) @markup.heading.marker
17+(heading2
18+ (text) @markup.heading.2) @markup.heading.marker
19+(heading3
20+ (text) @markup.heading.3) @markup.heading.marker
21+
22+(ulist
23+ (indicator) @markup.list.unnumbered)
24+(quote
25+ (indicator) @markup.quote
26+ (text) @markup.italic)
runtime/queries/gleam/highlights.scm+16−5
@@ -21,6 +21,8 @@
21
21
22; Functions
22; Functions
23(unqualified_import (identifier) @function)
23(unqualified_import (identifier) @function)
24+(unqualified_import "type" (type_identifier) @type)
25+(unqualified_import (type_identifier) @constructor)
24(function
26(function
25 name: (identifier) @function)
27 name: (identifier) @function)
26(external_function
28(external_function
@@ -43,6 +45,13 @@
43(tuple_access
45(tuple_access
44 index: (integer) @variable.other.member)
46 index: (integer) @variable.other.member)
45
47
48+; Attributes
49+(attribute
50+ "@" @attribute
51+ name: (identifier) @attribute)
52+
53+(attribute_value (identifier) @constant)
54+
46; Type names
55; Type names
47(remote_type_identifier) @type
56(remote_type_identifier) @type
48(type_identifier) @type
57(type_identifier) @type
@@ -60,10 +69,6 @@
60(identifier) @variable
69(identifier) @variable
61(discard) @comment.unused
70(discard) @comment.unused
62
71
63−; Operators
64−(binary_expression
65− operator: _ @operator)
66−
67; Keywords
72; Keywords
68[
73[
69 (visibility_modifier) ; "pub"
74 (visibility_modifier) ; "pub"
@@ -72,6 +77,7 @@
72 "assert"
77 "assert"
73 "case"
78 "case"
74 "const"
79 "const"
80+ ; DEPRECATED: 'external' was removed in v0.30.
75 "external"
81 "external"
76 "fn"
82 "fn"
77 "if"
83 "if"
@@ -79,11 +85,16 @@
79 "let"
85 "let"
80 "panic"
86 "panic"
81 "todo"
87 "todo"
82− "try"
83 "type"
88 "type"
84 "use"
89 "use"
85] @keyword
90] @keyword
86
91
92+; Operators
93+(binary_expression
94+ operator: _ @operator)
95+(boolean_negation "!" @operator)
96+(integer_negation "-" @operator)
97+
87; Punctuation
98; Punctuation
88[
99[
89 "("
100 "("
runtime/queries/gn/highlights.scmnew file+42
@@ -0,0 +1,42 @@
1+; Copyright (C) 2021 Will Cassella (github@willcassella.com)
2+;
3+; Licensed under the Apache License, Version 2.0 (the "License");
4+; you may not use this file except in compliance with the License.
5+; You may obtain a copy of the License at
6+;
7+; http://www.apache.org/licenses/LICENSE-2.0
8+;
9+; Unless required by applicable law or agreed to in writing, software
10+; distributed under the License is distributed on an "AS IS" BASIS,
11+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+; See the License for the specific language governing permissions and
13+; limitations under the License.
14+
15+(identifier) @variable.builtin
16+
17+(scope_access field: (_) @variable.other.member)
18+
19+(call target: (_) @function)
20+
21+[ "if" "else" ] @keyword.control.conditional
22+
23+[
24+ (assign_op)
25+ (arithmetic_binary_op)
26+ (comparison_binary_op)
27+ (equivalence_binary_op)
28+ (logical_and_binary_op)
29+ (logical_or_binary_op)
30+ (negation_unary_op)
31+] @operator
32+
33+[ "(" ")" "[" "]" "{" "}" ] @punctuation.bracket
34+[ "." "," ] @punctuation.delimiter
35+
36+(string) @string
37+(string_escape) @constant.character.escape
38+(string_expansion [ "$" "${" "}" ] @constant.character.escape)
39+[ (integer) (hex) ] @constant.numeric
40+(boolean) @constant.builtin.boolean
41+
42+(comment) @comment
runtime/queries/gn/injections.scmnew file+15
@@ -0,0 +1,15 @@
1+; Copyright (C) 2021 Will Cassella (github@willcassella.com)
2+;
3+; Licensed under the Apache License, Version 2.0 (the "License");
4+; you may not use this file except in compliance with the License.
5+; You may obtain a copy of the License at
6+;
7+; http://www.apache.org/licenses/LICENSE-2.0
8+;
9+; Unless required by applicable law or agreed to in writing, software
10+; distributed under the License is distributed on an "AS IS" BASIS,
11+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+; See the License for the specific language governing permissions and
13+; limitations under the License.
14+
15+((comment) @injection.content (#set! injection.language "comment"))
runtime/queries/gn/locals.scmnew file+16
@@ -0,0 +1,16 @@
1+; Copyright (C) 2021 Will Cassella (github@willcassella.com)
2+;
3+; Licensed under the Apache License, Version 2.0 (the "License");
4+; you may not use this file except in compliance with the License.
5+; You may obtain a copy of the License at
6+;
7+; http://www.apache.org/licenses/LICENSE-2.0
8+;
9+; Unless required by applicable law or agreed to in writing, software
10+; distributed under the License is distributed on an "AS IS" BASIS,
11+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+; See the License for the specific language governing permissions and
13+; limitations under the License.
14+
15+(source_file) @local.scope
16+(assignment target: (identifier) @local.definition)
runtime/queries/go/highlights.scm+13−5
@@ -11,6 +11,18 @@
11 function: (selector_expression
11 function: (selector_expression
12 field: (field_identifier) @function.method))
12 field: (field_identifier) @function.method))
13
13
14+
15+; Types
16+
17+(type_parameter_list
18+ (parameter_declaration
19+ name: (identifier) @type.parameter))
20+
21+((type_identifier) @type.builtin
22+ (match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
23+
24+(type_identifier) @type
25+
14; Function definitions
26; Function definitions
15
27
16(function_declaration
28(function_declaration
@@ -24,20 +36,16 @@
24
36
25; Identifiers
37; Identifiers
26
38
27−((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$"))
28(const_spec
39(const_spec
29 name: (identifier) @constant)
40 name: (identifier) @constant)
30
41
31(parameter_declaration (identifier) @variable.parameter)
42(parameter_declaration (identifier) @variable.parameter)
32(variadic_parameter_declaration (identifier) @variable.parameter)
43(variadic_parameter_declaration (identifier) @variable.parameter)
33
44
34−((type_identifier) @type.builtin
35− (match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
36−
37−(type_identifier) @type
38(type_spec
45(type_spec
39 name: (type_identifier) @constructor)
46 name: (type_identifier) @constructor)
40(field_identifier) @variable.other.member
47(field_identifier) @variable.other.member
48+(keyed_element (literal_element (identifier) @variable.other.member))
41(identifier) @variable
49(identifier) @variable
42(package_identifier) @namespace
50(package_identifier) @namespace
43
51
runtime/queries/go/locals.scm+10−2
@@ -1,9 +1,17 @@
1; Scopes
1; Scopes
2
2
3+[
4+ (function_declaration)
5+ (type_declaration)
6+ (block)
3−(block) @local.scope
7+] @local.scope
4
8
5; Definitions
9; Definitions
6
10
11+(type_parameter_list
12+ (parameter_declaration
13+ name: (identifier) @local.definition))
14+
7(parameter_declaration (identifier) @local.definition)
15(parameter_declaration (identifier) @local.definition)
8(variadic_parameter_declaration (identifier) @local.definition)
16(variadic_parameter_declaration (identifier) @local.definition)
9
17
@@ -27,4 +35,4 @@
27
35
28(identifier) @local.reference
36(identifier) @local.reference
29(field_identifier) @local.reference
37(field_identifier) @local.reference
30−
38+(type_identifier) @local.reference
runtime/queries/hare/highlights.scm+4−1
@@ -36,7 +36,10 @@
36 "break"
36 "break"
37] @keyword.control.repeat
37] @keyword.control.repeat
38
38
39+[
40+ "return"
41+ "yield"
39−"return" @keyword.control.return
42+] @keyword.control.return
40
43
41[
44[
42 "abort"
45 "abort"
runtime/queries/haskell/highlights.scm+24−11
@@ -33,6 +33,11 @@
33;; ----------------------------------------------------------------------------
33;; ----------------------------------------------------------------------------
34;; Keywords, operators, includes
34;; Keywords, operators, includes
35
35
36+[
37+ "forall"
38+ "∀"
39+] @keyword.control.repeat
40+
36(pragma) @constant.macro
41(pragma) @constant.macro
37
42
38[
43[
@@ -68,10 +73,7 @@
68 "@"
73 "@"
69] @operator
74] @operator
70
75
71−(qualified_module (module) @constructor)
72−(qualified_type (module) @namespace)
76+(module) @namespace
73−(qualified_variable (module) @namespace)
74−(import (module) @namespace)
75
77
76[
78[
77 (where)
79 (where)
@@ -92,8 +94,6 @@
92 "do"
94 "do"
93 "mdo"
95 "mdo"
94 "rec"
96 "rec"
95− "forall"
96− "∀"
97 "infix"
97 "infix"
98 "infixl"
98 "infixl"
99 "infixr"
99 "infixr"
@@ -104,22 +104,35 @@
104;; Functions and variables
104;; Functions and variables
105
105
106(signature name: (variable) @type)
106(signature name: (variable) @type)
107+(function
107−(function name: (variable) @function)
108+ name: (variable) @function
108−
109+ patterns: (patterns))
109−(variable) @variable
110+((signature (fun)) . (function (variable) @function))
110−"_" @variable.builtin
111+((signature (context (fun))) . (function (variable) @function))
112+((signature (forall (context (fun)))) . (function (variable) @function))
111
113
112(exp_infix (variable) @operator) ; consider infix functions as operators
114(exp_infix (variable) @operator) ; consider infix functions as operators
113
115
114−("@" @namespace) ; "as" pattern operator, e.g. x@Constructor
116+(exp_infix (exp_name) @function)
117+(exp_apply . (exp_name (variable) @function))
118+(exp_apply . (exp_name (qualified_variable (variable) @function)))
115
119
120+(variable) @variable
121+(pat_wildcard) @variable
116
122
117;; ----------------------------------------------------------------------------
123;; ----------------------------------------------------------------------------
118;; Types
124;; Types
119
125
120(type) @type
126(type) @type
127+(type_variable) @type.parameter
121
128
122(constructor) @constructor
129(constructor) @constructor
123
130
124; True or False
131; True or False
125((constructor) @_bool (#match? @_bool "(True|False)")) @constant.builtin.boolean
132((constructor) @_bool (#match? @_bool "(True|False)")) @constant.builtin.boolean
133+
134+;; ----------------------------------------------------------------------------
135+;; Quasi-quotes
136+
137+(quoter) @function
138+; Highlighting of quasiquote_body is handled by injections.scm
runtime/queries/janet/highlights.scmnew file+1
@@ -0,0 +1 @@
1+; inherits: clojure
runtime/queries/java/indents.scmnew file+35
@@ -0,0 +1,35 @@
1+[
2+ (class_body)
3+ (enum_body)
4+ (interface_body)
5+ (constructor_body)
6+ (annotation_type_body)
7+ (module_body)
8+ (block)
9+ (switch_block)
10+ (array_initializer)
11+ (argument_list)
12+ (formal_parameters)
13+ (annotation_argument_list)
14+ (element_value_array_initializer)
15+] @indent
16+
17+[
18+ "}"
19+ ")"
20+ "]"
21+] @outdent
22+
23+; Single statement after if/while/for without brackets
24+(if_statement
25+ consequence: (_) @indent
26+ (#not-kind-eq? @indent "block")
27+ (#set! "scope" "all"))
28+(while_statement
29+ body: (_) @indent
30+ (#not-kind-eq? @indent "block")
31+ (#set! "scope" "all"))
32+(for_statement
33+ (_) @indent
34+ (#not-kind-eq? @indent "block")
35+ (#set! "scope" "all"))
runtime/queries/jinja/highlights.scmnew file+7
@@ -0,0 +1,7 @@
1+(expression) @string
2+(statement) @variable.builtin
3+(keyword) @keyword
4+(comment) @comment
5+(identifier) @variable.parameter
6+(operator) @operator
7+(string) @string
runtime/queries/jinja/injections.scmnew file+4
@@ -0,0 +1,4 @@
1+((source_file) @injection.content
2+ (#set! injection.combined)
3+ (#set! injection.include-children)
4+ (#set! injection.language "html"))
runtime/queries/json5/highlights.scmnew file+11
@@ -0,0 +1,11 @@
1+(string) @string
2+
3+(identifier) @constant
4+
5+(number) @constant.numeric
6+
7+(null) @constant.builtin
8+
9+[(true) (false)] @constant.builtin.boolean
10+
11+(comment) @comment
runtime/queries/kdl/highlights.scm+4−4
@@ -1,12 +1,12 @@
1−(comment) @comment
2(single_line_comment) @comment
1(single_line_comment) @comment
2+(multi_line_comment) @comment
3
3
4(node
4(node
5− name: (identifier) @function)
5+ (identifier) @variable)
6+
6(prop (identifier) @attribute)
7(prop (identifier) @attribute)
7−(type) @type
8
8
9−(bare_identifier) @variable.other.member
9+(type (_) @type) @punctuation.bracket
10
10
11(keyword) @keyword
11(keyword) @keyword
12
12
runtime/queries/kdl/indents.scmnew file+3
@@ -0,0 +1,3 @@
1+(node_children) @indent
2+
3+ "}" @outdent
runtime/queries/kdl/textobjects.scmnew file+27
@@ -0,0 +1,27 @@
1+(type (_) @test.inside) @test.around
2+
3+(node
4+ children: (node_children)? @class.inside) @class.around
5+
6+(node
7+ children: (node_children)? @function.inside) @function.around
8+
9+(node (identifier) @function.movement)
10+
11+[
12+ (single_line_comment)
13+ (multi_line_comment)
14+] @comment.inside
15+
16+[
17+ (single_line_comment)+
18+ (multi_line_comment)+
19+] @comment.around
20+
21+[
22+ (prop)
23+ (value)
24+] @parameter.inside
25+
26+(value (type) ? (_) @parameter.inside @parameter.movement . ) @parameter.around
27+
runtime/queries/kotlin/highlights.scm+4−1
@@ -244,7 +244,10 @@
244 . (identifier)) @namespace
244 . (identifier)) @namespace
245
245
246((type_identifier) @type.builtin
246((type_identifier) @type.builtin
247− (#match? @function.builtin "^(Byte|Short|Int|Long|UByte|UShort|UInt|ULong|Float|Double|Boolean|Char|String|Array|ByteArray|ShortArray|IntArray|LongArray|UByteArray|UShortArray|UIntArray|ULongArray|FloatArray|DoubleArray|BooleanArray|CharArray|Map|Set|List|EmptyMap|EmptySet|EmptyList|MutableMap|MutableSet|MutableList)$"))
247+ (#match? @type.builtin "^(Byte|Short|Int|Long|UByte|UShort|UInt|ULong|Float|Double|Boolean|Char|String|Array|ByteArray|ShortArray|IntArray|LongArray|UByteArray|UShortArray|UIntArray|ULongArray|FloatArray|DoubleArray|BooleanArray|CharArray|Map|Set|List|EmptyMap|EmptySet|EmptyList|MutableMap|MutableSet|MutableList)$"))
248+
249+(type_parameter
250+ (type_identifier) @type.parameter)
248
251
249(type_identifier) @type
252(type_identifier) @type
250
253
runtime/queries/kotlin/locals.scmnew file+15
@@ -0,0 +1,15 @@
1+; Scopes
2+
3+[
4+ (class_declaration)
5+ (function_declaration)
6+] @local.scope
7+
8+; Definitions
9+
10+(type_parameter
11+ (type_identifier) @local.definition)
12+
13+; References
14+
15+(type_identifier) @local.reference
runtime/queries/log/highlights.scmnew file+10
@@ -0,0 +1,10 @@
1+(trace) @comment
2+(debug) @hint
3+(info) @info
4+(warn) @warning
5+(error) @error
6+(year_month_day) @keyword
7+(time) @constant
8+(string_literal) @string
9+(number) @constant.numeric
10+(constant) @constant.builtin
runtime/queries/lpf/highlights.scmnew file+19
@@ -0,0 +1,19 @@
1+[
2+ "SYSCONFIG"
3+ "BLOCK"
4+ "LOCATE"
5+ "COMP"
6+ "FREQUENCY"
7+ "PORT"
8+ "IOBUF"
9+] @keyword
10+
11+["SITE"] @keyword.storage
12+
13+["="] @operator
14+
15+((number) @constant.numeric)
16+
17+((string) @string)
18+((line_comment) @comment)
19+
runtime/queries/markdown.inline/injections.scm+4−1
@@ -1,4 +1,7 @@
1
1
2+((html_tag) @injection.content
3+ (#set! injection.language "html")
2−((html_tag) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children))
4+ (#set! injection.include-unnamed-children)
5+ (#set! injection.combined))
3
6
4((latex_block) @injection.content (#set! injection.language "latex") (#set! injection.include-unnamed-children))
7((latex_block) @injection.content (#set! injection.language "latex") (#set! injection.include-unnamed-children))
runtime/queries/nix/injections.scm+14

This diff is too big to show here.

runtime/queries/nu/folds.scmdeleted−4

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/nu/locals.scmdeleted−13

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/ocaml/highlights.scm+2

This diff is too big to show here.

runtime/queries/perl/highlights.scm−2

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/perl/injections.scm+2

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/python/highlights.scm+6

This diff is too big to show here.

runtime/queries/python/indents.scm+9

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/rust/highlights.scm+7

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/rust/injections.scm+6

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/scheme/highlights.scm+5

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/queries/yaml/injections.scm+52

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/README.md+8

This diff is too big to show here.

runtime/themes/autumn.toml+62−65

This diff is too big to show here.

runtime/themes/autumn_night.toml+6−6

This diff is too big to show here.

runtime/themes/catppuccin_frappe.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_latte.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_macchiato.toml+2

This diff is too big to show here.

runtime/themes/catppuccin_mocha.toml+11

This diff is too big to show here.

runtime/themes/cyan_light.tomlnew file+154

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/darcula.toml+10−4

This diff is too big to show here.

runtime/themes/dracula.toml+2

This diff is too big to show here.

runtime/themes/everblush.toml+1

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/jellybeans.toml+1−1

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/material_darker.tomlnew file+20

This diff is too big to show here.

runtime/themes/material_deep_ocean.tomlnew file+123

This diff is too big to show here.

runtime/themes/material_oceanic.tomlnew file+21

This diff is too big to show here.

runtime/themes/material_palenight.tomlnew file+19

This diff is too big to show here.

runtime/themes/modus_operandi.tomlnew file+194

This diff is too big to show here.

runtime/themes/modus_operandi_deuteranopia.tomlnew file+46

This diff is too big to show here.

runtime/themes/modus_operandi_tinted.tomlnew file+45

This diff is too big to show here.

runtime/themes/modus_operandi_tritanopia.tomlnew file+73

This diff is too big to show here.

runtime/themes/modus_vivendi.tomlnew file+242

This diff is too big to show here.

runtime/themes/modus_vivendi_deuteranopia.tomlnew file+158

This diff is too big to show here.

runtime/themes/modus_vivendi_tinted.tomlnew file+144

This diff is too big to show here.

runtime/themes/modus_vivendi_tritanopia.tomlnew file+186

This diff is too big to show here.

runtime/themes/molokai.toml+1−1

This diff is too big to show here.

runtime/themes/monokai.toml+5

This diff is too big to show here.

runtime/themes/naysayer.toml+2−2

This diff is too big to show here.

runtime/themes/new_moon.tomlnew file+124

This diff is too big to show here.

runtime/themes/nightfox.toml+2−2

This diff is too big to show here.

runtime/themes/noctis.toml+26−26

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/nord.toml+23−13

This diff is too big to show here.

runtime/themes/onedark.toml+1

This diff is too big to show here.

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

This diff is too big to show here.

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

This diff is too big to show here.

runtime/themes/poimandres.tomlnew file+113

This diff is too big to show here.

runtime/themes/poimandres_storm.tomlnew file+19

This diff is too big to show here.

runtime/themes/rose_pine.toml+31−29

This diff is too big to show here.

runtime/themes/starlight.tomlnew file+113

This diff is too big to show here.

runtime/themes/vim_dark_high_contrast.tomlnew file+72

This diff is too big to show here.

runtime/themes/yellowed.tomlnew file+135

This diff is too big to show here.

runtime/themes/zenburn.toml+1−1

This diff is too big to show here.

runtime/tutor+157−3

This diff is too big to show here.

rust-toolchain.toml+2−2

This diff is too big to show here.

xtask/Cargo.toml+12−6

This diff is too big to show here.