on master help sign in
Merge branch 'master' into debug
bf53affDmitry Sharshakov
.gitmodules+4
@@ -118,3 +118,7 @@
118 path = helix-syntax/languages/tree-sitter-zig
118 path = helix-syntax/languages/tree-sitter-zig
119 url = https://github.com/maxxnino/tree-sitter-zig
119 url = https://github.com/maxxnino/tree-sitter-zig
120 shallow = true
120 shallow = true
121+[submodule "helix-syntax/languages/tree-sitter-svelte"]
122+ path = helix-syntax/languages/tree-sitter-svelte
123+ url = https://github.com/Himujjal/tree-sitter-svelte
124+ shallow = true
Cargo.lock+95−22
@@ -13,15 +13,15 @@ dependencies = [
13
13
14[[package]]
14[[package]]
15name = "anyhow"
15name = "anyhow"
16−version = "1.0.43"
16+version = "1.0.44"
17source = "registry+https://github.com/rust-lang/crates.io-index"
17source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
18+checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
19
19
20[[package]]
20[[package]]
21name = "arc-swap"
21name = "arc-swap"
22−version = "1.3.2"
22+version = "1.4.0"
23source = "registry+https://github.com/rust-lang/crates.io-index"
23source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00"
24+checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
25
25
26[[package]]
26[[package]]
27name = "autocfg"
27name = "autocfg"
@@ -41,9 +41,17 @@ version = "0.2.16"
41source = "registry+https://github.com/rust-lang/crates.io-index"
41source = "registry+https://github.com/rust-lang/crates.io-index"
42checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
42checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
43dependencies = [
43dependencies = [
44+ "lazy_static",
44 "memchr",
45 "memchr",
46+ "regex-automata",
45]
47]
46
48
49+[[package]]
50+name = "bytecount"
51+version = "0.6.2"
52+source = "registry+https://github.com/rust-lang/crates.io-index"
53+checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
54+
47[[package]]
55[[package]]
48name = "bytes"
56name = "bytes"
49version = "1.0.1"
57version = "1.0.1"
@@ -58,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
58
66
59[[package]]
67[[package]]
60name = "cc"
68name = "cc"
61−version = "1.0.69"
69+version = "1.0.70"
62source = "registry+https://github.com/rust-lang/crates.io-index"
70source = "registry+https://github.com/rust-lang/crates.io-index"
63−checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
71+checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
64
72
65[[package]]
73[[package]]
66name = "cfg-if"
74name = "cfg-if"
@@ -174,6 +182,15 @@ dependencies = [
174 "cfg-if",
182 "cfg-if",
175]
183]
176
184
185+[[package]]
186+name = "encoding_rs_io"
187+version = "0.1.7"
188+source = "registry+https://github.com/rust-lang/crates.io-index"
189+checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
190+dependencies = [
191+ "encoding_rs",
192+]
193+
177[[package]]
194[[package]]
178name = "error-code"
195name = "error-code"
179version = "2.3.0"
196version = "2.3.0"
@@ -300,6 +317,45 @@ dependencies = [
300 "regex",
317 "regex",
301]
318]
302
319
320+[[package]]
321+name = "grep-matcher"
322+version = "0.1.5"
323+source = "registry+https://github.com/rust-lang/crates.io-index"
324+checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc"
325+dependencies = [
326+ "memchr",
327+]
328+
329+[[package]]
330+name = "grep-regex"
331+version = "0.1.9"
332+source = "registry+https://github.com/rust-lang/crates.io-index"
333+checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06"
334+dependencies = [
335+ "aho-corasick",
336+ "bstr",
337+ "grep-matcher",
338+ "log",
339+ "regex",
340+ "regex-syntax",
341+ "thread_local",
342+]
343+
344+[[package]]
345+name = "grep-searcher"
346+version = "0.1.8"
347+source = "registry+https://github.com/rust-lang/crates.io-index"
348+checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d"
349+dependencies = [
350+ "bstr",
351+ "bytecount",
352+ "encoding_rs",
353+ "encoding_rs_io",
354+ "grep-matcher",
355+ "log",
356+ "memmap2",
357+]
358+
303[[package]]
359[[package]]
304name = "helix-core"
360name = "helix-core"
305version = "0.4.1"
361version = "0.4.1"
@@ -375,6 +431,8 @@ dependencies = [
375 "fern",
431 "fern",
376 "futures-util",
432 "futures-util",
377 "fuzzy-matcher",
433 "fuzzy-matcher",
434+ "grep-regex",
435+ "grep-searcher",
378 "helix-core",
436 "helix-core",
379 "helix-dap",
437 "helix-dap",
380 "helix-lsp",
438 "helix-lsp",
@@ -570,6 +628,15 @@ version = "2.4.1"
570source = "registry+https://github.com/rust-lang/crates.io-index"
628source = "registry+https://github.com/rust-lang/crates.io-index"
571checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
629checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
572
630
631+[[package]]
632+name = "memmap2"
633+version = "0.3.1"
634+source = "registry+https://github.com/rust-lang/crates.io-index"
635+checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
636+dependencies = [
637+ "libc",
638+]
639+
573[[package]]
640[[package]]
574name = "mio"
641name = "mio"
575version = "0.7.13"
642version = "0.7.13"
@@ -771,6 +838,12 @@ dependencies = [
771 "regex-syntax",
838 "regex-syntax",
772]
839]
773
840
841+[[package]]
842+name = "regex-automata"
843+version = "0.1.10"
844+source = "registry+https://github.com/rust-lang/crates.io-index"
845+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
846+
774[[package]]
847[[package]]
775name = "regex-syntax"
848name = "regex-syntax"
776version = "0.6.25"
849version = "0.6.25"
@@ -829,9 +902,9 @@ dependencies = [
829
902
830[[package]]
903[[package]]
831name = "serde_json"
904name = "serde_json"
832−version = "1.0.67"
905+version = "1.0.68"
833source = "registry+https://github.com/rust-lang/crates.io-index"
906source = "registry+https://github.com/rust-lang/crates.io-index"
834−checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
907+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
835dependencies = [
908dependencies = [
836 "itoa",
909 "itoa",
837 "ryu",
910 "ryu",
@@ -851,9 +924,9 @@ dependencies = [
851
924
852[[package]]
925[[package]]
853name = "signal-hook"
926name = "signal-hook"
854−version = "0.3.9"
927+version = "0.3.10"
855source = "registry+https://github.com/rust-lang/crates.io-index"
928source = "registry+https://github.com/rust-lang/crates.io-index"
856−checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
929+checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
857dependencies = [
930dependencies = [
858 "libc",
931 "libc",
859 "signal-hook-registry",
932 "signal-hook-registry",
@@ -893,9 +966,9 @@ dependencies = [
893
966
894[[package]]
967[[package]]
895name = "similar"
968name = "similar"
896−version = "1.3.0"
969+version = "2.0.0"
897source = "registry+https://github.com/rust-lang/crates.io-index"
970source = "registry+https://github.com/rust-lang/crates.io-index"
898−checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
971+checksum = "6bf11003835e462f07851028082d2a1c89d956180ce4b4b50e07fb085ec4131a"
899
972
900[[package]]
973[[package]]
901name = "slab"
974name = "slab"
@@ -948,18 +1021,18 @@ dependencies = [
948
1021
949[[package]]
1022[[package]]
950name = "thiserror"
1023name = "thiserror"
951−version = "1.0.28"
1024+version = "1.0.29"
952source = "registry+https://github.com/rust-lang/crates.io-index"
1025source = "registry+https://github.com/rust-lang/crates.io-index"
953−checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec"
1026+checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
954dependencies = [
1027dependencies = [
955 "thiserror-impl",
1028 "thiserror-impl",
956]
1029]
957
1030
958[[package]]
1031[[package]]
959name = "thiserror-impl"
1032name = "thiserror-impl"
960−version = "1.0.28"
1033+version = "1.0.29"
961source = "registry+https://github.com/rust-lang/crates.io-index"
1034source = "registry+https://github.com/rust-lang/crates.io-index"
962−checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71"
1035+checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
963dependencies = [
1036dependencies = [
964 "proc-macro2",
1037 "proc-macro2",
965 "quote",
1038 "quote",
@@ -1001,9 +1074,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1001
1074
1002[[package]]
1075[[package]]
1003name = "tokio"
1076name = "tokio"
1004−version = "1.10.1"
1077+version = "1.11.0"
1005source = "registry+https://github.com/rust-lang/crates.io-index"
1078source = "registry+https://github.com/rust-lang/crates.io-index"
1006−checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
1079+checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
1007dependencies = [
1080dependencies = [
1008 "autocfg",
1081 "autocfg",
1009 "bytes",
1082 "bytes",
@@ -1052,9 +1125,9 @@ dependencies = [
1052
1125
1053[[package]]
1126[[package]]
1054name = "tree-sitter"
1127name = "tree-sitter"
1055−version = "0.19.5"
1128+version = "0.20.0"
1056source = "registry+https://github.com/rust-lang/crates.io-index"
1129source = "registry+https://github.com/rust-lang/crates.io-index"
1057−checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0"
1130+checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
1058dependencies = [
1131dependencies = [
1059 "cc",
1132 "cc",
1060 "regex",
1133 "regex",
@@ -1098,9 +1171,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
1098
1171
1099[[package]]
1172[[package]]
1100name = "unicode-width"
1173name = "unicode-width"
1101−version = "0.1.8"
1174+version = "0.1.9"
1102source = "registry+https://github.com/rust-lang/crates.io-index"
1175source = "registry+https://github.com/rust-lang/crates.io-index"
1103−checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
1176+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
1104
1177
1105[[package]]
1178[[package]]
1106name = "unicode-xid"
1179name = "unicode-xid"
TODO.md−6
@@ -1,12 +1,8 @@
1
1
2- tree sitter:
2- tree sitter:
3− - lua
4 - markdown
3 - markdown
5− - zig
6 - regex
4 - regex
7− - vue
8 - kotlin
5 - kotlin
9− - julia
10 - clojure
6 - clojure
11 - erlang
7 - erlang
12
8
@@ -26,8 +22,6 @@ as you type completion!
26
22
27- [ ] lsp: signature help
23- [ ] lsp: signature help
28
24
29−- [ ] search: smart case by default: insensitive unless upper detected
30−
312
252
32- [ ] macro recording
26- [ ] macro recording
33- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
27- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
book/book.toml+2−1
@@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"]
3language = "en"
3language = "en"
4multilingual = false
4multilingual = false
5src = "src"
5src = "src"
6−theme = "colibri"
7edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
6edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
8
7
9[output.html]
8[output.html]
10cname = "docs.helix-editor.com"
9cname = "docs.helix-editor.com"
10+default-theme = "colibri"
11+preferred-dark-theme = "colibri"
book/src/configuration.md+15
@@ -5,6 +5,21 @@ To override global configuration parameters, create a `config.toml` file located
5* Linux and Mac: `~/.config/helix/config.toml`
5* Linux and Mac: `~/.config/helix/config.toml`
6* Windows: `%AppData%\helix\config.toml`
6* Windows: `%AppData%\helix\config.toml`
7
7
8+## Editor
9+
10+`[editor]` section of the config.
11+
12+| Key | Description | Default |
13+|--|--|---------|
14+| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` |
15+| `mouse` | Enable mouse mode. | `true` |
16+| `middle-click-paste` | Middle click paste support. | `true` |
17+| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
18+| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
19+| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
20+| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
21+| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
22+
8## LSP
23## LSP
9
24
10To display all language server messages in the status line add the following to your `config.toml`:
25To display all language server messages in the status line add the following to your `config.toml`:
book/src/from-vim.md+2
@@ -7,4 +7,6 @@ going to act on (a word, a paragraph, a line, etc) is selected first and the
7action itself (delete, change, yank, etc) comes second. A cursor is simply a
7action itself (delete, change, yank, etc) comes second. A cursor is simply a
8single width selection.
8single width selection.
9
9
10+See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
11+
10> TODO: Mention texobjects, surround, registers
12> TODO: Mention texobjects, surround, registers
book/src/install.md+3−1
@@ -23,7 +23,9 @@ shell for working on Helix.
23
23
24### Arch Linux
24### Arch Linux
25
25
26+Releases are available in the `community` repository.
27+
26−Binary packages are available on AUR:
28+Packages are also available on AUR:
27- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
29- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
28- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
30- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
29
31
book/src/keymap.md+97−85
@@ -4,40 +4,40 @@
4
4
5### Movement
5### Movement
6
6
7−> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
7+> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
8−
8+
9−| Key | Description | Command |
9+| Key | Description | Command |
10−| ----- | ----------- | ------- |
10+| ----- | ----------- | ------- |
11−| `h`, `Left` | Move left | `move_char_left` |
11+| `h`, `Left` | Move left | `move_char_left` |
12−| `j`, `Down` | Move down | `move_char_right` |
12+| `j`, `Down` | Move down | `move_char_right` |
13−| `k`, `Up` | Move up | `move_line_up` |
13+| `k`, `Up` | Move up | `move_line_up` |
14−| `l`, `Right` | Move right | `move_line_down` |
14+| `l`, `Right` | Move right | `move_line_down` |
15−| `w` | Move next word start | `move_next_word_start` |
15+| `w` | Move next word start | `move_next_word_start` |
16−| `b` | Move previous word start | `move_prev_word_start` |
16+| `b` | Move previous word start | `move_prev_word_start` |
17−| `e` | Move next word end | `move_next_word_end` |
17+| `e` | Move next word end | `move_next_word_end` |
18−| `W` | Move next WORD start | `move_next_long_word_start` |
18+| `W` | Move next WORD start | `move_next_long_word_start` |
19−| `B` | Move previous WORD start | `move_prev_long_word_start` |
19+| `B` | Move previous WORD start | `move_prev_long_word_start` |
20−| `E` | Move next WORD end | `move_next_long_word_end` |
20+| `E` | Move next WORD end | `move_next_long_word_end` |
21−| `t` | Find 'till next char | `find_till_char` |
21+| `t` | Find 'till next char | `find_till_char` |
22−| `f` | Find next char | `find_next_char` |
22+| `f` | Find next char | `find_next_char` |
23−| `T` | Find 'till previous char | `till_prev_char` |
23+| `T` | Find 'till previous char | `till_prev_char` |
24−| `F` | Find previous char | `find_prev_char` |
24+| `F` | Find previous char | `find_prev_char` |
25−| `Home` | Move to the start of the line | `goto_line_start` |
25+| `Home` | Move to the start of the line | `goto_line_start` |
26−| `End` | Move to the end of the line | `goto_line_end` |
26+| `End` | Move to the end of the line | `goto_line_end` |
27−| `PageUp` | Move page up | `page_up` |
27+| `PageUp` | Move page up | `page_up` |
28−| `PageDown` | Move page down | `page_down` |
28+| `PageDown` | Move page down | `page_down` |
29−| `Ctrl-u` | Move half page up | `half_page_up` |
29+| `Ctrl-u` | Move half page up | `half_page_up` |
30−| `Ctrl-d` | Move half page down | `half_page_down` |
30+| `Ctrl-d` | Move half page down | `half_page_down` |
31−| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` |
31+| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
32−| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
32+| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33−| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
33+| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34−| `g` | Enter [goto mode](#goto-mode) | N/A |
34+| `g` | Enter [goto mode](#goto-mode) | N/A |
35−| `m` | Enter [match mode](#match-mode) | N/A |
35+| `m` | Enter [match mode](#match-mode) | N/A |
36−| `:` | Enter command mode | `command_mode` |
36+| `:` | Enter command mode | `command_mode` |
37−| `z` | Enter [view mode](#view-mode) | N/A |
37+| `z` | Enter [view mode](#view-mode) | N/A |
38+| `Z` | Enter sticky [view mode](#view-mode) | N/A |
38−| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) | N/A |
39+| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
39−| `Space` | Enter [space mode](#space-mode) | N/A |
40+| `Space` | Enter [space mode](#space-mode) | N/A |
40−| `K` | Show documentation for the item under the cursor | `hover` |
41
41
42### Changes
42### Changes
43
43
@@ -66,6 +66,16 @@
66| `d` | Delete selection | `delete_selection` |
66| `d` | Delete selection | `delete_selection` |
67| `c` | Change selection (delete and enter insert mode) | `change_selection` |
67| `c` | Change selection (delete and enter insert mode) | `change_selection` |
68
68
69+#### Shell
70+
71+| Key | Description | Command |
72+| ------ | ----------- | ------- |
73+| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
74+| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
75+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
76+| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
77+
78+
69### Selection manipulation
79### Selection manipulation
70
80
71| Key | Description | Command |
81| Key | Description | Command |
@@ -75,6 +85,7 @@
75| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
85| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
76| `;` | Collapse selection onto a single cursor | `collapse_selection` |
86| `;` | Collapse selection onto a single cursor | `collapse_selection` |
77| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
87| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
88+| `,` | Keep only the primary selection | `keep_primary_selection` |
78| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
89| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
79| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
90| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
80| `(` | Rotate main selection forward | `rotate_selections_backward` |
91| `(` | Rotate main selection forward | `rotate_selections_backward` |
@@ -86,22 +97,13 @@
86| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
97| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
87| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
98| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
88| `J` | Join lines inside selection | `join_selections` |
99| `J` | Join lines inside selection | `join_selections` |
89−| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` |
100+| `K` | Keep selections matching the regex | `keep_selections` |
90−| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` |
101+| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
91| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
102| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
92
103
93−### Insert Mode
94−
95−| Key | Description | Command |
96−| ----- | ----------- | ------- |
97−| `Escape` | Switch to normal mode | `normal_mode` |
98−| `Ctrl-x` | Autocomplete | `completion` |
99−| `Ctrl-w` | Delete previous word | `delete_word_backward` |
100−
101### Search
104### Search
102
105
103−> TODO: The search implementation isn't ideal yet -- we don't support searching
106+> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
104−in reverse, or searching via smartcase.
105
107
106| Key | Description | Command |
108| Key | Description | Command |
107| ----- | ----------- | ------- |
109| ----- | ----------- | ------- |
@@ -110,41 +112,17 @@ in reverse, or searching via smartcase.
110| `N` | Add next search match to selection | `extend_search_next` |
112| `N` | Add next search match to selection | `extend_search_next` |
111| `*` | Use current selection as the search pattern | `search_selection` |
113| `*` | Use current selection as the search pattern | `search_selection` |
112
114
113−### Unimpaired
115+### Minor modes
114−
115−Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
116−
117−| Key | Description | Command |
118−| ----- | ----------- | ------- |
119−| `[d` | Go to previous diagnostic | `goto_prev_diag` |
120−| `]d` | Go to next diagnostic | `goto_next_diag` |
121−| `[D` | Go to first diagnostic in document | `goto_first_diag` |
122−| `]D` | Go to last diagnostic in document | `goto_last_diag` |
123−| `[space` | Add newline above | `add_newline_above` |
124−| `]space` | Add newline below | `add_newline_below` |
125−
126−### Shell
127
116
128−| Key | Description | Command |
117+These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
129−| ------ | ----------- | ------- |
130−| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` |
131−| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
132−| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
133−| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
134−| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
135−
136−## Select / extend mode
137−
138−I'm still pondering whether to keep this mode or not. It changes movement
139−commands to extend the existing selection instead of replacing it.
140−
141−> NOTE: It's a bit confusing at the moment because extend hasn't been
142−> implemented for all movement commands yet.
143
118
144−## View mode
119+#### View mode
145
120
146View mode is intended for scrolling and manipulating the view without changing
121View mode is intended for scrolling and manipulating the view without changing
147−the selection.
122+the selection. The "sticky" variant of this mode is persistent; use the Escape
123+key to return to normal mode after usage (useful when you're simply looking
124+over text and not actively editing it).
125+
148
126
149| Key | Description | Command |
127| Key | Description | Command |
150| ----- | ----------- | ------- |
128| ----- | ----------- | ------- |
@@ -154,8 +132,12 @@ the selection.
154| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
132| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
155| `j` | Scroll the view downwards | `scroll_down` |
133| `j` | Scroll the view downwards | `scroll_down` |
156| `k` | Scroll the view upwards | `scroll_up` |
134| `k` | Scroll the view upwards | `scroll_up` |
135+| `f` | Move page down | `page_down` |
136+| `b` | Move page up | `page_up` |
137+| `d` | Move half page down | `half_page_down` |
138+| `u` | Move half page up | `half_page_up` |
157
139
158−## Goto mode
140+#### Goto mode
159
141
160Jumps to various locations.
142Jumps to various locations.
161
143
@@ -177,7 +159,7 @@ Jumps to various locations.
177| `i` | Go to implementation | `goto_implementation` |
159| `i` | Go to implementation | `goto_implementation` |
178| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
160| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
179
161
180−## Match mode
162+#### Match mode
181
163
182Enter this mode using `m` from normal mode. See the relavant section
164Enter this mode using `m` from normal mode. See the relavant section
183in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
165in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
@@ -192,11 +174,9 @@ and [textobject](./usage.md#textobject) usage.
192| `a` `<object>` | Select around textobject | `select_textobject_around` |
174| `a` `<object>` | Select around textobject | `select_textobject_around` |
193| `i` `<object>` | Select inside textobject | `select_textobject_inner` |
175| `i` `<object>` | Select inside textobject | `select_textobject_inner` |
194
176
195−## Object mode
196−
197TODO: Mappings for selecting syntax nodes (a superset of `[`).
177TODO: Mappings for selecting syntax nodes (a superset of `[`).
198
178
199−## Window mode
179+#### Window mode
200
180
201This layer is similar to vim keybindings as kakoune does not support window.
181This layer is similar to vim keybindings as kakoune does not support window.
202
182
@@ -207,24 +187,56 @@ This layer is similar to vim keybindings as kakoune does not support window.
207| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
187| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
208| `q`, `Ctrl-q` | Close current window | `wclose` |
188| `q`, `Ctrl-q` | Close current window | `wclose` |
209
189
210−## Space mode
190+#### Space mode
211
191
212−This layer is a kludge of mappings I had under leader key in neovim.
192+This layer is a kludge of mappings, mostly pickers.
213
193
214| Key | Description | Command |
194| Key | Description | Command |
215| ----- | ----------- | ------- |
195| ----- | ----------- | ------- |
196+| `k` | Show documentation for the item under the cursor | `hover` |
216| `f` | Open file picker | `file_picker` |
197| `f` | Open file picker | `file_picker` |
217| `b` | Open buffer picker | `buffer_picker` |
198| `b` | Open buffer picker | `buffer_picker` |
218| `s` | Open symbol picker (current document) | `symbol_picker` |
199| `s` | Open symbol picker (current document) | `symbol_picker` |
219| `a` | Apply code action | `code_action` |
200| `a` | Apply code action | `code_action` |
220| `'` | Open last fuzzy picker | `last_picker` |
201| `'` | Open last fuzzy picker | `last_picker` |
221| `w` | Enter [window mode](#window-mode) | N/A |
202| `w` | Enter [window mode](#window-mode) | N/A |
222−| `space` | Keep primary selection TODO: it's here because space mode replaced it | `keep_primary_selection` |
223| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
203| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
224| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
204| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
225| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
205| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
226| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
206| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
227| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
207| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
208+| `/` | Global search in workspace folder | `global_search` |
209+
210+> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
211+
212+#### Unimpaired
213+
214+Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
215+
216+| Key | Description | Command |
217+| ----- | ----------- | ------- |
218+| `[d` | Go to previous diagnostic | `goto_prev_diag` |
219+| `]d` | Go to next diagnostic | `goto_next_diag` |
220+| `[D` | Go to first diagnostic in document | `goto_first_diag` |
221+| `]D` | Go to last diagnostic in document | `goto_last_diag` |
222+| `[space` | Add newline above | `add_newline_above` |
223+| `]space` | Add newline below | `add_newline_below` |
224+
225+## Insert Mode
226+
227+| Key | Description | Command |
228+| ----- | ----------- | ------- |
229+| `Escape` | Switch to normal mode | `normal_mode` |
230+| `Ctrl-x` | Autocomplete | `completion` |
231+| `Ctrl-w` | Delete previous word | `delete_word_backward` |
232+
233+## Select / extend mode
234+
235+I'm still pondering whether to keep this mode or not. It changes movement
236+commands (including goto) to extend the existing selection instead of replacing it.
237+
238+> NOTE: It's a bit confusing at the moment because extend hasn't been
239+> implemented for all movement commands yet.
228
240
229# Picker
241# Picker
230
242
book/src/remapping.md+2
@@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
49| Null | `"null"` |
49| Null | `"null"` |
50| Escape | `"esc"` |
50| Escape | `"esc"` |
51
51
52+Keys can be disabled by binding them to the `no_op` command.
53+
52Commands 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)
54Commands 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)
book/src/themes.md+129−60
@@ -30,7 +30,52 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as
30"key.key" = "#ffffff"
30"key.key" = "#ffffff"
31```
31```
32
32
33−Possible modifiers:
33+### Color palettes
34+
35+It's recommended define a palette of named colors, and refer to them from the
36+configuration values in your theme. To do this, add a table called
37+`palette` to your theme file:
38+
39+```toml
40+ui.background = "white"
41+ui.text = "black"
42+
43+[palette]
44+white = "#ffffff"
45+black = "#000000"
46+```
47+
48+Remember that the `[palette]` table includes all keys after its header,
49+so you should define the palette after normal theme options.
50+
51+The default palette uses the terminal's default 16 colors, and the colors names
52+are listed below. The `[palette]` section in the config file takes precedence
53+over it and is merged into the default palette.
54+
55+| Color Name |
56+| --- |
57+| `black` |
58+| `red` |
59+| `green` |
60+| `yellow` |
61+| `blue` |
62+| `magenta` |
63+| `cyan` |
64+| `gray` |
65+| `light-red` |
66+| `light-green` |
67+| `light-yellow` |
68+| `light-blue` |
69+| `light-magenta` |
70+| `light-cyan` |
71+| `light-gray` |
72+| `white` |
73+
74+### Modifiers
75+
76+The following values may be used as modifiers.
77+
78+Less common modifiers might not be supported by your terminal emulator.
34
79
35| Modifier |
80| Modifier |
36| --- |
81| --- |
@@ -38,44 +83,88 @@ Possible modifiers:
38| `dim` |
83| `dim` |
39| `italic` |
84| `italic` |
40| `underlined` |
85| `underlined` |
41−| `slow\_blink` |
86+| `slow_blink` |
42−| `rapid\_blink` |
87+| `rapid_blink` |
43| `reversed` |
88| `reversed` |
44| `hidden` |
89| `hidden` |
45−| `crossed\_out` |
90+| `crossed_out` |
91+
92+### Scopes
93+
94+The following is a list of scopes available to use for styling.
95+
96+#### Syntax highlighting
97+
98+These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme).
99+
100+For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`.
101+
102+We use a similar set of scopes as
103+[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
104+[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
105+
106+- `escape` (TODO: rename to (constant).character.escape)
107+
108+- `type` - Types
109+ - `builtin` - Primitive types provided by the language (`int`, `usize`)
110+
111+- `constant` (TODO: constant.other.placeholder for %v)
112+ - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
113+ - `boolean`
114+ - `character`
115+
116+- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
117+- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
118+ - `regexp` - Regular expressions
119+ - `special`
120+ - `path`
121+ - `url`
122+
123+- `comment` - Code comments
124+ - `line` - Single line comments (`//`)
125+ - `block` - Block comments (e.g. (`/* */`)
126+ - `documentation` - Documentation comments (e.g. `///` in Rust)
127+
128+- `variable` - Variables
129+ - `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
130+ - `parameter` - Function parameters
131+ - `property`
132+ - `function` (TODO: ?)
133+
134+- `label`
135+
136+- `punctuation`
137+ - `delimiter` - Commas, colons
138+ - `bracket` - Parentheses, angle brackets, etc.
139+
140+- `keyword`
141+ - `control`
142+ - `conditional` - `if`, `else`
143+ - `repeat` - `for`, `while`, `loop`
144+ - `import` - `import`, `export`
145+ - (TODO: return?)
146+ - `directive` - Preprocessor directives (`#if` in C)
147+ - `function` - `fn`, `func`
148+
149+- `operator` - `||`, `+=`, `>`, `or`
150+
151+- `function`
152+ - `builtin`
153+ - `method`
154+ - `macro`
155+ - `special` (preprocesor in C)
156+
157+- `tag` - Tags (e.g. `<body>` in HTML)
158+
159+- `namespace`
160+
161+#### Interface
162+
163+These scopes are used for theming the editor interface.
46
164
47−Possible keys:
48
165
49| Key | Notes |
166| Key | Notes |
50| --- | --- |
167| --- | --- |
51−| `attribute` | |
52−| `keyword` | |
53−| `keyword.directive` | Preprocessor directives (\#if in C) |
54−| `keyword.control` | Control flow |
55−| `namespace` | |
56−| `punctuation` | |
57−| `punctuation.delimiter` | |
58−| `operator` | |
59−| `special` | |
60−| `property` | |
61−| `variable` | |
62−| `variable.parameter` | |
63−| `type` | |
64−| `type.builtin` | |
65−| `type.enum.variant` | Enum variants |
66−| `constructor` | |
67−| `function` | |
68−| `function.macro` | |
69−| `function.builtin` | |
70−| `comment` | |
71−| `variable.builtin` | |
72−| `constant` | |
73−| `constant.builtin` | |
74−| `string` | |
75−| `number` | |
76−| `escape` | Escaped characters |
77−| `label` | For lifetimes |
78−| `module` | |
79| `ui.background` | |
168| `ui.background` | |
80| `ui.cursor` | |
169| `ui.cursor` | |
81| `ui.cursor.insert` | |
170| `ui.cursor.insert` | |
@@ -84,8 +173,8 @@ Possible keys:
84| `ui.cursor.primary` | Cursor with primary selection |
173| `ui.cursor.primary` | Cursor with primary selection |
85| `ui.linenr` | |
174| `ui.linenr` | |
86| `ui.linenr.selected` | |
175| `ui.linenr.selected` | |
87−| `ui.statusline` | |
176+| `ui.statusline` | Statusline |
88−| `ui.statusline.inactive` | |
177+| `ui.statusline.inactive` | Statusline (unfocused document) |
89| `ui.popup` | |
178| `ui.popup` | |
90| `ui.window` | |
179| `ui.window` | |
91| `ui.help` | |
180| `ui.help` | |
@@ -97,29 +186,9 @@ Possible keys:
97| `ui.menu.selected` | |
186| `ui.menu.selected` | |
98| `ui.selection` | For selections in the editing area |
187| `ui.selection` | For selections in the editing area |
99| `ui.selection.primary` | |
188| `ui.selection.primary` | |
100−| `warning` | LSP warning |
189+| `warning` | Diagnostics warning (gutter) |
101−| `error` | LSP error |
190+| `error` | Diagnostics error (gutter) |
102−| `info` | LSP info |
191+| `info` | Diagnostics info (gutter) |
103−| `hint` | LSP hint |
192+| `hint` | Diagnostics hint (gutter) |
104−
193+| `diagnostic` | For text in editing area |
105−These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
106
194
107−For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
108−
109−## Color palettes
110−
111−You can define a palette of named colors, and refer to them from the
112−configuration values in your theme. To do this, add a table called
113−`palette` to your theme file:
114−
115−```toml
116−ui.background = "white"
117−ui.text = "black"
118−
119−[palette]
120−white = "#ffffff"
121−black = "#000000"
122−```
123−
124−Remember that the `[palette]` table includes all keys after its header,
125−so you should define the palette after normal theme options.
book/theme/css/general.css+13
@@ -114,6 +114,19 @@ h6:target::before {
114 margin-bottom: .875em;
114 margin-bottom: .875em;
115}
115}
116
116
117+.content ul li {
118+margin-bottom: .25rem;
119+}
120+.content ul {
121+ list-style-type: square;
122+}
123+.content ul ul, .content ol ul {
124+ margin-bottom: .5rem;
125+}
126+.content li p {
127+ margin-bottom: .5em;
128+}
129+
117.content p { line-height: 1.45em; }
130.content p { line-height: 1.45em; }
118.content ol { line-height: 1.45em; }
131.content ol { line-height: 1.45em; }
119.content ul { line-height: 1.45em; }
132.content ul { line-height: 1.45em; }
book/theme/css/variables.css+14−10
@@ -69,7 +69,7 @@
69
69
70 --links: #2b79a2;
70 --links: #2b79a2;
71
71
72− --inline-code-color: #c5c8c6;;
72+ --inline-code-color: #c5c8c6;
73
73
74 --theme-popup-bg: #141617;
74 --theme-popup-bg: #141617;
75 --theme-popup-border: #43484d;
75 --theme-popup-border: #43484d;
@@ -110,7 +110,7 @@
110
110
111 --links: #20609f;
111 --links: #20609f;
112
112
113− --inline-code-color: #301900;
113+ --inline-code-color: #a39e9b;
114
114
115 --theme-popup-bg: #fafafa;
115 --theme-popup-bg: #fafafa;
116 --theme-popup-border: #cccccc;
116 --theme-popup-border: #cccccc;
@@ -151,7 +151,7 @@
151
151
152 --links: #2b79a2;
152 --links: #2b79a2;
153
153
154− --inline-code-color: #c5c8c6;;
154+ --inline-code-color: #c5c8c6;
155
155
156 --theme-popup-bg: #161923;
156 --theme-popup-bg: #161923;
157 --theme-popup-border: #737480;
157 --theme-popup-border: #737480;
@@ -192,7 +192,7 @@
192
192
193 --links: #2b79a2;
193 --links: #2b79a2;
194
194
195− --inline-code-color: #6e6b5e;
195+ --inline-code-color: #c5c8c6;
196
196
197 --theme-popup-bg: #e1e1db;
197 --theme-popup-bg: #e1e1db;
198 --theme-popup-border: #b38f6b;
198 --theme-popup-border: #b38f6b;
@@ -234,7 +234,7 @@
234
234
235 --links: #2b79a2;
235 --links: #2b79a2;
236
236
237− --inline-code-color: #c5c8c6;;
237+ --inline-code-color: #6e6b5e;
238
238
239 --theme-popup-bg: #141617;
239 --theme-popup-bg: #141617;
240 --theme-popup-border: #43484d;
240 --theme-popup-border: #43484d;
@@ -261,6 +261,7 @@
261.colibri {
261.colibri {
262 --bg: #3b224c;
262 --bg: #3b224c;
263 --fg: #bcbdd0;
263 --fg: #bcbdd0;
264+ --heading-fg: #fff;
264
265
265 --sidebar-bg: #281733;
266 --sidebar-bg: #281733;
266 --sidebar-fg: #c8c9db;
267 --sidebar-fg: #c8c9db;
@@ -276,18 +277,19 @@
276 /* --links: #a4a0e8; */
277 /* --links: #a4a0e8; */
277 --links: #ECCDBA;
278 --links: #ECCDBA;
278
279
279− --inline-code-color: #c5c8c6;;
280+ --inline-code-color: hsl(48.7, 7.8%, 70%);
280
281
281 --theme-popup-bg: #161923;
282 --theme-popup-bg: #161923;
282 --theme-popup-border: #737480;
283 --theme-popup-border: #737480;
283 --theme-hover: rgba(0,0,0, .2);
284 --theme-hover: rgba(0,0,0, .2);
284
285
285− --quote-bg: hsl(226, 15%, 17%);
286+ --quote-bg: #281733;
286 --quote-border: hsl(226, 15%, 22%);
287 --quote-border: hsl(226, 15%, 22%);
287
288
288− --table-border-color: hsl(226, 23%, 16%);
289+ --table-border-color: hsl(226, 23%, 76%);
289− --table-header-bg: hsl(226, 23%, 31%);
290+ --table-header-bg: hsla(226, 23%, 31%, 0);
290 --table-alternate-bg: hsl(226, 23%, 14%);
291 --table-alternate-bg: hsl(226, 23%, 14%);
292+ --table-border-line: hsla(201deg, 20%, 92%, 0.2);
291
293
292 --searchbar-border-color: #aaa;
294 --searchbar-border-color: #aaa;
293 --searchbar-bg: #aeaec6;
295 --searchbar-bg: #aeaec6;
@@ -300,6 +302,7 @@
300}
302}
301
303
302.colibri {
304.colibri {
305+/*
303 --bg: #ffffff;
306 --bg: #ffffff;
304 --fg: #452859;
307 --fg: #452859;
305 --fg: #5a5977;
308 --fg: #5a5977;
@@ -318,7 +321,7 @@
318
321
319 --links: #6F44F0;
322 --links: #6F44F0;
320
323
321− --inline-code-color: #697C81;
324+ --inline-code-color: #a39e9b;
322
325
323 --theme-popup-bg: #161923;
326 --theme-popup-bg: #161923;
324 --theme-popup-border: #737480;
327 --theme-popup-border: #737480;
@@ -341,4 +344,5 @@
341 --searchresults-border-color: #5c5c68;
344 --searchresults-border-color: #5c5c68;
342 --searchresults-li-bg: #242430;
345 --searchresults-li-bg: #242430;
343 --search-mark-bg: #a2cff5;
346 --search-mark-bg: #a2cff5;
347+*/
344}
348}
book/theme/highlight.css+37−64
@@ -1,83 +1,56 @@
1−/*
1+pre code.hljs {
2− * An increased contrast highlighting scheme loosely based on the
2+ display:block;
3− * "Base16 Atelier Dune Light" theme by Bram de Haan
3+ overflow-x:auto;
4− * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)
4+ padding:1em
5− * Original Base16 color scheme by Chris Kempson
5+}
6− * (https://github.com/chriskempson/base16)
6+code.hljs {
7− */
7+ padding:3px 5px
8−
8+}
9−/* Comment */
9+.hljs {
10+ background:#2f1e2e;
11+ color:#a39e9b
12+}
10.hljs-comment,
13.hljs-comment,
11.hljs-quote {
14.hljs-quote {
12− color: #575757;
15+ color:#8d8687
13}
16}
14−
15−/* Red */
16−.hljs-variable,
17−.hljs-template-variable,
18−.hljs-attribute,
19−.hljs-tag,
20−.hljs-name,
21−.hljs-regexp,
22.hljs-link,
17.hljs-link,
18+.hljs-meta,
23.hljs-name,
19.hljs-name,
20+.hljs-regexp,
21+.hljs-selector-class,
24.hljs-selector-id,
22.hljs-selector-id,
25−.hljs-selector-class {
23+.hljs-tag,
24+.hljs-template-variable,
25+.hljs-variable {
26− color: #d70025;
26+ color:#ef6155
27}
27}
28−
29−/* Orange */
30−.hljs-number,
31−.hljs-meta,
32.hljs-built_in,
28.hljs-built_in,
33−.hljs-builtin-name,
29+.hljs-deletion,
34.hljs-literal,
30.hljs-literal,
31+.hljs-number,
32+.hljs-params,
35−.hljs-type,
33+.hljs-type {
36−.hljs-params {
37− color: #b21e00;
34+ color:#f99b15
38}
35}
39−
36+.hljs-attribute,
40−/* Green */
37+.hljs-section,
41−.hljs-string,
42−.hljs-symbol,
43−.hljs-bullet {
38+.hljs-title {
44− color: #008200;
39+ color:#fec418
45}
40}
46−
41+.hljs-addition,
47−/* Blue */
42+.hljs-bullet,
48−.hljs-title,
43+.hljs-string,
49−.hljs-section {
44+.hljs-symbol {
50− color: #0030f2;
45+ color:#48b685
51}
46}
52−
53−/* Purple */
54.hljs-keyword,
47.hljs-keyword,
55.hljs-selector-tag {
48.hljs-selector-tag {
56− color: #9d00ec;
57−}
58−
59−.hljs {
60− display: block;
61− overflow-x: auto;
62− background: #f6f7f6;
63− color: #000;
49+ color:#815ba4
64− padding: 0.5em;
65}
50}
66−
67.hljs-emphasis {
51.hljs-emphasis {
68− font-style: italic;
52+ font-style:italic
69}
53}
70−
71.hljs-strong {
54.hljs-strong {
72− font-weight: bold;
55+ font-weight:700
73−}
74−
75−.hljs-addition {
76− color: #22863a;
77− background-color: #f0fff4;
78−}
79−
80−.hljs-deletion {
81− color: #b31d28;
82− background-color: #ffeef0;
83}
56}
flake.lock+12−12
@@ -2,11 +2,11 @@
2 "nodes": {
2 "nodes": {
3 "devshell": {
3 "devshell": {
4 "locked": {
4 "locked": {
5− "lastModified": 1625086391,
5+ "lastModified": 1630239564,
6− "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=",
6+ "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
7 "owner": "numtide",
7 "owner": "numtide",
8 "repo": "devshell",
8 "repo": "devshell",
9− "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7",
9+ "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
10 "type": "github"
10 "type": "github"
11 },
11 },
12 "original": {
12 "original": {
@@ -40,11 +40,11 @@
40 "rustOverlay": "rustOverlay"
40 "rustOverlay": "rustOverlay"
41 },
41 },
42 "locked": {
42 "locked": {
43− "lastModified": 1628489367,
43+ "lastModified": 1631254163,
44− "narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=",
44+ "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
45 "owner": "yusdacra",
45 "owner": "yusdacra",
46 "repo": "nix-cargo-integration",
46 "repo": "nix-cargo-integration",
47− "rev": "0dc8383aae5f791a48e34120edb04670b947dc0b",
47+ "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
48 "type": "github"
48 "type": "github"
49 },
49 },
50 "original": {
50 "original": {
@@ -55,11 +55,11 @@
55 },
55 },
56 "nixpkgs": {
56 "nixpkgs": {
57 "locked": {
57 "locked": {
58− "lastModified": 1628465643,
58+ "lastModified": 1631206977,
59− "narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=",
59+ "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
60 "owner": "nixos",
60 "owner": "nixos",
61 "repo": "nixpkgs",
61 "repo": "nixpkgs",
62− "rev": "6ef4f522d63f22b40004319778761040d3197390",
62+ "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
63 "type": "github"
63 "type": "github"
64 },
64 },
65 "original": {
65 "original": {
@@ -79,11 +79,11 @@
79 "rustOverlay": {
79 "rustOverlay": {
80 "flake": false,
80 "flake": false,
81 "locked": {
81 "locked": {
82− "lastModified": 1628475192,
82+ "lastModified": 1631240108,
83− "narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=",
83+ "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
84 "owner": "oxalica",
84 "owner": "oxalica",
85 "repo": "rust-overlay",
85 "repo": "rust-overlay",
86− "rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468",
86+ "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
87 "type": "github"
87 "type": "github"
88 },
88 },
89 "original": {
89 "original": {
flake.nix+1−16
@@ -30,22 +30,7 @@
30 };
30 };
31 # link languages and theme toml files since helix-view expects them
31 # link languages and theme toml files since helix-view expects them
32 helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
32 helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
33− helix-syntax = prev: {
33+ helix-syntax = _prev: {
34− src =
35− let
36− pkgs = common.pkgs;
37− helix = pkgs.fetchgit {
38− url = "https://github.com/helix-editor/helix.git";
39− rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5";
40− fetchSubmodules = true;
41− sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ=";
42− };
43− in
44− pkgs.runCommand prev.src.name { } ''
45− mkdir -p $out
46− ln -s ${prev.src}/* $out
47− ln -sf ${helix}/helix-syntax/languages $out
48− '';
49 preConfigure = "mkdir -p ../runtime/grammars";
34 preConfigure = "mkdir -p ../runtime/grammars";
50 postInstall = "cp -r ../runtime $out/runtime";
35 postInstall = "cp -r ../runtime $out/runtime";
51 };
36 };
helix-core/Cargo.toml+2−2
@@ -23,7 +23,7 @@ unicode-segmentation = "1.8"
23unicode-width = "0.1"
23unicode-width = "0.1"
24unicode-general-category = "0.4"
24unicode-general-category = "0.4"
25# slab = "0.4.2"
25# slab = "0.4.2"
26−tree-sitter = "0.19"
26+tree-sitter = "0.20"
27once_cell = "1.8"
27once_cell = "1.8"
28arc-swap = "1"
28arc-swap = "1"
29regex = "1"
29regex = "1"
@@ -31,7 +31,7 @@ regex = "1"
31serde = { version = "1.0", features = ["derive"] }
31serde = { version = "1.0", features = ["derive"] }
32toml = "0.5"
32toml = "0.5"
33
33
34−similar = "1.3"
34+similar = "2.0"
35
35
36etcetera = "0.3"
36etcetera = "0.3"
37
37
helix-core/src/indent.rs+9−3
@@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos(
316pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
316pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
317 let mut scopes = Vec::new();
317 let mut scopes = Vec::new();
318 if let Some(syntax) = syntax {
318 if let Some(syntax) = syntax {
319− let byte_start = text.char_to_byte(pos);
319+ let pos = text.char_to_byte(pos);
320− let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) {
320+ let mut node = match syntax
321+ .tree()
322+ .root_node()
323+ .descendant_for_byte_range(pos, pos)
324+ {
321 Some(node) => node,
325 Some(node) => node,
322 None => return scopes,
326 None => return scopes,
323 };
327 };
@@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&
325 scopes.push(node.kind());
329 scopes.push(node.kind());
326
330
327 while let Some(parent) = node.parent() {
331 while let Some(parent) = node.parent() {
328− scopes.push(parent.kind())
332+ scopes.push(parent.kind());
333+ node = parent;
329 }
334 }
330 }
335 }
331
336
@@ -449,6 +454,7 @@ where
449 highlight_config: OnceCell::new(),
454 highlight_config: OnceCell::new(),
450 config: None,
455 config: None,
451 //
456 //
457+ injection_regex: None,
452 roots: vec![],
458 roots: vec![],
453 comment_token: None,
459 comment_token: None,
454 auto_format: false,
460 auto_format: false,
helix-core/src/selection.rs+9
@@ -360,6 +360,15 @@ impl Selection {
360 self.normalize()
360 self.normalize()
361 }
361 }
362
362
363+ /// Adds a new range to the selection and makes it the primary range.
364+ pub fn remove(mut self, index: usize) -> Self {
365+ self.ranges.remove(index);
366+ if index < self.primary_index || self.primary_index == self.ranges.len() {
367+ self.primary_index -= 1;
368+ }
369+ self
370+ }
371+
363 /// Map selections over a set of changes. Useful for adjusting the selection position after
372 /// Map selections over a set of changes. Useful for adjusting the selection position after
364 /// applying changes to a document.
373 /// applying changes to a document.
365 pub fn map(self, changes: &ChangeSet) -> Self {
374 pub fn map(self, changes: &ChangeSet) -> Self {
helix-core/src/syntax.rs+83−44
@@ -21,6 +21,15 @@ use std::{
21use once_cell::sync::{Lazy, OnceCell};
21use once_cell::sync::{Lazy, OnceCell};
22use serde::{Deserialize, Serialize};
22use serde::{Deserialize, Serialize};
23
23
24+fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
25+where
26+ D: serde::Deserializer<'de>,
27+{
28+ Option::<String>::deserialize(deserializer)?
29+ .map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom))
30+ .transpose()
31+}
32+
24#[derive(Debug, Serialize, Deserialize)]
33#[derive(Debug, Serialize, Deserialize)]
25pub struct Configuration {
34pub struct Configuration {
26 pub language: Vec<LanguageConfiguration>,
35 pub language: Vec<LanguageConfiguration>,
@@ -42,7 +51,8 @@ pub struct LanguageConfiguration {
42 pub auto_format: bool,
51 pub auto_format: bool,
43
52
44 // content_regex
53 // content_regex
54+ #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
45− // injection_regex
55+ pub injection_regex: Option<Regex>,
46 // first_line_regex
56 // first_line_regex
47 //
57 //
48 #[serde(skip)]
58 #[serde(skip)]
@@ -182,8 +192,12 @@ impl LanguageConfiguration {
182 &highlights_query,
192 &highlights_query,
183 &injections_query,
193 &injections_query,
184 &locals_query,
194 &locals_query,
185− )
195+ );
186− .unwrap(); // TODO: no unwrap
196+
197+ let config = match config {
198+ Ok(config) => config,
199+ Err(err) => panic!("{}", err),
200+ }; // TODO: avoid panic
187 config.configure(scopes);
201 config.configure(scopes);
188 Some(Arc::new(config))
202 Some(Arc::new(config))
189 }
203 }
@@ -277,6 +291,30 @@ impl Loader {
277 .cloned()
291 .cloned()
278 }
292 }
279
293
294+ pub fn language_configuration_for_injection_string(
295+ &self,
296+ string: &str,
297+ ) -> Option<Arc<LanguageConfiguration>> {
298+ let mut best_match_length = 0;
299+ let mut best_match_position = None;
300+ for (i, configuration) in self.language_configs.iter().enumerate() {
301+ if let Some(injection_regex) = &configuration.injection_regex {
302+ if let Some(mat) = injection_regex.find(string) {
303+ let length = mat.end() - mat.start();
304+ if length > best_match_length {
305+ best_match_position = Some(i);
306+ best_match_length = length;
307+ }
308+ }
309+ }
310+ }
311+
312+ if let Some(i) = best_match_position {
313+ let configuration = &self.language_configs[i];
314+ return Some(configuration.clone());
315+ }
316+ None
317+ }
280 pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
318 pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
281 self.language_configs.iter()
319 self.language_configs.iter()
282 }
320 }
@@ -314,16 +352,6 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
314 Cow::from(source.slice(start_char..end_char))
352 Cow::from(source.slice(start_char..end_char))
315}
353}
316
354
317−fn node_to_bytes<'a>(node: Node, source: RopeSlice<'a>) -> Cow<'a, [u8]> {
318− let start_char = source.byte_to_char(node.start_byte());
319− let end_char = source.byte_to_char(node.end_byte());
320− let fragment = source.slice(start_char..end_char);
321− match fragment.as_str() {
322− Some(fragment) => Cow::Borrowed(fragment.as_bytes()),
323− None => Cow::Owned(String::from(fragment).into_bytes()),
324− }
325−}
326−
327impl Syntax {
355impl Syntax {
328 // buffer, grammar, config, grammars, sync_timeout?
356 // buffer, grammar, config, grammars, sync_timeout?
329 pub fn new(
357 pub fn new(
@@ -416,16 +444,11 @@ impl Syntax {
416 let config_ref =
444 let config_ref =
417 unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
445 unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
418
446
419− // TODO: if reusing cursors this might need resetting
447+ // if reusing cursors & no range this resets to whole range
420− if let Some(range) = &range {
421− cursor_ref.set_byte_range(range.start, range.end);
448+ cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
422− }
423
449
424 let captures = cursor_ref
450 let captures = cursor_ref
425− .captures(query_ref, tree_ref.root_node(), move |n: Node| {
451+ .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
426− // &source[n.byte_range()]
427− node_to_bytes(n, source)
428− })
429 .peekable();
452 .peekable();
430
453
431 // manually craft the root layer based on the existing tree
454 // manually craft the root layer based on the existing tree
@@ -539,10 +562,7 @@ impl LanguageLayer {
539 // let mut injections_by_pattern_index =
562 // let mut injections_by_pattern_index =
540 // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
563 // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
541 // let matches =
564 // let matches =
542− // cursor.matches(combined_injections_query, tree.root_node(), |n: Node| {
565+ // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
543− // // &source[n.byte_range()]
544− // node_to_bytes(n, source)
545− // });
546 // for mat in matches {
566 // for mat in matches {
547 // let entry = &mut injections_by_pattern_index[mat.pattern_index];
567 // let entry = &mut injections_by_pattern_index[mat.pattern_index];
548 // let (language_name, content_node, include_children) =
568 // let (language_name, content_node, include_children) =
@@ -754,7 +774,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
754use std::{iter, mem, ops, str, usize};
774use std::{iter, mem, ops, str, usize};
755use tree_sitter::{
775use tree_sitter::{
756 Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
776 Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
757− QueryMatch, Range, Tree,
777+ QueryMatch, Range, TextProvider, Tree,
758};
778};
759
779
760const CANCELLATION_CHECK_INTERVAL: usize = 100;
780const CANCELLATION_CHECK_INTERVAL: usize = 100;
@@ -814,7 +834,7 @@ struct LocalScope<'a> {
814}
834}
815
835
816#[derive(Debug)]
836#[derive(Debug)]
817−struct HighlightIter<'a, 'tree: 'a, F>
837+struct HighlightIter<'a, F>
818where
838where
819 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
839 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
820{
840{
@@ -822,16 +842,41 @@ where
822 byte_offset: usize,
842 byte_offset: usize,
823 injection_callback: F,
843 injection_callback: F,
824 cancellation_flag: Option<&'a AtomicUsize>,
844 cancellation_flag: Option<&'a AtomicUsize>,
825− layers: Vec<HighlightIterLayer<'a, 'tree>>,
845+ layers: Vec<HighlightIterLayer<'a>>,
826 iter_count: usize,
846 iter_count: usize,
827 next_event: Option<HighlightEvent>,
847 next_event: Option<HighlightEvent>,
828 last_highlight_range: Option<(usize, usize, usize)>,
848 last_highlight_range: Option<(usize, usize, usize)>,
829}
849}
830
850
851+// Adapter to convert rope chunks to bytes
852+struct ChunksBytes<'a> {
853+ chunks: ropey::iter::Chunks<'a>,
854+}
855+impl<'a> Iterator for ChunksBytes<'a> {
856+ type Item = &'a [u8];
857+ fn next(&mut self) -> Option<Self::Item> {
858+ self.chunks.next().map(str::as_bytes)
859+ }
860+}
861+
862+struct RopeProvider<'a>(RopeSlice<'a>);
863+impl<'a> TextProvider<'a> for RopeProvider<'a> {
864+ type I = ChunksBytes<'a>;
865+
866+ fn text(&mut self, node: Node) -> Self::I {
867+ let start_char = self.0.byte_to_char(node.start_byte());
868+ let end_char = self.0.byte_to_char(node.end_byte());
869+ let fragment = self.0.slice(start_char..end_char);
870+ ChunksBytes {
871+ chunks: fragment.chunks(),
872+ }
873+ }
874+}
875+
831−struct HighlightIterLayer<'a, 'tree: 'a> {
876+struct HighlightIterLayer<'a> {
832 _tree: Option<Tree>,
877 _tree: Option<Tree>,
833 cursor: QueryCursor,
878 cursor: QueryCursor,
834− captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
879+ captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>,
835 config: &'a HighlightConfiguration,
880 config: &'a HighlightConfiguration,
836 highlight_end_stack: Vec<usize>,
881 highlight_end_stack: Vec<usize>,
837 scope_stack: Vec<LocalScope<'a>>,
882 scope_stack: Vec<LocalScope<'a>>,
@@ -839,7 +884,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
839 depth: usize,
884 depth: usize,
840}
885}
841
886
842−impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
887+impl<'a> fmt::Debug for HighlightIterLayer<'a> {
843 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
888 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
844 f.debug_struct("HighlightIterLayer").finish()
889 f.debug_struct("HighlightIterLayer").finish()
845 }
890 }
@@ -1010,7 +1055,7 @@ impl HighlightConfiguration {
1010 }
1055 }
1011}
1056}
1012
1057
1013−impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
1058+impl<'a> HighlightIterLayer<'a> {
1014 /// Create a new 'layer' of highlighting for this document.
1059 /// Create a new 'layer' of highlighting for this document.
1015 ///
1060 ///
1016 /// In the even that the new layer contains "combined injections" (injections where multiple
1061 /// In the even that the new layer contains "combined injections" (injections where multiple
@@ -1067,10 +1112,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
1067 let matches = cursor.matches(
1112 let matches = cursor.matches(
1068 combined_injections_query,
1113 combined_injections_query,
1069 tree.root_node(),
1114 tree.root_node(),
1070− |n: Node| {
1115+ RopeProvider(source),
1071− // &source[n.byte_range()]
1072− node_to_bytes(n, source)
1073− },
1074 );
1116 );
1075 for mat in matches {
1117 for mat in matches {
1076 let entry = &mut injections_by_pattern_index[mat.pattern_index];
1118 let entry = &mut injections_by_pattern_index[mat.pattern_index];
@@ -1117,10 +1159,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
1117 let cursor_ref =
1159 let cursor_ref =
1118 unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
1160 unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
1119 let captures = cursor_ref
1161 let captures = cursor_ref
1120− .captures(&config.query, tree_ref.root_node(), move |n: Node| {
1162+ .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
1121− // &source[n.byte_range()]
1122− node_to_bytes(n, source)
1123− })
1124 .peekable();
1163 .peekable();
1125
1164
1126 result.push(HighlightIterLayer {
1165 result.push(HighlightIterLayer {
@@ -1274,7 +1313,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
1274 }
1313 }
1275}
1314}
1276
1315
1277−impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
1316+impl<'a, F> HighlightIter<'a, F>
1278where
1317where
1279 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
1318 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
1280{
1319{
@@ -1325,7 +1364,7 @@ where
1325 }
1364 }
1326 }
1365 }
1327
1366
1328− fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
1367+ fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
1329 if let Some(sort_key) = layer.sort_key() {
1368 if let Some(sort_key) = layer.sort_key() {
1330 let mut i = 1;
1369 let mut i = 1;
1331 while i < self.layers.len() {
1370 while i < self.layers.len() {
@@ -1344,7 +1383,7 @@ where
1344 }
1383 }
1345}
1384}
1346
1385
1347−impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
1386+impl<'a, F> Iterator for HighlightIter<'a, F>
1348where
1387where
1349 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
1388 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
1350{
1389{
@@ -1608,7 +1647,7 @@ where
1608fn injection_for_match<'a>(
1647fn injection_for_match<'a>(
1609 config: &HighlightConfiguration,
1648 config: &HighlightConfiguration,
1610 query: &'a Query,
1649 query: &'a Query,
1611− query_match: &QueryMatch<'a>,
1650+ query_match: &QueryMatch<'a, 'a>,
1612 source: RopeSlice<'a>,
1651 source: RopeSlice<'a>,
1613) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
1652) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
1614 let content_capture_index = config.injection_content_capture_index;
1653 let content_capture_index = config.injection_content_capture_index;
helix-lsp/Cargo.toml+1−1
@@ -23,5 +23,5 @@ lsp-types = { version = "0.89", 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.10", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
26+tokio = { version = "1.11", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27tokio-stream = "0.1"
27tokio-stream = "0.1"
helix-lsp/src/client.rs+56−44
@@ -3,17 +3,23 @@ use crate::{
3 Call, Error, OffsetEncoding, Result,
3 Call, Error, OffsetEncoding, Result,
4};
4};
5
5
6−use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope};
6+use helix_core::{find_root, ChangeSet, Rope};
7use jsonrpc_core as jsonrpc;
7use jsonrpc_core as jsonrpc;
8use lsp_types as lsp;
8use lsp_types as lsp;
9use serde_json::Value;
9use serde_json::Value;
10use std::future::Future;
10use std::future::Future;
11use std::process::Stdio;
11use std::process::Stdio;
12+use std::sync::{
12−use std::sync::atomic::{AtomicU64, Ordering};
13+ atomic::{AtomicU64, Ordering},
14+ Arc,
15+};
13use tokio::{
16use tokio::{
14 io::{BufReader, BufWriter},
17 io::{BufReader, BufWriter},
15 process::{Child, Command},
18 process::{Child, Command},
19+ sync::{
16− sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
20+ mpsc::{channel, UnboundedReceiver, UnboundedSender},
21+ Notify, OnceCell,
22+ },
17};
23};
18
24
19#[derive(Debug)]
25#[derive(Debug)]
@@ -22,18 +28,19 @@ pub struct Client {
22 _process: Child,
28 _process: Child,
23 server_tx: UnboundedSender<Payload>,
29 server_tx: UnboundedSender<Payload>,
24 request_counter: AtomicU64,
30 request_counter: AtomicU64,
25− capabilities: Option<lsp::ServerCapabilities>,
31+ pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
26 offset_encoding: OffsetEncoding,
32 offset_encoding: OffsetEncoding,
27 config: Option<Value>,
33 config: Option<Value>,
28}
34}
29
35
30impl Client {
36impl Client {
37+ #[allow(clippy::type_complexity)]
31 pub fn start(
38 pub fn start(
32 cmd: &str,
39 cmd: &str,
33 args: &[String],
40 args: &[String],
34 config: Option<Value>,
41 config: Option<Value>,
35 id: usize,
42 id: usize,
36− ) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> {
43+ ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
37 let process = Command::new(cmd)
44 let process = Command::new(cmd)
38 .args(args)
45 .args(args)
39 .stdin(Stdio::piped())
46 .stdin(Stdio::piped())
@@ -50,22 +57,20 @@ impl Client {
50 let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
57 let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
51 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
58 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
52
59
60+ let (server_rx, server_tx, initialize_notify) =
53− let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id);
61+ Transport::start(reader, writer, stderr, id);
54
62
55 let client = Self {
63 let client = Self {
56 id,
64 id,
57 _process: process,
65 _process: process,
58 server_tx,
66 server_tx,
59 request_counter: AtomicU64::new(0),
67 request_counter: AtomicU64::new(0),
60− capabilities: None,
68+ capabilities: OnceCell::new(),
61 offset_encoding: OffsetEncoding::Utf8,
69 offset_encoding: OffsetEncoding::Utf8,
62 config,
70 config,
63 };
71 };
64
72
65− // TODO: async client.initialize()
66− // maybe use an arc<atomic> flag
67−
68− Ok((client, server_rx))
73+ Ok((client, server_rx, initialize_notify))
69 }
74 }
70
75
71 pub fn id(&self) -> usize {
76 pub fn id(&self) -> usize {
@@ -88,9 +93,13 @@ impl Client {
88 }
93 }
89 }
94 }
90
95
96+ pub fn is_initialized(&self) -> bool {
97+ self.capabilities.get().is_some()
98+ }
99+
91 pub fn capabilities(&self) -> &lsp::ServerCapabilities {
100 pub fn capabilities(&self) -> &lsp::ServerCapabilities {
92 self.capabilities
101 self.capabilities
93− .as_ref()
102+ .get()
94 .expect("language server not yet initialized!")
103 .expect("language server not yet initialized!")
95 }
104 }
96
105
@@ -143,7 +152,8 @@ impl Client {
143 })
152 })
144 .map_err(|e| Error::Other(e.into()))?;
153 .map_err(|e| Error::Other(e.into()))?;
145
154
155+ // TODO: specifiable timeout, delay other calls until initialize success
146− timeout(Duration::from_secs(2), rx.recv())
156+ timeout(Duration::from_secs(20), rx.recv())
147 .await
157 .await
148 .map_err(|_| Error::Timeout)? // return Timeout
158 .map_err(|_| Error::Timeout)? // return Timeout
149 .ok_or(Error::StreamClosed)?
159 .ok_or(Error::StreamClosed)?
@@ -151,7 +161,7 @@ impl Client {
151 }
161 }
152
162
153 /// Send a RPC notification to the language server.
163 /// Send a RPC notification to the language server.
154− fn notify<R: lsp::notification::Notification>(
164+ pub fn notify<R: lsp::notification::Notification>(
155 &self,
165 &self,
156 params: R::Params,
166 params: R::Params,
157 ) -> impl Future<Output = Result<()>>
167 ) -> impl Future<Output = Result<()>>
@@ -213,7 +223,7 @@ impl Client {
213 // General messages
223 // General messages
214 // -------------------------------------------------------------------------------------------
224 // -------------------------------------------------------------------------------------------
215
225
216− pub(crate) async fn initialize(&mut self) -> Result<()> {
226+ pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
217 // TODO: delay any requests that are triggered prior to initialize
227 // TODO: delay any requests that are triggered prior to initialize
218 let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
228 let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
219
229
@@ -281,14 +291,7 @@ impl Client {
281 locale: None, // TODO
291 locale: None, // TODO
282 };
292 };
283
293
284− let response = self.request::<lsp::request::Initialize>(params).await?;
294+ self.request::<lsp::request::Initialize>(params).await
285− self.capabilities = Some(response.capabilities);
286−
287− // next up, notify<initialized>
288− self.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
289− .await?;
290−
291− Ok(())
292 }
295 }
293
296
294 pub async fn shutdown(&self) -> Result<()> {
297 pub async fn shutdown(&self) -> Result<()> {
@@ -356,7 +359,6 @@ impl Client {
356 //
359 //
357 // Calculation is therefore a bunch trickier.
360 // Calculation is therefore a bunch trickier.
358
361
359− // TODO: stolen from syntax.rs, share
360 use helix_core::RopeSlice;
362 use helix_core::RopeSlice;
361 fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
363 fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
362 let lsp::Position {
364 let lsp::Position {
@@ -366,7 +368,12 @@ impl Client {
366
368
367 let mut chars = text.chars().peekable();
369 let mut chars = text.chars().peekable();
368 while let Some(ch) = chars.next() {
370 while let Some(ch) = chars.next() {
371+ // LSP only considers \n, \r or \r\n as line endings
372+ if ch == '\n' || ch == '\r' {
373+ // consume a \r\n
369− if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
374+ if ch == '\r' && chars.peek() == Some(&'\n') {
375+ chars.next();
376+ }
370 line += 1;
377 line += 1;
371 character = 0;
378 character = 0;
372 } else {
379 } else {
@@ -441,7 +448,7 @@ impl Client {
441 ) -> Option<impl Future<Output = Result<()>>> {
448 ) -> Option<impl Future<Output = Result<()>>> {
442 // figure out what kind of sync the server supports
449 // figure out what kind of sync the server supports
443
450
444− let capabilities = self.capabilities.as_ref().unwrap();
451+ let capabilities = self.capabilities.get().unwrap();
445
452
446 let sync_capabilities = match capabilities.text_document_sync {
453 let sync_capabilities = match capabilities.text_document_sync {
447 Some(lsp::TextDocumentSyncCapability::Kind(kind))
454 Some(lsp::TextDocumentSyncCapability::Kind(kind))
@@ -459,7 +466,7 @@ impl Client {
459 // range = None -> whole document
466 // range = None -> whole document
460 range: None, //Some(Range)
467 range: None, //Some(Range)
461 range_length: None, // u64 apparently deprecated
468 range_length: None, // u64 apparently deprecated
462− text: "".to_string(),
469+ text: new_text.to_string(),
463 }]
470 }]
464 }
471 }
465 lsp::TextDocumentSyncKind::Incremental => {
472 lsp::TextDocumentSyncKind::Incremental => {
@@ -487,12 +494,12 @@ impl Client {
487
494
488 // will_save / will_save_wait_until
495 // will_save / will_save_wait_until
489
496
490− pub async fn text_document_did_save(
497+ pub fn text_document_did_save(
491 &self,
498 &self,
492 text_document: lsp::TextDocumentIdentifier,
499 text_document: lsp::TextDocumentIdentifier,
493 text: &Rope,
500 text: &Rope,
494− ) -> Result<()> {
501+ ) -> Option<impl Future<Output = Result<()>>> {
495− let capabilities = self.capabilities.as_ref().unwrap();
502+ let capabilities = self.capabilities.get().unwrap();
496
503
497 let include_text = match &capabilities.text_document_sync {
504 let include_text = match &capabilities.text_document_sync {
498 Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
505 Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
@@ -504,17 +511,18 @@ impl Client {
504 include_text,
511 include_text,
505 }) => include_text.unwrap_or(false),
512 }) => include_text.unwrap_or(false),
506 // Supported(false)
513 // Supported(false)
507− _ => return Ok(()),
514+ _ => return None,
508 },
515 },
509 // unsupported
516 // unsupported
510− _ => return Ok(()),
517+ _ => return None,
511 };
518 };
512
519
513− self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
520+ Some(self.notify::<lsp::notification::DidSaveTextDocument>(
521+ lsp::DidSaveTextDocumentParams {
514− text_document,
522+ text_document,
515− text: include_text.then(|| text.into()),
523+ text: include_text.then(|| text.into()),
516− })
524+ },
517− .await
525+ ))
518 }
526 }
519
527
520 pub fn completion(
528 pub fn completion(
@@ -580,19 +588,19 @@ impl Client {
580
588
581 // formatting
589 // formatting
582
590
583− pub async fn text_document_formatting(
591+ pub fn text_document_formatting(
584 &self,
592 &self,
585 text_document: lsp::TextDocumentIdentifier,
593 text_document: lsp::TextDocumentIdentifier,
586 options: lsp::FormattingOptions,
594 options: lsp::FormattingOptions,
587 work_done_token: Option<lsp::ProgressToken>,
595 work_done_token: Option<lsp::ProgressToken>,
588− ) -> anyhow::Result<Vec<lsp::TextEdit>> {
596+ ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
589− let capabilities = self.capabilities.as_ref().unwrap();
597+ let capabilities = self.capabilities.get().unwrap();
590
598
591 // check if we're able to format
599 // check if we're able to format
592 match capabilities.document_formatting_provider {
600 match capabilities.document_formatting_provider {
593 Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
601 Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
594 // None | Some(false)
602 // None | Some(false)
595− _ => return Ok(Vec::new()),
603+ _ => return None,
596 };
604 };
597 // TODO: return err::unavailable so we can fall back to tree sitter formatting
605 // TODO: return err::unavailable so we can fall back to tree sitter formatting
598
606
@@ -602,9 +610,13 @@ impl Client {
602 work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
610 work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
603 };
611 };
604
612
605− let response = self.request::<lsp::request::Formatting>(params).await?;
613+ let request = self.call::<lsp::request::Formatting>(params);
606
614
615+ Some(async move {
616+ let json = request.await?;
617+ let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
607− Ok(response.unwrap_or_default())
618+ Ok(response.unwrap_or_default())
619+ })
608 }
620 }
609
621
610 pub async fn text_document_range_formatting(
622 pub async fn text_document_range_formatting(
@@ -614,7 +626,7 @@ impl Client {
614 options: lsp::FormattingOptions,
626 options: lsp::FormattingOptions,
615 work_done_token: Option<lsp::ProgressToken>,
627 work_done_token: Option<lsp::ProgressToken>,
616 ) -> anyhow::Result<Vec<lsp::TextEdit>> {
628 ) -> anyhow::Result<Vec<lsp::TextEdit>> {
617− let capabilities = self.capabilities.as_ref().unwrap();
629+ let capabilities = self.capabilities.get().unwrap();
618
630
619 // check if we're able to format
631 // check if we're able to format
620 match capabilities.document_range_formatting_provider {
632 match capabilities.document_range_formatting_provider {
helix-lsp/src/lib.rs+57−53
@@ -226,6 +226,8 @@ impl MethodCall {
226
226
227#[derive(Debug, PartialEq, Clone)]
227#[derive(Debug, PartialEq, Clone)]
228pub enum Notification {
228pub enum Notification {
229+ // we inject this notification to signal the LSP is ready
230+ Initialized,
229 PublishDiagnostics(lsp::PublishDiagnosticsParams),
231 PublishDiagnostics(lsp::PublishDiagnosticsParams),
230 ShowMessage(lsp::ShowMessageParams),
232 ShowMessage(lsp::ShowMessageParams),
231 LogMessage(lsp::LogMessageParams),
233 LogMessage(lsp::LogMessageParams),
@@ -237,6 +239,7 @@ impl Notification {
237 use lsp::notification::Notification as _;
239 use lsp::notification::Notification as _;
238
240
239 let notification = match method {
241 let notification = match method {
242+ lsp::notification::Initialized::METHOD => Self::Initialized,
240 lsp::notification::PublishDiagnostics::METHOD => {
243 lsp::notification::PublishDiagnostics::METHOD => {
241 let params: lsp::PublishDiagnosticsParams = params
244 let params: lsp::PublishDiagnosticsParams = params
242 .parse()
245 .parse()
@@ -294,7 +297,7 @@ impl Registry {
294 }
297 }
295 }
298 }
296
299
297− pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
300+ pub fn get_by_id(&self, id: usize) -> Option<&Client> {
298 self.inner
301 self.inner
299 .values()
302 .values()
300 .find(|(client_id, _)| client_id == &id)
303 .find(|(client_id, _)| client_id == &id)
@@ -302,33 +305,60 @@ impl Registry {
302 }
305 }
303
306
304 pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
307 pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
305− if let Some(config) = &language_config.language_server {
308+ let config = match &language_config.language_server {
306− // avoid borrow issues
309+ Some(config) => config,
307− let inner = &mut self.inner;
310+ None => return Err(Error::LspNotDefined),
308− let s_incoming = &mut self.incoming;
311+ };
309−
312+
310− match inner.entry(language_config.scope.clone()) {
313+ match self.inner.entry(language_config.scope.clone()) {
311− Entry::Occupied(entry) => Ok(entry.get().1.clone()),
314+ Entry::Occupied(entry) => Ok(entry.get().1.clone()),
312− Entry::Vacant(entry) => {
315+ Entry::Vacant(entry) => {
313− // initialize a new client
316+ // initialize a new client
314− let id = self.counter.fetch_add(1, Ordering::Relaxed);
317+ let id = self.counter.fetch_add(1, Ordering::Relaxed);
315− let (mut client, incoming) = Client::start(
318+ let (client, incoming, initialize_notify) = Client::start(
316− &config.command,
319+ &config.command,
317− &config.args,
320+ &config.args,
318− serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(),
321+ serde_json::from_str(language_config.config.as_deref().unwrap_or(""))
322+ .map_err(|e| {
323+ log::error!(
324+ "LSP Config, {}, in `languages.toml` for `{}`",
325+ e,
326+ language_config.scope()
327+ )
328+ })
329+ .ok(),
319− id,
330+ id,
320− )?;
331+ )?;
321− // TODO: run this async without blocking
322− futures_executor::block_on(client.initialize())?;
323− s_incoming.push(UnboundedReceiverStream::new(incoming));
332+ self.incoming.push(UnboundedReceiverStream::new(incoming));
324− let client = Arc::new(client);
333+ let client = Arc::new(client);
325−
334+
335+ // Initialize the client asynchronously
336+ let _client = client.clone();
337+ tokio::spawn(async move {
338+ use futures_util::TryFutureExt;
339+ let value = _client
340+ .capabilities
341+ .get_or_try_init(|| {
342+ _client
343+ .initialize()
344+ .map_ok(|response| response.capabilities)
345+ })
346+ .await;
347+
348+ value.expect("failed to initialize capabilities");
349+
350+ // next up, notify<initialized>
351+ _client
352+ .notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
353+ .await
354+ .unwrap();
355+
356+ initialize_notify.notify_one();
357+ });
358+
326− entry.insert((id, client.clone()));
359+ entry.insert((id, client.clone()));
327− Ok(client)
360+ Ok(client)
328− }
329 }
361 }
330− } else {
331− Err(Error::LspNotDefined)
332 }
362 }
333 }
363 }
334
364
@@ -415,32 +445,6 @@ impl LspProgressMap {
415 }
445 }
416}
446}
417
447
418−// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>>
419−// spawn one server per language type, need to spawn one per workspace if server doesn't support
420−// workspaces
421−//
422−// could also be a client per root dir
423−//
424−// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily
425−// accessible during edit/save callbacks
426−//
427−// the event loop needs to process all incoming streams, maybe we can just have that be a separate
428−// task that's continually running and store the state on the client, then use read lock to
429−// retrieve data during render
430−// -> PROBLEM: how do you trigger an update on the editor side when data updates?
431−//
432−// -> The data updates should pull all events until we run out so we don't frequently re-render
433−//
434−//
435−// v2:
436−//
437−// there should be a registry of lsp clients, one per language type (or workspace).
438−// the clients should lazy init on first access
439−// the client.initialize() should be called async and we buffer any requests until that completes
440−// there needs to be a way to process incoming lsp messages from all clients.
441−// -> notifications need to be dispatched to wherever
442−// -> requests need to generate a reply and travel back to the same lsp!
443−
444#[cfg(test)]
448#[cfg(test)]
445mod tests {
449mod tests {
446 use super::{lsp, util::*, OffsetEncoding};
450 use super::{lsp, util::*, OffsetEncoding};
helix-lsp/src/transport.rs+115−22
@@ -1,7 +1,7 @@
1−use crate::Result;
1+use crate::{Error, Result};
2use anyhow::Context;
2use anyhow::Context;
3use jsonrpc_core as jsonrpc;
3use jsonrpc_core as jsonrpc;
4−use log::{debug, error, info, warn};
4+use log::{error, info};
5use serde::{Deserialize, Serialize};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
6use serde_json::Value;
7use std::collections::HashMap;
7use std::collections::HashMap;
@@ -11,7 +11,7 @@ use tokio::{
11 process::{ChildStderr, ChildStdin, ChildStdout},
11 process::{ChildStderr, ChildStdin, ChildStdout},
12 sync::{
12 sync::{
13 mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
13 mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
14− Mutex,
14+ Mutex, Notify,
15 },
15 },
16};
16};
17
17
@@ -51,9 +51,11 @@ impl Transport {
51 ) -> (
51 ) -> (
52 UnboundedReceiver<(usize, jsonrpc::Call)>,
52 UnboundedReceiver<(usize, jsonrpc::Call)>,
53 UnboundedSender<Payload>,
53 UnboundedSender<Payload>,
54+ Arc<Notify>,
54 ) {
55 ) {
55 let (client_tx, rx) = unbounded_channel();
56 let (client_tx, rx) = unbounded_channel();
56 let (tx, client_rx) = unbounded_channel();
57 let (tx, client_rx) = unbounded_channel();
58+ let notify = Arc::new(Notify::new());
57
59
58 let transport = Self {
60 let transport = Self {
59 id,
61 id,
@@ -62,11 +64,21 @@ impl Transport {
62
64
63 let transport = Arc::new(transport);
65 let transport = Arc::new(transport);
64
66
65− tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx));
67+ tokio::spawn(Self::recv(
68+ transport.clone(),
69+ server_stdout,
70+ client_tx.clone(),
71+ ));
66 tokio::spawn(Self::err(transport.clone(), server_stderr));
72 tokio::spawn(Self::err(transport.clone(), server_stderr));
67− tokio::spawn(Self::send(transport, server_stdin, client_rx));
73+ tokio::spawn(Self::send(
68−
74+ transport,
75+ server_stdin,
76+ client_tx,
77+ client_rx,
78+ notify.clone(),
79+ ));
80+
69− (rx, tx)
81+ (rx, tx, notify)
70 }
82 }
71
83
72 async fn recv_server_message(
84 async fn recv_server_message(
@@ -76,14 +88,18 @@ impl Transport {
76 let mut content_length = None;
88 let mut content_length = None;
77 loop {
89 loop {
78 buffer.truncate(0);
90 buffer.truncate(0);
79− reader.read_line(buffer).await?;
91+ if reader.read_line(buffer).await? == 0 {
92+ return Err(Error::StreamClosed);
93+ };
94+
80− let header = buffer.trim();
95+ // debug!("<- header {:?}", buffer);
81
96
82− if header.is_empty() {
97+ if buffer == "\r\n" {
98+ // look for an empty CRLF line
83 break;
99 break;
84 }
100 }
85
101
86− debug!("<- header {}", header);
102+ let header = buffer.trim();
87
103
88 let parts = header.split_once(": ");
104 let parts = header.split_once(": ");
89
105
@@ -96,7 +112,8 @@ impl Transport {
96 // Workaround: Some non-conformant language servers will output logging and other garbage
112 // Workaround: Some non-conformant language servers will output logging and other garbage
97 // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
113 // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
98 // the server. Skip such lines and log a warning.
114 // the server. Skip such lines and log a warning.
115+
99− warn!("Failed to parse header: {:?}", header);
116+ // warn!("Failed to parse header: {:?}", header);
100 }
117 }
101 }
118 }
102 }
119 }
@@ -121,8 +138,10 @@ impl Transport {
121 buffer: &mut String,
138 buffer: &mut String,
122 ) -> Result<()> {
139 ) -> Result<()> {
123 buffer.truncate(0);
140 buffer.truncate(0);
124− err.read_line(buffer).await?;
141+ if err.read_line(buffer).await? == 0 {
142+ return Err(Error::StreamClosed);
143+ };
125− error!("err <- {}", buffer);
144+ error!("err <- {:?}", buffer);
126
145
127 Ok(())
146 Ok(())
128 }
147 }
@@ -255,16 +274,90 @@ impl Transport {
255 async fn send(
274 async fn send(
256 transport: Arc<Self>,
275 transport: Arc<Self>,
257 mut server_stdin: BufWriter<ChildStdin>,
276 mut server_stdin: BufWriter<ChildStdin>,
277+ client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
258 mut client_rx: UnboundedReceiver<Payload>,
278 mut client_rx: UnboundedReceiver<Payload>,
279+ initialize_notify: Arc<Notify>,
259 ) {
280 ) {
281+ let mut pending_messages: Vec<Payload> = Vec::new();
282+ let mut is_pending = true;
283+
284+ // Determine if a message is allowed to be sent early
285+ fn is_initialize(payload: &Payload) -> bool {
286+ use lsp_types::{
287+ notification::{Initialized, Notification},
288+ request::{Initialize, Request},
289+ };
290+ match payload {
291+ Payload::Request {
292+ value: jsonrpc::MethodCall { method, .. },
293+ ..
294+ } if method == Initialize::METHOD => true,
295+ Payload::Notification(jsonrpc::Notification { method, .. })
296+ if method == Initialized::METHOD =>
297+ {
298+ true
299+ }
300+ _ => false,
301+ }
302+ }
303+
304+ // TODO: events that use capabilities need to do the right thing
305+
306+ loop {
307+ tokio::select! {
308+ biased;
309+ _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe
310+ // server successfully initialized
311+ is_pending = false;
312+
313+ use lsp_types::notification::Notification;
314+ // Hack: inject an initialized notification so we trigger code that needs to happen after init
315+ let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification {
316+ jsonrpc: None,
317+
318+ method: lsp_types::notification::Initialized::METHOD.to_string(),
319+ params: jsonrpc::Params::None,
320+ }));
321+ match transport.process_server_message(&client_tx, notification).await {
322+ Ok(_) => {}
323+ Err(err) => {
324+ error!("err: <- {:?}", err);
325+ }
326+ }
327+
328+ // drain the pending queue and send payloads to server
329+ for msg in pending_messages.drain(..) {
330+ log::info!("Draining pending message {:?}", msg);
331+ match transport.send_payload_to_server(&mut server_stdin, msg).await {
332+ Ok(_) => {}
333+ Err(err) => {
334+ error!("err: <- {:?}", err);
335+ }
336+ }
337+ }
338+ }
260− while let Some(msg) = client_rx.recv().await {
339+ msg = client_rx.recv() => {
261− match transport
340+ if let Some(msg) = msg {
341+ if is_pending && !is_initialize(&msg) {
342+ // ignore notifications
343+ if let Payload::Notification(_) = msg {
344+ continue;
345+ }
346+
347+ log::info!("Language server not initialized, delaying request");
348+ pending_messages.push(msg);
349+ } else {
262− .send_payload_to_server(&mut server_stdin, msg)
350+ match transport.send_payload_to_server(&mut server_stdin, msg).await {
263− .await
264− {
265− Ok(_) => {}
351+ Ok(_) => {}
266− Err(err) => {
352+ Err(err) => {
267− error!("err: <- {:?}", err);
353+ error!("err: <- {:?}", err);
354+ }
355+ }
356+ }
357+ } else {
358+ // channel closed
359+ break;
360+ }
268 }
361 }
269 }
362 }
270 }
363 }
helix-syntax/Cargo.toml+1−1
@@ -11,7 +11,7 @@ homepage = "https://helix-editor.com"
11include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
11include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
12
12
13[dependencies]
13[dependencies]
14−tree-sitter = "0.19"
14+tree-sitter = "0.20"
15libloading = "0.7"
15libloading = "0.7"
16anyhow = "1"
16anyhow = "1"
17
17
helix-syntax/build.rs+1−2
@@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) {
158 .is_none()
158 .is_none()
159 {
159 {
160 eprintln!(
160 eprintln!(
161− "The directory {} is empty, did you use 'git clone --recursive'?",
161+ "The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?",
162 dir
162 dir
163 );
163 );
164− eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
165 std::process::exit(1);
164 std::process::exit(1);
166 }
165 }
167
166
helix-syntax/languages/tree-sitter-julia+1−1

Submodule changed.

helix-syntax/languages/tree-sitter-ledger+1−1

Submodule changed.

helix-syntax/languages/tree-sitter-sveltenew file+1

Submodule changed.

helix-term/Cargo.toml+4
@@ -56,5 +56,9 @@ toml = "0.5"
56serde_json = "1.0"
56serde_json = "1.0"
57serde = { version = "1.0", features = ["derive"] }
57serde = { version = "1.0", features = ["derive"] }
58
58
59+# ripgrep for global search
60+grep-regex = "0.1.9"
61+grep-searcher = "0.1.8"
62+
59[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
63[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
60signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
64signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
helix-term/src/application.rs+53−54
@@ -434,16 +434,42 @@ impl Application {
434 };
434 };
435
435
436 match notification {
436 match notification {
437− Notification::PublishDiagnostics(params) => {
437+ Notification::Initialized => {
438− let path = Some(params.uri.to_file_path().unwrap());
438+ let language_server =
439+ match self.editor.language_servers.get_by_id(server_id) {
440+ Some(language_server) => language_server,
441+ None => {
442+ warn!("can't find language server with id `{}`", server_id);
443+ return;
444+ }
445+ };
439
446
440− let doc = self
441− .editor
442− .documents
447+ let docs = self.editor.documents().filter(|doc| {
443− .iter_mut()
448+ doc.language_server().map(|server| server.id()) == Some(server_id)
444− .find(|(_, doc)| doc.path() == path.as_ref());
449+ });
445
450
451+ // trigger textDocument/didOpen for docs that are already open
452+ for doc in docs {
453+ // TODO: extract and share with editor.open
454+ let language_id = doc
455+ .language()
456+ .and_then(|s| s.split('.').last()) // source.rust
457+ .map(ToOwned::to_owned)
458+ .unwrap_or_default();
459+
460+ tokio::spawn(language_server.text_document_did_open(
461+ doc.url().unwrap(),
462+ doc.version(),
463+ doc.text(),
464+ language_id,
465+ ));
466+ }
467+ }
468+ Notification::PublishDiagnostics(params) => {
469+ let path = params.uri.to_file_path().unwrap();
470+ let doc = self.editor.document_by_path_mut(&path);
471+
446− if let Some((_, doc)) = doc {
472+ if let Some(doc) = doc {
447 let text = doc.text();
473 let text = doc.text();
448
474
449 let diagnostics = params
475 let diagnostics = params
@@ -506,7 +532,7 @@ impl Application {
506 log::warn!("unhandled window/showMessage: {:?}", params);
532 log::warn!("unhandled window/showMessage: {:?}", params);
507 }
533 }
508 Notification::LogMessage(params) => {
534 Notification::LogMessage(params) => {
509− log::warn!("unhandled window/logMessage: {:?}", params);
535+ log::info!("window/logMessage: {:?}", params);
510 }
536 }
511 Notification::ProgressMessage(params) => {
537 Notification::ProgressMessage(params) => {
512 let lsp::ProgressParams { token, value } = params;
538 let lsp::ProgressParams { token, value } = params;
@@ -588,10 +614,27 @@ impl Application {
588 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
614 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
589 method, params, id, ..
615 method, params, id, ..
590 }) => {
616 }) => {
617+ let language_server = match self.editor.language_servers.get_by_id(server_id) {
618+ Some(language_server) => language_server,
619+ None => {
620+ warn!("can't find language server with id `{}`", server_id);
621+ return;
622+ }
623+ };
624+
591 let call = match MethodCall::parse(&method, params) {
625 let call = match MethodCall::parse(&method, params) {
592 Some(call) => call,
626 Some(call) => call,
593 None => {
627 None => {
594 error!("Method not found {}", method);
628 error!("Method not found {}", method);
629+ // language_server.reply(
630+ // call.id,
631+ // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
632+ // Err(helix_lsp::jsonrpc::Error {
633+ // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
634+ // message: "Method not found".to_string(),
635+ // data: None,
636+ // }),
637+ // );
595 return;
638 return;
596 }
639 }
597 };
640 };
@@ -604,53 +647,9 @@ impl Application {
604 if spinner.is_stopped() {
647 if spinner.is_stopped() {
605 spinner.start();
648 spinner.start();
606 }
649 }
607−
608− let doc = self.editor.documents().find(|doc| {
609− doc.language_server()
610− .map(|server| server.id() == server_id)
611− .unwrap_or_default()
612− });
613− match doc {
614− Some(doc) => {
615− // it's ok to unwrap, we check for the language server before
616− let server = doc.language_server().unwrap();
617− tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
650+ tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
618− }
619− None => {
620− if let Some(server) =
621− self.editor.language_servers.get_by_id(server_id)
622− {
623− log::warn!(
624− "missing document with language server id `{}`",
625− server_id
626− );
627− tokio::spawn(server.reply(
628− id,
629− Err(helix_lsp::jsonrpc::Error {
630− code: helix_lsp::jsonrpc::ErrorCode::InternalError,
631− message: "document missing".to_string(),
632− data: None,
633− }),
634− ));
635− } else {
636− log::warn!(
637− "can't find language server with id `{}`",
638− server_id
639− );
640− }
641− }
642− }
643 }
651 }
644 }
652 }
645− // self.language_server.reply(
646− // call.id,
647− // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
648− // Err(helix_lsp::jsonrpc::Error {
649− // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
650− // message: "Method not found".to_string(),
651− // data: None,
652− // }),
653− // );
654 }
653 }
655 e => unreachable!("{:?}", e),
654 e => unreachable!("{:?}", e),
656 }
655 }
helix-term/src/commands.rs+435−227
@@ -9,7 +9,7 @@ use helix_core::{
9 match_brackets,
9 match_brackets,
10 movement::{self, Direction},
10 movement::{self, Direction},
11 object, pos_at_coords,
11 object, pos_at_coords,
12− regex::{self, Regex},
12+ regex::{self, Regex, RegexBuilder},
13 register::Register,
13 register::Register,
14 search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
14 search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
15 RopeSlice, Selection, SmallVec, Tendril, Transaction,
15 RopeSlice, Selection, SmallVec, Tendril, Transaction,
@@ -35,7 +35,7 @@ use crate::{
35};
35};
36
36
37use crate::job::{self, Job, Jobs};
37use crate::job::{self, Job, Jobs};
38−use futures_util::FutureExt;
38+use futures_util::{FutureExt, StreamExt};
39use std::num::NonZeroUsize;
39use std::num::NonZeroUsize;
40use std::{collections::HashMap, fmt, future::Future};
40use std::{collections::HashMap, fmt, future::Future};
41
41
@@ -47,8 +47,13 @@ use std::{
47use once_cell::sync::Lazy;
47use once_cell::sync::Lazy;
48use serde::de::{self, Deserialize, Deserializer};
48use serde::de::{self, Deserialize, Deserializer};
49
49
50+use grep_regex::RegexMatcherBuilder;
51+use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
52+use ignore::{DirEntry, WalkBuilder, WalkState};
53+use tokio_stream::wrappers::UnboundedReceiverStream;
54+
50pub struct Context<'a> {
55pub struct Context<'a> {
51− pub selected_register: helix_view::RegisterSelection,
56+ pub register: Option<char>,
52 pub count: Option<NonZeroUsize>,
57 pub count: Option<NonZeroUsize>,
53 pub editor: &'a mut Editor,
58 pub editor: &'a mut Editor,
54
59
@@ -166,6 +171,7 @@ impl Command {
166
171
167 #[rustfmt::skip]
172 #[rustfmt::skip]
168 commands!(
173 commands!(
174+ no_op, "Do nothing",
169 move_char_left, "Move left",
175 move_char_left, "Move left",
170 move_char_right, "Move right",
176 move_char_right, "Move right",
171 move_line_up, "Move up",
177 move_line_up, "Move up",
@@ -184,6 +190,9 @@ impl Command {
184 move_next_long_word_end, "Move to end of next long word",
190 move_next_long_word_end, "Move to end of next long word",
185 extend_next_word_start, "Extend to beginning of next word",
191 extend_next_word_start, "Extend to beginning of next word",
186 extend_prev_word_start, "Extend to beginning of previous word",
192 extend_prev_word_start, "Extend to beginning of previous word",
193+ extend_next_long_word_start, "Extend to beginning of next long word",
194+ extend_prev_long_word_start, "Extend to beginning of previous long word",
195+ extend_next_long_word_end, "Extend to end of next long word",
187 extend_next_word_end, "Extend to end of next word",
196 extend_next_word_end, "Extend to end of next word",
188 find_till_char, "Move till next occurance of char",
197 find_till_char, "Move till next occurance of char",
189 find_next_char, "Move to next occurance of char",
198 find_next_char, "Move to next occurance of char",
@@ -209,6 +218,7 @@ impl Command {
209 search_next, "Select next search match",
218 search_next, "Select next search match",
210 extend_search_next, "Add next search match to selection",
219 extend_search_next, "Add next search match to selection",
211 search_selection, "Use current selection as search pattern",
220 search_selection, "Use current selection as search pattern",
221+ global_search, "Global Search in workspace folder",
212 extend_line, "Select current line, if already selected, extend to next line",
222 extend_line, "Select current line, if already selected, extend to next line",
213 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
223 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
214 delete_selection, "Delete selection",
224 delete_selection, "Delete selection",
@@ -253,6 +263,9 @@ impl Command {
253 // TODO: different description ?
263 // TODO: different description ?
254 goto_line_end_newline, "Goto line end",
264 goto_line_end_newline, "Goto line end",
255 goto_first_nonwhitespace, "Goto first non-blank in line",
265 goto_first_nonwhitespace, "Goto first non-blank in line",
266+ extend_to_line_start, "Extend to line start",
267+ extend_to_line_end, "Extend to line end",
268+ extend_to_line_end_newline, "Extend to line end",
256 signature_help, "Show signature help",
269 signature_help, "Show signature help",
257 insert_tab, "Insert tab char",
270 insert_tab, "Insert tab char",
258 insert_newline, "Insert newline char",
271 insert_newline, "Insert newline char",
@@ -281,6 +294,7 @@ impl Command {
281 join_selections, "Join lines inside selection",
294 join_selections, "Join lines inside selection",
282 keep_selections, "Keep selections matching regex",
295 keep_selections, "Keep selections matching regex",
283 keep_primary_selection, "Keep primary selection",
296 keep_primary_selection, "Keep primary selection",
297+ remove_primary_selection, "Remove primary selection",
284 completion, "Invoke completion popup",
298 completion, "Invoke completion popup",
285 hover, "Show docs for item under cursor",
299 hover, "Show docs for item under cursor",
286 toggle_comments, "Comment/uncomment selections",
300 toggle_comments, "Comment/uncomment selections",
@@ -372,52 +386,58 @@ impl PartialEq for Command {
372 }
386 }
373}
387}
374
388
375−fn move_char_left(cx: &mut Context) {
389+fn no_op(_cx: &mut Context) {}
390+
391+fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
392+where
393+ F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range,
394+{
376 let count = cx.count();
395 let count = cx.count();
377 let (view, doc) = current!(cx.editor);
396 let (view, doc) = current!(cx.editor);
378 let text = doc.text().slice(..);
397 let text = doc.text().slice(..);
379
398
380− let selection = doc.selection(view.id).clone().transform(|range| {
399+ let selection = doc
381− movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
400+ .selection(view.id)
382− });
401+ .clone()
402+ .transform(|range| move_fn(text, range, dir, count, behaviour));
383 doc.set_selection(view.id, selection);
403 doc.set_selection(view.id, selection);
384}
404}
385
405
386−fn move_char_right(cx: &mut Context) {
406+use helix_core::movement::{move_horizontally, move_vertically};
387− let count = cx.count();
388− let (view, doc) = current!(cx.editor);
389− let text = doc.text().slice(..);
390
407
391− let selection = doc.selection(view.id).clone().transform(|range| {
408+fn move_char_left(cx: &mut Context) {
392− movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
409+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
393− });
394− doc.set_selection(view.id, selection);
395}
410}
396
411
397−fn move_line_up(cx: &mut Context) {
412+fn move_char_right(cx: &mut Context) {
398− let count = cx.count();
413+ move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
399− let (view, doc) = current!(cx.editor);
414+}
400− let text = doc.text().slice(..);
401
415
402− let selection = doc.selection(view.id).clone().transform(|range| {
416+fn move_line_up(cx: &mut Context) {
403− movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
417+ move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
404− });
405− doc.set_selection(view.id, selection);
406}
418}
407
419
408fn move_line_down(cx: &mut Context) {
420fn move_line_down(cx: &mut Context) {
409− let count = cx.count();
421+ move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
410− let (view, doc) = current!(cx.editor);
422+}
411− let text = doc.text().slice(..);
412
423
413− let selection = doc.selection(view.id).clone().transform(|range| {
424+fn extend_char_left(cx: &mut Context) {
414− movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
425+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
415− });
416− doc.set_selection(view.id, selection);
417}
426}
418
427
428+fn extend_char_right(cx: &mut Context) {
429+ move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend)
430+}
431+
432+fn extend_line_up(cx: &mut Context) {
433+ move_impl(cx, move_vertically, Direction::Backward, Movement::Extend)
434+}
435+
419−fn goto_line_end(cx: &mut Context) {
436+fn extend_line_down(cx: &mut Context) {
420− let (view, doc) = current!(cx.editor);
437+ move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
438+}
439+
440+fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
421 let text = doc.text().slice(..);
441 let text = doc.text().slice(..);
422
442
423 let selection = doc.selection(view.id).clone().transform(|range| {
443 let selection = doc.selection(view.id).clone().transform(|range| {
@@ -427,26 +447,60 @@ fn goto_line_end(cx: &mut Context) {
427 let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
447 let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
428 .max(line_start);
448 .max(line_start);
429
449
430− range.put_cursor(text, pos, doc.mode == Mode::Select)
450+ range.put_cursor(text, pos, movement == Movement::Extend)
431 });
451 });
432 doc.set_selection(view.id, selection);
452 doc.set_selection(view.id, selection);
433}
453}
434
454
435−fn goto_line_end_newline(cx: &mut Context) {
455+fn goto_line_end(cx: &mut Context) {
436 let (view, doc) = current!(cx.editor);
456 let (view, doc) = current!(cx.editor);
457+ goto_line_end_impl(
458+ view,
459+ doc,
460+ if doc.mode == Mode::Select {
461+ Movement::Extend
462+ } else {
463+ Movement::Move
464+ },
465+ )
466+}
467+
468+fn extend_to_line_end(cx: &mut Context) {
469+ let (view, doc) = current!(cx.editor);
470+ goto_line_end_impl(view, doc, Movement::Extend)
471+}
472+
473+fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) {
437 let text = doc.text().slice(..);
474 let text = doc.text().slice(..);
438
475
439 let selection = doc.selection(view.id).clone().transform(|range| {
476 let selection = doc.selection(view.id).clone().transform(|range| {
440 let line = range.cursor_line(text);
477 let line = range.cursor_line(text);
441 let pos = line_end_char_index(&text, line);
478 let pos = line_end_char_index(&text, line);
442
479
443− range.put_cursor(text, pos, doc.mode == Mode::Select)
480+ range.put_cursor(text, pos, movement == Movement::Extend)
444 });
481 });
445 doc.set_selection(view.id, selection);
482 doc.set_selection(view.id, selection);
446}
483}
447
484
448−fn goto_line_start(cx: &mut Context) {
485+fn goto_line_end_newline(cx: &mut Context) {
486+ let (view, doc) = current!(cx.editor);
487+ goto_line_end_newline_impl(
488+ view,
489+ doc,
490+ if doc.mode == Mode::Select {
491+ Movement::Extend
492+ } else {
493+ Movement::Move
494+ },
495+ )
496+}
497+
498+fn extend_to_line_end_newline(cx: &mut Context) {
449 let (view, doc) = current!(cx.editor);
499 let (view, doc) = current!(cx.editor);
500+ goto_line_end_newline_impl(view, doc, Movement::Extend)
501+}
502+
503+fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) {
450 let text = doc.text().slice(..);
504 let text = doc.text().slice(..);
451
505
452 let selection = doc.selection(view.id).clone().transform(|range| {
506 let selection = doc.selection(view.id).clone().transform(|range| {
@@ -454,11 +508,29 @@ fn goto_line_start(cx: &mut Context) {
454
508
455 // adjust to start of the line
509 // adjust to start of the line
456 let pos = text.line_to_char(line);
510 let pos = text.line_to_char(line);
457− range.put_cursor(text, pos, doc.mode == Mode::Select)
511+ range.put_cursor(text, pos, movement == Movement::Extend)
458 });
512 });
459 doc.set_selection(view.id, selection);
513 doc.set_selection(view.id, selection);
460}
514}
461
515
516+fn goto_line_start(cx: &mut Context) {
517+ let (view, doc) = current!(cx.editor);
518+ goto_line_start_impl(
519+ view,
520+ doc,
521+ if doc.mode == Mode::Select {
522+ Movement::Extend
523+ } else {
524+ Movement::Move
525+ },
526+ )
527+}
528+
529+fn extend_to_line_start(cx: &mut Context) {
530+ let (view, doc) = current!(cx.editor);
531+ goto_line_start_impl(view, doc, Movement::Extend)
532+}
533+
462fn goto_first_nonwhitespace(cx: &mut Context) {
534fn goto_first_nonwhitespace(cx: &mut Context) {
463 let (view, doc) = current!(cx.editor);
535 let (view, doc) = current!(cx.editor);
464 let text = doc.text().slice(..);
536 let text = doc.text().slice(..);
@@ -512,7 +584,10 @@ fn goto_window_bottom(cx: &mut Context) {
512 goto_window(cx, Align::Bottom)
584 goto_window(cx, Align::Bottom)
513}
585}
514
586
515−fn move_next_word_start(cx: &mut Context) {
587+fn move_word_impl<F>(cx: &mut Context, move_fn: F)
588+where
589+ F: Fn(RopeSlice, Range, usize) -> Range,
590+{
516 let count = cx.count();
591 let count = cx.count();
517 let (view, doc) = current!(cx.editor);
592 let (view, doc) = current!(cx.editor);
518 let text = doc.text().slice(..);
593 let text = doc.text().slice(..);
@@ -520,68 +595,32 @@ fn move_next_word_start(cx: &mut Context) {
520 let selection = doc
595 let selection = doc
521 .selection(view.id)
596 .selection(view.id)
522 .clone()
597 .clone()
523− .transform(|range| movement::move_next_word_start(text, range, count));
598+ .transform(|range| move_fn(text, range, count));
524 doc.set_selection(view.id, selection);
599 doc.set_selection(view.id, selection);
525}
600}
526
601
527−fn move_prev_word_start(cx: &mut Context) {
602+fn move_next_word_start(cx: &mut Context) {
528− let count = cx.count();
603+ move_word_impl(cx, movement::move_next_word_start)
529− let (view, doc) = current!(cx.editor);
604+}
530− let text = doc.text().slice(..);
531
605
532− let selection = doc
606+fn move_prev_word_start(cx: &mut Context) {
533− .selection(view.id)
534− .clone()
535− .transform(|range| movement::move_prev_word_start(text, range, count));
607+ move_word_impl(cx, movement::move_prev_word_start)
536− doc.set_selection(view.id, selection);
537}
608}
538
609
539fn move_next_word_end(cx: &mut Context) {
610fn move_next_word_end(cx: &mut Context) {
540− let count = cx.count();
541− let (view, doc) = current!(cx.editor);
542− let text = doc.text().slice(..);
543−
544− let selection = doc
545− .selection(view.id)
546− .clone()
547− .transform(|range| movement::move_next_word_end(text, range, count));
611+ move_word_impl(cx, movement::move_next_word_end)
548− doc.set_selection(view.id, selection);
549}
612}
550
613
551fn move_next_long_word_start(cx: &mut Context) {
614fn move_next_long_word_start(cx: &mut Context) {
552− let count = cx.count();
553− let (view, doc) = current!(cx.editor);
554− let text = doc.text().slice(..);
555−
556− let selection = doc
557− .selection(view.id)
558− .clone()
559− .transform(|range| movement::move_next_long_word_start(text, range, count));
615+ move_word_impl(cx, movement::move_next_long_word_start)
560− doc.set_selection(view.id, selection);
561}
616}
562
617
563fn move_prev_long_word_start(cx: &mut Context) {
618fn move_prev_long_word_start(cx: &mut Context) {
564− let count = cx.count();
565− let (view, doc) = current!(cx.editor);
566− let text = doc.text().slice(..);
567−
568− let selection = doc
569− .selection(view.id)
570− .clone()
571− .transform(|range| movement::move_prev_long_word_start(text, range, count));
619+ move_word_impl(cx, movement::move_prev_long_word_start)
572− doc.set_selection(view.id, selection);
573}
620}
574
621
575fn move_next_long_word_end(cx: &mut Context) {
622fn move_next_long_word_end(cx: &mut Context) {
576− let count = cx.count();
577− let (view, doc) = current!(cx.editor);
578− let text = doc.text().slice(..);
579−
580− let selection = doc
581− .selection(view.id)
582− .clone()
583− .transform(|range| movement::move_next_long_word_end(text, range, count));
623+ move_word_impl(cx, movement::move_next_long_word_end)
584− doc.set_selection(view.id, selection);
585}
624}
586
625
587fn goto_file_start(cx: &mut Context) {
626fn goto_file_start(cx: &mut Context) {
@@ -600,43 +639,44 @@ fn goto_file_end(cx: &mut Context) {
600 doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
639 doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
601}
640}
602
641
603−fn extend_next_word_start(cx: &mut Context) {
642+fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
643+where
644+ F: Fn(RopeSlice, Range, usize) -> Range,
645+{
604 let count = cx.count();
646 let count = cx.count();
605 let (view, doc) = current!(cx.editor);
647 let (view, doc) = current!(cx.editor);
606 let text = doc.text().slice(..);
648 let text = doc.text().slice(..);
607
649
608 let selection = doc.selection(view.id).clone().transform(|range| {
650 let selection = doc.selection(view.id).clone().transform(|range| {
609− let word = movement::move_next_word_start(text, range, count);
651+ let word = extend_fn(text, range, count);
610 let pos = word.cursor(text);
652 let pos = word.cursor(text);
611 range.put_cursor(text, pos, true)
653 range.put_cursor(text, pos, true)
612 });
654 });
613 doc.set_selection(view.id, selection);
655 doc.set_selection(view.id, selection);
614}
656}
615
657
616−fn extend_prev_word_start(cx: &mut Context) {
658+fn extend_next_word_start(cx: &mut Context) {
617− let count = cx.count();
659+ extend_word_impl(cx, movement::move_next_word_start)
618− let (view, doc) = current!(cx.editor);
660+}
619− let text = doc.text().slice(..);
620
661
621− let selection = doc.selection(view.id).clone().transform(|range| {
662+fn extend_prev_word_start(cx: &mut Context) {
622− let word = movement::move_prev_word_start(text, range, count);
663+ extend_word_impl(cx, movement::move_prev_word_start)
623− let pos = word.cursor(text);
624− range.put_cursor(text, pos, true)
625− });
626− doc.set_selection(view.id, selection);
627}
664}
628
665
629fn extend_next_word_end(cx: &mut Context) {
666fn extend_next_word_end(cx: &mut Context) {
630− let count = cx.count();
667+ extend_word_impl(cx, movement::move_next_word_end)
631− let (view, doc) = current!(cx.editor);
668+}
632− let text = doc.text().slice(..);
633
669
634− let selection = doc.selection(view.id).clone().transform(|range| {
670+fn extend_next_long_word_start(cx: &mut Context) {
635− let word = movement::move_next_word_end(text, range, count);
671+ extend_word_impl(cx, movement::move_next_long_word_start)
636− let pos = word.cursor(text);
672+}
637− range.put_cursor(text, pos, true)
673+
674+fn extend_prev_long_word_start(cx: &mut Context) {
675+ extend_word_impl(cx, movement::move_prev_long_word_start)
638− });
676+}
639− doc.set_selection(view.id, selection);
677+
678+fn extend_next_long_word_end(cx: &mut Context) {
679+ extend_word_impl(cx, movement::move_next_long_word_end)
640}
680}
641
681
642#[inline]
682#[inline]
@@ -852,12 +892,25 @@ fn replace(cx: &mut Context) {
852 })
892 })
853}
893}
854
894
855−fn switch_case(cx: &mut Context) {
895+fn switch_case_impl<F>(cx: &mut Context, change_fn: F)
896+where
897+ F: Fn(Cow<str>) -> Tendril,
898+{
856 let (view, doc) = current!(cx.editor);
899 let (view, doc) = current!(cx.editor);
857 let selection = doc.selection(view.id);
900 let selection = doc.selection(view.id);
858 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
901 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
859− let text: Tendril = range
860− .fragment(doc.text().slice(..))
902+ let text: Tendril = change_fn(range.fragment(doc.text().slice(..)));
903+
904+ (range.from(), range.to(), Some(text))
905+ });
906+
907+ doc.apply(&transaction, view.id);
908+ doc.append_changes_to_history(view.id);
909+}
910+
911+fn switch_case(cx: &mut Context) {
912+ switch_case_impl(cx, |string| {
913+ string
861 .chars()
914 .chars()
862 .flat_map(|ch| {
915 .flat_map(|ch| {
863 if ch.is_lowercase() {
916 if ch.is_lowercase() {
@@ -868,39 +921,16 @@ fn switch_case(cx: &mut Context) {
868 vec![ch]
921 vec![ch]
869 }
922 }
870 })
923 })
871− .collect();
924+ .collect()
872−
873− (range.from(), range.to(), Some(text))
874 });
925 });
875−
876− doc.apply(&transaction, view.id);
877− doc.append_changes_to_history(view.id);
878}
926}
879
927
880fn switch_to_uppercase(cx: &mut Context) {
928fn switch_to_uppercase(cx: &mut Context) {
881− let (view, doc) = current!(cx.editor);
929+ switch_case_impl(cx, |string| string.to_uppercase().into());
882− let selection = doc.selection(view.id);
883− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
884− let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
885−
886− (range.from(), range.to(), Some(text))
887− });
888−
889− doc.apply(&transaction, view.id);
890− doc.append_changes_to_history(view.id);
891}
930}
892
931
893fn switch_to_lowercase(cx: &mut Context) {
932fn switch_to_lowercase(cx: &mut Context) {
894− let (view, doc) = current!(cx.editor);
933+ switch_case_impl(cx, |string| string.to_lowercase().into());
895− let selection = doc.selection(view.id);
896− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
897− let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
898−
899− (range.from(), range.to(), Some(text))
900− });
901−
902− doc.apply(&transaction, view.id);
903− doc.append_changes_to_history(view.id);
904}
934}
905
935
906pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
936pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
@@ -976,28 +1006,6 @@ fn half_page_down(cx: &mut Context) {
976 scroll(cx, offset, Direction::Forward);
1006 scroll(cx, offset, Direction::Forward);
977}
1007}
978
1008
979−fn extend_char_left(cx: &mut Context) {
980− let count = cx.count();
981− let (view, doc) = current!(cx.editor);
982− let text = doc.text().slice(..);
983−
984− let selection = doc.selection(view.id).clone().transform(|range| {
985− movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
986− });
987− doc.set_selection(view.id, selection);
988−}
989−
990−fn extend_char_right(cx: &mut Context) {
991− let count = cx.count();
992− let (view, doc) = current!(cx.editor);
993− let text = doc.text().slice(..);
994−
995− let selection = doc.selection(view.id).clone().transform(|range| {
996− movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
997− });
998− doc.set_selection(view.id, selection);
999−}
1000−
1001fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
1009fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
1002 let count = cx.count();
1010 let count = cx.count();
1003 let (view, doc) = current!(cx.editor);
1011 let (view, doc) = current!(cx.editor);
@@ -1068,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) {
1068 copy_selection_on_line(cx, Direction::Forward)
1076 copy_selection_on_line(cx, Direction::Forward)
1069}
1077}
1070
1078
1071−fn extend_line_up(cx: &mut Context) {
1072− let count = cx.count();
1073− let (view, doc) = current!(cx.editor);
1074− let text = doc.text().slice(..);
1075−
1076− let selection = doc.selection(view.id).clone().transform(|range| {
1077− movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
1078− });
1079− doc.set_selection(view.id, selection);
1080−}
1081−
1082−fn extend_line_down(cx: &mut Context) {
1083− let count = cx.count();
1084− let (view, doc) = current!(cx.editor);
1085− let text = doc.text().slice(..);
1086−
1087− let selection = doc.selection(view.id).clone().transform(|range| {
1088− movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
1089− });
1090− doc.set_selection(view.id, selection);
1091−}
1092−
1093fn select_all(cx: &mut Context) {
1079fn select_all(cx: &mut Context) {
1094 let (view, doc) = current!(cx.editor);
1080 let (view, doc) = current!(cx.editor);
1095
1081
@@ -1098,23 +1084,42 @@ fn select_all(cx: &mut Context) {
1098}
1084}
1099
1085
1100fn select_regex(cx: &mut Context) {
1086fn select_regex(cx: &mut Context) {
1087+ let reg = cx.register.unwrap_or('/');
1101− let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| {
1088+ let prompt = ui::regex_prompt(
1089+ cx,
1090+ "select:".into(),
1091+ Some(reg),
1092+ move |view, doc, regex, event| {
1093+ if event != PromptEvent::Update {
1094+ return;
1095+ }
1102− let text = doc.text().slice(..);
1096+ let text = doc.text().slice(..);
1097+ if let Some(selection) =
1103− if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), &regex)
1098+ selection::select_on_matches(text, doc.selection(view.id), &regex)
1104− {
1099+ {
1105− doc.set_selection(view.id, selection);
1100+ doc.set_selection(view.id, selection);
1101+ }
1106− }
1102+ },
1107− });
1103+ );
1108
1104
1109 cx.push_layer(Box::new(prompt));
1105 cx.push_layer(Box::new(prompt));
1110}
1106}
1111
1107
1112fn split_selection(cx: &mut Context) {
1108fn split_selection(cx: &mut Context) {
1109+ let reg = cx.register.unwrap_or('/');
1113− let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| {
1110+ let prompt = ui::regex_prompt(
1111+ cx,
1112+ "split:".into(),
1113+ Some(reg),
1114+ move |view, doc, regex, event| {
1115+ if event != PromptEvent::Update {
1116+ return;
1117+ }
1114− let text = doc.text().slice(..);
1118+ let text = doc.text().slice(..);
1115− let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
1119+ let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
1116− doc.set_selection(view.id, selection);
1120+ doc.set_selection(view.id, selection);
1121+ },
1117− });
1122+ );
1118
1123
1119 cx.push_layer(Box::new(prompt));
1124 cx.push_layer(Box::new(prompt));
1120}
1125}
@@ -1168,6 +1173,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
1168
1173
1169// TODO: use one function for search vs extend
1174// TODO: use one function for search vs extend
1170fn search(cx: &mut Context) {
1175fn search(cx: &mut Context) {
1176+ let reg = cx.register.unwrap_or('/');
1171 let (_, doc) = current!(cx.editor);
1177 let (_, doc) = current!(cx.editor);
1172
1178
1173 // TODO: could probably share with select_on_matches?
1179 // TODO: could probably share with select_on_matches?
@@ -1176,23 +1182,44 @@ fn search(cx: &mut Context) {
1176 // feed chunks into the regex yet
1182 // feed chunks into the regex yet
1177 let contents = doc.text().slice(..).to_string();
1183 let contents = doc.text().slice(..).to_string();
1178
1184
1179− let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| {
1185+ let prompt = ui::regex_prompt(
1186+ cx,
1187+ "search:".into(),
1188+ Some(reg),
1189+ move |view, doc, regex, event| {
1190+ if event != PromptEvent::Update {
1191+ return;
1192+ }
1180− search_impl(doc, view, &contents, &regex, false);
1193+ search_impl(doc, view, &contents, &regex, false);
1181− // TODO: only store on enter (accept), not update
1194+ },
1182− registers.write('/', vec![regex.as_str().to_string()]);
1183− });
1195+ );
1184
1196
1185 cx.push_layer(Box::new(prompt));
1197 cx.push_layer(Box::new(prompt));
1186}
1198}
1187
1199
1188fn search_next_impl(cx: &mut Context, extend: bool) {
1200fn search_next_impl(cx: &mut Context, extend: bool) {
1189 let (view, doc) = current!(cx.editor);
1201 let (view, doc) = current!(cx.editor);
1190− let registers = &mut cx.editor.registers;
1202+ let registers = &cx.editor.registers;
1191 if let Some(query) = registers.read('/') {
1203 if let Some(query) = registers.read('/') {
1192− let query = query.first().unwrap();
1204+ let query = query.last().unwrap();
1193 let contents = doc.text().slice(..).to_string();
1205 let contents = doc.text().slice(..).to_string();
1206+ let case_insensitive = if cx.editor.config.smart_case {
1207+ !query.chars().any(char::is_uppercase)
1208+ } else {
1209+ false
1210+ };
1194− let regex = Regex::new(query).unwrap();
1211+ if let Ok(regex) = RegexBuilder::new(query)
1212+ .case_insensitive(case_insensitive)
1213+ .build()
1214+ {
1195− search_impl(doc, view, &contents, &regex, extend);
1215+ search_impl(doc, view, &contents, &regex, extend);
1216+ } else {
1217+ // get around warning `mutable_borrow_reservation_conflict`
1218+ // which will be a hard error in the future
1219+ // see: https://github.com/rust-lang/rust/issues/59159
1220+ let query = query.clone();
1221+ cx.editor.set_error(format!("Invalid regex: {}", query));
1222+ }
1196 }
1223 }
1197}
1224}
1198
1225
@@ -1209,8 +1236,119 @@ fn search_selection(cx: &mut Context) {
1209 let contents = doc.text().slice(..);
1236 let contents = doc.text().slice(..);
1210 let query = doc.selection(view.id).primary().fragment(contents);
1237 let query = doc.selection(view.id).primary().fragment(contents);
1211 let regex = regex::escape(&query);
1238 let regex = regex::escape(&query);
1212− cx.editor.registers.write('/', vec![regex]);
1239+ cx.editor.registers.get_mut('/').push(regex);
1213− search_next(cx);
1240+ let msg = format!("register '{}' set to '{}'", '\\', query);
1241+ cx.editor.set_status(msg);
1242+}
1243+
1244+fn global_search(cx: &mut Context) {
1245+ let (all_matches_sx, all_matches_rx) =
1246+ tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1247+ let smart_case = cx.editor.config.smart_case;
1248+ let prompt = ui::regex_prompt(
1249+ cx,
1250+ "global search:".into(),
1251+ None,
1252+ move |_view, _doc, regex, event| {
1253+ if event != PromptEvent::Validate {
1254+ return;
1255+ }
1256+
1257+ if let Ok(matcher) = RegexMatcherBuilder::new()
1258+ .case_smart(smart_case)
1259+ .build(regex.as_str())
1260+ {
1261+ let searcher = SearcherBuilder::new()
1262+ .binary_detection(BinaryDetection::quit(b'\x00'))
1263+ .build();
1264+
1265+ let search_root = std::env::current_dir()
1266+ .expect("Global search error: Failed to get current dir");
1267+ WalkBuilder::new(search_root).build_parallel().run(|| {
1268+ let mut searcher_cl = searcher.clone();
1269+ let matcher_cl = matcher.clone();
1270+ let all_matches_sx_cl = all_matches_sx.clone();
1271+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1272+ let dent = match dent {
1273+ Ok(dent) => dent,
1274+ Err(_) => return WalkState::Continue,
1275+ };
1276+
1277+ match dent.file_type() {
1278+ Some(fi) => {
1279+ if !fi.is_file() {
1280+ return WalkState::Continue;
1281+ }
1282+ }
1283+ None => return WalkState::Continue,
1284+ }
1285+
1286+ let result_sink = sinks::UTF8(|line_num, _| {
1287+ match all_matches_sx_cl
1288+ .send((line_num as usize - 1, dent.path().to_path_buf()))
1289+ {
1290+ Ok(_) => Ok(true),
1291+ Err(_) => Ok(false),
1292+ }
1293+ });
1294+ let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1295+
1296+ if let Err(err) = result {
1297+ log::error!("Global search error: {}, {}", dent.path().display(), err);
1298+ }
1299+ WalkState::Continue
1300+ })
1301+ });
1302+ } else {
1303+ // Otherwise do nothing
1304+ // log::warn!("Global Search Invalid Pattern")
1305+ }
1306+ },
1307+ );
1308+
1309+ cx.push_layer(Box::new(prompt));
1310+
1311+ let show_picker = async move {
1312+ let all_matches: Vec<(usize, PathBuf)> =
1313+ UnboundedReceiverStream::new(all_matches_rx).collect().await;
1314+ let call: job::Callback =
1315+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1316+ if all_matches.is_empty() {
1317+ editor.set_status("No matches found".to_string());
1318+ return;
1319+ }
1320+ let picker = FilePicker::new(
1321+ all_matches,
1322+ move |(_line_num, path)| path.to_str().unwrap().into(),
1323+ move |editor: &mut Editor, (line_num, path), action| {
1324+ match editor.open(path.into(), action) {
1325+ Ok(_) => {}
1326+ Err(e) => {
1327+ editor.set_error(format!(
1328+ "Failed to open file '{}': {}",
1329+ path.display(),
1330+ e
1331+ ));
1332+ return;
1333+ }
1334+ }
1335+
1336+ let line_num = *line_num;
1337+ let (view, doc) = current!(editor);
1338+ let text = doc.text();
1339+ let start = text.line_to_char(line_num);
1340+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
1341+
1342+ doc.set_selection(view.id, Selection::single(start, end));
1343+ align_view(doc, view, Align::Center);
1344+ },
1345+ |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
1346+ );
1347+ compositor.push(Box::new(picker));
1348+ });
1349+ Ok(call)
1350+ };
1351+ cx.jobs.callback(show_picker);
1214}
1352}
1215
1353
1216fn extend_line(cx: &mut Context) {
1354fn extend_line(cx: &mut Context) {
@@ -1268,7 +1406,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
1268}
1406}
1269
1407
1270fn delete_selection(cx: &mut Context) {
1408fn delete_selection(cx: &mut Context) {
1271− let reg_name = cx.selected_register.name();
1409+ let reg_name = cx.register.unwrap_or('"');
1272 let (view, doc) = current!(cx.editor);
1410 let (view, doc) = current!(cx.editor);
1273 let registers = &mut cx.editor.registers;
1411 let registers = &mut cx.editor.registers;
1274 let reg = registers.get_mut(reg_name);
1412 let reg = registers.get_mut(reg_name);
@@ -1281,7 +1419,7 @@ fn delete_selection(cx: &mut Context) {
1281}
1419}
1282
1420
1283fn change_selection(cx: &mut Context) {
1421fn change_selection(cx: &mut Context) {
1284− let reg_name = cx.selected_register.name();
1422+ let reg_name = cx.register.unwrap_or('"');
1285 let (view, doc) = current!(cx.editor);
1423 let (view, doc) = current!(cx.editor);
1286 let registers = &mut cx.editor.registers;
1424 let registers = &mut cx.editor.registers;
1287 let reg = registers.get_mut(reg_name);
1425 let reg = registers.get_mut(reg_name);
@@ -1403,8 +1541,11 @@ mod cmd {
1403 args: &[&str],
1541 args: &[&str],
1404 _event: PromptEvent,
1542 _event: PromptEvent,
1405 ) -> anyhow::Result<()> {
1543 ) -> anyhow::Result<()> {
1544+ use helix_core::path::expand_tilde;
1406 let path = args.get(0).context("wrong argument count")?;
1545 let path = args.get(0).context("wrong argument count")?;
1546+ let _ = cx
1547+ .editor
1407− let _ = cx.editor.open(path.into(), Action::Replace)?;
1548+ .open(expand_tilde(Path::new(path)), Action::Replace)?;
1408 Ok(())
1549 Ok(())
1409 }
1550 }
1410
1551
@@ -1604,7 +1745,7 @@ mod cmd {
1604
1745
1605 /// Results an error if there are modified buffers remaining and sets editor error,
1746 /// Results an error if there are modified buffers remaining and sets editor error,
1606 /// otherwise returns `Ok(())`
1747 /// otherwise returns `Ok(())`
1607− fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
1748+ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
1608 let modified: Vec<_> = editor
1749 let modified: Vec<_> = editor
1609 .documents()
1750 .documents()
1610 .filter(|doc| doc.is_modified())
1751 .filter(|doc| doc.is_modified())
@@ -2577,7 +2718,7 @@ fn apply_workspace_edit(
2577) {
2718) {
2578 if let Some(ref changes) = workspace_edit.changes {
2719 if let Some(ref changes) = workspace_edit.changes {
2579 log::debug!("workspace changes: {:?}", changes);
2720 log::debug!("workspace changes: {:?}", changes);
2580− editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2721+ editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2581 return;
2722 return;
2582 // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
2723 // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
2583 // TODO: find some example that uses workspace changes, and test it
2724 // TODO: find some example that uses workspace changes, and test it
@@ -2595,8 +2736,30 @@ fn apply_workspace_edit(
2595 match document_changes {
2736 match document_changes {
2596 lsp::DocumentChanges::Edits(document_edits) => {
2737 lsp::DocumentChanges::Edits(document_edits) => {
2597 for document_edit in document_edits {
2738 for document_edit in document_edits {
2598− let (view, doc) = current!(editor);
2739+ let path = document_edit
2599− assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
2740+ .text_document
2741+ .uri
2742+ .to_file_path()
2743+ .expect("unable to convert URI to filepath");
2744+ let current_view_id = view!(editor).id;
2745+ let doc = editor
2746+ .document_by_path_mut(path)
2747+ .expect("Document for document_changes not found");
2748+
2749+ // Need to determine a view for apply/append_changes_to_history
2750+ let selections = doc.selections();
2751+ let view_id = if selections.contains_key(&current_view_id) {
2752+ // use current if possible
2753+ current_view_id
2754+ } else {
2755+ // Hack: we take the first available view_id
2756+ selections
2757+ .keys()
2758+ .next()
2759+ .copied()
2760+ .expect("No view_id available")
2761+ };
2762+
2600 let edits = document_edit
2763 let edits = document_edit
2601 .edits
2764 .edits
2602 .iter()
2765 .iter()
@@ -2614,8 +2777,8 @@ fn apply_workspace_edit(
2614 edits,
2777 edits,
2615 offset_encoding,
2778 offset_encoding,
2616 );
2779 );
2617− doc.apply(&transaction, view.id);
2780+ doc.apply(&transaction, view_id);
2618− doc.append_changes_to_history(view.id);
2781+ doc.append_changes_to_history(view_id);
2619 }
2782 }
2620 }
2783 }
2621 lsp::DocumentChanges::Operations(operations) => {
2784 lsp::DocumentChanges::Operations(operations) => {
@@ -2779,6 +2942,10 @@ fn open_above(cx: &mut Context) {
2779fn normal_mode(cx: &mut Context) {
2942fn normal_mode(cx: &mut Context) {
2780 let (view, doc) = current!(cx.editor);
2943 let (view, doc) = current!(cx.editor);
2781
2944
2945+ if doc.mode == Mode::Normal {
2946+ return;
2947+ }
2948+
2782 doc.mode = Mode::Normal;
2949 doc.mode = Mode::Normal;
2783
2950
2784 doc.append_changes_to_history(view.id);
2951 doc.append_changes_to_history(view.id);
@@ -3325,17 +3492,20 @@ pub mod insert {
3325 }
3492 }
3326
3493
3327 use helix_core::auto_pairs;
3494 use helix_core::auto_pairs;
3328− const HOOKS: &[Hook] = &[auto_pairs::hook, insert];
3329− const POST_HOOKS: &[PostHook] = &[completion, signature_help];
3330
3495
3331 pub fn insert_char(cx: &mut Context, c: char) {
3496 pub fn insert_char(cx: &mut Context, c: char) {
3332 let (view, doc) = current!(cx.editor);
3497 let (view, doc) = current!(cx.editor);
3333
3498
3499+ let hooks: &[Hook] = match cx.editor.config.auto_pairs {
3500+ true => &[auto_pairs::hook, insert],
3501+ false => &[insert],
3502+ };
3503+
3334 let text = doc.text();
3504 let text = doc.text();
3335 let selection = doc.selection(view.id).clone().cursors(text.slice(..));
3505 let selection = doc.selection(view.id).clone().cursors(text.slice(..));
3336
3506
3337 // run through insert hooks, stopping on the first one that returns Some(t)
3507 // run through insert hooks, stopping on the first one that returns Some(t)
3338− for hook in HOOKS {
3508+ for hook in hooks {
3339 if let Some(transaction) = hook(text, &selection, c) {
3509 if let Some(transaction) = hook(text, &selection, c) {
3340 doc.apply(&transaction, view.id);
3510 doc.apply(&transaction, view.id);
3341 break;
3511 break;
@@ -3345,7 +3515,7 @@ pub mod insert {
3345 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3515 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3346 // this could also generically look at Transaction, but it's a bit annoying to look at
3516 // this could also generically look at Transaction, but it's a bit annoying to look at
3347 // Operation instead of Change.
3517 // Operation instead of Change.
3348− for hook in POST_HOOKS {
3518+ for hook in &[completion, signature_help] {
3349 hook(cx, c);
3519 hook(cx, c);
3350 }
3520 }
3351 }
3521 }
@@ -3510,12 +3680,12 @@ fn yank(cx: &mut Context) {
3510 let msg = format!(
3680 let msg = format!(
3511 "yanked {} selection(s) to register {}",
3681 "yanked {} selection(s) to register {}",
3512 values.len(),
3682 values.len(),
3513− cx.selected_register.name()
3683+ cx.register.unwrap_or('"')
3514 );
3684 );
3515
3685
3516 cx.editor
3686 cx.editor
3517 .registers
3687 .registers
3518− .write(cx.selected_register.name(), values);
3688+ .write(cx.register.unwrap_or('"'), values);
3519
3689
3520 cx.editor.set_status(msg);
3690 cx.editor.set_status(msg);
3521 exit_select_mode(cx);
3691 exit_select_mode(cx);
@@ -3624,7 +3794,14 @@ fn paste_impl(
3624 .iter()
3794 .iter()
3625 .any(|value| get_line_ending_of_str(value).is_some());
3795 .any(|value| get_line_ending_of_str(value).is_some());
3626
3796
3797+ // Only compiled once.
3798+ #[allow(clippy::trivial_regex)]
3799+ static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
3627− let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
3800+ let mut values = values
3801+ .iter()
3802+ .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
3803+ .map(|value| Tendril::from(value.as_ref()))
3804+ .chain(repeat);
3628
3805
3629 let text = doc.text();
3806 let text = doc.text();
3630 let selection = doc.selection(view.id);
3807 let selection = doc.selection(view.id);
@@ -3688,7 +3865,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) {
3688}
3865}
3689
3866
3690fn replace_with_yanked(cx: &mut Context) {
3867fn replace_with_yanked(cx: &mut Context) {
3691− let reg_name = cx.selected_register.name();
3868+ let reg_name = cx.register.unwrap_or('"');
3692 let (view, doc) = current!(cx.editor);
3869 let (view, doc) = current!(cx.editor);
3693 let registers = &mut cx.editor.registers;
3870 let registers = &mut cx.editor.registers;
3694
3871
@@ -3739,7 +3916,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) {
3739}
3916}
3740
3917
3741fn paste_after(cx: &mut Context) {
3918fn paste_after(cx: &mut Context) {
3742− let reg_name = cx.selected_register.name();
3919+ let reg_name = cx.register.unwrap_or('"');
3743 let (view, doc) = current!(cx.editor);
3920 let (view, doc) = current!(cx.editor);
3744 let registers = &mut cx.editor.registers;
3921 let registers = &mut cx.editor.registers;
3745
3922
@@ -3753,7 +3930,7 @@ fn paste_after(cx: &mut Context) {
3753}
3930}
3754
3931
3755fn paste_before(cx: &mut Context) {
3932fn paste_before(cx: &mut Context) {
3756− let reg_name = cx.selected_register.name();
3933+ let reg_name = cx.register.unwrap_or('"');
3757 let (view, doc) = current!(cx.editor);
3934 let (view, doc) = current!(cx.editor);
3758 let registers = &mut cx.editor.registers;
3935 let registers = &mut cx.editor.registers;
3759
3936
@@ -3934,24 +4111,49 @@ fn join_selections(cx: &mut Context) {
3934
4111
3935fn keep_selections(cx: &mut Context) {
4112fn keep_selections(cx: &mut Context) {
3936 // keep selections matching regex
4113 // keep selections matching regex
4114+ let reg = cx.register.unwrap_or('/');
3937− let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| {
4115+ let prompt = ui::regex_prompt(
4116+ cx,
4117+ "keep:".into(),
4118+ Some(reg),
4119+ move |view, doc, regex, event| {
4120+ if event != PromptEvent::Update {
4121+ return;
4122+ }
3938− let text = doc.text().slice(..);
4123+ let text = doc.text().slice(..);
3939
4124
3940− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
4125+ if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
3941− doc.set_selection(view.id, selection);
4126+ doc.set_selection(view.id, selection);
4127+ }
3942− }
4128+ },
3943− });
4129+ );
3944
4130
3945 cx.push_layer(Box::new(prompt));
4131 cx.push_layer(Box::new(prompt));
3946}
4132}
3947
4133
3948fn keep_primary_selection(cx: &mut Context) {
4134fn keep_primary_selection(cx: &mut Context) {
3949 let (view, doc) = current!(cx.editor);
4135 let (view, doc) = current!(cx.editor);
4136+ // TODO: handle count
3950
4137
3951 let range = doc.selection(view.id).primary();
4138 let range = doc.selection(view.id).primary();
3952 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
4139 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
3953}
4140}
3954
4141
4142+fn remove_primary_selection(cx: &mut Context) {
4143+ let (view, doc) = current!(cx.editor);
4144+ // TODO: handle count
4145+
4146+ let selection = doc.selection(view.id);
4147+ if selection.len() == 1 {
4148+ cx.editor.set_error("no selections remaining".to_owned());
4149+ return;
4150+ }
4151+ let index = selection.primary_index();
4152+ let selection = selection.clone().remove(index);
4153+
4154+ doc.set_selection(view.id, selection);
4155+}
4156+
3955fn completion(cx: &mut Context) {
4157fn completion(cx: &mut Context) {
3956 // trigger on trigger char, or if user calls it
4158 // trigger on trigger char, or if user calls it
3957 // (or on word char typing??)
4159 // (or on word char typing??)
@@ -4259,6 +4461,12 @@ fn vsplit(cx: &mut Context) {
4259}
4461}
4260
4462
4261fn wclose(cx: &mut Context) {
4463fn wclose(cx: &mut Context) {
4464+ if cx.editor.tree.views().count() == 1 {
4465+ if let Err(err) = cmd::buffers_remaining_impl(cx.editor) {
4466+ cx.editor.set_error(err.to_string());
4467+ return;
4468+ }
4469+ }
4262 let view_id = view!(cx.editor).id;
4470 let view_id = view!(cx.editor).id;
4263 // close current split
4471 // close current split
4264 cx.editor.close(view_id, /* close_buffer */ false);
4472 cx.editor.close(view_id, /* close_buffer */ false);
@@ -4267,7 +4475,7 @@ fn wclose(cx: &mut Context) {
4267fn select_register(cx: &mut Context) {
4475fn select_register(cx: &mut Context) {
4268 cx.on_next_key(move |cx, event| {
4476 cx.on_next_key(move |cx, event| {
4269 if let Some(ch) = event.char() {
4477 if let Some(ch) = event.char() {
4270− cx.editor.selected_register.select(ch);
4478+ cx.editor.selected_register = Some(ch);
4271 }
4479 }
4272 })
4480 })
4273}
4481}
helix-term/src/job.rs+2−2
@@ -61,7 +61,7 @@ impl Jobs {
61 }
61 }
62
62
63 pub fn handle_callback(
63 pub fn handle_callback(
64− &mut self,
64+ &self,
65 editor: &mut Editor,
65 editor: &mut Editor,
66 compositor: &mut Compositor,
66 compositor: &mut Compositor,
67 call: anyhow::Result<Option<Callback>>,
67 call: anyhow::Result<Option<Callback>>,
@@ -84,7 +84,7 @@ impl Jobs {
84 }
84 }
85 }
85 }
86
86
87− pub fn add(&mut self, j: Job) {
87+ pub fn add(&self, j: Job) {
88 if j.wait {
88 if j.wait {
89 self.wait_futures.push(j.future);
89 self.wait_futures.push(j.future);
90 } else {
90 } else {
helix-term/src/keymap.rs+110−37
@@ -4,6 +4,7 @@ use helix_core::hashmap;
4use helix_view::{document::Mode, info::Info, input::KeyEvent};
4use helix_view::{document::Mode, info::Info, input::KeyEvent};
5use serde::Deserialize;
5use serde::Deserialize;
6use std::{
6use std::{
7+ borrow::Cow,
7 collections::HashMap,
8 collections::HashMap,
8 ops::{Deref, DerefMut},
9 ops::{Deref, DerefMut},
9};
10};
@@ -47,13 +48,13 @@ macro_rules! keymap {
47 };
48 };
48
49
49 (@trie
50 (@trie
50− { $label:literal $($($key:literal)|+ => $value:tt,)+ }
51+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
51 ) => {
52 ) => {
52− keymap!({ $label $($($key)|+ => $value,)+ })
53+ keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
53 };
54 };
54
55
55 (
56 (
56− { $label:literal $($($key:literal)|+ => $value:tt,)+ }
57+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
57 ) => {
58 ) => {
58 // modified from the hashmap! macro
59 // modified from the hashmap! macro
59 {
60 {
@@ -70,7 +71,9 @@ macro_rules! keymap {
70 _order.push(_key);
71 _order.push(_key);
71 )+
72 )+
72 )*
73 )*
73− $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
74+ let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
75+ $( _node.is_sticky = $sticky; )?
76+ $crate::keymap::KeyTrie::Node(_node)
74 }
77 }
75 };
78 };
76}
79}
@@ -84,6 +87,8 @@ pub struct KeyTrieNode {
84 map: HashMap<KeyEvent, KeyTrie>,
87 map: HashMap<KeyEvent, KeyTrie>,
85 #[serde(skip)]
88 #[serde(skip)]
86 order: Vec<KeyEvent>,
89 order: Vec<KeyEvent>,
90+ #[serde(skip)]
91+ pub is_sticky: bool,
87}
92}
88
93
89impl KeyTrieNode {
94impl KeyTrieNode {
@@ -92,6 +97,7 @@ impl KeyTrieNode {
92 name: name.to_string(),
97 name: name.to_string(),
93 map,
98 map,
94 order,
99 order,
100+ is_sticky: false,
95 }
101 }
96 }
102 }
97
103
@@ -119,12 +125,10 @@ impl KeyTrieNode {
119 }
125 }
120 }
126 }
121 }
127 }
122−}
123
128
124−impl From<KeyTrieNode> for Info {
129+ pub fn infobox(&self) -> Info {
125− fn from(node: KeyTrieNode) -> Self {
126− let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
130+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
127− for (&key, trie) in node.iter() {
131+ for (&key, trie) in self.iter() {
128 let desc = match trie {
132 let desc = match trie {
129 KeyTrie::Leaf(cmd) => cmd.doc(),
133 KeyTrie::Leaf(cmd) => cmd.doc(),
130 KeyTrie::Node(n) => n.name(),
134 KeyTrie::Node(n) => n.name(),
@@ -136,16 +140,16 @@ impl From<KeyTrieNode> for Info {
136 }
140 }
137 }
141 }
138 body.sort_unstable_by_key(|(_, keys)| {
142 body.sort_unstable_by_key(|(_, keys)| {
139− node.order.iter().position(|&k| k == keys[0]).unwrap()
143+ self.order.iter().position(|&k| k == keys[0]).unwrap()
140 });
144 });
141− let prefix = format!("{} ", node.name());
145+ let prefix = format!("{} ", self.name());
142 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
146 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
143 body = body
147 body = body
144 .into_iter()
148 .into_iter()
145 .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
149 .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
146 .collect();
150 .collect();
147 }
151 }
148− Info::new(node.name(), body)
152+ Info::new(self.name(), body)
149 }
153 }
150}
154}
151
155
@@ -218,7 +222,7 @@ impl KeyTrie {
218}
222}
219
223
220#[derive(Debug, Clone, PartialEq)]
224#[derive(Debug, Clone, PartialEq)]
221−pub enum KeymapResult {
225+pub enum KeymapResultKind {
222 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
226 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
223 Pending(KeyTrieNode),
227 Pending(KeyTrieNode),
224 Matched(Command),
228 Matched(Command),
@@ -229,14 +233,31 @@ pub enum KeymapResult {
229 Cancelled(Vec<KeyEvent>),
233 Cancelled(Vec<KeyEvent>),
230}
234}
231
235
236+/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
237+/// reference to the sticky node if one is currently active.
238+pub struct KeymapResult<'a> {
239+ pub kind: KeymapResultKind,
240+ pub sticky: Option<&'a KeyTrieNode>,
241+}
242+
243+impl<'a> KeymapResult<'a> {
244+ pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self {
245+ Self { kind, sticky }
246+ }
247+}
248+
232#[derive(Debug, Clone, PartialEq, Deserialize)]
249#[derive(Debug, Clone, PartialEq, Deserialize)]
233pub struct Keymap {
250pub struct Keymap {
234 /// Always a Node
251 /// Always a Node
235 #[serde(flatten)]
252 #[serde(flatten)]
236 root: KeyTrie,
253 root: KeyTrie,
237− /// Stores pending keys waiting for the next key
254+ /// Stores pending keys waiting for the next key. This is relative to a
255+ /// sticky node if one is in use.
238 #[serde(skip)]
256 #[serde(skip)]
239 state: Vec<KeyEvent>,
257 state: Vec<KeyEvent>,
258+ /// Stores the sticky node if one is activated.
259+ #[serde(skip)]
260+ sticky: Option<KeyTrieNode>,
240}
261}
241
262
242impl Keymap {
263impl Keymap {
@@ -244,6 +265,7 @@ impl Keymap {
244 Keymap {
265 Keymap {
245 root,
266 root,
246 state: Vec::new(),
267 state: Vec::new(),
268+ sticky: None,
247 }
269 }
248 }
270 }
249
271
@@ -251,27 +273,61 @@ impl Keymap {
251 &self.root
273 &self.root
252 }
274 }
253
275
276+ pub fn sticky(&self) -> Option<&KeyTrieNode> {
277+ self.sticky.as_ref()
278+ }
279+
254 /// Returns list of keys waiting to be disambiguated.
280 /// Returns list of keys waiting to be disambiguated.
255 pub fn pending(&self) -> &[KeyEvent] {
281 pub fn pending(&self) -> &[KeyEvent] {
256 &self.state
282 &self.state
257 }
283 }
258
284
259− /// Lookup `key` in the keymap to try and find a command to execute
285+ /// Lookup `key` in the keymap to try and find a command to execute. Escape
286+ /// key cancels pending keystrokes. If there are no pending keystrokes but a
287+ /// sticky node is in use, it will be cleared.
260 pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
288 pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
289+ if let key!(Esc) = key {
290+ if !self.state.is_empty() {
291+ return KeymapResult::new(
292+ // Note that Esc is not included here
293+ KeymapResultKind::Cancelled(self.state.drain(..).collect()),
294+ self.sticky(),
295+ );
296+ }
297+ self.sticky = None;
298+ }
299+
261− let &first = self.state.get(0).unwrap_or(&key);
300+ let first = self.state.get(0).unwrap_or(&key);
262− let trie = match self.root.search(&[first]) {
301+ let trie_node = match self.sticky {
302+ Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
303+ None => Cow::Borrowed(&self.root),
304+ };
305+
306+ let trie = match trie_node.search(&[*first]) {
263− Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
307+ Some(&KeyTrie::Leaf(cmd)) => {
308+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
309+ }
264− None => return KeymapResult::NotFound,
310+ None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
265 Some(t) => t,
311 Some(t) => t,
266 };
312 };
313+
267 self.state.push(key);
314 self.state.push(key);
268 match trie.search(&self.state[1..]) {
315 match trie.search(&self.state[1..]) {
269− Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
316+ Some(&KeyTrie::Node(ref map)) => {
317+ if map.is_sticky {
318+ self.state.clear();
319+ self.sticky = Some(map.clone());
320+ }
321+ KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
322+ }
270− Some(&KeyTrie::Leaf(command)) => {
323+ Some(&KeyTrie::Leaf(cmd)) => {
271 self.state.clear();
324 self.state.clear();
272− KeymapResult::Matched(command)
325+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
273 }
326 }
327+ None => KeymapResult::new(
274− None => KeymapResult::Cancelled(self.state.drain(..).collect()),
328+ KeymapResultKind::Cancelled(self.state.drain(..).collect()),
329+ self.sticky(),
330+ ),
275 }
331 }
276 }
332 }
277
333
@@ -380,7 +436,6 @@ impl Default for Keymaps {
380 "A" => append_to_line,
436 "A" => append_to_line,
381 "o" => open_below,
437 "o" => open_below,
382 "O" => open_above,
438 "O" => open_above,
383− // [<space> ]<space> equivalents too (add blank new line, no edit)
384
439
385 "d" => delete_selection,
440 "d" => delete_selection,
386 // TODO: also delete without yanking
441 // TODO: also delete without yanking
@@ -440,12 +495,11 @@ impl Default for Keymaps {
440 "<" => unindent,
495 "<" => unindent,
441 "=" => format_selections,
496 "=" => format_selections,
442 "J" => join_selections,
497 "J" => join_selections,
443− // TODO: conflicts hover/doc
444 "K" => keep_selections,
498 "K" => keep_selections,
445 // TODO: and another method for inverse
499 // TODO: and another method for inverse
446
500
447− // TODO: clashes with space mode
448− "space" => keep_primary_selection,
501+ "," => keep_primary_selection,
502+ "A-," => remove_primary_selection,
449
503
450 // "q" => record_macro,
504 // "q" => record_macro,
451 // "Q" => replay_macro,
505 // "Q" => replay_macro,
@@ -473,7 +527,6 @@ impl Default for Keymaps {
473
527
474 // move under <space>c
528 // move under <space>c
475 "C-c" => toggle_comments,
529 "C-c" => toggle_comments,
476− "K" => hover,
477
530
478 // z family for save/restore/combine from/to sels from register
531 // z family for save/restore/combine from/to sels from register
479
532
@@ -516,7 +569,8 @@ impl Default for Keymaps {
516 "p" => paste_clipboard_after,
569 "p" => paste_clipboard_after,
517 "P" => paste_clipboard_before,
570 "P" => paste_clipboard_before,
518 "R" => replace_selections_with_clipboard,
571 "R" => replace_selections_with_clipboard,
519− "space" => keep_primary_selection,
572+ "/" => global_search,
573+ "k" => hover,
520 },
574 },
521 "z" => { "View"
575 "z" => { "View"
522 "z" | "c" => align_view_center,
576 "z" | "c" => align_view_center,
@@ -525,6 +579,22 @@ impl Default for Keymaps {
525 "m" => align_view_middle,
579 "m" => align_view_middle,
526 "k" => scroll_up,
580 "k" => scroll_up,
527 "j" => scroll_down,
581 "j" => scroll_down,
582+ "b" => page_up,
583+ "f" => page_down,
584+ "u" => half_page_up,
585+ "d" => half_page_down,
586+ },
587+ "Z" => { "View" sticky=true
588+ "z" | "c" => align_view_center,
589+ "t" => align_view_top,
590+ "b" => align_view_bottom,
591+ "m" => align_view_middle,
592+ "k" => scroll_up,
593+ "j" => scroll_down,
594+ "b" => page_up,
595+ "f" => page_down,
596+ "u" => half_page_up,
597+ "d" => half_page_down,
528 },
598 },
529
599
530 "\"" => select_register,
600 "\"" => select_register,
@@ -545,14 +615,17 @@ impl Default for Keymaps {
545 "w" => extend_next_word_start,
615 "w" => extend_next_word_start,
546 "b" => extend_prev_word_start,
616 "b" => extend_prev_word_start,
547 "e" => extend_next_word_end,
617 "e" => extend_next_word_end,
618+ "W" => extend_next_long_word_start,
619+ "B" => extend_prev_long_word_start,
620+ "E" => extend_next_long_word_end,
548
621
549 "t" => extend_till_char,
622 "t" => extend_till_char,
550 "f" => extend_next_char,
623 "f" => extend_next_char,
551 "T" => extend_till_prev_char,
624 "T" => extend_till_prev_char,
552 "F" => extend_prev_char,
625 "F" => extend_prev_char,
553
626
554− "home" => goto_line_start,
627+ "home" => extend_to_line_start,
555− "end" => goto_line_end,
628+ "end" => extend_to_line_end,
556 "esc" => exit_select_mode,
629 "esc" => exit_select_mode,
557
630
558 "v" => normal_mode,
631 "v" => normal_mode,
@@ -617,19 +690,19 @@ fn merge_partial_keys() {
617
690
618 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
691 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
619 assert_eq!(
692 assert_eq!(
620− keymap.get(key!('i')),
693+ keymap.get(key!('i')).kind,
621− KeymapResult::Matched(Command::normal_mode),
694+ KeymapResultKind::Matched(Command::normal_mode),
622 "Leaf should replace leaf"
695 "Leaf should replace leaf"
623 );
696 );
624 assert_eq!(
697 assert_eq!(
625− keymap.get(key!('无')),
698+ keymap.get(key!('无')).kind,
626− KeymapResult::Matched(Command::insert_mode),
699+ KeymapResultKind::Matched(Command::insert_mode),
627 "New leaf should be present in merged keymap"
700 "New leaf should be present in merged keymap"
628 );
701 );
629 // Assumes that z is a node in the default keymap
702 // Assumes that z is a node in the default keymap
630 assert_eq!(
703 assert_eq!(
631− keymap.get(key!('z')),
704+ keymap.get(key!('z')).kind,
632− KeymapResult::Matched(Command::jump_backward),
705+ KeymapResultKind::Matched(Command::jump_backward),
633 "Leaf should replace node"
706 "Leaf should replace node"
634 );
707 );
635 // Assumes that `g` is a node in default keymap
708 // Assumes that `g` is a node in default keymap
helix-term/src/ui/completion.rs+31−14
@@ -262,8 +262,7 @@ impl Component for Completion {
262 .cursor(doc.text().slice(..));
262 .cursor(doc.text().slice(..));
263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
264 - view.offset.row) as u16;
264 - view.offset.row) as u16;
265−
266− let mut doc = match &option.documentation {
265+ let mut markdown_doc = match &option.documentation {
267 Some(lsp::Documentation::String(contents))
266 Some(lsp::Documentation::String(contents))
268 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
267 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
269 kind: lsp::MarkupKind::PlainText,
268 kind: lsp::MarkupKind::PlainText,
@@ -311,24 +310,42 @@ impl Component for Completion {
311 None => return,
310 None => return,
312 };
311 };
313
312
314− let half = area.height / 2;
313+ let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
315− let height = 15.min(half);
314+ let (popup_width, _popup_height) = self.popup.get_size();
316− // we want to make sure the cursor is visible (not hidden behind the documentation)
315+ let mut width = area
317− let y = if cursor_pos + area.y
316+ .width
318− >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
317+ .saturating_sub(popup_x)
319− {
318+ .saturating_sub(popup_width);
320− 0
319+ let area = if width > 30 {
320+ let mut height = area.height.saturating_sub(popup_y);
321+ let x = popup_x + popup_width;
322+ let y = popup_y;
323+
324+ if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
325+ width = rel_width;
326+ height = rel_height;
327+ }
328+ Rect::new(x, y, width, height)
321 } else {
329 } else {
330+ let half = area.height / 2;
331+ let height = 15.min(half);
332+ // we want to make sure the cursor is visible (not hidden behind the documentation)
333+ let y = if cursor_pos + area.y
334+ >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
335+ {
336+ 0
337+ } else {
322− // -2 to subtract command line + statusline. a bit of a hack, because of splits.
338+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
323− area.height.saturating_sub(height).saturating_sub(2)
339+ area.height.saturating_sub(height).saturating_sub(2)
324− };
340+ };
325
341
326− let area = Rect::new(0, y, area.width, height);
342+ Rect::new(0, y, area.width, height)
343+ };
327
344
328 // clear area
345 // clear area
329 let background = cx.editor.theme.get("ui.popup");
346 let background = cx.editor.theme.get("ui.popup");
330 surface.clear_with(area, background);
347 surface.clear_with(area, background);
331− doc.render(area, surface, cx);
348+ markdown_doc.render(area, surface, cx);
332 }
349 }
333 }
350 }
334}
351}
helix-term/src/ui/editor.rs+23−22
@@ -3,7 +3,7 @@ use crate::{
3 compositor::{Component, Compositor, Context, EventResult},
3 compositor::{Component, Compositor, Context, EventResult},
4 job::Callback,
4 job::Callback,
5 key,
5 key,
6− keymap::{KeymapResult, Keymaps},
6+ keymap::{KeymapResult, KeymapResultKind, Keymaps},
7 ui::{Completion, ProgressSpinners},
7 ui::{Completion, ProgressSpinners},
8};
8};
9
9
@@ -165,8 +165,7 @@ impl EditorView {
165 let scopes = theme.scopes();
165 let scopes = theme.scopes();
166 syntax
166 syntax
167 .highlight_iter(text.slice(..), Some(range), None, |language| {
167 .highlight_iter(text.slice(..), Some(range), None, |language| {
168− loader
168+ loader.language_configuration_for_injection_string(language)
169− .language_config_for_scope(&format!("source.{}", language))
170 .and_then(|language_config| {
169 .and_then(|language_config| {
171 let config = language_config.highlight_config(scopes)?;
170 let config = language_config.highlight_config(scopes)?;
172 let config_ref = config.as_ref();
171 let config_ref = config.as_ref();
@@ -852,7 +851,7 @@ impl EditorView {
852
851
853 /// Handle events by looking them up in `self.keymaps`. Returns None
852 /// Handle events by looking them up in `self.keymaps`. Returns None
854 /// if event was handled (a command was executed or a subkeymap was
853 /// if event was handled (a command was executed or a subkeymap was
855− /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
854+ /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
856 /// otherwise.
855 /// otherwise.
857 fn handle_keymap_event(
856 fn handle_keymap_event(
858 &mut self,
857 &mut self,
@@ -860,8 +859,6 @@ impl EditorView {
860 cxt: &mut commands::Context,
859 cxt: &mut commands::Context,
861 event: KeyEvent,
860 event: KeyEvent,
862 ) -> Option<KeymapResult> {
861 ) -> Option<KeymapResult> {
863− self.autoinfo = None;
864−
865 if let Some(picker) = cxt.editor.debug_config_picker.clone() {
862 if let Some(picker) = cxt.editor.debug_config_picker.clone() {
866 match event {
863 match event {
867 KeyEvent {
864 KeyEvent {
@@ -912,29 +909,32 @@ impl EditorView {
912 return None;
909 return None;
913 }
910 }
914
911
915− match self.keymaps.get_mut(&mode).unwrap().get(event) {
912+ let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
913+ self.autoinfo = key_result.sticky.map(|node| node.infobox());
914+
915+ match &key_result.kind {
916− KeymapResult::Matched(command) => command.execute(cxt),
916+ KeymapResultKind::Matched(command) => command.execute(cxt),
917− KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
917+ KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
918− k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
918+ KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
919 }
919 }
920 None
920 None
921 }
921 }
922
922
923 fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
923 fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
924 if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
924 if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
925− match keyresult {
925+ match keyresult.kind {
926− KeymapResult::NotFound => {
926+ KeymapResultKind::NotFound => {
927 if let Some(ch) = event.char() {
927 if let Some(ch) = event.char() {
928 commands::insert::insert_char(cx, ch)
928 commands::insert::insert_char(cx, ch)
929 }
929 }
930 }
930 }
931− KeymapResult::Cancelled(pending) => {
931+ KeymapResultKind::Cancelled(pending) => {
932 for ev in pending {
932 for ev in pending {
933 match ev.char() {
933 match ev.char() {
934 Some(ch) => commands::insert::insert_char(cx, ch),
934 Some(ch) => commands::insert::insert_char(cx, ch),
935 None => {
935 None => {
936− if let KeymapResult::Matched(command) =
936+ if let KeymapResultKind::Matched(command) =
937− self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
937+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
938 {
938 {
939 command.execute(cx);
939 command.execute(cx);
940 }
940 }
@@ -972,7 +972,7 @@ impl EditorView {
972 // debug_assert!(cxt.count != 0);
972 // debug_assert!(cxt.count != 0);
973
973
974 // set the register
974 // set the register
975− cxt.selected_register = cxt.editor.selected_register.take();
975+ cxt.register = cxt.editor.selected_register.take();
976
976
977 self.handle_keymap_event(mode, cxt, event);
977 self.handle_keymap_event(mode, cxt, event);
978 if self.keymaps.pending().is_empty() {
978 if self.keymaps.pending().is_empty() {
@@ -1196,9 +1196,9 @@ impl EditorView {
1196impl Component for EditorView {
1196impl Component for EditorView {
1197 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
1197 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
1198 let mut cxt = commands::Context {
1198 let mut cxt = commands::Context {
1199− selected_register: helix_view::RegisterSelection::default(),
1200 editor: &mut cx.editor,
1199 editor: &mut cx.editor,
1201 count: None,
1200 count: None,
1201+ register: None,
1202 callback: None,
1202 callback: None,
1203 on_next_key_callback: None,
1203 on_next_key_callback: None,
1204 jobs: cx.jobs,
1204 jobs: cx.jobs,
@@ -1288,11 +1288,12 @@ impl Component for EditorView {
1288 // how we entered insert mode is important, and we should track that so
1288 // how we entered insert mode is important, and we should track that so
1289 // we can repeat the side effect.
1289 // we can repeat the side effect.
1290
1290
1291+ self.last_insert.0 =
1291− self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
1292+ match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
1292− KeymapResult::Matched(command) => command,
1293+ KeymapResultKind::Matched(command) => command,
1293− // FIXME: insert mode can only be entered through single KeyCodes
1294+ // FIXME: insert mode can only be entered through single KeyCodes
1294− _ => unimplemented!(),
1295+ _ => unimplemented!(),
1295− };
1296+ };
1296 self.last_insert.1.clear();
1297 self.last_insert.1.clear();
1297 }
1298 }
1298 (Mode::Insert, Mode::Normal) => {
1299 (Mode::Insert, Mode::Normal) => {
helix-term/src/ui/markdown.rs+25−5
@@ -88,7 +88,7 @@ fn parse<'a>(
88 if let Some(theme) = theme {
88 if let Some(theme) = theme {
89 let rope = Rope::from(text.as_ref());
89 let rope = Rope::from(text.as_ref());
90 let syntax = loader
90 let syntax = loader
91− .language_config_for_scope(&format!("source.{}", language))
91+ .language_configuration_for_injection_string(language)
92 .and_then(|config| config.highlight_config(theme.scopes()))
92 .and_then(|config| config.highlight_config(theme.scopes()))
93 .map(|config| Syntax::new(&rope, config));
93 .map(|config| Syntax::new(&rope, config));
94
94
@@ -215,10 +215,30 @@ impl Component for Markdown {
215 }
215 }
216
216
217 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
217 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
218− let contents = parse(&self.contents, None, &self.config_loader);
219 let padding = 2;
218 let padding = 2;
220− let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
219+ if padding >= viewport.1 || padding >= viewport.0 {
221− let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
220+ return None;
221+ }
222+ let contents = parse(&self.contents, None, &self.config_loader);
223+ let max_text_width = (viewport.0 - padding).min(120);
224+ let mut text_width = 0;
225+ let mut height = padding;
226+ for content in contents {
227+ height += 1;
228+ let content_width = content.width() as u16;
229+ if content_width > max_text_width {
230+ text_width = max_text_width;
231+ height += content_width / max_text_width;
232+ } else if content_width > text_width {
233+ text_width = content_width;
234+ }
235+
236+ if height >= viewport.1 {
237+ height = viewport.1;
238+ break;
239+ }
240+ }
241+
222− Some((width, height))
242+ Some((text_width + padding, height))
223 }
243 }
224}
244}
helix-term/src/ui/menu.rs+43−33
@@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
33
33
34 scroll: usize,
34 scroll: usize,
35 size: (u16, u16),
35 size: (u16, u16),
36+ viewport: (u16, u16),
37+ recalculate: bool,
36}
38}
37
39
38impl<T: Item> Menu<T> {
40impl<T: Item> Menu<T> {
@@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
51 callback_fn: Box::new(callback_fn),
53 callback_fn: Box::new(callback_fn),
52 scroll: 0,
54 scroll: 0,
53 size: (0, 0),
55 size: (0, 0),
56+ viewport: (0, 0),
57+ recalculate: true,
54 };
58 };
55
59
56 // TODO: scoring on empty input should just use a fastpath
60 // TODO: scoring on empty input should just use a fastpath
@@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
83 // reset cursor position
87 // reset cursor position
84 self.cursor = None;
88 self.cursor = None;
85 self.scroll = 0;
89 self.scroll = 0;
90+ self.recalculate = true;
86 }
91 }
87
92
88 pub fn move_up(&mut self) {
93 pub fn move_up(&mut self) {
@@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
99 self.adjust_scroll();
104 self.adjust_scroll();
100 }
105 }
101
106
107+ fn recalculate_size(&mut self, viewport: (u16, u16)) {
108+ let n = self
109+ .options
110+ .first()
111+ .map(|option| option.row().cells.len())
112+ .unwrap_or_default();
113+ let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
114+ let row = option.row();
115+ // maintain max for each column
116+ for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
117+ let width = cell.content.width();
118+ if width > *acc {
119+ *acc = width;
120+ }
121+ }
122+
123+ acc
124+ });
125+ let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
126+ let width = len.min(viewport.0 as usize);
127+
128+ self.widths = max_lens
129+ .into_iter()
130+ .map(|len| Constraint::Length(len as u16))
131+ .collect();
132+
133+ let height = self.matches.len().min(10).min(viewport.1 as usize);
134+
135+ self.size = (width as u16, height as u16);
136+
137+ // adjust scroll offsets if size changed
138+ self.adjust_scroll();
139+ self.recalculate = false;
140+ }
141+
102 fn adjust_scroll(&mut self) {
142 fn adjust_scroll(&mut self) {
103 let win_height = self.size.1 as usize;
143 let win_height = self.size.1 as usize;
104 if let Some(cursor) = self.cursor {
144 if let Some(cursor) = self.cursor {
@@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> {
221 }
261 }
222
262
223 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
263 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
224− let n = self
264+ if viewport != self.viewport || self.recalculate {
225− .options
265+ self.recalculate_size(viewport);
226− .first()
266+ }
227− .map(|option| option.row().cells.len())
228− .unwrap_or_default();
229− let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
230− let row = option.row();
231− // maintain max for each column
232− for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
233− let width = cell.content.width();
234− if width > *acc {
235− *acc = width;
236− }
237− }
238−
239− acc
240− });
241− let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
242− let width = len.min(viewport.0 as usize);
243−
244− self.widths = max_lens
245− .into_iter()
246− .map(|len| Constraint::Length(len as u16))
247− .collect();
248−
249− let height = self.options.len().min(10).min(viewport.1 as usize);
250−
251− self.size = (width as u16, height as u16);
252−
253− // adjust scroll offsets if size changed
254− self.adjust_scroll();
255
267
256 Some(self.size)
268 Some(self.size)
257 }
269 }
258
270
259− // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
260−
261 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
271 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
262 let theme = &cx.editor.theme;
272 let theme = &cx.editor.theme;
263 let style = theme
273 let style = theme
helix-term/src/ui/mod.rs+23−6
@@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner};
20pub use text::Text;
20pub use text::Text;
21
21
22use helix_core::regex::Regex;
22use helix_core::regex::Regex;
23−use helix_core::register::Registers;
23+use helix_core::regex::RegexBuilder;
24use helix_view::{Document, Editor, View};
24use helix_view::{Document, Editor, View};
25
25
26use std::path::PathBuf;
26use std::path::PathBuf;
@@ -28,7 +28,8 @@ use std::path::PathBuf;
28pub fn regex_prompt(
28pub fn regex_prompt(
29 cx: &mut crate::commands::Context,
29 cx: &mut crate::commands::Context,
30 prompt: std::borrow::Cow<'static, str>,
30 prompt: std::borrow::Cow<'static, str>,
31+ history_register: Option<char>,
31− fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
32+ fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
32) -> Prompt {
33) -> Prompt {
33 let (view, doc) = current!(cx.editor);
34 let (view, doc) = current!(cx.editor);
34 let view_id = view.id;
35 let view_id = view.id;
@@ -36,7 +37,7 @@ pub fn regex_prompt(
36
37
37 Prompt::new(
38 Prompt::new(
38 prompt,
39 prompt,
39− None,
40+ history_register,
40 |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
41 |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
41 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
42 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
42 match event {
43 match event {
@@ -46,6 +47,14 @@ pub fn regex_prompt(
46 }
47 }
47 PromptEvent::Validate => {
48 PromptEvent::Validate => {
48 // TODO: push_jump to store selection just before jump
49 // TODO: push_jump to store selection just before jump
50+
51+ match Regex::new(input) {
52+ Ok(regex) => {
53+ let (view, doc) = current!(cx.editor);
54+ fun(view, doc, regex, event);
55+ }
56+ Err(_err) => (), // TODO: mark command line as error
57+ }
49 }
58 }
50 PromptEvent::Update => {
59 PromptEvent::Update => {
51 // skip empty input, TODO: trigger default
60 // skip empty input, TODO: trigger default
@@ -53,15 +62,23 @@ pub fn regex_prompt(
53 return;
62 return;
54 }
63 }
55
64
65+ let case_insensitive = if cx.editor.config.smart_case {
66+ !input.chars().any(char::is_uppercase)
67+ } else {
68+ false
69+ };
70+
56− match Regex::new(input) {
71+ match RegexBuilder::new(input)
72+ .case_insensitive(case_insensitive)
73+ .build()
74+ {
57 Ok(regex) => {
75 Ok(regex) => {
58 let (view, doc) = current!(cx.editor);
76 let (view, doc) = current!(cx.editor);
59− let registers = &mut cx.editor.registers;
60
77
61 // revert state to what it was before the last update
78 // revert state to what it was before the last update
62 doc.set_selection(view.id, snapshot.clone());
79 doc.set_selection(view.id, snapshot.clone());
63
80
64− fun(view, doc, registers, regex);
81+ fun(view, doc, regex, event);
65
82
66 view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
83 view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
67 }
84 }
helix-term/src/ui/picker.rs+13−12
@@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> {
124 }) {
124 }) {
125 // align to middle
125 // align to middle
126 let first_line = line
126 let first_line = line
127− .map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines())))
127+ .map(|(start, end)| {
128− .map(|(start, _)| start)
128+ let height = end.saturating_sub(start) + 1;
129− .unwrap_or(0)
129+ let middle = start + (height.saturating_sub(1) / 2);
130− .saturating_sub(inner.height as usize / 2);
130+ middle.saturating_sub(inner.height as usize / 2).min(start)
131+ })
132+ .unwrap_or(0);
133+
131 let offset = Position::new(first_line, 0);
134 let offset = Position::new(first_line, 0);
132
135
133 let highlights = EditorView::doc_syntax_highlights(
136 let highlights = EditorView::doc_syntax_highlights(
@@ -268,17 +271,15 @@ impl<T> Picker<T> {
268 }
271 }
269
272
270 pub fn move_up(&mut self) {
273 pub fn move_up(&mut self) {
274+ let len = self.matches.len();
271− self.cursor = self.cursor.saturating_sub(1);
275+ let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
276+ self.cursor = pos;
272 }
277 }
273
278
274 pub fn move_down(&mut self) {
279 pub fn move_down(&mut self) {
275− if self.matches.is_empty() {
280+ let len = self.matches.len();
276− return;
281+ let pos = (self.cursor + 1) % len;
277− }
278−
279− if self.cursor < self.matches.len() - 1 {
280− self.cursor += 1;
282+ self.cursor = pos;
281− }
282 }
283 }
283
284
284 pub fn selection(&self) -> Option<&T> {
285 pub fn selection(&self) -> Option<&T> {
helix-term/src/ui/popup.rs+38−23
@@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
16}
16}
17
17
18impl<T: Component> Popup<T> {
18impl<T: Component> Popup<T> {
19− // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
20− // rendering)
21 pub fn new(contents: T) -> Self {
19 pub fn new(contents: T) -> Self {
22 Self {
20 Self {
23 contents,
21 contents,
@@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
31 self.position = pos;
29 self.position = pos;
32 }
30 }
33
31
32+ pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
33+ let position = self
34+ .position
35+ .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
36+
37+ let (width, height) = self.size;
38+
39+ // if there's a orientation preference, use that
40+ // if we're on the top part of the screen, do below
41+ // if we're on the bottom part, do above
42+
43+ // -- make sure frame doesn't stick out of bounds
44+ let mut rel_x = position.col as u16;
45+ let mut rel_y = position.row as u16;
46+ if viewport.width <= rel_x + width {
47+ rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
48+ }
49+
50+ // TODO: be able to specify orientation preference. We want above for most popups, below
51+ // for menus/autocomplete.
52+ if viewport.height > rel_y + height {
53+ rel_y += 1 // position below point
54+ } else {
55+ rel_y = rel_y.saturating_sub(height) // position above point
56+ }
57+
58+ (rel_x, rel_y)
59+ }
60+
61+ pub fn get_size(&self) -> (u16, u16) {
62+ (self.size.0, self.size.1)
63+ }
64+
34 pub fn scroll(&mut self, offset: usize, direction: bool) {
65 pub fn scroll(&mut self, offset: usize, direction: bool) {
35 if direction {
66 if direction {
36 self.scroll += offset;
67 self.scroll += offset;
@@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> {
106 }
137 }
107
138
108 fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
139 fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
109− cx.scroll = Some(self.scroll);
140+ // trigger required_size so we recalculate if the child changed
141+ self.required_size((viewport.width, viewport.height));
110
142
111− let position = self
143+ cx.scroll = Some(self.scroll);
112− .position
113− .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
114−
115− let (width, height) = self.size;
116−
117− // -- make sure frame doesn't stick out of bounds
118− let mut rel_x = position.col as u16;
119− let mut rel_y = position.row as u16;
120− if viewport.width <= rel_x + width {
121− rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
122− };
123
144
124− // TODO: be able to specify orientation preference. We want above for most popups, below
145+ let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
125− // for menus/autocomplete.
126− if height <= rel_y {
127− rel_y = rel_y.saturating_sub(height) // position above point
128− } else {
129− rel_y += 1 // position below point
130− }
131
146
132 // clip to viewport
147 // clip to viewport
133− let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));
148+ let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
134
149
135 // clear area
150 // clear area
136 let background = cx.editor.theme.get("ui.popup");
151 let background = cx.editor.theme.get("ui.popup");
helix-term/src/ui/text.rs+15−5
@@ -5,11 +5,17 @@ use helix_view::graphics::Rect;
5
5
6pub struct Text {
6pub struct Text {
7 contents: String,
7 contents: String,
8+ size: (u16, u16),
9+ viewport: (u16, u16),
8}
10}
9
11
10impl Text {
12impl Text {
11 pub fn new(contents: String) -> Self {
13 pub fn new(contents: String) -> Self {
14+ Self {
12− Self { contents }
15+ contents,
16+ size: (0, 0),
17+ viewport: (0, 0),
18+ }
13 }
19 }
14}
20}
15impl Component for Text {
21impl Component for Text {
@@ -24,9 +30,13 @@ impl Component for Text {
24 }
30 }
25
31
26 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
32 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
33+ if viewport != self.viewport {
27− let contents = tui::text::Text::from(self.contents.clone());
34+ let contents = tui::text::Text::from(self.contents.clone());
28− let width = std::cmp::min(contents.width() as u16, viewport.0);
35+ let width = std::cmp::min(contents.width() as u16, viewport.0);
29− let height = std::cmp::min(contents.height() as u16, viewport.1);
36+ let height = std::cmp::min(contents.height() as u16, viewport.1);
30− Some((width, height))
37+ self.size = (width, height);
38+ self.viewport = viewport;
39+ }
40+ Some(self.size)
31 }
41 }
32}
42}
helix-view/src/document.rs+67−16
@@ -386,21 +386,24 @@ impl Document {
386 /// If supported, returns the changes that should be applied to this document in order
386 /// If supported, returns the changes that should be applied to this document in order
387 /// to format it nicely.
387 /// to format it nicely.
388 pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
388 pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
389− if let Some(language_server) = self.language_server.clone() {
389+ if let Some(language_server) = self.language_server() {
390 let text = self.text.clone();
390 let text = self.text.clone();
391+ let offset_encoding = language_server.offset_encoding();
392+ let request = language_server.text_document_formatting(
391− let id = self.identifier();
393+ self.identifier(),
394+ lsp::FormattingOptions::default(),
395+ None,
396+ )?;
397+
392 let fut = async move {
398 let fut = async move {
393− let edits = language_server
394− .text_document_formatting(id, lsp::FormattingOptions::default(), None)
395− .await
396− .unwrap_or_else(|e| {
399+ let edits = request.await.unwrap_or_else(|e| {
397− log::warn!("LSP formatting failed: {}", e);
400+ log::warn!("LSP formatting failed: {}", e);
398− Default::default()
401+ Default::default()
399− });
402+ });
400 LspFormatting {
403 LspFormatting {
401 doc: text,
404 doc: text,
402 edits,
405 edits,
403− offset_encoding: language_server.offset_encoding(),
406+ offset_encoding,
404 }
407 }
405 };
408 };
406 Some(fut)
409 Some(fut)
@@ -469,9 +472,14 @@ impl Document {
469 to_writer(&mut file, encoding, &text).await?;
472 to_writer(&mut file, encoding, &text).await?;
470
473
471 if let Some(language_server) = language_server {
474 if let Some(language_server) = language_server {
472− language_server
475+ if !language_server.is_initialized() {
476+ return Ok(());
477+ }
478+ if let Some(notification) =
473− .text_document_did_save(identifier, &text)
479+ language_server.text_document_did_save(identifier, &text)
480+ {
474− .await?;
481+ notification.await?;
482+ }
475 }
483 }
476
484
477 Ok(())
485 Ok(())
@@ -646,7 +654,7 @@ impl Document {
646 // }
654 // }
647
655
648 // emit lsp notification
656 // emit lsp notification
649− if let Some(language_server) = &self.language_server {
657+ if let Some(language_server) = self.language_server() {
650 let notify = language_server.text_document_did_change(
658 let notify = language_server.text_document_did_change(
651 self.versioned_identifier(),
659 self.versioned_identifier(),
652 &old_doc,
660 &old_doc,
@@ -795,9 +803,18 @@ impl Document {
795 self.version
803 self.version
796 }
804 }
797
805
798− #[inline]
799 pub fn language_server(&self) -> Option<&helix_lsp::Client> {
806 pub fn language_server(&self) -> Option<&helix_lsp::Client> {
800− self.language_server.as_deref()
807+ let server = self.language_server.as_deref();
808+ let initialized = server
809+ .map(|server| server.is_initialized())
810+ .unwrap_or(false);
811+
812+ // only resolve language_server if it's initialized
813+ if initialized {
814+ server
815+ } else {
816+ None
817+ }
801 }
818 }
802
819
803 #[inline]
820 #[inline]
@@ -891,6 +908,40 @@ impl Default for Document {
891mod test {
908mod test {
892 use super::*;
909 use super::*;
893
910
911+ #[test]
912+ fn changeset_to_changes_ignore_line_endings() {
913+ use helix_lsp::{lsp, Client, OffsetEncoding};
914+ let text = Rope::from("hello\r\nworld");
915+ let mut doc = Document::from(text, None);
916+ let view = ViewId::default();
917+ doc.set_selection(view, Selection::single(0, 0));
918+
919+ let transaction =
920+ Transaction::change(doc.text(), vec![(5, 7, Some("\n".into()))].into_iter());
921+ let old_doc = doc.text().clone();
922+ doc.apply(&transaction, view);
923+ let changes = Client::changeset_to_changes(
924+ &old_doc,
925+ doc.text(),
926+ transaction.changes(),
927+ OffsetEncoding::Utf8,
928+ );
929+
930+ assert_eq!(doc.text(), "hello\nworld");
931+
932+ assert_eq!(
933+ changes,
934+ &[lsp::TextDocumentContentChangeEvent {
935+ range: Some(lsp::Range::new(
936+ lsp::Position::new(0, 5),
937+ lsp::Position::new(1, 0)
938+ )),
939+ text: "\n".into(),
940+ range_length: None,
941+ }]
942+ );
943+ }
944+
894 #[test]
945 #[test]
895 fn changeset_to_changes() {
946 fn changeset_to_changes() {
896 use helix_lsp::{lsp, Client, OffsetEncoding};
947 use helix_lsp::{lsp, Client, OffsetEncoding};
helix-view/src/editor.rs+31−21
@@ -3,7 +3,7 @@ use crate::{
3 graphics::{CursorKind, Rect},
3 graphics::{CursorKind, Rect},
4 theme::{self, Theme},
4 theme::{self, Theme},
5 tree::Tree,
5 tree::Tree,
6− Document, DocumentId, RegisterSelection, View, ViewId,
6+ Document, DocumentId, View, ViewId,
7};
7};
8
8
9use futures_util::future;
9use futures_util::future;
@@ -44,6 +44,10 @@ pub struct Config {
44 pub line_number: LineNumber,
44 pub line_number: LineNumber,
45 /// Middle click paste support. Defaults to true
45 /// Middle click paste support. Defaults to true
46 pub middle_click_paste: bool,
46 pub middle_click_paste: bool,
47+ /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
48+ pub smart_case: bool,
49+ /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
50+ pub auto_pairs: bool,
47}
51}
48
52
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
53#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -69,6 +73,8 @@ impl Default for Config {
69 },
73 },
70 line_number: LineNumber::Absolute,
74 line_number: LineNumber::Absolute,
71 middle_click_paste: true,
75 middle_click_paste: true,
76+ smart_case: true,
77+ auto_pairs: true,
72 }
78 }
73 }
79 }
74}
80}
@@ -78,7 +84,7 @@ pub struct Editor {
78 pub tree: Tree,
84 pub tree: Tree,
79 pub documents: SlotMap<DocumentId, Document>,
85 pub documents: SlotMap<DocumentId, Document>,
80 pub count: Option<std::num::NonZeroUsize>,
86 pub count: Option<std::num::NonZeroUsize>,
81− pub selected_register: RegisterSelection,
87+ pub selected_register: Option<char>,
82 pub registers: Registers,
88 pub registers: Registers,
83 pub theme: Theme,
89 pub theme: Theme,
84 pub language_servers: helix_lsp::Registry,
90 pub language_servers: helix_lsp::Registry,
@@ -125,7 +131,7 @@ impl Editor {
125 tree: Tree::new(area),
131 tree: Tree::new(area),
126 documents: SlotMap::with_key(),
132 documents: SlotMap::with_key(),
127 count: None,
133 count: None,
128− selected_register: RegisterSelection::default(),
134+ selected_register: None,
129 theme: themes.default(),
135 theme: themes.default(),
130 language_servers,
136 language_servers,
131 debugger: None,
137 debugger: None,
@@ -270,26 +276,31 @@ impl Editor {
270 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
276 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
271
277
272 // try to find a language server based on the language name
278 // try to find a language server based on the language name
273− let language_server = doc
279+ let language_server = doc.language.as_ref().and_then(|language| {
274− .language
275− .as_ref()
276− .and_then(|language| self.language_servers.get(language).ok());
280+ self.language_servers
281+ .get(language)
282+ .map_err(|e| {
283+ log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
284+ })
285+ .ok()
286+ });
277
287
278 if let Some(language_server) = language_server {
288 if let Some(language_server) = language_server {
279− doc.set_language_server(Some(language_server.clone()));
280−
281 let language_id = doc
289 let language_id = doc
282 .language()
290 .language()
283 .and_then(|s| s.split('.').last()) // source.rust
291 .and_then(|s| s.split('.').last()) // source.rust
284 .map(ToOwned::to_owned)
292 .map(ToOwned::to_owned)
285 .unwrap_or_default();
293 .unwrap_or_default();
286
294
295+ // TODO: this now races with on_init code if the init happens too quickly
287 tokio::spawn(language_server.text_document_did_open(
296 tokio::spawn(language_server.text_document_did_open(
288 doc.url().unwrap(),
297 doc.url().unwrap(),
289 doc.version(),
298 doc.version(),
290 doc.text(),
299 doc.text(),
291 language_id,
300 language_id,
292 ));
301 ));
302+
303+ doc.set_language_server(Some(language_server));
293 }
304 }
294
305
295 let id = self.documents.insert(doc);
306 let id = self.documents.insert(doc);
@@ -308,14 +319,9 @@ impl Editor {
308
319
309 if close_buffer {
320 if close_buffer {
310 // get around borrowck issues
321 // get around borrowck issues
311− let language_servers = &mut self.language_servers;
312 let doc = &self.documents[view.doc];
322 let doc = &self.documents[view.doc];
313
323
314− let language_server = doc
315− .language
316− .as_ref()
317− .and_then(|language| language_servers.get(language).ok());
318− if let Some(language_server) = language_server {
324+ if let Some(language_server) = doc.language_server() {
319 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
325 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
320 }
326 }
321 self.documents.remove(view.doc);
327 self.documents.remove(view.doc);
@@ -345,20 +351,24 @@ impl Editor {
345 view.ensure_cursor_in_view(doc, self.config.scrolloff)
351 view.ensure_cursor_in_view(doc, self.config.scrolloff)
346 }
352 }
347
353
354+ #[inline]
348 pub fn document(&self, id: DocumentId) -> Option<&Document> {
355 pub fn document(&self, id: DocumentId) -> Option<&Document> {
349 self.documents.get(id)
356 self.documents.get(id)
350 }
357 }
351
358
359+ #[inline]
352 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
360 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
353 self.documents.get_mut(id)
361 self.documents.get_mut(id)
354 }
362 }
355
363
364+ #[inline]
356 pub fn documents(&self) -> impl Iterator<Item = &Document> {
365 pub fn documents(&self) -> impl Iterator<Item = &Document> {
357− self.documents.iter().map(|(_id, doc)| doc)
366+ self.documents.values()
358 }
367 }
359
368
369+ #[inline]
360 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
370 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
361− self.documents.iter_mut().map(|(_id, doc)| doc)
371+ self.documents.values_mut()
362 }
372 }
363
373
364 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
374 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
@@ -366,10 +376,10 @@ impl Editor {
366 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
376 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
367 }
377 }
368
378
369− // pub fn current_document(&self) -> Document {
379+ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
370− // let id = self.view().doc;
380+ self.documents_mut()
371− // let doc = &mut editor.documents[id];
381+ .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
372− // }
382+ }
373
383
374 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
384 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
375 let view = view!(self);
385 let view = view!(self);
helix-view/src/graphics.rs+3−3
@@ -224,13 +224,13 @@ pub enum Color {
224 Magenta,
224 Magenta,
225 Cyan,
225 Cyan,
226 Gray,
226 Gray,
227− DarkGray,
228 LightRed,
227 LightRed,
229 LightGreen,
228 LightGreen,
230 LightYellow,
229 LightYellow,
231 LightBlue,
230 LightBlue,
232 LightMagenta,
231 LightMagenta,
233 LightCyan,
232 LightCyan,
233+ LightGray,
234 White,
234 White,
235 Rgb(u8, u8, u8),
235 Rgb(u8, u8, u8),
236 Indexed(u8),
236 Indexed(u8),
@@ -250,14 +250,14 @@ impl From<Color> for crossterm::style::Color {
250 Color::Blue => CColor::DarkBlue,
250 Color::Blue => CColor::DarkBlue,
251 Color::Magenta => CColor::DarkMagenta,
251 Color::Magenta => CColor::DarkMagenta,
252 Color::Cyan => CColor::DarkCyan,
252 Color::Cyan => CColor::DarkCyan,
253− Color::Gray => CColor::Grey,
253+ Color::Gray => CColor::DarkGrey,
254− Color::DarkGray => CColor::DarkGrey,
255 Color::LightRed => CColor::Red,
254 Color::LightRed => CColor::Red,
256 Color::LightGreen => CColor::Green,
255 Color::LightGreen => CColor::Green,
257 Color::LightBlue => CColor::Blue,
256 Color::LightBlue => CColor::Blue,
258 Color::LightYellow => CColor::Yellow,
257 Color::LightYellow => CColor::Yellow,
259 Color::LightMagenta => CColor::Magenta,
258 Color::LightMagenta => CColor::Magenta,
260 Color::LightCyan => CColor::Cyan,
259 Color::LightCyan => CColor::Cyan,
260+ Color::LightGray => CColor::Grey,
261 Color::White => CColor::White,
261 Color::White => CColor::White,
262 Color::Indexed(i) => CColor::AnsiValue(i),
262 Color::Indexed(i) => CColor::AnsiValue(i),
263 Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
263 Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
helix-view/src/lib.rs−2
@@ -8,7 +8,6 @@ pub mod graphics;
8pub mod info;
8pub mod info;
9pub mod input;
9pub mod input;
10pub mod keyboard;
10pub mod keyboard;
11−pub mod register_selection;
12pub mod theme;
11pub mod theme;
13pub mod tree;
12pub mod tree;
14pub mod view;
13pub mod view;
@@ -20,6 +19,5 @@ slotmap::new_key_type! {
20
19
21pub use document::Document;
20pub use document::Document;
22pub use editor::Editor;
21pub use editor::Editor;
23−pub use register_selection::RegisterSelection;
24pub use theme::Theme;
22pub use theme::Theme;
25pub use view::View;
23pub use view::View;
helix-view/src/macros.rs+17
@@ -1,3 +1,14 @@
1+//! These are macros to make getting very nested fields in the `Editor` struct easier
2+//! These are macros instead of functions because functions will have to take `&mut self`
3+//! However, rust doesn't know that you only want a partial borrow instead of borrowing the
4+//! entire struct which `&mut self` says. This makes it impossible to do other mutable
5+//! stuff to the struct because it is already borrowed. Because macros are expanded,
6+//! this circumvents the problem because it is just like indexing fields by hand and then
7+//! putting a `&mut` in front of it. This way rust can see that we are only borrowing a
8+//! part of the struct and not the entire thing.
9+
10+/// Get the current view and document mutably as a tuple.
11+/// Returns `(&mut View, &mut Document)`
1#[macro_export]
12#[macro_export]
2macro_rules! current {
13macro_rules! current {
3 ( $( $editor:ident ).+ ) => {{
14 ( $( $editor:ident ).+ ) => {{
@@ -7,6 +18,8 @@ macro_rules! current {
7 }};
18 }};
8}
19}
9
20
21+/// Get the current document mutably.
22+/// Returns `&mut Document`
10#[macro_export]
23#[macro_export]
11macro_rules! doc_mut {
24macro_rules! doc_mut {
12 ( $( $editor:ident ).+ ) => {{
25 ( $( $editor:ident ).+ ) => {{
@@ -14,6 +27,8 @@ macro_rules! doc_mut {
14 }};
27 }};
15}
28}
16
29
30+/// Get the current view mutably.
31+/// Returns `&mut View`
17#[macro_export]
32#[macro_export]
18macro_rules! view_mut {
33macro_rules! view_mut {
19 ( $( $editor:ident ).+ ) => {{
34 ( $( $editor:ident ).+ ) => {{
@@ -21,6 +36,8 @@ macro_rules! view_mut {
21 }};
36 }};
22}
37}
23
38
39+/// Get the current view immutably
40+/// Returns `&View`
24#[macro_export]
41#[macro_export]
25macro_rules! view {
42macro_rules! view {
26 ( $( $editor:ident ).+ ) => {{
43 ( $( $editor:ident ).+ ) => {{
helix-view/src/register_selection.rsdeleted−48
@@ -1,48 +0,0 @@
1−/// Register selection and configuration
2−///
3−/// This is a kind a of specialized `Option<char>` for register selection.
4−/// Point is to keep whether the register selection has been explicitely
5−/// set or not while being convenient by knowing the default register name.
6−#[derive(Debug)]
7−pub struct RegisterSelection {
8− selected: char,
9− default_name: char,
10−}
11−
12−impl RegisterSelection {
13− pub fn new(default_name: char) -> Self {
14− Self {
15− selected: default_name,
16− default_name,
17− }
18− }
19−
20− pub fn select(&mut self, name: char) {
21− self.selected = name;
22− }
23−
24− pub fn take(&mut self) -> Self {
25− Self {
26− selected: std::mem::replace(&mut self.selected, self.default_name),
27− default_name: self.default_name,
28− }
29− }
30−
31− pub fn is_default(&self) -> bool {
32− self.selected == self.default_name
33− }
34−
35− pub fn name(&self) -> char {
36− self.selected
37− }
38−}
39−
40−impl Default for RegisterSelection {
41− fn default() -> Self {
42− let default_name = '"';
43− Self {
44− selected: default_name,
45− default_name,
46− }
47− }
48−}
helix-view/src/theme.rs+27−2
@@ -5,6 +5,7 @@ use std::{
5};
5};
6
6
7use anyhow::Context;
7use anyhow::Context;
8+use helix_core::hashmap;
8use log::warn;
9use log::warn;
9use once_cell::sync::Lazy;
10use once_cell::sync::Lazy;
10use serde::{Deserialize, Deserializer};
11use serde::{Deserialize, Deserializer};
@@ -142,13 +143,37 @@ struct ThemePalette {
142
143
143impl Default for ThemePalette {
144impl Default for ThemePalette {
144 fn default() -> Self {
145 fn default() -> Self {
145− Self::new(HashMap::new())
146+ Self {
147+ palette: hashmap! {
148+ "black".to_string() => Color::Black,
149+ "red".to_string() => Color::Red,
150+ "green".to_string() => Color::Green,
151+ "yellow".to_string() => Color::Yellow,
152+ "blue".to_string() => Color::Blue,
153+ "magenta".to_string() => Color::Magenta,
154+ "cyan".to_string() => Color::Cyan,
155+ "gray".to_string() => Color::Gray,
156+ "light-red".to_string() => Color::LightRed,
157+ "light-green".to_string() => Color::LightGreen,
158+ "light-yellow".to_string() => Color::LightYellow,
159+ "light-blue".to_string() => Color::LightBlue,
160+ "light-magenta".to_string() => Color::LightMagenta,
161+ "light-cyan".to_string() => Color::LightCyan,
162+ "light-gray".to_string() => Color::LightGray,
163+ "white".to_string() => Color::White,
164+ },
165+ }
146 }
166 }
147}
167}
148
168
149impl ThemePalette {
169impl ThemePalette {
150 pub fn new(palette: HashMap<String, Color>) -> Self {
170 pub fn new(palette: HashMap<String, Color>) -> Self {
171+ let ThemePalette {
172+ palette: mut default,
173+ } = ThemePalette::default();
174+
175+ default.extend(palette);
151− Self { palette }
176+ Self { palette: default }
152 }
177 }
153
178
154 pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
179 pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
languages.toml+38−1
@@ -65,6 +65,7 @@ file-types = ["ex", "exs"]
65roots = []
65roots = []
66comment-token = "#"
66comment-token = "#"
67
67
68+language-server = { command = "elixir-ls" }
68indent = { tab-width = 2, unit = " " }
69indent = { tab-width = 2, unit = " " }
69
70
70[[language]]
71[[language]]
@@ -198,6 +199,17 @@ roots = []
198language-server = { command = "typescript-language-server", args = ["--stdio"] }
199language-server = { command = "typescript-language-server", args = ["--stdio"] }
199indent = { tab-width = 2, unit = " " }
200indent = { tab-width = 2, unit = " " }
200
201
202+[[language]]
203+name = "tsx"
204+scope = "source.tsx"
205+injection-regex = "^(tsx)$" # |typescript
206+file-types = ["tsx"]
207+roots = []
208+# TODO: highlights-jsx, highlights-params
209+
210+language-server = { command = "typescript-language-server", args = ["--stdio"] }
211+indent = { tab-width = 2, unit = " " }
212+
201[[language]]
213[[language]]
202name = "css"
214name = "css"
203scope = "source.css"
215scope = "source.css"
@@ -236,6 +248,7 @@ file-types = ["nix"]
236roots = []
248roots = []
237comment-token = "#"
249comment-token = "#"
238
250
251+language-server = { command = "rnix-lsp" }
239indent = { tab-width = 2, unit = " " }
252indent = { tab-width = 2, unit = " " }
240
253
241[[language]]
254[[language]]
@@ -286,7 +299,22 @@ injection-regex = "julia"
286file-types = ["jl"]
299file-types = ["jl"]
287roots = []
300roots = []
288comment-token = "#"
301comment-token = "#"
289−language-server = { command = "julia", args = [ "--startup-file=no", "--history-file=no", "-e", "using LanguageServer;using Pkg;import StaticLint;import SymbolServer;env_path = dirname(Pkg.Types.Context().env.project_file);server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, \"\");server.runlinter = true;run(server);" ] }
302+language-server = { command = "julia", args = [
303+ "--startup-file=no",
304+ "--history-file=no",
305+ "--quiet",
306+ "-e",
307+ """
308+ using LanguageServer;
309+ using Pkg;
310+ import StaticLint;
311+ env_path = dirname(Pkg.Types.Context().env.project_file);
312+
313+ server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
314+ server.runlinter = true;
315+ run(server);
316+ """,
317+ ] }
290indent = { tab-width = 2, unit = " " }
318indent = { tab-width = 2, unit = " " }
291
319
292[[language]]
320[[language]]
@@ -331,6 +359,15 @@ roots = []
331comment-token = "--"
359comment-token = "--"
332indent = { tab-width = 2, unit = " " }
360indent = { tab-width = 2, unit = " " }
333
361
362+[[language]]
363+name = "svelte"
364+scope = "source.svelte"
365+injection-regex = "svelte"
366+file-types = ["svelte"]
367+roots = []
368+indent = { tab-width = 2, unit = " " }
369+language-server = { command = "svelteserver", args = ["--stdio"] }
370+
334[[language]]
371[[language]]
335name = "yaml"
372name = "yaml"
336scope = "source.yaml"
373scope = "source.yaml"
runtime/queries/c/highlights.scm+1−1
@@ -61,7 +61,7 @@
61
61
62(null) @constant
62(null) @constant
63(number_literal) @number
63(number_literal) @number
64−(char_literal) @number
64+(char_literal) @string
65
65
66(call_expression
66(call_expression
67 function: (identifier) @function)
67 function: (identifier) @function)
runtime/queries/go/highlights.scm+35−4
@@ -17,9 +17,18 @@
17
17
18; Identifiers
18; Identifiers
19
19
20+((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$"))
21+(const_spec
22+ name: (identifier) @constant)
23+
24+(parameter_declaration (identifier) @variable.parameter)
25+(variadic_parameter_declaration (identifier) @variable.parameter)
26+
20(type_identifier) @type
27(type_identifier) @type
21(field_identifier) @property
28(field_identifier) @property
22(identifier) @variable
29(identifier) @variable
30+(package_identifier) @variable
31+
23
32
24; Operators
33; Operators
25
34
@@ -79,10 +88,8 @@
79 "go"
88 "go"
80 "goto"
89 "goto"
81 "if"
90 "if"
82− "import"
83 "interface"
91 "interface"
84 "map"
92 "map"
85− "package"
86 "range"
93 "range"
87 "return"
94 "return"
88 "select"
95 "select"
@@ -92,6 +99,29 @@
92 "var"
99 "var"
93] @keyword
100] @keyword
94
101
102+[
103+ "import"
104+ "package"
105+] @keyword.control.import
106+
107+; Delimiters
108+
109+[
110+ ":"
111+ "."
112+ ","
113+ ";"
114+] @punctuation.delimiter
115+
116+[
117+ "("
118+ ")"
119+ "["
120+ "]"
121+ "{"
122+ "}"
123+] @punctuation.bracket
124+
95; Literals
125; Literals
96
126
97[
127[
@@ -111,7 +141,8 @@
111[
141[
112 (true)
142 (true)
113 (false)
143 (false)
114− (nil)
144+] @constant.builtin.boolean
145+
115−] @constant.builtin
146+(nil) @constant.builtin
116
147
117(comment) @comment
148(comment) @comment
runtime/queries/go/locals.scmnew file+30
@@ -0,0 +1,30 @@
1+; Scopes
2+
3+(block) @local.scope
4+
5+; Definitions
6+
7+(parameter_declaration (identifier) @local.definition)
8+(variadic_parameter_declaration (identifier) @local.definition)
9+
10+(short_var_declaration
11+ left: (expression_list
12+ (identifier) @local.definition))
13+
14+(var_spec
15+ name: (identifier) @local.definition)
16+
17+(for_statement
18+ (range_clause
19+ left: (expression_list
20+ (identifier) @local.definition)))
21+
22+(const_declaration
23+ (const_spec
24+ name: (identifier) @local.definition))
25+
26+; References
27+
28+(identifier) @local.reference
29+(field_identifier) @local.reference
30+
runtime/queries/haskell/highlights.scm+8−6
@@ -2,19 +2,19 @@
2(operator) @operator
2(operator) @operator
3(exp_name (constructor) @constructor)
3(exp_name (constructor) @constructor)
4(constructor_operator) @operator
4(constructor_operator) @operator
5−(module) @module_name
5+(module) @namespace
6(type) @type
6(type) @type
7(type) @class
7(type) @class
8(constructor) @constructor
8(constructor) @constructor
9(pragma) @pragma
9(pragma) @pragma
10(comment) @comment
10(comment) @comment
11(signature name: (variable) @fun_type_name)
11(signature name: (variable) @fun_type_name)
12−(function name: (variable) @fun_name)
12+(function name: (variable) @function)
13(constraint class: (class_name (type)) @class)
13(constraint class: (class_name (type)) @class)
14(class (class_head class: (class_name (type)) @class))
14(class (class_head class: (class_name (type)) @class))
15(instance (instance_head class: (class_name (type)) @class))
15(instance (instance_head class: (class_name (type)) @class))
16−(integer) @literal
16+(integer) @number
17−(exp_literal (float)) @literal
17+(exp_literal (float)) @number
18(char) @literal
18(char) @literal
19(con_unit) @literal
19(con_unit) @literal
20(con_list) @literal
20(con_list) @literal
@@ -39,5 +39,7 @@
39"do" @keyword
39"do" @keyword
40"mdo" @keyword
40"mdo" @keyword
41"rec" @keyword
41"rec" @keyword
42+[
42−"(" @paren
43+ "("
43−")" @paren
44+ ")"
45+] @punctuation.bracket
runtime/queries/javascript/highlights.scm+1−1
@@ -87,7 +87,7 @@
87 (template_string)
87 (template_string)
88] @string
88] @string
89
89
90−(regex) @string.special
90+(regex) @string.regexp
91(number) @number
91(number) @number
92
92
93; Tokens
93; Tokens
runtime/queries/julia/highlights.scm+43−39
@@ -1,9 +1,3 @@
1−(identifier) @variable
2−;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation)
3−;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables
4− ;(match? @type "^[A-Z][^_]"))
5−((identifier) @constant
6− (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$"))
7
1
8[
2[
9 (triple_string)
3 (triple_string)
@@ -28,43 +22,43 @@
28(call_expression
22(call_expression
29 (identifier) @function)
23 (identifier) @function)
30(call_expression
24(call_expression
31− (field_expression (identifier) @method .))
25+ (field_expression (identifier) @function.method .))
32(broadcast_call_expression
26(broadcast_call_expression
33 (identifier) @function)
27 (identifier) @function)
34(broadcast_call_expression
28(broadcast_call_expression
35− (field_expression (identifier) @method .))
29+ (field_expression (identifier) @function.method .))
36(parameter_list
30(parameter_list
37− (identifier) @parameter)
31+ (identifier) @variable.parameter)
38(parameter_list
32(parameter_list
39 (optional_parameter .
33 (optional_parameter .
40− (identifier) @parameter))
34+ (identifier) @variable.parameter))
41(typed_parameter
35(typed_parameter
42− (identifier) @parameter
36+ (identifier) @variable.parameter
43 (identifier) @type)
37 (identifier) @type)
44(type_parameter_list
38(type_parameter_list
45 (identifier) @type)
39 (identifier) @type)
46(typed_parameter
40(typed_parameter
47− (identifier) @parameter
41+ (identifier) @variable.parameter
48 (parameterized_identifier) @type)
42 (parameterized_identifier) @type)
49(function_expression
43(function_expression
50− . (identifier) @parameter)
44+ . (identifier) @variable.parameter)
51−(spread_parameter) @parameter
45+(spread_parameter) @variable.parameter
52(spread_parameter
46(spread_parameter
53− (identifier) @parameter)
47+ (identifier) @variable.parameter)
54(named_argument
48(named_argument
55− . (identifier) @parameter)
49+ . (identifier) @variable.parameter)
56(argument_list
50(argument_list
57 (typed_expression
51 (typed_expression
58− (identifier) @parameter
52+ (identifier) @variable.parameter
59 (identifier) @type))
53 (identifier) @type))
60(argument_list
54(argument_list
61 (typed_expression
55 (typed_expression
62− (identifier) @parameter
56+ (identifier) @variable.parameter
63 (parameterized_identifier) @type))
57 (parameterized_identifier) @type))
64
58
65;; Symbol expressions (:my-wanna-be-lisp-keyword)
59;; Symbol expressions (:my-wanna-be-lisp-keyword)
66(quote_expression
60(quote_expression
67− (identifier)) @symbol
61+ (identifier)) @string.special.symbol
68
62
69;; Parsing error! foo (::Type) get's parsed as two quote expressions
63;; Parsing error! foo (::Type) get's parsed as two quote expressions
70(argument_list
64(argument_list
@@ -76,7 +70,7 @@
76 (identifier) @type)
70 (identifier) @type)
77(parameterized_identifier (_)) @type
71(parameterized_identifier (_)) @type
78(argument_list
72(argument_list
79− (typed_expression . (identifier) @parameter))
73+ (typed_expression . (identifier) @variable.parameter))
80
74
81(typed_expression
75(typed_expression
82 (identifier) @type .)
76 (identifier) @type .)
@@ -113,13 +107,13 @@
113"end" @keyword
107"end" @keyword
114
108
115(if_statement
109(if_statement
116− ["if" "end"] @conditional)
110+ ["if" "end"] @keyword.control.conditional)
117(elseif_clause
111(elseif_clause
118− ["elseif"] @conditional)
112+ ["elseif"] @keyword.control.conditional)
119(else_clause
113(else_clause
120− ["else"] @conditional)
114+ ["else"] @keyword.control.conditional)
121(ternary_expression
115(ternary_expression
122− ["?" ":"] @conditional)
116+ ["?" ":"] @keyword.control.conditional)
123
117
124(function_definition ["function" "end"] @keyword.function)
118(function_definition ["function" "end"] @keyword.function)
125
119
@@ -134,47 +128,57 @@
134 "type"
128 "type"
135] @keyword
129] @keyword
136
130
137−((identifier) @keyword (#any-of? @keyword "global" "local"))
131+((identifier) @keyword (match? @keyword "global|local"))
138
132
139(compound_expression
133(compound_expression
140 ["begin" "end"] @keyword)
134 ["begin" "end"] @keyword)
141(try_statement
135(try_statement
142− ["try" "end" ] @exception)
136+ ["try" "end" ] @keyword.control.exception)
143(finally_clause
137(finally_clause
144− "finally" @exception)
138+ "finally" @keyword.control.exception)
145(catch_clause
139(catch_clause
146− "catch" @exception)
140+ "catch" @keyword.control.exception)
147(quote_statement
141(quote_statement
148 ["quote" "end"] @keyword)
142 ["quote" "end"] @keyword)
149(let_statement
143(let_statement
150 ["let" "end"] @keyword)
144 ["let" "end"] @keyword)
151(for_statement
145(for_statement
152− ["for" "end"] @repeat)
146+ ["for" "end"] @keyword.control.repeat)
153(while_statement
147(while_statement
154− ["while" "end"] @repeat)
148+ ["while" "end"] @keyword.control.repeat)
155−(break_statement) @repeat
149+(break_statement) @keyword.control.repeat
156−(continue_statement) @repeat
150+(continue_statement) @keyword.control.repeat
157(for_binding
151(for_binding
158− "in" @repeat)
152+ "in" @keyword.control.repeat)
159(for_clause
153(for_clause
160− "for" @repeat)
154+ "for" @keyword.control.repeat)
161(do_clause
155(do_clause
162 ["do" "end"] @keyword)
156 ["do" "end"] @keyword)
163
157
164(export_statement
158(export_statement
165− ["export"] @include)
159+ ["export"] @keyword.control.import)
166
160
167[
161[
168 "using"
162 "using"
169 "module"
163 "module"
170 "import"
164 "import"
171−] @include
165+] @keyword.control.import
172
166
173−((identifier) @include (#eq? @include "baremodule"))
167+((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule"))
174
168
175(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
169(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
176−(((identifier) @boolean) (eq? @boolean "true"))
170+(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true"))
177−(((identifier) @boolean) (eq? @boolean "false"))
171+(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false"))
172+
178
173
179["::" ":" "." "," "..." "!"] @punctuation.delimiter
174["::" ":" "." "," "..." "!"] @punctuation.delimiter
180["[" "]" "(" ")" "{" "}"] @punctuation.bracket
175["[" "]" "(" ")" "{" "}"] @punctuation.bracket
176+
177+["="] @operator
178+
179+(identifier) @variable
180+;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation)
181+;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables
182+ ;(match? @type "^[A-Z][^_]"))
183+((identifier) @constant
184+ (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$"))
runtime/queries/latex/highlights.scm+2−2
@@ -259,7 +259,7 @@
259
259
260(comment) @comment
260(comment) @comment
261
261
262−(bracket_group) @parameter
262+(bracket_group) @variable.parameter
263
263
264[(math_operator) "="] @operator
264[(math_operator) "="] @operator
265
265
@@ -312,7 +312,7 @@
312 key: (word) @text.reference)
312 key: (word) @text.reference)
313
313
314(key_val_pair
314(key_val_pair
315− key: (_) @parameter
315+ key: (_) @variable.parameter
316 value: (_))
316 value: (_))
317
317
318["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
318["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
runtime/queries/ledger/injections.scm+1
@@ -1 +1,2 @@
1(comment) @comment
1(comment) @comment
2+(note) @comment
runtime/queries/lua/highlights.scm+9−9
@@ -23,27 +23,27 @@
23 "for"
23 "for"
24 "do"
24 "do"
25 "end"
25 "end"
26−] @keyword.control.loop)
26+] @keyword.control.repeat)
27
27
28(for_in_statement
28(for_in_statement
29[
29[
30 "for"
30 "for"
31 "do"
31 "do"
32 "end"
32 "end"
33−] @keyword.control.loop)
33+] @keyword.control.repeat)
34
34
35(while_statement
35(while_statement
36[
36[
37 "while"
37 "while"
38 "do"
38 "do"
39 "end"
39 "end"
40−] @keyword.control.loop)
40+] @keyword.control.repeat)
41
41
42(repeat_statement
42(repeat_statement
43[
43[
44 "repeat"
44 "repeat"
45 "until"
45 "until"
46−] @keyword.control.loop)
46+] @keyword.control.repeat)
47
47
48(do_statement
48(do_statement
49[
49[
@@ -65,7 +65,7 @@
65 "not"
65 "not"
66 "and"
66 "and"
67 "or"
67 "or"
68−] @keyword.operator
68+] @operator
69
69
70[
70[
71"="
71"="
@@ -108,7 +108,7 @@
108[
108[
109(false)
109(false)
110(true)
110(true)
111−] @boolean
111+] @constant.builtin.boolean
112(nil) @constant.builtin
112(nil) @constant.builtin
113(spread) @constant ;; "..."
113(spread) @constant ;; "..."
114((identifier) @constant
114((identifier) @constant
@@ -116,7 +116,7 @@
116
116
117;; Parameters
117;; Parameters
118(parameters
118(parameters
119− (identifier) @parameter)
119+ (identifier) @variable.parameter)
120
120
121; ;; Functions
121; ;; Functions
122(function [(function_name) (identifier)] @function)
122(function [(function_name) (identifier)] @function)
@@ -139,8 +139,8 @@
139
139
140(function_call
140(function_call
141 [
141 [
142− ((identifier) @variable (method) @method)
142+ ((identifier) @variable (method) @function.method)
143− ((_) (method) @method)
143+ ((_) (method) @function.method)
144 (identifier) @function
144 (identifier) @function
145 (field_expression (property_identifier) @function)
145 (field_expression (property_identifier) @function)
146 ]
146 ]
runtime/queries/ocaml/highlights.scm+4−4
@@ -25,12 +25,12 @@
25
25
26(external (value_name) @function)
26(external (value_name) @function)
27
27
28−(method_name) @method
28+(method_name) @function.method
29
29
30; Variables
30; Variables
31;----------
31;----------
32
32
33−(value_pattern) @parameter
33+(value_pattern) @variable.parameter
34
34
35; Application
35; Application
36;------------
36;------------
@@ -60,7 +60,7 @@
60
60
61[(number) (signed_number)] @number
61[(number) (signed_number)] @number
62
62
63−(character) @character
63+(character) @constant.character
64
64
65(string) @string
65(string) @string
66
66
@@ -92,7 +92,7 @@
92
92
93["include" "open"] @include
93["include" "open"] @include
94
94
95−["for" "to" "downto" "while" "do" "done"] @keyword.control.loop
95+["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
96
96
97; Macros
97; Macros
98;-------
98;-------
runtime/queries/ruby/highlights.scm+1−1
@@ -100,7 +100,7 @@
100 (bare_symbol)
100 (bare_symbol)
101] @string.special.symbol
101] @string.special.symbol
102
102
103−(regex) @string.special.regex
103+(regex) @string.regexp
104(escape_sequence) @escape
104(escape_sequence) @escape
105
105
106[
106[
runtime/queries/rust/highlights.scm+2−2
@@ -17,7 +17,7 @@
17
17
18(escape_sequence) @escape
18(escape_sequence) @escape
19(primitive_type) @type.builtin
19(primitive_type) @type.builtin
20−(boolean_literal) @constant.builtin
20+(boolean_literal) @constant.builtin.boolean
21[
21[
22 (integer_literal)
22 (integer_literal)
23 (float_literal)
23 (float_literal)
@@ -149,7 +149,7 @@
149
149
150(mutable_specifier) @keyword.mut
150(mutable_specifier) @keyword.mut
151
151
152−
152+; TODO: variable.mut to highlight mutable identifiers via locals.scm
153
153
154; -------
154; -------
155; Guess Other Types
155; Guess Other Types
runtime/queries/rust/locals.scmnew file+17
@@ -0,0 +1,17 @@
1+; Scopes
2+
3+(block) @local.scope
4+
5+; Definitions
6+
7+(parameter
8+ (identifier) @local.definition)
9+
10+(let_declaration
11+ pattern: (identifier) @local.definition)
12+
13+(closure_parameters (identifier)) @local.definition
14+
15+; References
16+(identifier) @local.reference
17+
runtime/queries/svelte/highlights.scmnew file+68
@@ -0,0 +1,68 @@
1+; Special identifiers
2+;--------------------
3+
4+; TODO:
5+((element (start_tag (tag_name) @_tag) (text) @markup.heading)
6+ (#match? @_tag "^(h[0-9]|title)$"))
7+
8+((element (start_tag (tag_name) @_tag) (text) @markup.bold)
9+ (#match? @_tag "^(strong|b)$"))
10+
11+((element (start_tag (tag_name) @_tag) (text) @markup.italic)
12+ (#match? @_tag "^(em|i)$"))
13+
14+; ((element (start_tag (tag_name) @_tag) (text) @markup.strike)
15+; (#match? @_tag "^(s|del)$"))
16+
17+((element (start_tag (tag_name) @_tag) (text) @markup.underline)
18+ (#eq? @_tag "u"))
19+
20+((element (start_tag (tag_name) @_tag) (text) @markup.inline)
21+ (#match? @_tag "^(code|kbd)$"))
22+
23+((element (start_tag (tag_name) @_tag) (text) @markup.underline.link)
24+ (#eq? @_tag "a"))
25+
26+((attribute
27+ (attribute_name) @_attr
28+ (quoted_attribute_value (attribute_value) @markup.undeline.link))
29+ (#match? @_attr "^(href|src)$"))
30+
31+(tag_name) @tag
32+(attribute_name) @property
33+(erroneous_end_tag_name) @error
34+(comment) @comment
35+
36+[
37+ (attribute_value)
38+ (quoted_attribute_value)
39+] @string
40+
41+[
42+ (text)
43+ (raw_text_expr)
44+] @none
45+
46+[
47+ (special_block_keyword)
48+ (then)
49+ (as)
50+] @keyword
51+
52+[
53+ "{"
54+ "}"
55+] @punctuation.brackets
56+
57+"=" @operator
58+
59+[
60+ "<"
61+ ">"
62+ "</"
63+ "/>"
64+ "#"
65+ ":"
66+ "/"
67+ "@"
68+] @punctuation.definition.tag
runtime/queries/svelte/indents.tomlnew file+18
@@ -0,0 +1,18 @@
1+indent = [
2+ "element"
3+ "if_statement"
4+ "each_statement"
5+ "await_statement"
6+]
7+
8+outdent = [
9+ "end_tag"
10+ "else_statement"
11+ "if_end_expr"
12+ "each_end_expr"
13+ "await_end_expr"
14+ ">"
15+ "/>"
16+]
17+
18+ignore = "comment"
runtime/queries/svelte/injections.scmnew file+30
@@ -0,0 +1,30 @@
1+; injections.scm
2+; --------------
3+((style_element
4+ (raw_text) @injection.content)
5+ (#set! injection.language "css"))
6+
7+((attribute
8+ (attribute_name) @_attr
9+ (quoted_attribute_value (attribute_value) @css))
10+ (#eq? @_attr "style"))
11+
12+((script_element
13+ (raw_text) @injection.content)
14+ (#set! injection.language "javascript"))
15+
16+((raw_text_expr) @injection.content
17+ (#set! injection.language "javascript"))
18+
19+(
20+ (script_element
21+ (start_tag
22+ (attribute
23+ (quoted_attribute_value (attribute_value) @_lang)))
24+ (raw_text) @injection.content)
25+ (#match? @_lang "(ts|typescript)")
26+ (#set! injection.language "typescript")
27+)
28+
29+(comment) @comment
30+
runtime/queries/tsx/highlights.scmnew file+1
@@ -0,0 +1 @@
1+; inherits: typescript
runtime/queries/yaml/highlights.scm+1−1
@@ -1,6 +1,6 @@
1(block_mapping_pair key: (_) @property)
1(block_mapping_pair key: (_) @property)
2(flow_mapping (_ key: (_) @property))
2(flow_mapping (_ key: (_) @property))
3−(boolean_scalar) @boolean
3+(boolean_scalar) @constant.builtin.boolean
4(null_scalar) @constant.builtin
4(null_scalar) @constant.builtin
5(double_quote_scalar) @string
5(double_quote_scalar) @string
6(single_quote_scalar) @string
6(single_quote_scalar) @string
runtime/themes/dark_plus.toml+2−1
@@ -34,6 +34,7 @@
34"comment" = { fg = "#6A9955" }
34"comment" = { fg = "#6A9955" }
35
35
36"string" = { fg = "#ce9178" }
36"string" = { fg = "#ce9178" }
37+"string.regexp" = { fg = "regex" }
37"number" = { fg = "#b5cea8" }
38"number" = { fg = "#b5cea8" }
38"escape" = { fg = "#d7ba7d" }
39"escape" = { fg = "#d7ba7d" }
39
40
@@ -61,7 +62,7 @@
61"ui.text.focus" = { fg = "#ffffff" }
62"ui.text.focus" = { fg = "#ffffff" }
62
63
63"warning" = { fg = "#cca700" }
64"warning" = { fg = "#cca700" }
64−"error" = { fg = "#f48771" }
65+"error" = { fg = "#ff1212" }
65"info" = { fg = "#75beff" }
66"info" = { fg = "#75beff" }
66"hint" = { fg = "#eeeeeeb3" }
67"hint" = { fg = "#eeeeeeb3" }
67
68
runtime/themes/everforest_dark.tomlnew file+87
@@ -0,0 +1,87 @@
1+# Everforest (Dark Hard)
2+# Author: CptPotato
3+
4+# Original Author:
5+# URL: https://github.com/sainnhe/everforest
6+# Filename: autoload/everforest.vim
7+# Author: sainnhe
8+# Email: sainnhe@gmail.com
9+# License: MIT License
10+
11+"escape" = "orange"
12+"type" = "yellow"
13+"constant" = "purple"
14+"number" = "purple"
15+"string" = "grey2"
16+"comment" = "grey0"
17+"variable" = "fg"
18+"variable.builtin" = "blue"
19+"variable.parameter" = "fg"
20+"variable.property" = "fg"
21+"label" = "aqua"
22+"punctuation" = "grey2"
23+"punctuation.delimiter" = "grey2"
24+"punctuation.bracket" = "fg"
25+"keyword" = "red"
26+"operator" = "orange"
27+"function" = "green"
28+"function.builtin" = "blue"
29+"function.macro" = "aqua"
30+"tag" = "yellow"
31+"namespace" = "aqua"
32+"attribute" = "aqua"
33+"constructor" = "yellow"
34+"module" = "blue"
35+"property" = "fg"
36+"special" = "orange"
37+
38+"ui.background" = { bg = "bg0" }
39+"ui.cursor" = { fg = "bg0", bg = "fg" }
40+"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
41+"ui.cursor.insert" = { fg = "bg0", bg = "grey1" }
42+"ui.cursor.select" = { fg = "bg0", bg = "blue" }
43+"ui.linenr" = "grey0"
44+"ui.linenr.selected" = "fg"
45+"ui.statusline" = { fg = "grey2", bg = "bg2" }
46+"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" }
47+"ui.popup" = { fg = "grey2", bg = "bg1" }
48+"ui.window" = { fg = "grey2", bg = "bg1" }
49+"ui.help" = { fg = "fg", bg = "bg1" }
50+"ui.text" = "fg"
51+"ui.text.focus" = "fg"
52+"ui.menu" = { fg = "fg", bg = "bg2" }
53+"ui.menu.selected" = { fg = "bg0", bg = "green" }
54+"ui.selection" = { bg = "bg3" }
55+
56+"hint" = "blue"
57+"info" = "aqua"
58+"warning" = "yellow"
59+"error" = "red"
60+"diagnostic" = { modifiers = ["underlined"] }
61+
62+
63+[palette]
64+
65+bg0 = "#2b3339"
66+bg1 = "#323c41"
67+bg2 = "#3a454a"
68+bg3 = "#445055"
69+bg4 = "#4c555b"
70+bg5 = "#53605c"
71+bg_visual = "#503946"
72+bg_red = "#4e3e43"
73+bg_green = "#404d44"
74+bg_blue = "#394f5a"
75+bg_yellow = "#4a4940"
76+
77+fg = "#d3c6aa"
78+red = "#e67e80"
79+orange = "#e69875"
80+yellow = "#dbbc7f"
81+green = "#a7c080"
82+aqua = "#83c092"
83+blue = "#7fbbb3"
84+purple = "#d699b6"
85+grey0 = "#7a8478"
86+grey1 = "#859289"
87+grey2 = "#9da9a0"
runtime/themes/monokai.toml+1
@@ -34,6 +34,7 @@
34"comment" = { fg = "#88846F" }
34"comment" = { fg = "#88846F" }
35
35
36"string" = { fg = "#e6db74" }
36"string" = { fg = "#e6db74" }
37+"string.regexp" = { fg = "regex" }
37"number" = { fg = "#ae81ff" }
38"number" = { fg = "#ae81ff" }
38"escape" = { fg = "#ae81ff" }
39"escape" = { fg = "#ae81ff" }
39
40
rust-toolchain.tomlnew file+3
@@ -0,0 +1,3 @@
1+[toolchain]
2+channel = "stable"
3+components = ["rustfmt", "rust-src"]
theme.toml+3−4
@@ -9,7 +9,8 @@ special = "honey"
9property = "white"
9property = "white"
10variable = "lavender"
10variable = "lavender"
11# variable = "almond" # TODO: metavariables only
11# variable = "almond" # TODO: metavariables only
12−"variable.parameter" = "lavender"
12+# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }
13+"variable.parameter" = { fg = "lavender" }
13"variable.builtin" = "mint"
14"variable.builtin" = "mint"
14type = "white"
15type = "white"
15"type.builtin" = "white" # TODO: distinguish?
16"type.builtin" = "white" # TODO: distinguish?
@@ -28,9 +29,7 @@ escape = "honey"
28label = "honey"
29label = "honey"
29
30
30# TODO: diferentiate doc comment
31# TODO: diferentiate doc comment
31−# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors
32+# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
32−
33−module = "#ff0000"
34
33
35"ui.background" = { bg = "midnight" }
34"ui.background" = { bg = "midnight" }
36"ui.linenr" = { fg = "comet" }
35"ui.linenr" = { fg = "comet" }