on master help sign in
Merge remote-tracking branch 'origin/master' into debug
9ed930bBlaž Hrastnik
.github/ISSUE_TEMPLATE/bug_report.md+1−1
@@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
17### Environment
17### Environment
18
18
19- Platform: <!-- macOS / Windows / Linux -->
19- Platform: <!-- macOS / Windows / Linux -->
20−- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
20+- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
21
21
22<details><summary>~/.cache/helix/helix.log</summary>
22<details><summary>~/.cache/helix/helix.log</summary>
23
23
.github/workflows/build.yml+9−9
@@ -25,19 +25,19 @@ jobs:
25 override: true
25 override: true
26
26
27 - name: Cache cargo registry
27 - name: Cache cargo registry
28− uses: actions/cache@v2.1.6
28+ uses: actions/cache@v2.1.7
29 with:
29 with:
30 path: ~/.cargo/registry
30 path: ~/.cargo/registry
31 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
31 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
32
32
33 - name: Cache cargo index
33 - name: Cache cargo index
34− uses: actions/cache@v2.1.6
34+ uses: actions/cache@v2.1.7
35 with:
35 with:
36 path: ~/.cargo/git
36 path: ~/.cargo/git
37 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
37 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
38
38
39 - name: Cache cargo target dir
39 - name: Cache cargo target dir
40− uses: actions/cache@v2.1.6
40+ uses: actions/cache@v2.1.7
41 with:
41 with:
42 path: target
42 path: target
43 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
43 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
64 override: true
64 override: true
65
65
66 - name: Cache cargo registry
66 - name: Cache cargo registry
67− uses: actions/cache@v2.1.6
67+ uses: actions/cache@v2.1.7
68 with:
68 with:
69 path: ~/.cargo/registry
69 path: ~/.cargo/registry
70 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
70 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
71
71
72 - name: Cache cargo index
72 - name: Cache cargo index
73− uses: actions/cache@v2.1.6
73+ uses: actions/cache@v2.1.7
74 with:
74 with:
75 path: ~/.cargo/git
75 path: ~/.cargo/git
76 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
76 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
77
77
78 - name: Cache cargo target dir
78 - name: Cache cargo target dir
79− uses: actions/cache@v2.1.6
79+ uses: actions/cache@v2.1.7
80 with:
80 with:
81 path: target
81 path: target
82 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
82 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -109,19 +109,19 @@ jobs:
109 components: rustfmt, clippy
109 components: rustfmt, clippy
110
110
111 - name: Cache cargo registry
111 - name: Cache cargo registry
112− uses: actions/cache@v2.1.6
112+ uses: actions/cache@v2.1.7
113 with:
113 with:
114 path: ~/.cargo/registry
114 path: ~/.cargo/registry
115 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
115 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
116
116
117 - name: Cache cargo index
117 - name: Cache cargo index
118− uses: actions/cache@v2.1.6
118+ uses: actions/cache@v2.1.7
119 with:
119 with:
120 path: ~/.cargo/git
120 path: ~/.cargo/git
121 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
121 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
122
122
123 - name: Cache cargo target dir
123 - name: Cache cargo target dir
124− uses: actions/cache@v2.1.6
124+ uses: actions/cache@v2.1.7
125 with:
125 with:
126 path: target
126 path: target
127 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
127 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
.gitmodules+8
@@ -142,3 +142,11 @@
142 path = helix-syntax/languages/tree-sitter-perl
142 path = helix-syntax/languages/tree-sitter-perl
143 url = https://github.com/ganezdragon/tree-sitter-perl
143 url = https://github.com/ganezdragon/tree-sitter-perl
144 shallow = true
144 shallow = true
145+[submodule "helix-syntax/languages/tree-sitter-wgsl"]
146+ path = helix-syntax/languages/tree-sitter-wgsl
147+ url = https://github.com/szebniok/tree-sitter-wgsl
148+ shallow = true
149+[submodule "helix-syntax/tree-sitter-llvm"]
150+ path = helix-syntax/languages/tree-sitter-llvm
151+ url = https://github.com/benwilliamgraham/tree-sitter-llvm
152+ shallow = true
Cargo.lock+18−19
@@ -13,9 +13,9 @@ dependencies = [
13
13
14[[package]]
14[[package]]
15name = "anyhow"
15name = "anyhow"
16−version = "1.0.46"
16+version = "1.0.51"
17source = "registry+https://github.com/rust-lang/crates.io-index"
17source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
18+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
19
19
20[[package]]
20[[package]]
21name = "arc-swap"
21name = "arc-swap"
@@ -258,15 +258,15 @@ dependencies = [
258
258
259[[package]]
259[[package]]
260name = "futures-core"
260name = "futures-core"
261−version = "0.3.17"
261+version = "0.3.18"
262source = "registry+https://github.com/rust-lang/crates.io-index"
262source = "registry+https://github.com/rust-lang/crates.io-index"
263−checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
263+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
264
264
265[[package]]
265[[package]]
266name = "futures-executor"
266name = "futures-executor"
267−version = "0.3.17"
267+version = "0.3.18"
268source = "registry+https://github.com/rust-lang/crates.io-index"
268source = "registry+https://github.com/rust-lang/crates.io-index"
269−checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
269+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
270dependencies = [
270dependencies = [
271 "futures-core",
271 "futures-core",
272 "futures-task",
272 "futures-task",
@@ -275,17 +275,16 @@ dependencies = [
275
275
276[[package]]
276[[package]]
277name = "futures-task"
277name = "futures-task"
278−version = "0.3.17"
278+version = "0.3.18"
279source = "registry+https://github.com/rust-lang/crates.io-index"
279source = "registry+https://github.com/rust-lang/crates.io-index"
280−checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
280+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
281
281
282[[package]]
282[[package]]
283name = "futures-util"
283name = "futures-util"
284−version = "0.3.17"
284+version = "0.3.18"
285source = "registry+https://github.com/rust-lang/crates.io-index"
285source = "registry+https://github.com/rust-lang/crates.io-index"
286−checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
286+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
287dependencies = [
287dependencies = [
288− "autocfg",
289 "futures-core",
288 "futures-core",
290 "futures-task",
289 "futures-task",
291 "pin-project-lite",
290 "pin-project-lite",
@@ -914,9 +913,9 @@ dependencies = [
914
913
915[[package]]
914[[package]]
916name = "serde_json"
915name = "serde_json"
917−version = "1.0.70"
916+version = "1.0.72"
918source = "registry+https://github.com/rust-lang/crates.io-index"
917source = "registry+https://github.com/rust-lang/crates.io-index"
919−checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
918+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
920dependencies = [
919dependencies = [
921 "itoa",
920 "itoa",
922 "ryu",
921 "ryu",
@@ -1086,9 +1085,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1086
1085
1087[[package]]
1086[[package]]
1088name = "tokio"
1087name = "tokio"
1089−version = "1.13.1"
1088+version = "1.14.0"
1090source = "registry+https://github.com/rust-lang/crates.io-index"
1089source = "registry+https://github.com/rust-lang/crates.io-index"
1091−checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
1090+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
1092dependencies = [
1091dependencies = [
1093 "autocfg",
1092 "autocfg",
1094 "bytes",
1093 "bytes",
@@ -1106,9 +1105,9 @@ dependencies = [
1106
1105
1107[[package]]
1106[[package]]
1108name = "tokio-macros"
1107name = "tokio-macros"
1109−version = "1.5.0"
1108+version = "1.6.0"
1110source = "registry+https://github.com/rust-lang/crates.io-index"
1109source = "registry+https://github.com/rust-lang/crates.io-index"
1111−checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
1110+checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
1112dependencies = [
1111dependencies = [
1113 "proc-macro2",
1112 "proc-macro2",
1114 "quote",
1113 "quote",
@@ -1137,9 +1136,9 @@ dependencies = [
1137
1136
1138[[package]]
1137[[package]]
1139name = "tree-sitter"
1138name = "tree-sitter"
1140−version = "0.20.0"
1139+version = "0.20.1"
1141source = "registry+https://github.com/rust-lang/crates.io-index"
1140source = "registry+https://github.com/rust-lang/crates.io-index"
1142−checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
1141+checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
1143dependencies = [
1142dependencies = [
1144 "cc",
1143 "cc",
1145 "regex",
1144 "regex",
README.md+1−1
@@ -69,7 +69,7 @@ Contributors are very welcome! **No contribution is too small and all contributi
69
69
70Some suggestions to get started:
70Some suggestions to get started:
71
71
72−- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
72+- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
73- Help with packaging on various distributions needed!
73- Help with packaging on various distributions needed!
74- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
74- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
75 * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
75 * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
book/src/configuration.md+12
@@ -24,6 +24,18 @@ To override global configuration parameters, create a `config.toml` file located
24| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
24| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
25| `auto-info` | Whether to display infoboxes | `true` |
25| `auto-info` | Whether to display infoboxes | `true` |
26
26
27+`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
28+
29+| Key | Description | Default |
30+|--|--|---------|
31+|`hidden` | Enables ignoring hidden files. | true
32+|`parents` | Enables reading ignore files from parent directories. | true
33+|`ignore` | Enables reading `.ignore` files. | true
34+|`git-ignore` | Enables reading `.gitignore` files. | true
35+|`git-global` | Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. | true
36+|`git-exclude` | Enables reading `.git/info/exclude` files. | true
37+|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
38+
27## LSP
39## LSP
28
40
29To display all language server messages in the status line add the following to your `config.toml`:
41To display all language server messages in the status line add the following to your `config.toml`:
book/src/install.md+1−3
@@ -25,9 +25,7 @@ shell for working on Helix.
25
25
26Releases are available in the `community` repository.
26Releases are available in the `community` repository.
27
27
28−Packages are also available on AUR:
28+A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
29−- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
30−- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
31
29
32## Build from source
30## Build from source
33
31
book/src/keymap.md+42−36
@@ -45,44 +45,46 @@
45
45
46### Changes
46### Changes
47
47
48−| Key | Description | Command |
48+| Key | Description | Command |
49−| ----- | ----------- | ------- |
49+| ----- | ----------- | ------- |
50−| `r` | Replace with a character | `replace` |
50+| `r` | Replace with a character | `replace` |
51−| `R` | Replace with yanked text | `replace_with_yanked` |
51+| `R` | Replace with yanked text | `replace_with_yanked` |
52−| `~` | Switch case of the selected text | `switch_case` |
52+| `~` | Switch case of the selected text | `switch_case` |
53−| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
53+| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
54−| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
54+| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
55−| `i` | Insert before selection | `insert_mode` |
55+| `i` | Insert before selection | `insert_mode` |
56−| `a` | Insert after selection (append) | `append_mode` |
56+| `a` | Insert after selection (append) | `append_mode` |
57−| `I` | Insert at the start of the line | `prepend_to_line` |
57+| `I` | Insert at the start of the line | `prepend_to_line` |
58−| `A` | Insert at the end of the line | `append_to_line` |
58+| `A` | Insert at the end of the line | `append_to_line` |
59−| `o` | Open new line below selection | `open_below` |
59+| `o` | Open new line below selection | `open_below` |
60−| `O` | Open new line above selection | `open_above` |
60+| `O` | Open new line above selection | `open_above` |
61−| `.` | Repeat last change | N/A |
61+| `.` | Repeat last change | N/A |
62−| `u` | Undo change | `undo` |
62+| `u` | Undo change | `undo` |
63−| `U` | Redo change | `redo` |
63+| `U` | Redo change | `redo` |
64−| `Alt-u` | Move backward in history | `earlier` |
64+| `Alt-u` | Move backward in history | `earlier` |
65−| `Alt-U` | Move forward in history | `later` |
65+| `Alt-U` | Move forward in history | `later` |
66−| `y` | Yank selection | `yank` |
66+| `y` | Yank selection | `yank` |
67−| `p` | Paste after selection | `paste_after` |
67+| `p` | Paste after selection | `paste_after` |
68−| `P` | Paste before selection | `paste_before` |
68+| `P` | Paste before selection | `paste_before` |
69−| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
69+| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
70−| `>` | Indent selection | `indent` |
70+| `>` | Indent selection | `indent` |
71−| `<` | Unindent selection | `unindent` |
71+| `<` | Unindent selection | `unindent` |
72−| `=` | Format selection (**LSP**) | `format_selections` |
72+| `=` | Format selection (**LSP**) | `format_selections` |
73−| `d` | Delete selection | `delete_selection` |
73+| `d` | Delete selection | `delete_selection` |
74+| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
74−| `c` | Change selection (delete and enter insert mode) | `change_selection` |
75+| `c` | Change selection (delete and enter insert mode) | `change_selection` |
76+| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
75−| `Ctrl-a` | Increment object (number) under cursor | `increment` |
77+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
76−| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
78+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
77
79
78#### Shell
80#### Shell
79
81
80−| Key | Description | Command |
82+| Key | Description | Command |
81−| ------ | ----------- | ------- |
83+| ------ | ----------- | ------- |
82−| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
84+| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
83−| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
85+| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
84−| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
86+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
85−| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
87+| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
86
88
87
89
88### Selection manipulation
90### Selection manipulation
@@ -92,6 +94,7 @@
92| `s` | Select all regex matches inside selections | `select_regex` |
94| `s` | Select all regex matches inside selections | `select_regex` |
93| `S` | Split selection into subselections on regex matches | `split_selection` |
95| `S` | Split selection into subselections on regex matches | `split_selection` |
94| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
96| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
97+| `&` | Align selection in columns | `align_selections` |
95| `_` | Trim whitespace from the selection | `trim_selections` |
98| `_` | Trim whitespace from the selection | `trim_selections` |
96| `;` | Collapse selection onto a single cursor | `collapse_selection` |
99| `;` | Collapse selection onto a single cursor | `collapse_selection` |
97| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
100| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
@@ -157,6 +160,7 @@ Jumps to various locations.
157| ----- | ----------- | ------- |
160| ----- | ----------- | ------- |
158| `g` | Go to the start of the file | `goto_file_start` |
161| `g` | Go to the start of the file | `goto_file_start` |
159| `e` | Go to the end of the file | `goto_last_line` |
162| `e` | Go to the end of the file | `goto_last_line` |
163+| `f` | Go to files in the selection | `goto_file` |
160| `h` | Go to the start of the line | `goto_line_start` |
164| `h` | Go to the start of the line | `goto_line_start` |
161| `l` | Go to the end of the line | `goto_line_end` |
165| `l` | Go to the end of the line | `goto_line_end` |
162| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
166| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
@@ -199,6 +203,8 @@ This layer is similar to vim keybindings as kakoune does not support window.
199| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
203| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
200| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
204| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
201| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
205| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
206+| `f` | Go to files in the selection in horizontal splits | `goto_file` |
207+| `F` | Go to files in the selection in vertical splits | `goto_file` |
202| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
208| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
203| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
209| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
204| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
210| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
@@ -314,7 +320,7 @@ Keys to use within prompt, Remapping currently not supported.
314| `Ctrl-u` | Delete to start of line |
320| `Ctrl-u` | Delete to start of line |
315| `Ctrl-k` | Delete to end of line |
321| `Ctrl-k` | Delete to end of line |
316| `backspace`, `Ctrl-h` | Delete previous char |
322| `backspace`, `Ctrl-h` | Delete previous char |
317−| `delete`, `Ctrl-d` | Delete previous char |
323+| `delete`, `Ctrl-d` | Delete next char |
318| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
324| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
319| `Ctrl-p`, `Up` | Select previous history |
325| `Ctrl-p`, `Up` | Select previous history |
320| `Ctrl-n`, `Down` | Select next history |
326| `Ctrl-n`, `Down` | Select next history |
book/src/remapping.md+2−1
@@ -53,4 +53,5 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
53
53
54Keys can be disabled by binding them to the `no_op` command.
54Keys can be disabled by binding them to the `no_op` command.
55
55
56+Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
56−Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
57+> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
book/src/themes.md+3−2
@@ -145,11 +145,12 @@ We use a similar set of scopes as
145 - `conditional` - `if`, `else`
145 - `conditional` - `if`, `else`
146 - `repeat` - `for`, `while`, `loop`
146 - `repeat` - `for`, `while`, `loop`
147 - `import` - `import`, `export`
147 - `import` - `import`, `export`
148− - (TODO: return?)
148+ - `return`
149+ - `operator` - `or`, `in`
149 - `directive` - Preprocessor directives (`#if` in C)
150 - `directive` - Preprocessor directives (`#if` in C)
150 - `function` - `fn`, `func`
151 - `function` - `fn`, `func`
151
152
152−- `operator` - `||`, `+=`, `>`, `or`
153+- `operator` - `||`, `+=`, `>`
153
154
154- `function`
155- `function`
155 - `builtin`
156 - `builtin`
book/src/usage.md+2
@@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
23| `/` | Last search |
23| `/` | Last search |
24| `:` | Last executed command |
24| `:` | Last executed command |
25| `"` | Last yanked text |
25| `"` | Last yanked text |
26+| `_` | Black hole |
26
27
27> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
28> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
29+> The black hole register works as a no-op register, meaning no data will be written to / read from it.
28
30
29## Surround
31## Surround
30
32
helix-core/src/diagnostic.rs+2−2
@@ -1,7 +1,7 @@
1//! LSP diagnostic utility types.
1//! LSP diagnostic utility types.
2
2
3/// Describes the severity level of a [`Diagnostic`].
3/// Describes the severity level of a [`Diagnostic`].
4−#[derive(Debug, Eq, PartialEq)]
4+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
5pub enum Severity {
5pub enum Severity {
6 Error,
6 Error,
7 Warning,
7 Warning,
@@ -17,7 +17,7 @@ pub struct Range {
17}
17}
18
18
19/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
19/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
20−#[derive(Debug)]
20+#[derive(Debug, Clone)]
21pub struct Diagnostic {
21pub struct Diagnostic {
22 pub range: Range,
22 pub range: Range,
23 pub line: usize,
23 pub line: usize,
helix-core/src/match_brackets.rs+66−29
@@ -1,3 +1,5 @@
1+use tree_sitter::Node;
2+
1use crate::{Rope, Syntax};
3use crate::{Rope, Syntax};
2
4
3const PAIRS: &[(char, char)] = &[
5const PAIRS: &[(char, char)] = &[
@@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
6 ('[', ']'),
8 ('[', ']'),
7 ('<', '>'),
9 ('<', '>'),
8 ('\'', '\''),
10 ('\'', '\''),
9− ('"', '"'),
11+ ('\"', '\"'),
10];
12];
13+
11// limit matching pairs to only ( ) { } [ ] < >
14// limit matching pairs to only ( ) { } [ ] < >
12
15
16+// Returns the position of the matching bracket under cursor.
17+//
18+// If the cursor is one the opening bracket, the position of
19+// the closing bracket is returned. If the cursor in the closing
20+// bracket, the position of the opening bracket is returned.
21+//
22+// If the cursor is not on a bracket, `None` is returned.
23+#[must_use]
24+pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
25+ if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
26+ return None;
27+ }
28+ find_pair(syntax, doc, pos, false)
29+}
30+
31+// Returns the position of the bracket that is closing the current scope.
32+//
33+// If the cursor is on an opening or closing bracket, the function
34+// behaves equivalent to [`find_matching_bracket`].
35+//
36+// If the cursor position is within a scope, the function searches
37+// for the surrounding scope that is surrounded by brackets and
38+// returns the position of the closing bracket for that scope.
39+//
40+// If no surrounding scope is found, the function returns `None`.
13#[must_use]
41#[must_use]
14−pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
42+pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
43+ find_pair(syntax, doc, pos, true)
44+}
45+
46+fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
15 let tree = syntax.tree();
47 let tree = syntax.tree();
48+ let pos = doc.char_to_byte(pos);
16
49
17− let byte_pos = doc.char_to_byte(pos);
50+ let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
18
51
19− // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
52+ loop {
20− // return the other edge.
53+ let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
54+ let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
21
55
22− let node = match tree
56+ if is_valid_pair(doc, start_char, end_char) {
23− .root_node()
57+ if end_byte == pos {
24− .named_descendant_for_byte_range(byte_pos, byte_pos)
58+ return Some(start_char);
25− {
59+ }
26− Some(node) => node,
60+ // We return the end char if the cursor is either on the start char
27− None => return None,
61+ // or at some arbitrary position between start and end char.
62+ return Some(end_char);
28− };
63+ }
29
64
30− if node.is_error() {
65+ if traverse_parents {
66+ node = node.parent()?;
67+ } else {
31− return None;
68+ return None;
69+ }
32 }
70 }
71+}
33
72
73+fn is_valid_bracket(c: char) -> bool {
74+ PAIRS.iter().any(|(l, r)| *l == c || *r == c)
75+}
76+
77+fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
78+ PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
79+}
80+
81+fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
34 let len = doc.len_bytes();
82 let len = doc.len_bytes();
83+
35 let start_byte = node.start_byte();
84 let start_byte = node.start_byte();
36− let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
85+ let end_byte = node.end_byte().saturating_sub(1);
86+
37 if start_byte >= len || end_byte >= len {
87 if start_byte >= len || end_byte >= len {
38 return None;
88 return None;
39 }
89 }
40
90
41− let start_char = doc.byte_to_char(start_byte);
42− let end_char = doc.byte_to_char(end_byte);
43−
44− if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
45− if start_byte == byte_pos {
46− return Some(end_char);
47− }
48−
49− if end_byte == byte_pos {
91+ Some((start_byte, end_byte))
50− return Some(start_char);
51− }
52− }
53−
54− None
55}
92}
helix-core/src/register.rs+11−3
@@ -15,7 +15,11 @@ impl Register {
15 }
15 }
16
16
17 pub fn new_with_values(name: char, values: Vec<String>) -> Self {
17 pub fn new_with_values(name: char, values: Vec<String>) -> Self {
18+ if name == '_' {
19+ Self::new(name)
20+ } else {
18− Self { name, values }
21+ Self { name, values }
22+ }
19 }
23 }
20
24
21 pub const fn name(&self) -> char {
25 pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
27 }
31 }
28
32
29 pub fn write(&mut self, values: Vec<String>) {
33 pub fn write(&mut self, values: Vec<String>) {
34+ if self.name != '_' {
30− self.values = values;
35+ self.values = values;
36+ }
31 }
37 }
32
38
33 pub fn push(&mut self, value: String) {
39 pub fn push(&mut self, value: String) {
40+ if self.name != '_' {
34− self.values.push(value);
41+ self.values.push(value);
42+ }
35 }
43 }
36}
44}
37
45
helix-core/src/selection.rs+3−3
@@ -308,10 +308,10 @@ impl Range {
308}
308}
309
309
310impl From<(usize, usize)> for Range {
310impl From<(usize, usize)> for Range {
311− fn from(tuple: (usize, usize)) -> Self {
311+ fn from((anchor, head): (usize, usize)) -> Self {
312 Self {
312 Self {
313− anchor: tuple.0,
313+ anchor,
314− head: tuple.1,
314+ head,
315 horiz: None,
315 horiz: None,
316 }
316 }
317 }
317 }
helix-core/src/surround.rs+83−65
@@ -1,4 +1,4 @@
1−use crate::{search, Selection};
1+use crate::{search, Range, Selection};
2use ropey::RopeSlice;
2use ropey::RopeSlice;
3
3
4pub const PAIRS: &[(char, char)] = &[
4pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
35pub fn find_nth_pairs_pos(
35pub fn find_nth_pairs_pos(
36 text: RopeSlice,
36 text: RopeSlice,
37 ch: char,
37 ch: char,
38− pos: usize,
38+ range: Range,
39 n: usize,
39 n: usize,
40) -> Option<(usize, usize)> {
40) -> Option<(usize, usize)> {
41− let (open, close) = get_pair(ch);
42−
43− if text.len_chars() < 2 || pos >= text.len_chars() {
41+ if text.len_chars() < 2 || range.to() >= text.len_chars() {
44 return None;
42 return None;
45 }
43 }
46
44
45+ let (open, close) = get_pair(ch);
46+ let pos = range.cursor(text);
47+
47 if open == close {
48 if open == close {
48 if Some(open) == text.get_char(pos) {
49 if Some(open) == text.get_char(pos) {
49− // Special case: cursor is directly on a matching char.
50+ // Cursor is directly on match char. We return no match
50− match pos {
51− 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
52− _ if (pos + 1) == text.len_chars() => {
53− Some((search::find_nth_prev(text, open, pos, n)?, pos))
54− }
55− // We return no match because there's no way to know which
51+ // because there's no way to know which side of the char
56− // side of the char we should be searching on.
52+ // we should be searching on.
57− _ => None,
53+ return None;
58− }
59− } else {
60− Some((
61− search::find_nth_prev(text, open, pos, n)?,
62− search::find_nth_next(text, close, pos, n)?,
63− ))
64 }
54 }
55+ Some((
56+ search::find_nth_prev(text, open, pos, n)?,
57+ search::find_nth_next(text, close, pos, n)?,
58+ ))
65 } else {
59 } else {
66 Some((
60 Some((
67 find_nth_open_pair(text, open, close, pos, n)?,
61 find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
160) -> Option<Vec<usize>> {
154) -> Option<Vec<usize>> {
161 let mut change_pos = Vec::new();
155 let mut change_pos = Vec::new();
162
156
163− for range in selection {
157+ for &range in selection {
164− let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
158+ let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
165 if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
159 if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
166 return None;
160 return None;
167 }
161 }
@@ -178,67 +172,91 @@ mod test {
178 use ropey::Rope;
172 use ropey::Rope;
179 use smallvec::SmallVec;
173 use smallvec::SmallVec;
180
174
181− #[test]
175+ fn check_find_nth_pair_pos(
182− fn test_find_nth_pairs_pos() {
176+ text: &str,
177+ cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
178+ ) {
183− let doc = Rope::from("some (text) here");
179+ let doc = Rope::from(text);
184 let slice = doc.slice(..);
180 let slice = doc.slice(..);
185
181
186− // cursor on [t]ext
182+ for (cursor_pos, ch, n, expected_range) in cases {
187− assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
188− assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
189− // cursor on so[m]e
190− assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
183+ let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
191− // cursor on bracket itself
184+ assert_eq!(
192− assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
185+ range, expected_range,
193− assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
186+ "Expected {:?}, got {:?}",
187+ expected_range, range
188+ );
189+ }
194 }
190 }
195
191
196 #[test]
192 #[test]
197− fn test_find_nth_pairs_pos_skip() {
193+ fn test_find_nth_pairs_pos() {
198− let doc = Rope::from("(so (many (good) text) here)");
194+ check_find_nth_pair_pos(
199− let slice = doc.slice(..);
195+ "some (text) here",
196+ vec![
197+ // cursor on [t]ext
198+ (6, '(', 1, Some((5, 10))),
199+ (6, ')', 1, Some((5, 10))),
200+ // cursor on so[m]e
201+ (2, '(', 1, None),
202+ // cursor on bracket itself
203+ (5, '(', 1, Some((5, 10))),
204+ (10, '(', 1, Some((5, 10))),
205+ ],
206+ );
207+ }
200
208
209+ #[test]
210+ fn test_find_nth_pairs_pos_skip() {
211+ check_find_nth_pair_pos(
212+ "(so (many (good) text) here)",
213+ vec![
201− // cursor on go[o]d
214+ // cursor on go[o]d
202− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
215+ (13, '(', 1, Some((10, 15))),
203− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
216+ (13, '(', 2, Some((4, 21))),
204− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
217+ (13, '(', 3, Some((0, 27))),
218+ ],
219+ );
205 }
220 }
206
221
207 #[test]
222 #[test]
208 fn test_find_nth_pairs_pos_same() {
223 fn test_find_nth_pairs_pos_same() {
224+ check_find_nth_pair_pos(
209− let doc = Rope::from("'so 'many 'good' text' here'");
225+ "'so 'many 'good' text' here'",
210− let slice = doc.slice(..);
226+ vec![
211−
212− // cursor on go[o]d
227+ // cursor on go[o]d
213− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
228+ (13, '\'', 1, Some((10, 15))),
214− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
229+ (13, '\'', 2, Some((4, 21))),
215− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
230+ (13, '\'', 3, Some((0, 27))),
216− // cursor on the quotes
231+ // cursor on the quotes
217− assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
232+ (10, '\'', 1, None),
218− // this is the best we can do since opening and closing pairs are same
233+ ],
219− assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
234+ )
220− assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
221 }
235 }
222
236
223 #[test]
237 #[test]
224 fn test_find_nth_pairs_pos_step() {
238 fn test_find_nth_pairs_pos_step() {
239+ check_find_nth_pair_pos(
225− let doc = Rope::from("((so)((many) good (text))(here))");
240+ "((so)((many) good (text))(here))",
226− let slice = doc.slice(..);
241+ vec![
227−
228− // cursor on go[o]d
242+ // cursor on go[o]d
229− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
243+ (15, '(', 1, Some((5, 24))),
230− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
244+ (15, '(', 2, Some((0, 31))),
245+ ],
246+ )
231 }
247 }
232
248
233 #[test]
249 #[test]
234 fn test_find_nth_pairs_pos_mixed() {
250 fn test_find_nth_pairs_pos_mixed() {
251+ check_find_nth_pair_pos(
235− let doc = Rope::from("(so [many {good} text] here)");
252+ "(so [many {good} text] here)",
236− let slice = doc.slice(..);
253+ vec![
237−
238− // cursor on go[o]d
254+ // cursor on go[o]d
239− assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
255+ (13, '{', 1, Some((10, 15))),
240− assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
256+ (13, '[', 1, Some((4, 21))),
241− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
257+ (13, '(', 1, Some((0, 27))),
258+ ],
259+ )
242 }
260 }
243
261
244 #[test]
262 #[test]
helix-core/src/textobject.rs+6−4
@@ -114,7 +114,7 @@ pub fn textobject_surround(
114 ch: char,
114 ch: char,
115 count: usize,
115 count: usize,
116) -> Range {
116) -> Range {
117− surround::find_nth_pairs_pos(slice, ch, range.head, count)
117+ surround::find_nth_pairs_pos(slice, ch, range, count)
118 .map(|(anchor, head)| match textobject {
118 .map(|(anchor, head)| match textobject {
119 TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
119 TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
120 TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
120 TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
170
170
171 #[test]
171 #[test]
172 fn test_textobject_word() {
172 fn test_textobject_word() {
173− // (text, [(cursor position, textobject, final range), ...])
173+ // (text, [(char position, textobject, final range), ...])
174 let tests = &[
174 let tests = &[
175 (
175 (
176 "cursor at beginning of doc",
176 "cursor at beginning of doc",
@@ -269,7 +269,9 @@ mod test {
269 let slice = doc.slice(..);
269 let slice = doc.slice(..);
270 for &case in scenario {
270 for &case in scenario {
271 let (pos, objtype, expected_range) = case;
271 let (pos, objtype, expected_range) = case;
272+ // cursor is a single width selection
273+ let range = Range::new(pos, pos + 1);
272− let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
274+ let result = textobject_word(slice, range, objtype, 1, false);
273 assert_eq!(
275 assert_eq!(
274 result,
276 result,
275 expected_range.into(),
277 expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
283
285
284 #[test]
286 #[test]
285 fn test_textobject_surround() {
287 fn test_textobject_surround() {
286− // (text, [(cursor position, textobject, final range, count), ...])
288+ // (text, [(cursor position, textobject, final range, surround char, count), ...])
287 let tests = &[
289 let tests = &[
288 (
290 (
289 "simple (single) surround pairs",
291 "simple (single) surround pairs",
helix-lsp/Cargo.toml+1−1
@@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] }
23serde = { version = "1.0", features = ["derive"] }
23serde = { version = "1.0", features = ["derive"] }
24serde_json = "1.0"
24serde_json = "1.0"
25thiserror = "1.0"
25thiserror = "1.0"
26−tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
26+tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27tokio-stream = "0.1.8"
27tokio-stream = "0.1.8"
helix-syntax/languages/tree-sitter-wgslnew file+1

Submodule changed.

helix-term/src/application.rs+2−2
@@ -122,7 +122,7 @@ impl Application {
122 if first.is_dir() {
122 if first.is_dir() {
123 std::env::set_current_dir(&first)?;
123 std::env::set_current_dir(&first)?;
124 editor.new_file(Action::VerticalSplit);
124 editor.new_file(Action::VerticalSplit);
125− compositor.push(Box::new(ui::file_picker(".".into())));
125+ compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
126 } else {
126 } else {
127 let nr_of_files = args.files.len();
127 let nr_of_files = args.files.len();
128 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
128 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -270,7 +270,7 @@ impl Application {
270 use crate::commands::{insert::idle_completion, Context};
270 use crate::commands::{insert::idle_completion, Context};
271 use helix_view::document::Mode;
271 use helix_view::document::Mode;
272
272
273− if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
273+ if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
274 return;
274 return;
275 }
275 }
276 let editor_view = self
276 let editor_view = self
helix-term/src/commands.rs+265−63
@@ -13,7 +13,6 @@ use helix_core::{
13 numbers::NumberIncrementor,
13 numbers::NumberIncrementor,
14 object, pos_at_coords,
14 object, pos_at_coords,
15 regex::{self, Regex, RegexBuilder},
15 regex::{self, Regex, RegexBuilder},
16− register::Register,
17 search, selection, surround, textobject,
16 search, selection, surround, textobject,
18 unicode::width::UnicodeWidthChar,
17 unicode::width::UnicodeWidthChar,
19 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
18 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
@@ -236,7 +235,9 @@ impl Command {
236 extend_line, "Select current line, if already selected, extend to next line",
235 extend_line, "Select current line, if already selected, extend to next line",
237 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
236 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
238 delete_selection, "Delete selection",
237 delete_selection, "Delete selection",
238+ delete_selection_noyank, "Delete selection, without yanking",
239 change_selection, "Change selection (delete and enter insert mode)",
239 change_selection, "Change selection (delete and enter insert mode)",
240+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
240 collapse_selection, "Collapse selection onto a single cursor",
241 collapse_selection, "Collapse selection onto a single cursor",
241 flip_selections, "Flip selection cursor and anchor",
242 flip_selections, "Flip selection cursor and anchor",
242 insert_mode, "Insert before selection",
243 insert_mode, "Insert before selection",
@@ -262,6 +263,9 @@ impl Command {
262 goto_implementation, "Goto implementation",
263 goto_implementation, "Goto implementation",
263 goto_file_start, "Goto file start/line",
264 goto_file_start, "Goto file start/line",
264 goto_file_end, "Goto file end",
265 goto_file_end, "Goto file end",
266+ goto_file, "Goto files in the selection",
267+ goto_file_hsplit, "Goto files in the selection in horizontal splits",
268+ goto_file_vsplit, "Goto files in the selection in vertical splits",
265 goto_reference, "Goto references",
269 goto_reference, "Goto references",
266 goto_window_top, "Goto window top",
270 goto_window_top, "Goto window top",
267 goto_window_middle, "Goto window middle",
271 goto_window_middle, "Goto window middle",
@@ -318,6 +322,7 @@ impl Command {
318 join_selections, "Join lines inside selection",
322 join_selections, "Join lines inside selection",
319 keep_selections, "Keep selections matching regex",
323 keep_selections, "Keep selections matching regex",
320 remove_selections, "Remove selections matching regex",
324 remove_selections, "Remove selections matching regex",
325+ align_selections, "Align selections in column",
321 keep_primary_selection, "Keep primary selection",
326 keep_primary_selection, "Keep primary selection",
322 remove_primary_selection, "Remove primary selection",
327 remove_primary_selection, "Remove primary selection",
323 completion, "Invoke completion popup",
328 completion, "Invoke completion popup",
@@ -676,11 +681,83 @@ fn trim_selections(cx: &mut Context) {
676 };
681 };
677}
682}
678
683
684+// align text in selection
685+fn align_selections(cx: &mut Context) {
686+ let align_style = cx.count();
687+ if align_style > 3 {
688+ cx.editor.set_error(
689+ "align only accept 1,2,3 as count to set left/center/right align".to_string(),
690+ );
691+ return;
692+ }
693+
694+ let (view, doc) = current!(cx.editor);
695+ let text = doc.text().slice(..);
696+ let selection = doc.selection(view.id);
697+ let mut column_widths = vec![];
698+ let mut last_line = text.len_lines();
699+ let mut column = 0;
700+ // first of all, we need compute all column's width, let use max width of the selections in a column
701+ for sel in selection {
702+ let (l1, l2) = sel.line_range(text);
703+ if l1 != l2 {
704+ cx.editor
705+ .set_error("align cannot work with multi line selections".to_string());
706+ return;
707+ }
708+ // if the selection is not in the same line with last selection, we set the column to 0
709+ column = if l1 != last_line { 0 } else { column + 1 };
710+ last_line = l1;
711+
712+ if column < column_widths.len() {
713+ if sel.to() - sel.from() > column_widths[column] {
714+ column_widths[column] = sel.to() - sel.from();
715+ }
716+ } else {
717+ // a new column, current selection width is the temp width of the column
718+ column_widths.push(sel.to() - sel.from());
719+ }
720+ }
721+ last_line = text.len_lines();
722+ // once we get the with of each column, we transform each selection with to it's column width based on the align style
723+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
724+ let l = range.cursor_line(text);
725+ column = if l != last_line { 0 } else { column + 1 };
726+ last_line = l;
727+
728+ (
729+ range.from(),
730+ range.to(),
731+ Some(
732+ align_fragment_to_width(&range.fragment(text), column_widths[column], align_style)
733+ .into(),
734+ ),
735+ )
736+ });
737+
738+ doc.apply(&transaction, view.id);
739+ doc.append_changes_to_history(view.id);
740+}
741+
742+fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
743+ let trimed = fragment.trim_matches(|c| c == ' ');
744+ let mut s = " ".repeat(width - trimed.chars().count());
745+ match align_style {
746+ 1 => s.insert_str(0, trimed), // left align
747+ 2 => s.insert_str(s.len() / 2, trimed), // center align
748+ 3 => s.push_str(trimed), // right align
749+ n => unimplemented!("{}", n),
750+ }
751+ s
752+}
753+
679fn goto_window(cx: &mut Context, align: Align) {
754fn goto_window(cx: &mut Context, align: Align) {
755+ let count = cx.count() - 1;
680 let (view, doc) = current!(cx.editor);
756 let (view, doc) = current!(cx.editor);
681
757
682 let height = view.inner_area().height as usize;
758 let height = view.inner_area().height as usize;
683
759
760+ // respect user given count if any
684 // - 1 so we have at least one gap in the middle.
761 // - 1 so we have at least one gap in the middle.
685 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
762 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
686 // as we type
763 // as we type
@@ -689,11 +766,12 @@ fn goto_window(cx: &mut Context, align: Align) {
689 let last_line = view.last_line(doc);
766 let last_line = view.last_line(doc);
690
767
691 let line = match align {
768 let line = match align {
692− Align::Top => (view.offset.row + scrolloff),
769+ Align::Top => (view.offset.row + scrolloff + count),
693− Align::Center => (view.offset.row + (height / 2)),
770+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
694− Align::Bottom => last_line.saturating_sub(scrolloff),
771+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
695 }
772 }
696− .min(last_line.saturating_sub(scrolloff));
773+ .min(last_line.saturating_sub(scrolloff))
774+ .max(view.offset.row + scrolloff);
697
775
698 let pos = doc.text().line_to_char(line);
776 let pos = doc.text().line_to_char(line);
699
777
@@ -782,6 +860,49 @@ fn goto_file_end(cx: &mut Context) {
782 doc.set_selection(view.id, selection);
860 doc.set_selection(view.id, selection);
783}
861}
784
862
863+fn goto_file(cx: &mut Context) {
864+ goto_file_impl(cx, Action::Replace);
865+}
866+
867+fn goto_file_hsplit(cx: &mut Context) {
868+ goto_file_impl(cx, Action::HorizontalSplit);
869+}
870+
871+fn goto_file_vsplit(cx: &mut Context) {
872+ goto_file_impl(cx, Action::VerticalSplit);
873+}
874+
875+fn goto_file_impl(cx: &mut Context, action: Action) {
876+ let (view, doc) = current_ref!(cx.editor);
877+ let text = doc.text();
878+ let selections = doc.selection(view.id);
879+ let mut paths: Vec<_> = selections
880+ .iter()
881+ .map(|r| text.slice(r.from()..r.to()).to_string())
882+ .collect();
883+ let primary = selections.primary();
884+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
885+ let current_word = movement::move_next_long_word_start(
886+ text.slice(..),
887+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
888+ 1,
889+ );
890+ paths.clear();
891+ paths.push(
892+ text.slice(current_word.from()..current_word.to())
893+ .to_string(),
894+ );
895+ }
896+ for sel in paths {
897+ let p = sel.trim();
898+ if !p.is_empty() {
899+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
900+ cx.editor.set_error(format!("Open file failed: {:?}", e));
901+ }
902+ }
903+ }
904+}
905+
785fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
906fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
786where
907where
787 F: Fn(RopeSlice, Range, usize) -> Range,
908 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1459,6 +1580,7 @@ fn global_search(cx: &mut Context) {
1459 let (all_matches_sx, all_matches_rx) =
1580 let (all_matches_sx, all_matches_rx) =
1460 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1581 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1461 let smart_case = cx.editor.config.smart_case;
1582 let smart_case = cx.editor.config.smart_case;
1583+ let file_picker_config = cx.editor.config.file_picker.clone();
1462
1584
1463 let completions = search_completions(cx, None);
1585 let completions = search_completions(cx, None);
1464 let prompt = ui::regex_prompt(
1586 let prompt = ui::regex_prompt(
@@ -1487,41 +1609,55 @@ fn global_search(cx: &mut Context) {
1487
1609
1488 let search_root = std::env::current_dir()
1610 let search_root = std::env::current_dir()
1489 .expect("Global search error: Failed to get current dir");
1611 .expect("Global search error: Failed to get current dir");
1490− WalkBuilder::new(search_root).build_parallel().run(|| {
1612+ WalkBuilder::new(search_root)
1613+ .hidden(file_picker_config.hidden)
1614+ .parents(file_picker_config.parents)
1615+ .ignore(file_picker_config.ignore)
1616+ .git_ignore(file_picker_config.git_ignore)
1617+ .git_global(file_picker_config.git_global)
1618+ .git_exclude(file_picker_config.git_exclude)
1619+ .max_depth(file_picker_config.max_depth)
1620+ .build_parallel()
1621+ .run(|| {
1491− let mut searcher_cl = searcher.clone();
1622+ let mut searcher_cl = searcher.clone();
1492− let matcher_cl = matcher.clone();
1623+ let matcher_cl = matcher.clone();
1493− let all_matches_sx_cl = all_matches_sx.clone();
1624+ let all_matches_sx_cl = all_matches_sx.clone();
1494− Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1625+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1495− let dent = match dent {
1626+ let dent = match dent {
1496− Ok(dent) => dent,
1627+ Ok(dent) => dent,
1497− Err(_) => return WalkState::Continue,
1628+ Err(_) => return WalkState::Continue,
1498− };
1629+ };
1499−
1630+
1500− match dent.file_type() {
1631+ match dent.file_type() {
1501− Some(fi) => {
1632+ Some(fi) => {
1502− if !fi.is_file() {
1633+ if !fi.is_file() {
1503− return WalkState::Continue;
1634+ return WalkState::Continue;
1635+ }
1504 }
1636 }
1637+ None => return WalkState::Continue,
1505 }
1638 }
1506− None => return WalkState::Continue,
1507− }
1508
1639
1509− let result_sink = sinks::UTF8(|line_num, _| {
1640+ let result_sink = sinks::UTF8(|line_num, _| {
1510− match all_matches_sx_cl
1641+ match all_matches_sx_cl
1511− .send((line_num as usize - 1, dent.path().to_path_buf()))
1642+ .send((line_num as usize - 1, dent.path().to_path_buf()))
1512− {
1643+ {
1513− Ok(_) => Ok(true),
1644+ Ok(_) => Ok(true),
1514− Err(_) => Ok(false),
1645+ Err(_) => Ok(false),
1646+ }
1647+ });
1648+ let result =
1649+ searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1650+
1651+ if let Err(err) = result {
1652+ log::error!(
1653+ "Global search error: {}, {}",
1654+ dent.path().display(),
1655+ err
1656+ );
1515 }
1657 }
1516− });
1517− let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1518−
1519− if let Err(err) = result {
1520− log::error!("Global search error: {}, {}", dent.path().display(), err);
1521− }
1522− WalkState::Continue
1658+ WalkState::Continue
1659+ })
1523− })
1660+ });
1524− });
1525 } else {
1661 } else {
1526 // Otherwise do nothing
1662 // Otherwise do nothing
1527 // log::warn!("Global Search Invalid Pattern")
1663 // log::warn!("Global Search Invalid Pattern")
@@ -1626,19 +1762,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
1626 );
1762 );
1627}
1763}
1628
1764
1765+enum Operation {
1766+ Delete,
1767+ Change,
1768+}
1769+
1629−fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1770+fn delete_selection_impl(cx: &mut Context, op: Operation) {
1771+ let (view, doc) = current!(cx.editor);
1772+
1630 let text = doc.text().slice(..);
1773 let text = doc.text().slice(..);
1631− let selection = doc.selection(view_id);
1774+ let selection = doc.selection(view.id);
1632
1775
1776+ if cx.register != Some('_') {
1633− // first yank the selection
1777+ // first yank the selection
1634− let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1778+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1779+ let reg_name = cx.register.unwrap_or('"');
1780+ let registers = &mut cx.editor.registers;
1781+ let reg = registers.get_mut(reg_name);
1635− reg.write(values);
1782+ reg.write(values);
1783+ };
1636
1784
1637 // then delete
1785 // then delete
1638 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1786 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1639 (range.from(), range.to(), None)
1787 (range.from(), range.to(), None)
1640 });
1788 });
1641− doc.apply(&transaction, view_id);
1789+ doc.apply(&transaction, view.id);
1790+
1791+ match op {
1792+ Operation::Delete => {
1793+ doc.append_changes_to_history(view.id);
1794+ // exit select mode, if currently in select mode
1795+ exit_select_mode(cx);
1796+ }
1797+ Operation::Change => {
1798+ enter_insert_mode(doc);
1799+ }
1800+ }
1642}
1801}
1643
1802
1644#[inline]
1803#[inline]
@@ -1653,25 +1812,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
1653}
1812}
1654
1813
1655fn delete_selection(cx: &mut Context) {
1814fn delete_selection(cx: &mut Context) {
1656− let reg_name = cx.register.unwrap_or('"');
1657− let (view, doc) = current!(cx.editor);
1658− let registers = &mut cx.editor.registers;
1659− let reg = registers.get_mut(reg_name);
1660− delete_selection_impl(reg, doc, view.id);
1815+ delete_selection_impl(cx, Operation::Delete);
1661−
1816+}
1662− doc.append_changes_to_history(view.id);
1663
1817
1664− // exit select mode, if currently in select mode
1818+fn delete_selection_noyank(cx: &mut Context) {
1665− exit_select_mode(cx);
1819+ cx.register = Some('_');
1820+ delete_selection_impl(cx, Operation::Delete);
1666}
1821}
1667
1822
1668fn change_selection(cx: &mut Context) {
1823fn change_selection(cx: &mut Context) {
1669− let reg_name = cx.register.unwrap_or('"');
1824+ delete_selection_impl(cx, Operation::Change);
1670− let (view, doc) = current!(cx.editor);
1825+}
1671− let registers = &mut cx.editor.registers;
1826+
1672− let reg = registers.get_mut(reg_name);
1827+fn change_selection_noyank(cx: &mut Context) {
1828+ cx.register = Some('_');
1673− delete_selection_impl(reg, doc, view.id);
1829+ delete_selection_impl(cx, Operation::Change);
1674− enter_insert_mode(doc);
1675}
1830}
1676
1831
1677fn collapse_selection(cx: &mut Context) {
1832fn collapse_selection(cx: &mut Context) {
@@ -1820,7 +1975,7 @@ mod cmd {
1820 let jobs = &mut cx.jobs;
1975 let jobs = &mut cx.jobs;
1821 let (_, doc) = current!(cx.editor);
1976 let (_, doc) = current!(cx.editor);
1822
1977
1823− if let Some(path) = path {
1978+ if let Some(ref path) = path {
1824 doc.set_path(Some(path.as_ref()))
1979 doc.set_path(Some(path.as_ref()))
1825 .context("invalid filepath")?;
1980 .context("invalid filepath")?;
1826 }
1981 }
@@ -1840,6 +1995,11 @@ mod cmd {
1840 });
1995 });
1841 let future = doc.format_and_save(fmt);
1996 let future = doc.format_and_save(fmt);
1842 cx.jobs.add(Job::new(future).wait_before_exiting());
1997 cx.jobs.add(Job::new(future).wait_before_exiting());
1998+
1999+ if path.is_some() {
2000+ let id = doc.id();
2001+ let _ = cx.editor.refresh_language_server(id);
2002+ }
1843 Ok(())
2003 Ok(())
1844 }
2004 }
1845
2005
@@ -2466,6 +2626,26 @@ mod cmd {
2466 Ok(())
2626 Ok(())
2467 }
2627 }
2468
2628
2629+ pub(super) fn goto_line_number(
2630+ cx: &mut compositor::Context,
2631+ args: &[&str],
2632+ _event: PromptEvent,
2633+ ) -> anyhow::Result<()> {
2634+ if args.is_empty() {
2635+ bail!("Line number required");
2636+ }
2637+
2638+ let line = args[0].parse::<usize>()?;
2639+
2640+ goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
2641+
2642+ let (view, doc) = current!(cx.editor);
2643+
2644+ view.ensure_cursor_in_view(doc, line);
2645+
2646+ Ok(())
2647+ }
2648+
2469 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2649 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2470 TypableCommand {
2650 TypableCommand {
2471 name: "quit",
2651 name: "quit",
@@ -2768,6 +2948,13 @@ mod cmd {
2768 fun: tutor,
2948 fun: tutor,
2769 completer: None,
2949 completer: None,
2770 },
2950 },
2951+ TypableCommand {
2952+ name: "goto",
2953+ aliases: &["g"],
2954+ doc: "Go to line number.",
2955+ fun: goto_line_number,
2956+ completer: None,
2957+ }
2771 ];
2958 ];
2772
2959
2773 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2960 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2830,6 +3017,15 @@ fn command_mode(cx: &mut Context) {
2830 return;
3017 return;
2831 }
3018 }
2832
3019
3020+ // If command is numeric, interpret as line number and go there.
3021+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
3022+ if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
3023+ cx.editor.set_error(format!("{}", e));
3024+ }
3025+ return;
3026+ }
3027+
3028+ // Handle typable commands
2833 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
3029 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2834 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
3030 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2835 cx.editor.set_error(format!("{}", e));
3031 cx.editor.set_error(format!("{}", e));
@@ -2855,7 +3051,7 @@ fn command_mode(cx: &mut Context) {
2855
3051
2856fn file_picker(cx: &mut Context) {
3052fn file_picker(cx: &mut Context) {
2857 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
3053 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
2858− let picker = ui::file_picker(root);
3054+ let picker = ui::file_picker(root, &cx.editor.config);
2859 cx.push_layer(Box::new(picker));
3055 cx.push_layer(Box::new(picker));
2860}
3056}
2861
3057
@@ -3463,10 +3659,14 @@ fn push_jump(editor: &mut Editor) {
3463}
3659}
3464
3660
3465fn goto_line(cx: &mut Context) {
3661fn goto_line(cx: &mut Context) {
3466− if let Some(count) = cx.count {
3662+ goto_line_impl(&mut cx.editor, cx.count)
3467− push_jump(cx.editor);
3663+}
3468
3664
3665+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
3666+ if let Some(count) = count {
3667+ push_jump(editor);
3668+
3469− let (view, doc) = current!(cx.editor);
3669+ let (view, doc) = current!(editor);
3470 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
3670 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
3471 // If the last line is blank, don't jump to it.
3671 // If the last line is blank, don't jump to it.
3472 doc.text().len_lines().saturating_sub(2)
3672 doc.text().len_lines().saturating_sub(2)
@@ -5065,7 +5265,9 @@ fn match_brackets(cx: &mut Context) {
5065 if let Some(syntax) = doc.syntax() {
5265 if let Some(syntax) = doc.syntax() {
5066 let text = doc.text().slice(..);
5266 let text = doc.text().slice(..);
5067 let selection = doc.selection(view.id).clone().transform(|range| {
5267 let selection = doc.selection(view.id).clone().transform(|range| {
5268+ if let Some(pos) =
5068− if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
5269+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
5270+ {
5069 range.put_cursor(text, pos, doc.mode == Mode::Select)
5271 range.put_cursor(text, pos, doc.mode == Mode::Select)
5070 } else {
5272 } else {
5071 range
5273 range
helix-term/src/keymap.rs+8−3
@@ -512,6 +512,7 @@ impl Default for Keymaps {
512 "g" => { "Goto"
512 "g" => { "Goto"
513 "g" => goto_file_start,
513 "g" => goto_file_start,
514 "e" => goto_last_line,
514 "e" => goto_last_line,
515+ "f" => goto_file,
515 "h" => goto_line_start,
516 "h" => goto_line_start,
516 "l" => goto_line_end,
517 "l" => goto_line_end,
517 "s" => goto_first_nonwhitespace,
518 "s" => goto_first_nonwhitespace,
@@ -537,9 +538,9 @@ impl Default for Keymaps {
537 "O" => open_above,
538 "O" => open_above,
538
539
539 "d" => delete_selection,
540 "d" => delete_selection,
540− // TODO: also delete without yanking
541+ "A-d" => delete_selection_noyank,
541 "c" => change_selection,
542 "c" => change_selection,
542− // TODO: also change delete without yanking
543+ "A-c" => change_selection_noyank,
543
544
544 "C" => copy_selection_on_next_line,
545 "C" => copy_selection_on_next_line,
545 "A-C" => copy_selection_on_prev_line,
546 "A-C" => copy_selection_on_prev_line,
@@ -604,7 +605,7 @@ impl Default for Keymaps {
604 // "q" => record_macro,
605 // "q" => record_macro,
605 // "Q" => replay_macro,
606 // "Q" => replay_macro,
606
607
607− // & align selections
608+ "&" => align_selections,
608 "_" => trim_selections,
609 "_" => trim_selections,
609
610
610 "(" => rotate_selections_backward,
611 "(" => rotate_selections_backward,
@@ -622,6 +623,8 @@ impl Default for Keymaps {
622 "C-w" | "w" => rotate_view,
623 "C-w" | "w" => rotate_view,
623 "C-s" | "s" => hsplit,
624 "C-s" | "s" => hsplit,
624 "C-v" | "v" => vsplit,
625 "C-v" | "v" => vsplit,
626+ "f" => goto_file_hsplit,
627+ "F" => goto_file_vsplit,
625 "C-q" | "q" => wclose,
628 "C-q" | "q" => wclose,
626 "C-o" | "o" => wonly,
629 "C-o" | "o" => wonly,
627 "C-h" | "h" | "left" => jump_view_left,
630 "C-h" | "h" | "left" => jump_view_left,
@@ -670,6 +673,8 @@ impl Default for Keymaps {
670 "C-w" | "w" => rotate_view,
673 "C-w" | "w" => rotate_view,
671 "C-s" | "s" => hsplit,
674 "C-s" | "s" => hsplit,
672 "C-v" | "v" => vsplit,
675 "C-v" | "v" => vsplit,
676+ "f" => goto_file_hsplit,
677+ "F" => goto_file_vsplit,
673 "C-q" | "q" => wclose,
678 "C-q" | "q" => wclose,
674 "C-o" | "o" => wonly,
679 "C-o" | "o" => wonly,
675 "C-h" | "h" | "left" => jump_view_left,
680 "C-h" | "h" | "left" => jump_view_left,
helix-term/src/ui/editor.rs+127−162
@@ -18,7 +18,6 @@ use helix_core::{
18use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
18use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
19use helix_view::{
19use helix_view::{
20 document::{Mode, SCRATCH_BUFFER_NAME},
20 document::{Mode, SCRATCH_BUFFER_NAME},
21− editor::LineNumber,
22 graphics::{Color, CursorKind, Modifier, Rect, Style},
21 graphics::{Color, CursorKind, Modifier, Rect, Style},
23 info::Info,
22 info::Info,
24 input::KeyEvent,
23 input::KeyEvent,
@@ -392,7 +391,7 @@ impl EditorView {
392 use helix_core::match_brackets;
391 use helix_core::match_brackets;
393 let pos = doc.selection(view.id).primary().cursor(text);
392 let pos = doc.selection(view.id).primary().cursor(text);
394
393
395− let pos = match_brackets::find(syntax, doc.text(), pos)
394+ let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
396 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
395 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
397
396
398 if let Some(pos) = pos {
397 if let Some(pos) = pos {
@@ -430,22 +429,6 @@ impl EditorView {
430 let text = doc.text().slice(..);
429 let text = doc.text().slice(..);
431 let last_line = view.last_line(doc);
430 let last_line = view.last_line(doc);
432
431
433− let linenr = theme.get("ui.linenr");
434− let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
435−
436− let warning = theme.get("warning");
437− let error = theme.get("error");
438− let info = theme.get("info");
439− let hint = theme.get("hint");
440−
441− // Whether to draw the line number for the last line of the
442− // document or not. We only draw it if it's not an empty line.
443− let draw_last = text.line_to_byte(last_line) < text.len_bytes();
444−
445− let current_line = doc
446− .text()
447− .char_to_line(doc.selection(view.id).primary().cursor(text));
448−
449 // it's used inside an iterator so the collect isn't needless:
432 // it's used inside an iterator so the collect isn't needless:
450 // https://github.com/rust-lang/rust-clippy/issues/6164
433 // https://github.com/rust-lang/rust-clippy/issues/6164
451 #[allow(clippy::needless_collect)]
434 #[allow(clippy::needless_collect)]
@@ -455,146 +438,137 @@ impl EditorView {
455 .map(|range| range.cursor_line(text))
438 .map(|range| range.cursor_line(text))
456 .collect();
439 .collect();
457
440
458− let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
441+ use helix_view::gutter::GutterFn;
459− let mut stack_frame: Option<&StackFrame> = None;
442+ fn breakpoints<'doc>(
460− if let Some(path) = doc.path() {
443+ doc: &'doc Document,
461− breakpoints = all_breakpoints.get(path);
444+ _view: &View,
462− if let Some(debugger) = debugger {
445+ theme: &Theme,
463− // if we have a frame, and the frame path matches document
446+ _config: &Config,
464− if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
447+ _is_focused: bool,
465− {
448+ _width: usize,
466− let frame = debugger
449+ ) -> GutterFn<'doc> {
467− .stack_frames
450+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
468− .get(&thread_id)
451+ //
469− .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
470− if let Some(StackFrame {
471− source: Some(source),
472− ..
473− }) = &frame
474− {
475− if source.path.as_ref() == Some(path) {
476− stack_frame = frame;
477− }
478− };
479− };
480− }
452+ })
481 }
453 }
482−
454+ // let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
483− for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
455+ // let mut stack_frame: Option<&StackFrame> = None;
484− use helix_core::diagnostic::Severity;
456+ // if let Some(path) = doc.path() {
485− if let Ok(diagnostic) = doc.diagnostics().binary_search_by_key(&line, |d| d.line) {
457+ // breakpoints = all_breakpoints.get(path);
486− let diagnostic = &doc.diagnostics()[diagnostic];
458+ // if let Some(debugger) = debugger {
487− surface.set_stringn(
459+ // // if we have a frame, and the frame path matches document
488− viewport.x,
460+ // if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
489− viewport.y + i as u16,
461+ // {
490− "●",
462+ // let frame = debugger
491− 1,
463+ // .stack_frames
492− match diagnostic.severity {
464+ // .get(&thread_id)
493− Some(Severity::Error) => error,
465+ // .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
494− Some(Severity::Warning) | None => warning,
466+ // if let Some(StackFrame {
495− Some(Severity::Info) => info,
467+ // source: Some(source),
496− Some(Severity::Hint) => hint,
468+ // ..
469+ // }) = &frame
470+ // {
471+ // if source.path.as_ref() == Some(path) {
472+ // stack_frame = frame;
497− },
473+ // }
498− );
474+ // };
499− }
475+ // };
500−
476+ // }
501− let selected = cursors.contains(&line);
477+ // }
502−
478+
503− // TODO: debugger should translate received breakpoints to 0-indexing
479+ // TODO: debugger should translate received breakpoints to 0-indexing
504−
480+
505− if let Some(user) = breakpoints.as_ref() {
481+ // if let Some(user) = breakpoints.as_ref() {
506− let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
482+ // let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
507− debugger.iter().find(|breakpoint| {
483+ // debugger.iter().find(|breakpoint| {
508− if breakpoint.source.is_some()
484+ // if breakpoint.source.is_some()
509− && doc.path().is_some()
485+ // && doc.path().is_some()
510− && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
486+ // && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
511− {
487+ // {
512− match (breakpoint.line, breakpoint.end_line) {
488+ // match (breakpoint.line, breakpoint.end_line) {
513− #[allow(clippy::int_plus_one)]
489+ // #[allow(clippy::int_plus_one)]
514− (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
490+ // (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
515− (Some(l), None) => l - 1 == line,
491+ // (Some(l), None) => l - 1 == line,
516− _ => false,
492+ // _ => false,
517− }
493+ // }
518− } else {
494+ // } else {
519− false
495+ // false
520− }
496+ // }
521− })
497+ // })
522− } else {
498+ // } else {
523− None
499+ // None
524− };
500+ // };
525−
501+
526− if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
502+ // if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
527− {
503+ // {
528− let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
504+ // let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
529− let mut style =
505+ // let mut style =
530− if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
506+ // if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
531− error.add_modifier(Modifier::UNDERLINED)
507+ // error.add_modifier(Modifier::UNDERLINED)
532− } else if breakpoint.condition.is_some() {
508+ // } else if breakpoint.condition.is_some() {
533− error
509+ // error
534− } else if breakpoint.log_message.is_some() {
510+ // } else if breakpoint.log_message.is_some() {
535− info
511+ // info
536− } else {
512+ // } else {
537− warning
513+ // warning
538− };
514+ // };
539− if !verified {
515+ // if !verified {
540− // Faded colors
516+ // // Faded colors
541− style = if let Some(Color::Rgb(r, g, b)) = style.fg {
517+ // style = if let Some(Color::Rgb(r, g, b)) = style.fg {
542− style.fg(Color::Rgb(
518+ // style.fg(Color::Rgb(
543− ((r as f32) * 0.4).floor() as u8,
519+ // ((r as f32) * 0.4).floor() as u8,
544− ((g as f32) * 0.4).floor() as u8,
520+ // ((g as f32) * 0.4).floor() as u8,
545− ((b as f32) * 0.4).floor() as u8,
521+ // ((b as f32) * 0.4).floor() as u8,
546− ))
522+ // ))
547− } else {
523+ // } else {
548− style.fg(Color::Gray)
524+ // style.fg(Color::Gray)
549− }
525+ // }
550− };
526+ // };
551− surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
527+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
552− } else if let Some(breakpoint) = debugger_breakpoint {
528+ // } else if let Some(breakpoint) = debugger_breakpoint {
553− let style = if breakpoint.verified {
529+ // let style = if breakpoint.verified {
554− info
530+ // info
555− } else {
531+ // } else {
556− info.fg(Color::Gray)
532+ // info.fg(Color::Gray)
557− };
533+ // };
558− surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
534+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
559− }
535+ // }
560− }
536+ // }
561−
537+
562− if let Some(frame) = stack_frame {
538+ // if let Some(frame) = stack_frame {
563− if frame.line - 1 == line {
539+ // if frame.line - 1 == line {
564− surface.set_style(
540+ // surface.set_style(
565− Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
541+ // Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
566− helix_view::graphics::Style::default()
542+ // helix_view::graphics::Style::default()
567− .bg(helix_view::graphics::Color::LightYellow),
543+ // .bg(helix_view::graphics::Color::LightYellow),
544+ // );
545+ // }
546+ // }
547+
548+ let mut offset = 0;
549+
550+ // avoid lots of small allocations by reusing a text buffer for each line
551+ let mut text = String::with_capacity(8);
552+
553+ for (constructor, width) in view.gutters() {
554+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
555+ text.reserve(*width); // ensure there's enough space for the gutter
556+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
557+ let selected = cursors.contains(&line);
558+
559+ if let Some(style) = gutter(line, selected, &mut text) {
560+ surface.set_stringn(
561+ viewport.x + offset,
562+ viewport.y + i as u16,
563+ &text,
564+ *width,
565+ style,
568 );
566 );
569 }
567 }
568+ text.clear();
570 }
569 }
571
570
572− let text = if line == last_line && !draw_last {
571+ offset += *width as u16;
573− " ~".into()
574− } else {
575− let line = match config.line_number {
576− LineNumber::Absolute => line + 1,
577− LineNumber::Relative => {
578− if current_line == line {
579− line + 1
580− } else {
581− abs_diff(current_line, line)
582− }
583− }
584− };
585− format!("{:>5}", line)
586− };
587− surface.set_stringn(
588− viewport.x + 1,
589− viewport.y + i as u16,
590− text,
591− 5,
592− if selected && is_focused {
593− linenr_select
594− } else {
595− linenr
596− },
597− );
598 }
572 }
599 }
573 }
600
574
@@ -1364,12 +1338,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
1364 key.modifiers.remove(KeyModifiers::SHIFT)
1338 key.modifiers.remove(KeyModifiers::SHIFT)
1365 }
1339 }
1366}
1340}
1367−
1368−#[inline]
1369−const fn abs_diff(a: usize, b: usize) -> usize {
1370− if a > b {
1371− a - b
1372− } else {
1373− b - a
1374− }
1375−}
helix-term/src/ui/markdown.rs+5−2
@@ -55,7 +55,7 @@ fn parse<'a>(
55 fn to_span(text: pulldown_cmark::CowStr) -> Span {
55 fn to_span(text: pulldown_cmark::CowStr) -> Span {
56 use std::ops::Deref;
56 use std::ops::Deref;
57 Span::raw::<std::borrow::Cow<_>>(match text {
57 Span::raw::<std::borrow::Cow<_>>(match text {
58− CowStr::Borrowed(s) => s.to_string().into(), // could retain borrow
58+ CowStr::Borrowed(s) => s.into(),
59 CowStr::Boxed(s) => s.to_string().into(),
59 CowStr::Boxed(s) => s.to_string().into(),
60 CowStr::Inlined(s) => s.deref().to_owned().into(),
60 CowStr::Inlined(s) => s.deref().to_owned().into(),
61 })
61 })
@@ -179,7 +179,9 @@ fn parse<'a>(
179 spans.push(Span::raw(" "));
179 spans.push(Span::raw(" "));
180 }
180 }
181 Event::Rule => {
181 Event::Rule => {
182+ let mut span = Span::raw("---");
183+ span.style = code_style;
182− lines.push(Spans::from("---"));
184+ lines.push(Spans::from(span));
183 lines.push(Spans::default());
185 lines.push(Spans::default());
184 }
186 }
185 // TaskListMarker(bool) true if checked
187 // TaskListMarker(bool) true if checked
@@ -226,6 +228,7 @@ impl Component for Markdown {
226 return None;
228 return None;
227 }
229 }
228 let contents = parse(&self.contents, None, &self.config_loader);
230 let contents = parse(&self.contents, None, &self.config_loader);
231+ // TODO: account for tab width
229 let max_text_width = (viewport.0 - padding).min(120);
232 let max_text_width = (viewport.0 - padding).min(120);
230 let mut text_width = 0;
233 let mut text_width = 0;
231 let mut height = padding;
234 let mut height = padding;
helix-term/src/ui/mod.rs+10−1
@@ -93,13 +93,22 @@ pub fn regex_prompt(
93 )
93 )
94}
94}
95
95
96−pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
96+pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
97 use ignore::{types::TypesBuilder, WalkBuilder};
97 use ignore::{types::TypesBuilder, WalkBuilder};
98 use std::time;
98 use std::time;
99
99
100 // We want to exclude files that the editor can't handle yet
100 // We want to exclude files that the editor can't handle yet
101 let mut type_builder = TypesBuilder::new();
101 let mut type_builder = TypesBuilder::new();
102 let mut walk_builder = WalkBuilder::new(&root);
102 let mut walk_builder = WalkBuilder::new(&root);
103+ walk_builder
104+ .hidden(config.file_picker.hidden)
105+ .parents(config.file_picker.parents)
106+ .ignore(config.file_picker.ignore)
107+ .git_ignore(config.file_picker.git_ignore)
108+ .git_global(config.file_picker.git_global)
109+ .git_exclude(config.file_picker.git_exclude)
110+ .max_depth(config.file_picker.max_depth);
111+
103 let walk_builder = match type_builder.add(
112 let walk_builder = match type_builder.add(
104 "compressed",
113 "compressed",
105 "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
114 "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
helix-view/src/editor.rs+128−76
@@ -14,6 +14,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
14use std::{
14use std::{
15 collections::{BTreeMap, HashMap},
15 collections::{BTreeMap, HashMap},
16 io::stdin,
16 io::stdin,
17+ num::NonZeroUsize,
17 path::{Path, PathBuf},
18 path::{Path, PathBuf},
18 pin::Pin,
19 pin::Pin,
19 sync::Arc,
20 sync::Arc,
@@ -21,7 +22,7 @@ use std::{
21
22
22use tokio::time::{sleep, Duration, Instant, Sleep};
23use tokio::time::{sleep, Duration, Instant, Sleep};
23
24
24−use anyhow::Error;
25+use anyhow::{bail, Context, Error};
25
26
26pub use helix_core::diagnostic::Severity;
27pub use helix_core::diagnostic::Severity;
27pub use helix_core::register::Registers;
28pub use helix_core::register::Registers;
@@ -39,6 +40,46 @@ where
39 Ok(Duration::from_millis(millis))
40 Ok(Duration::from_millis(millis))
40}
41}
41
42
43+#[derive(Debug, Clone, PartialEq, Deserialize)]
44+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
45+pub struct FilePickerConfig {
46+ /// IgnoreOptions
47+ /// Enables ignoring hidden files.
48+ /// Whether to hide hidden files in file picker and global search results. Defaults to true.
49+ pub hidden: bool,
50+ /// Enables reading ignore files from parent directories. Defaults to true.
51+ pub parents: bool,
52+ /// Enables reading `.ignore` files.
53+ /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
54+ pub ignore: bool,
55+ /// Enables reading `.gitignore` files.
56+ /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
57+ pub git_ignore: bool,
58+ /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
59+ /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
60+ pub git_global: bool,
61+ /// Enables reading `.git/info/exclude` files.
62+ /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
63+ pub git_exclude: bool,
64+ /// WalkBuilder options
65+ /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
66+ pub max_depth: Option<usize>,
67+}
68+
69+impl Default for FilePickerConfig {
70+ fn default() -> Self {
71+ Self {
72+ hidden: true,
73+ parents: true,
74+ ignore: true,
75+ git_ignore: true,
76+ git_global: true,
77+ git_exclude: true,
78+ max_depth: None,
79+ }
80+ }
81+}
82+
42#[derive(Debug, Clone, PartialEq, Deserialize)]
83#[derive(Debug, Clone, PartialEq, Deserialize)]
43#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
84#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
44pub struct Config {
85pub struct Config {
@@ -66,9 +107,10 @@ pub struct Config {
66 pub completion_trigger_len: u8,
107 pub completion_trigger_len: u8,
67 /// Whether to display infoboxes. Defaults to true.
108 /// Whether to display infoboxes. Defaults to true.
68 pub auto_info: bool,
109 pub auto_info: bool,
110+ pub file_picker: FilePickerConfig,
69}
111}
70
112
71−#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
113+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
72#[serde(rename_all = "kebab-case")]
114#[serde(rename_all = "kebab-case")]
73pub enum LineNumber {
115pub enum LineNumber {
74 /// Show absolute line number
116 /// Show absolute line number
@@ -97,6 +139,7 @@ impl Default for Config {
97 idle_timeout: Duration::from_millis(400),
139 idle_timeout: Duration::from_millis(400),
98 completion_trigger_len: 2,
140 completion_trigger_len: 2,
99 auto_info: true,
141 auto_info: true,
142+ file_picker: FilePickerConfig::default(),
100 }
143 }
101 }
144 }
102}
145}
@@ -116,7 +159,7 @@ impl std::fmt::Debug for Motion {
116#[derive(Debug)]
159#[derive(Debug)]
117pub struct Editor {
160pub struct Editor {
118 pub tree: Tree,
161 pub tree: Tree,
119− pub next_document_id: usize,
162+ pub next_document_id: DocumentId,
120 pub documents: BTreeMap<DocumentId, Document>,
163 pub documents: BTreeMap<DocumentId, Document>,
121 pub count: Option<std::num::NonZeroUsize>,
164 pub count: Option<std::num::NonZeroUsize>,
122 pub selected_register: Option<char>,
165 pub selected_register: Option<char>,
@@ -154,8 +197,8 @@ pub enum Action {
154impl Editor {
197impl Editor {
155 pub fn new(
198 pub fn new(
156 mut area: Rect,
199 mut area: Rect,
157− themes: Arc<theme::Loader>,
200+ theme_loader: Arc<theme::Loader>,
158− config_loader: Arc<syntax::Loader>,
201+ syn_loader: Arc<syntax::Loader>,
159 config: Config,
202 config: Config,
160 ) -> Self {
203 ) -> Self {
161 let language_servers = helix_lsp::Registry::new();
204 let language_servers = helix_lsp::Registry::new();
@@ -165,17 +208,17 @@ impl Editor {
165
208
166 Self {
209 Self {
167 tree: Tree::new(area),
210 tree: Tree::new(area),
168− next_document_id: 0,
211+ next_document_id: DocumentId::default(),
169 documents: BTreeMap::new(),
212 documents: BTreeMap::new(),
170 count: None,
213 count: None,
171 selected_register: None,
214 selected_register: None,
172− theme: themes.default(),
215+ theme: theme_loader.default(),
173 language_servers,
216 language_servers,
174 debugger: None,
217 debugger: None,
175 debugger_events: SelectAll::new(),
218 debugger_events: SelectAll::new(),
176 breakpoints: HashMap::new(),
219 breakpoints: HashMap::new(),
177− syn_loader: config_loader,
220+ syn_loader,
178− theme_loader: themes,
221+ theme_loader,
179 registers: Registers::default(),
222 registers: Registers::default(),
180 clipboard_provider: get_clipboard_provider(),
223 clipboard_provider: get_clipboard_provider(),
181 status_msg: None,
224 status_msg: None,
@@ -232,7 +275,6 @@ impl Editor {
232 }
275 }
233
276
234 pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
277 pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
235− use anyhow::Context;
236 let theme = self
278 let theme = self
237 .theme_loader
279 .theme_loader
238 .load(theme.as_ref())
280 .load(theme.as_ref())
@@ -241,6 +283,53 @@ impl Editor {
241 Ok(())
283 Ok(())
242 }
284 }
243
285
286+ /// Refreshes the language server for a given document
287+ pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
288+ let doc = self.documents.get_mut(&doc_id)?;
289+ doc.detect_language(Some(&self.theme), &self.syn_loader);
290+ Self::launch_language_server(&mut self.language_servers, doc)
291+ }
292+
293+ /// Launch a language server for a given document
294+ fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
295+ // try to find a language server based on the language name
296+ let language_server = doc.language.as_ref().and_then(|language| {
297+ ls.get(language)
298+ .map_err(|e| {
299+ log::error!(
300+ "Failed to initialize the LSP for `{}` {{ {} }}",
301+ language.scope(),
302+ e
303+ )
304+ })
305+ .ok()
306+ });
307+ if let Some(language_server) = language_server {
308+ // only spawn a new lang server if the servers aren't the same
309+ if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
310+ if let Some(language_server) = doc.language_server() {
311+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
312+ }
313+ let language_id = doc
314+ .language()
315+ .and_then(|s| s.split('.').last()) // source.rust
316+ .map(ToOwned::to_owned)
317+ .unwrap_or_default();
318+
319+ // TODO: this now races with on_init code if the init happens too quickly
320+ tokio::spawn(language_server.text_document_did_open(
321+ doc.url().unwrap(),
322+ doc.version(),
323+ doc.text(),
324+ language_id,
325+ ));
326+
327+ doc.set_language_server(Some(language_server));
328+ }
329+ }
330+ Some(())
331+ }
332+
244 fn _refresh(&mut self) {
333 fn _refresh(&mut self) {
245 for (view, _) in self.tree.views_mut() {
334 for (view, _) in self.tree.views_mut() {
246 let doc = &self.documents[&view.doc];
335 let doc = &self.documents[&view.doc];
@@ -311,23 +400,22 @@ impl Editor {
311 }
400 }
312 Action::Load => {
401 Action::Load => {
313 let view_id = view!(self).id;
402 let view_id = view!(self).id;
314− if let Some(doc) = self.document_mut(id) {
403+ let doc = self.documents.get_mut(&id).unwrap();
315− if doc.selections().is_empty() {
404+ if doc.selections().is_empty() {
316− doc.selections.insert(view_id, Selection::point(0));
405+ doc.selections.insert(view_id, Selection::point(0));
317− }
318 }
406 }
319 return;
407 return;
320 }
408 }
321− Action::HorizontalSplit => {
409+ Action::HorizontalSplit | Action::VerticalSplit => {
322− let view = View::new(id);
323− let view_id = self.tree.split(view, Layout::Horizontal);
324− // initialize selection for view
325− let doc = self.documents.get_mut(&id).unwrap();
326− doc.selections.insert(view_id, Selection::point(0));
327− }
328− Action::VerticalSplit => {
329 let view = View::new(id);
410 let view = View::new(id);
330− let view_id = self.tree.split(view, Layout::Vertical);
411+ let view_id = self.tree.split(
412+ view,
413+ match action {
414+ Action::HorizontalSplit => Layout::Horizontal,
415+ Action::VerticalSplit => Layout::Vertical,
416+ _ => unreachable!(),
417+ },
418+ );
331 // initialize selection for view
419 // initialize selection for view
332 let doc = self.documents.get_mut(&id).unwrap();
420 let doc = self.documents.get_mut(&id).unwrap();
333 doc.selections.insert(view_id, Selection::point(0));
421 doc.selections.insert(view_id, Selection::point(0));
@@ -337,16 +425,19 @@ impl Editor {
337 self._refresh();
425 self._refresh();
338 }
426 }
339
427
428+ /// Generate an id for a new document and register it.
340− fn new_document(&mut self, mut document: Document) -> DocumentId {
429+ fn new_document(&mut self, mut doc: Document) -> DocumentId {
341− let id = DocumentId(self.next_document_id);
430+ let id = self.next_document_id;
431+ // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
432+ self.next_document_id =
342− self.next_document_id += 1;
433+ DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
343− document.id = id;
434+ doc.id = id;
344− self.documents.insert(id, document);
435+ self.documents.insert(id, doc);
345 id
436 id
346 }
437 }
347
438
348− fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
439+ fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
349− let id = self.new_document(document);
440+ let id = self.new_document(doc);
350 self.switch(id, action);
441 self.switch(id, action);
351 id
442 id
352 }
443 }
@@ -362,54 +453,16 @@ impl Editor {
362
453
363 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
454 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
364 let path = helix_core::path::get_canonicalized_path(&path)?;
455 let path = helix_core::path::get_canonicalized_path(&path)?;
365−
366− let id = self
367− .documents()
368− .find(|doc| doc.path() == Some(&path))
369− .map(|doc| doc.id);
456+ let id = self.document_by_path(&path).map(|doc| doc.id);
370
457
371 let id = if let Some(id) = id {
458 let id = if let Some(id) = id {
372 id
459 id
373 } else {
460 } else {
374 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
461 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
375
462
376− // try to find a language server based on the language name
377− let language_server = doc.language.as_ref().and_then(|language| {
378− self.language_servers
463+ let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
379− .get(language)
380− .map_err(|e| {
381− log::error!(
382− "Failed to initialize the LSP for `{}` {{ {} }}",
383− language.scope(),
384− e
385− )
386− })
387− .ok()
388− });
389−
390− if let Some(language_server) = language_server {
391− let language_id = doc
392− .language()
393− .and_then(|s| s.split('.').last()) // source.rust
394− .map(ToOwned::to_owned)
395− .unwrap_or_default();
396−
397− // TODO: this now races with on_init code if the init happens too quickly
398− tokio::spawn(language_server.text_document_did_open(
399− doc.url().unwrap(),
400− doc.version(),
401− doc.text(),
402− language_id,
403− ));
404−
405− doc.set_language_server(Some(language_server));
406− }
407
464
408− let id = DocumentId(self.next_document_id);
465+ self.new_document(doc)
409− self.next_document_id += 1;
410− doc.id = id;
411− self.documents.insert(id, doc);
412− id
413 };
466 };
414
467
415 self.switch(id, action);
468 self.switch(id, action);
@@ -432,11 +485,11 @@ impl Editor {
432 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
485 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
433 let doc = match self.documents.get(&doc_id) {
486 let doc = match self.documents.get(&doc_id) {
434 Some(doc) => doc,
487 Some(doc) => doc,
435− None => anyhow::bail!("document does not exist"),
488+ None => bail!("document does not exist"),
436 };
489 };
437
490
438 if !force && doc.is_modified() {
491 if !force && doc.is_modified() {
439− anyhow::bail!(
492+ bail!(
440 "buffer {:?} is modified",
493 "buffer {:?} is modified",
441 doc.relative_path()
494 doc.relative_path()
442 .map(|path| path.to_string_lossy().to_string())
495 .map(|path| path.to_string_lossy().to_string())
@@ -469,7 +522,7 @@ impl Editor {
469 // If the document we removed was visible in all views, we will have no more views. We don't
522 // If the document we removed was visible in all views, we will have no more views. We don't
470 // want to close the editor just for a simple buffer close, so we need to create a new view
523 // want to close the editor just for a simple buffer close, so we need to create a new view
471 // containing either an existing document, or a brand new document.
524 // containing either an existing document, or a brand new document.
472− if self.tree.views().peekable().peek().is_none() {
525+ if self.tree.views().next().is_none() {
473 let doc_id = self
526 let doc_id = self
474 .documents
527 .documents
475 .iter()
528 .iter()
@@ -554,8 +607,7 @@ impl Editor {
554 }
607 }
555
608
556 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
609 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
557− let view = view!(self);
610+ let (view, doc) = current_ref!(self);
558− let doc = &self.documents[&view.doc];
559 let cursor = doc
611 let cursor = doc
560 .selection(view.id)
612 .selection(view.id)
561 .primary()
613 .primary()
helix-view/src/gutter.rsnew file+95
@@ -0,0 +1,95 @@
1+use std::fmt::Write;
2+
3+use crate::{editor::Config, graphics::Style, Document, Theme, View};
4+
5+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
6+pub type Gutter =
7+ for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
8+
9+pub fn diagnostic<'doc>(
10+ doc: &'doc Document,
11+ _view: &View,
12+ theme: &Theme,
13+ _config: &Config,
14+ _is_focused: bool,
15+ _width: usize,
16+) -> GutterFn<'doc> {
17+ let warning = theme.get("warning");
18+ let error = theme.get("error");
19+ let info = theme.get("info");
20+ let hint = theme.get("hint");
21+ let diagnostics = doc.diagnostics();
22+
23+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
24+ use helix_core::diagnostic::Severity;
25+ if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
26+ write!(out, "●").unwrap();
27+ return Some(match diagnostic.severity {
28+ Some(Severity::Error) => error,
29+ Some(Severity::Warning) | None => warning,
30+ Some(Severity::Info) => info,
31+ Some(Severity::Hint) => hint,
32+ });
33+ }
34+ None
35+ })
36+}
37+
38+pub fn line_number<'doc>(
39+ doc: &'doc Document,
40+ view: &View,
41+ theme: &Theme,
42+ config: &Config,
43+ is_focused: bool,
44+ width: usize,
45+) -> GutterFn<'doc> {
46+ let text = doc.text().slice(..);
47+ let last_line = view.last_line(doc);
48+ // Whether to draw the line number for the last line of the
49+ // document or not. We only draw it if it's not an empty line.
50+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
51+
52+ let linenr = theme.get("ui.linenr");
53+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
54+
55+ let current_line = doc
56+ .text()
57+ .char_to_line(doc.selection(view.id).primary().cursor(text));
58+
59+ let config = config.line_number;
60+
61+ Box::new(move |line: usize, selected: bool, out: &mut String| {
62+ if line == last_line && !draw_last {
63+ write!(out, "{:>1$}", '~', width).unwrap();
64+ Some(linenr)
65+ } else {
66+ use crate::editor::LineNumber;
67+ let line = match config {
68+ LineNumber::Absolute => line + 1,
69+ LineNumber::Relative => {
70+ if current_line == line {
71+ line + 1
72+ } else {
73+ abs_diff(current_line, line)
74+ }
75+ }
76+ };
77+ let style = if selected && is_focused {
78+ linenr_select
79+ } else {
80+ linenr
81+ };
82+ write!(out, "{:>1$}", line, width).unwrap();
83+ Some(style)
84+ }
85+ })
86+}
87+
88+#[inline(always)]
89+const fn abs_diff(a: usize, b: usize) -> usize {
90+ if a > b {
91+ a - b
92+ } else {
93+ b - a
94+ }
95+}
helix-view/src/lib.rs+13−2
@@ -5,6 +5,7 @@ pub mod clipboard;
5pub mod document;
5pub mod document;
6pub mod editor;
6pub mod editor;
7pub mod graphics;
7pub mod graphics;
8+pub mod gutter;
8pub mod info;
9pub mod info;
9pub mod input;
10pub mod input;
10pub mod keyboard;
11pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
12pub mod tree;
13pub mod tree;
13pub mod view;
14pub mod view;
14
15
16+use std::num::NonZeroUsize;
17+
18+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
15−#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
19+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
16−pub struct DocumentId(usize);
20+pub struct DocumentId(NonZeroUsize);
21+
22+impl Default for DocumentId {
23+ fn default() -> DocumentId {
24+ // Safety: 1 is non-zero
25+ DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
26+ }
27+}
17
28
18slotmap::new_key_type! {
29slotmap::new_key_type! {
19 pub struct ViewId;
30 pub struct ViewId;
helix-view/src/view.rs+20−4
@@ -1,6 +1,10 @@
1use std::borrow::Cow;
1use std::borrow::Cow;
2
2
3+use crate::{
4+ graphics::Rect,
5+ gutter::{self, Gutter},
3−use crate::{graphics::Rect, Document, DocumentId, ViewId};
6+ Document, DocumentId, ViewId,
7+};
4use helix_core::{
8use helix_core::{
5 graphemes::{grapheme_width, RopeGraphemes},
9 graphemes::{grapheme_width, RopeGraphemes},
6 line_ending::line_end_char_index,
10 line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
60 }
64 }
61}
65}
62
66
67+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
68+
63#[derive(Debug)]
69#[derive(Debug)]
64pub struct View {
70pub struct View {
65 pub id: ViewId,
71 pub id: ViewId,
@@ -83,10 +89,19 @@ impl View {
83 }
89 }
84 }
90 }
85
91
92+ pub fn gutters(&self) -> &[(Gutter, usize)] {
93+ GUTTERS
94+ }
95+
86 pub fn inner_area(&self) -> Rect {
96 pub fn inner_area(&self) -> Rect {
87− // TODO: not ideal
97+ // TODO: cache this
88− const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
98+ let offset = self
99+ .gutters()
100+ .iter()
101+ .map(|(_, width)| *width as u16)
102+ .sum::<u16>()
103+ + 1; // +1 for some space between gutters and line
89− self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
104+ self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
90 }
105 }
91
106
92 //
107 //
@@ -296,6 +311,7 @@ mod tests {
296 use super::*;
311 use super::*;
297 use helix_core::Rope;
312 use helix_core::Rope;
298 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
313 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
314+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
299
315
300 #[test]
316 #[test]
301 fn test_text_pos_at_screen_coords() {
317 fn test_text_pos_at_screen_coords() {
languages.toml+25
@@ -508,3 +508,28 @@ shebangs = ["perl"]
508roots = []
508roots = []
509comment-token = "#"
509comment-token = "#"
510indent = { tab-width = 2, unit = " " }
510indent = { tab-width = 2, unit = " " }
511+
512+[[language]]
513+name = "racket"
514+scope = "source.rkt"
515+roots = []
516+file-types = ["rkt"]
517+shebangs = ["racket"]
518+comment-token = ";"
519+language-server = { command = "racket", args = ["-l", "racket-langserver"] }
520+
521+[[language]]
522+name = "wgsl"
523+scope = "source.wgsl"
524+file-types = ["wgsl"]
525+roots = []
526+comment-token = "//"
527+indent = { tab-width = 4, unit = " " }
528+
529+[[language]]
530+name = "llvm"
531+scope = "source.llvm"
532+roots = []
533+file-types = ["ll"]
534+comment-token = ";"
535+indent = { tab-width = 2, unit = " " }
runtime/queries/llvm/highlights.scmnew file+14
@@ -0,0 +1,14 @@
1+(type) @type
2+(statement) @keyword.operator
3+(number) @constant.numeric.integer
4+(comment) @comment
5+(string) @string
6+(label) @label
7+(keyword) @keyword
8+"ret" @keyword.control.return
9+(boolean) @constant.builtin.boolean
10+(float) @constant.numeric.float
11+(constant) @constant
12+(identifier) @variable
13+(symbol) @punctuation.delimiter
14+(bracket) @punctuation.bracket
runtime/queries/wgsl/highlights.scmnew file+102
@@ -0,0 +1,102 @@
1+(const_literal) @constant.numeric
2+
3+(type_declaration) @type
4+
5+(function_declaration
6+ (identifier) @function)
7+
8+(struct_declaration
9+ (identifier) @type)
10+
11+(type_constructor_or_function_call_expression
12+ (type_declaration) @function)
13+
14+(parameter
15+ (variable_identifier_declaration (identifier) @variable.parameter))
16+
17+[
18+ "struct"
19+ "bitcast"
20+ ; "block"
21+ "discard"
22+ "enable"
23+ "fallthrough"
24+ "fn"
25+ "let"
26+ "private"
27+ "read"
28+ "read_write"
29+ "return"
30+ "storage"
31+ "type"
32+ "uniform"
33+ "var"
34+ "workgroup"
35+ "write"
36+ (texel_format)
37+] @keyword ; TODO reserved keywords
38+
39+[
40+ (true)
41+ (false)
42+] @constant.builtin.boolean
43+
44+[ "," "." ":" ";" ] @punctuation.delimiter
45+
46+;; brackets
47+[
48+ "("
49+ ")"
50+ "["
51+ "]"
52+ "{"
53+ "}"
54+] @punctuation.bracket
55+
56+[
57+ "loop"
58+ "for"
59+ "break"
60+ "continue"
61+ "continuing"
62+] @keyword.control.repeat
63+
64+[
65+ "if"
66+ "else"
67+ "elseif"
68+ "switch"
69+ "case"
70+ "default"
71+] @keyword.control.conditional
72+
73+[
74+ "&"
75+ "&&"
76+ "/"
77+ "!"
78+ "="
79+ "=="
80+ "!="
81+ ">"
82+ ">="
83+ ">>"
84+ "<"
85+ "<="
86+ "<<"
87+ "%"
88+ "-"
89+ "+"
90+ "|"
91+ "||"
92+ "*"
93+ "~"
94+ "^"
95+] @operator
96+
97+(attribute
98+ (identifier) @variable.other.member)
99+
100+(comment) @comment
101+
102+(ERROR) @error
runtime/themes/solarized_dark.toml+6−6
@@ -28,18 +28,18 @@
28# 行号栏
28# 行号栏
29"ui.linenr" = { fg = "base0", bg = "base02" }
29"ui.linenr" = { fg = "base0", bg = "base02" }
30# 当前行号栏
30# 当前行号栏
31−"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
31+"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
32
32
33# 状态栏
33# 状态栏
34−"ui.statusline" = { fg = "base02", bg = "base1" }
34+"ui.statusline" = { fg = "base03", bg = "base0" }
35# 非活动状态栏
35# 非活动状态栏
36−"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
36+"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
37
37
38# 补全窗口, preview窗口
38# 补全窗口, preview窗口
39−"ui.popup" = { bg = "base1" }
39+"ui.popup" = { bg = "base02" }
40# 影响 补全选中 cmd弹出信息选中
40# 影响 补全选中 cmd弹出信息选中
41−"ui.menu.selected" = { fg = "base02", bg = "violet"}
41+"ui.menu.selected" = { fg = "base02", bg = "base2"}
42−"ui.menu" = { fg = "base02" }
42+"ui.menu" = { fg = "base1" }
43# ??
43# ??
44"ui.window" = { fg = "base3" }
44"ui.window" = { fg = "base3" }
45# 命令行 补全的帮助信息
45# 命令行 补全的帮助信息
runtime/themes/solarized_light.toml+6−6
@@ -28,18 +28,18 @@
28# 行号栏
28# 行号栏
29"ui.linenr" = { fg = "base0", bg = "base02" }
29"ui.linenr" = { fg = "base0", bg = "base02" }
30# 当前行号栏
30# 当前行号栏
31−"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
31+"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
32
32
33# 状态栏
33# 状态栏
34−"ui.statusline" = { fg = "base02", bg = "base1" }
34+"ui.statusline" = { fg = "base03", bg = "base0" }
35# 非活动状态栏
35# 非活动状态栏
36−"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
36+"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
37
37
38# 补全窗口, preview窗口
38# 补全窗口, preview窗口
39−"ui.popup" = { bg = "base1" }
39+"ui.popup" = { bg = "base02" }
40# 影响 补全选中 cmd弹出信息选中
40# 影响 补全选中 cmd弹出信息选中
41−"ui.menu.selected" = { fg = "base02", bg = "violet"}
41+"ui.menu.selected" = { fg = "base02", bg = "base2"}
42−"ui.menu" = { fg = "base02" }
42+"ui.menu" = { fg = "base1" }
43# ??
43# ??
44"ui.window" = { fg = "base3" }
44"ui.window" = { fg = "base3" }
45# 命令行 补全的帮助信息
45# 命令行 补全的帮助信息