on master help sign in
Merge branch 'master' into debug
commitbf53aff27d2d90b41bab01f4d628f0bd9fbcd589 authorDmitry Sharshakov <d3dx12.xx@gmail.com> dateSat 25 Sep 2021, 23:14 (5 years ago) parent413e477df55eaa
72 files changed+2129−1059
.gitmodules+4 Cargo.lock+95−22 TODO.md−6 book/book.toml+2−1 book/src/configuration.md+15 book/src/from-vim.md+2 book/src/install.md+3−1 book/src/keymap.md+97−85 book/src/remapping.md+2 book/src/themes.md+129−60 book/theme/css/general.css+13 book/theme/css/variables.css+14−10 book/theme/highlight.css+37−64 flake.lock+12−12 flake.nix+1−16 helix-core/Cargo.toml+2−2 helix-core/src/indent.rs+9−3 helix-core/src/selection.rs+9 helix-core/src/syntax.rs+83−44 helix-lsp/Cargo.toml+1−1 helix-lsp/src/client.rs+56−44 helix-lsp/src/lib.rs+57−53 helix-lsp/src/transport.rs+115−22 helix-syntax/Cargo.toml+1−1 helix-syntax/build.rs+1−2 helix-syntax/languages/tree-sitter-julia+1−1 helix-syntax/languages/tree-sitter-ledger+1−1 helix-syntax/languages/tree-sitter-svelte+1 helix-term/Cargo.toml+4 helix-term/src/application.rs+53−54 helix-term/src/commands.rs+435−227 helix-term/src/job.rs+2−2 helix-term/src/keymap.rs+110−37 helix-term/src/ui/completion.rs+31−14 helix-term/src/ui/editor.rs+23−22 helix-term/src/ui/markdown.rs+25−5 helix-term/src/ui/menu.rs+43−33 helix-term/src/ui/mod.rs+23−6 helix-term/src/ui/picker.rs+13−12 helix-term/src/ui/popup.rs+38−23 helix-term/src/ui/text.rs+15−5 helix-view/src/document.rs+67−16 helix-view/src/editor.rs+31−21 helix-view/src/graphics.rs+3−3 helix-view/src/lib.rs−2 helix-view/src/macros.rs+17 helix-view/src/register_selection.rs−48 helix-view/src/theme.rs+27−2 languages.toml+38−1 runtime/queries/c/highlights.scm+1−1 runtime/queries/go/highlights.scm+35−4 runtime/queries/go/locals.scm+30 runtime/queries/haskell/highlights.scm+8−6 runtime/queries/javascript/highlights.scm+1−1 runtime/queries/julia/highlights.scm+43−39 runtime/queries/latex/highlights.scm+2−2 runtime/queries/ledger/injections.scm+1 runtime/queries/lua/highlights.scm+9−9 runtime/queries/ocaml/highlights.scm+4−4 runtime/queries/ruby/highlights.scm+1−1 runtime/queries/rust/highlights.scm+2−2 runtime/queries/rust/locals.scm+17 runtime/queries/svelte/highlights.scm+68 runtime/queries/svelte/indents.toml+18 runtime/queries/svelte/injections.scm+30 runtime/queries/tsx/highlights.scm+1 runtime/queries/yaml/highlights.scm+1−1 runtime/themes/dark_plus.toml+2−1 runtime/themes/everforest_dark.toml+87 runtime/themes/monokai.toml+1 rust-toolchain.toml+3 theme.toml+3−4
.gitmodules+4
@@ -118,3 +118,7 @@
118118 path = helix-syntax/languages/tree-sitter-zig
119119 url = https://github.com/maxxnino/tree-sitter-zig
120120 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 = [
1313
1414[[package]]
1515name = "anyhow"
16−version = "1.0.43"
16+version = "1.0.44"
1717source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
18+checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
1919
2020[[package]]
2121name = "arc-swap"
22−version = "1.3.2"
22+version = "1.4.0"
2323source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00"
24+checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
2525
2626[[package]]
2727name = "autocfg"
@@ -41,9 +41,17 @@ version = "0.2.16"
4141source = "registry+https://github.com/rust-lang/crates.io-index"
4242checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
4343dependencies = [
44+ "lazy_static",
4445 "memchr",
46+ "regex-automata",
4547]
4648
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+
4755[[package]]
4856name = "bytes"
4957version = "1.0.1"
@@ -58,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
5866
5967[[package]]
6068name = "cc"
61−version = "1.0.69"
69+version = "1.0.70"
6270source = "registry+https://github.com/rust-lang/crates.io-index"
63−checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
71+checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
6472
6573[[package]]
6674name = "cfg-if"
@@ -174,6 +182,15 @@ dependencies = [
174182 "cfg-if",
175183]
176184
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+
177194[[package]]
178195name = "error-code"
179196version = "2.3.0"
@@ -300,6 +317,45 @@ dependencies = [
300317 "regex",
301318]
302319
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+
303359[[package]]
304360name = "helix-core"
305361version = "0.4.1"
@@ -375,6 +431,8 @@ dependencies = [
375431 "fern",
376432 "futures-util",
377433 "fuzzy-matcher",
434+ "grep-regex",
435+ "grep-searcher",
378436 "helix-core",
379437 "helix-dap",
380438 "helix-lsp",
@@ -570,6 +628,15 @@ version = "2.4.1"
570628source = "registry+https://github.com/rust-lang/crates.io-index"
571629checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
572630
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+
573640[[package]]
574641name = "mio"
575642version = "0.7.13"
@@ -771,6 +838,12 @@ dependencies = [
771838 "regex-syntax",
772839]
773840
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+
774847[[package]]
775848name = "regex-syntax"
776849version = "0.6.25"
@@ -829,9 +902,9 @@ dependencies = [
829902
830903[[package]]
831904name = "serde_json"
832−version = "1.0.67"
905+version = "1.0.68"
833906source = "registry+https://github.com/rust-lang/crates.io-index"
834−checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
907+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
835908dependencies = [
836909 "itoa",
837910 "ryu",
@@ -851,9 +924,9 @@ dependencies = [
851924
852925[[package]]
853926name = "signal-hook"
854−version = "0.3.9"
927+version = "0.3.10"
855928source = "registry+https://github.com/rust-lang/crates.io-index"
856−checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
929+checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
857930dependencies = [
858931 "libc",
859932 "signal-hook-registry",
@@ -893,9 +966,9 @@ dependencies = [
893966
894967[[package]]
895968name = "similar"
896−version = "1.3.0"
969+version = "2.0.0"
897970source = "registry+https://github.com/rust-lang/crates.io-index"
898−checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
971+checksum = "6bf11003835e462f07851028082d2a1c89d956180ce4b4b50e07fb085ec4131a"
899972
900973[[package]]
901974name = "slab"
@@ -948,18 +1021,18 @@ dependencies = [
9481021
9491022[[package]]
9501023name = "thiserror"
951−version = "1.0.28"
1024+version = "1.0.29"
9521025source = "registry+https://github.com/rust-lang/crates.io-index"
953−checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec"
1026+checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
9541027dependencies = [
9551028 "thiserror-impl",
9561029]
9571030
9581031[[package]]
9591032name = "thiserror-impl"
960−version = "1.0.28"
1033+version = "1.0.29"
9611034source = "registry+https://github.com/rust-lang/crates.io-index"
962−checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71"
1035+checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
9631036dependencies = [
9641037 "proc-macro2",
9651038 "quote",
@@ -1001,9 +1074,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
10011074
10021075[[package]]
10031076name = "tokio"
1004−version = "1.10.1"
1077+version = "1.11.0"
10051078source = "registry+https://github.com/rust-lang/crates.io-index"
1006−checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
1079+checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
10071080dependencies = [
10081081 "autocfg",
10091082 "bytes",
@@ -1052,9 +1125,9 @@ dependencies = [
10521125
10531126[[package]]
10541127name = "tree-sitter"
1055−version = "0.19.5"
1128+version = "0.20.0"
10561129source = "registry+https://github.com/rust-lang/crates.io-index"
1057−checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0"
1130+checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
10581131dependencies = [
10591132 "cc",
10601133 "regex",
@@ -1098,9 +1171,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
10981171
10991172[[package]]
11001173name = "unicode-width"
1101−version = "0.1.8"
1174+version = "0.1.9"
11021175source = "registry+https://github.com/rust-lang/crates.io-index"
1103−checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
1176+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
11041177
11051178[[package]]
11061179name = "unicode-xid"
TODO.md−6
@@ -1,12 +1,8 @@
11
22- tree sitter:
3− - lua
43 - markdown
5− - zig
64 - regex
7− - vue
85 - kotlin
9− - julia
106 - clojure
117 - erlang
128
@@ -26,8 +22,6 @@ as you type completion!
2622
2723- [ ] lsp: signature help
2824
29−- [ ] search: smart case by default: insensitive unless upper detected
30−
31252
3226- [ ] macro recording
3327- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
book/book.toml+2−1
@@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"]
33language = "en"
44multilingual = false
55src = "src"
6−theme = "colibri"
76edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
87
98[output.html]
109cname = "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
55* Linux and Mac: `~/.config/helix/config.toml`
66* Windows: `%AppData%\helix\config.toml`
77
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+
823## LSP
924
1025To 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
77action itself (delete, change, yank, etc) comes second. A cursor is simply a
88single width selection.
99
10+See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
11+
1012> TODO: Mention texobjects, surround, registers
book/src/install.md+3−1
@@ -23,7 +23,9 @@ shell for working on Helix.
2323
2424### Arch Linux
2525
26−Binary packages are available on AUR:
26+Releases are available in the `community` repository.
27+
28+Packages are also available on AUR:
2729- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
2830- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
2931
book/src/keymap.md+97−85
@@ -4,40 +4,40 @@
44
55### Movement
66
7−> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
8−
9−| Key | Description | Command |
10−| ----- | ----------- | ------- |
11−| `h`, `Left` | Move left | `move_char_left` |
12−| `j`, `Down` | Move down | `move_char_right` |
13−| `k`, `Up` | Move up | `move_line_up` |
14−| `l`, `Right` | Move right | `move_line_down` |
15−| `w` | Move next word start | `move_next_word_start` |
16−| `b` | Move previous word start | `move_prev_word_start` |
17−| `e` | Move next word end | `move_next_word_end` |
18−| `W` | Move next WORD start | `move_next_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` |
21−| `t` | Find 'till next char | `find_till_char` |
22−| `f` | Find next char | `find_next_char` |
23−| `T` | Find 'till previous char | `till_prev_char` |
24−| `F` | Find previous char | `find_prev_char` |
25−| `Home` | Move to the start of the line | `goto_line_start` |
26−| `End` | Move to the end of the line | `goto_line_end` |
27−| `PageUp` | Move page up | `page_up` |
28−| `PageDown` | Move page down | `page_down` |
29−| `Ctrl-u` | Move half page up | `half_page_up` |
30−| `Ctrl-d` | Move half page down | `half_page_down` |
31−| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` |
32−| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33−| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34−| `g` | Enter [goto mode](#goto-mode) | N/A |
35−| `m` | Enter [match mode](#match-mode) | N/A |
36−| `:` | Enter command mode | `command_mode` |
37−| `z` | Enter [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−| `Space` | Enter [space mode](#space-mode) | N/A |
40−| `K` | Show documentation for the item under the cursor | `hover` |
7+> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
8+
9+| Key | Description | Command |
10+| ----- | ----------- | ------- |
11+| `h`, `Left` | Move left | `move_char_left` |
12+| `j`, `Down` | Move down | `move_char_right` |
13+| `k`, `Up` | Move up | `move_line_up` |
14+| `l`, `Right` | Move right | `move_line_down` |
15+| `w` | Move next word start | `move_next_word_start` |
16+| `b` | Move previous word start | `move_prev_word_start` |
17+| `e` | Move next word end | `move_next_word_end` |
18+| `W` | Move next WORD start | `move_next_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` |
21+| `t` | Find 'till next char | `find_till_char` |
22+| `f` | Find next char | `find_next_char` |
23+| `T` | Find 'till previous char | `till_prev_char` |
24+| `F` | Find previous char | `find_prev_char` |
25+| `Home` | Move to the start of the line | `goto_line_start` |
26+| `End` | Move to the end of the line | `goto_line_end` |
27+| `PageUp` | Move page up | `page_up` |
28+| `PageDown` | Move page down | `page_down` |
29+| `Ctrl-u` | Move half page up | `half_page_up` |
30+| `Ctrl-d` | Move half page down | `half_page_down` |
31+| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
32+| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33+| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34+| `g` | Enter [goto mode](#goto-mode) | N/A |
35+| `m` | Enter [match mode](#match-mode) | N/A |
36+| `:` | Enter command mode | `command_mode` |
37+| `z` | Enter [view mode](#view-mode) | N/A |
38+| `Z` | Enter sticky [view mode](#view-mode) | N/A |
39+| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
40+| `Space` | Enter [space mode](#space-mode) | N/A |
4141
4242### Changes
4343
@@ -66,6 +66,16 @@
6666| `d` | Delete selection | `delete_selection` |
6767| `c` | Change selection (delete and enter insert mode) | `change_selection` |
6868
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+
6979### Selection manipulation
7080
7181| Key | Description | Command |
@@ -75,6 +85,7 @@
7585| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
7686| `;` | Collapse selection onto a single cursor | `collapse_selection` |
7787| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
88+| `,` | Keep only the primary selection | `keep_primary_selection` |
7889| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
7990| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
8091| `(` | Rotate main selection forward | `rotate_selections_backward` |
@@ -86,22 +97,13 @@
8697| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
8798| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
8899| `J` | Join lines inside selection | `join_selections` |
89−| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` |
90−| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` |
100+| `K` | Keep selections matching the regex | `keep_selections` |
101+| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
91102| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
92103
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−
101104### Search
102105
103−> TODO: The search implementation isn't ideal yet -- we don't support searching
104−in reverse, or searching via smartcase.
106+> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
105107
106108| Key | Description | Command |
107109| ----- | ----------- | ------- |
@@ -110,41 +112,17 @@ in reverse, or searching via smartcase.
110112| `N` | Add next search match to selection | `extend_search_next` |
111113| `*` | Use current selection as the search pattern | `search_selection` |
112114
113−### Unimpaired
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
115+### Minor modes
127116
128−| Key | Description | 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.
117+These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
143118
144−## View mode
119+#### View mode
145120
146121View 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+
148126
149127| Key | Description | Command |
150128| ----- | ----------- | ------- |
@@ -154,8 +132,12 @@ the selection.
154132| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
155133| `j` | Scroll the view downwards | `scroll_down` |
156134| `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` |
157139
158−## Goto mode
140+#### Goto mode
159141
160142Jumps to various locations.
161143
@@ -177,7 +159,7 @@ Jumps to various locations.
177159| `i` | Go to implementation | `goto_implementation` |
178160| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
179161
180−## Match mode
162+#### Match mode
181163
182164Enter this mode using `m` from normal mode. See the relavant section
183165in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
@@ -192,11 +174,9 @@ and [textobject](./usage.md#textobject) usage.
192174| `a` `<object>` | Select around textobject | `select_textobject_around` |
193175| `i` `<object>` | Select inside textobject | `select_textobject_inner` |
194176
195−## Object mode
196−
197177TODO: Mappings for selecting syntax nodes (a superset of `[`).
198178
199−## Window mode
179+#### Window mode
200180
201181This layer is similar to vim keybindings as kakoune does not support window.
202182
@@ -207,24 +187,56 @@ This layer is similar to vim keybindings as kakoune does not support window.
207187| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
208188| `q`, `Ctrl-q` | Close current window | `wclose` |
209189
210−## Space mode
190+#### Space mode
211191
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.
213193
214194| Key | Description | Command |
215195| ----- | ----------- | ------- |
196+| `k` | Show documentation for the item under the cursor | `hover` |
216197| `f` | Open file picker | `file_picker` |
217198| `b` | Open buffer picker | `buffer_picker` |
218199| `s` | Open symbol picker (current document) | `symbol_picker` |
219200| `a` | Apply code action | `code_action` |
220201| `'` | Open last fuzzy picker | `last_picker` |
221202| `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` |
223203| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
224204| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
225205| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
226206| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
227207| `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.
228240
229241# Picker
230242
book/src/remapping.md+2
@@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
4949| Null | `"null"` |
5050| Escape | `"esc"` |
5151
52+Keys can be disabled by binding them to the `no_op` command.
53+
5254Commands 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
3030"key.key" = "#ffffff"
3131```
3232
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.
3479
3580| Modifier |
3681| --- |
@@ -38,44 +83,88 @@ Possible modifiers:
3883| `dim` |
3984| `italic` |
4085| `underlined` |
41−| `slow\_blink` |
42−| `rapid\_blink` |
86+| `slow_blink` |
87+| `rapid_blink` |
4388| `reversed` |
4489| `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.
46164
47−Possible keys:
48165
49166| Key | Notes |
50167| --- | --- |
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` | |
79168| `ui.background` | |
80169| `ui.cursor` | |
81170| `ui.cursor.insert` | |
@@ -84,8 +173,8 @@ Possible keys:
84173| `ui.cursor.primary` | Cursor with primary selection |
85174| `ui.linenr` | |
86175| `ui.linenr.selected` | |
87−| `ui.statusline` | |
88−| `ui.statusline.inactive` | |
176+| `ui.statusline` | Statusline |
177+| `ui.statusline.inactive` | Statusline (unfocused document) |
89178| `ui.popup` | |
90179| `ui.window` | |
91180| `ui.help` | |
@@ -97,29 +186,9 @@ Possible keys:
97186| `ui.menu.selected` | |
98187| `ui.selection` | For selections in the editing area |
99188| `ui.selection.primary` | |
100−| `warning` | LSP warning |
101−| `error` | LSP error |
102−| `info` | LSP info |
103−| `hint` | LSP hint |
104−
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.
189+| `warning` | Diagnostics warning (gutter) |
190+| `error` | Diagnostics error (gutter) |
191+| `info` | Diagnostics info (gutter) |
192+| `hint` | Diagnostics hint (gutter) |
193+| `diagnostic` | For text in editing area |
106194
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 {
114114 margin-bottom: .875em;
115115}
116116
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+
117130.content p { line-height: 1.45em; }
118131.content ol { line-height: 1.45em; }
119132.content ul { line-height: 1.45em; }
book/theme/css/variables.css+14−10
@@ -69,7 +69,7 @@
6969
7070 --links: #2b79a2;
7171
72− --inline-code-color: #c5c8c6;;
72+ --inline-code-color: #c5c8c6;
7373
7474 --theme-popup-bg: #141617;
7575 --theme-popup-border: #43484d;
@@ -110,7 +110,7 @@
110110
111111 --links: #20609f;
112112
113− --inline-code-color: #301900;
113+ --inline-code-color: #a39e9b;
114114
115115 --theme-popup-bg: #fafafa;
116116 --theme-popup-border: #cccccc;
@@ -151,7 +151,7 @@
151151
152152 --links: #2b79a2;
153153
154− --inline-code-color: #c5c8c6;;
154+ --inline-code-color: #c5c8c6;
155155
156156 --theme-popup-bg: #161923;
157157 --theme-popup-border: #737480;
@@ -192,7 +192,7 @@
192192
193193 --links: #2b79a2;
194194
195− --inline-code-color: #6e6b5e;
195+ --inline-code-color: #c5c8c6;
196196
197197 --theme-popup-bg: #e1e1db;
198198 --theme-popup-border: #b38f6b;
@@ -234,7 +234,7 @@
234234
235235 --links: #2b79a2;
236236
237− --inline-code-color: #c5c8c6;;
237+ --inline-code-color: #6e6b5e;
238238
239239 --theme-popup-bg: #141617;
240240 --theme-popup-border: #43484d;
@@ -261,6 +261,7 @@
261261.colibri {
262262 --bg: #3b224c;
263263 --fg: #bcbdd0;
264+ --heading-fg: #fff;
264265
265266 --sidebar-bg: #281733;
266267 --sidebar-fg: #c8c9db;
@@ -276,18 +277,19 @@
276277 /* --links: #a4a0e8; */
277278 --links: #ECCDBA;
278279
279− --inline-code-color: #c5c8c6;;
280+ --inline-code-color: hsl(48.7, 7.8%, 70%);
280281
281282 --theme-popup-bg: #161923;
282283 --theme-popup-border: #737480;
283284 --theme-hover: rgba(0,0,0, .2);
284285
285− --quote-bg: hsl(226, 15%, 17%);
286+ --quote-bg: #281733;
286287 --quote-border: hsl(226, 15%, 22%);
287288
288− --table-border-color: hsl(226, 23%, 16%);
289− --table-header-bg: hsl(226, 23%, 31%);
289+ --table-border-color: hsl(226, 23%, 76%);
290+ --table-header-bg: hsla(226, 23%, 31%, 0);
290291 --table-alternate-bg: hsl(226, 23%, 14%);
292+ --table-border-line: hsla(201deg, 20%, 92%, 0.2);
291293
292294 --searchbar-border-color: #aaa;
293295 --searchbar-bg: #aeaec6;
@@ -300,6 +302,7 @@
300302}
301303
302304.colibri {
305+/*
303306 --bg: #ffffff;
304307 --fg: #452859;
305308 --fg: #5a5977;
@@ -318,7 +321,7 @@
318321
319322 --links: #6F44F0;
320323
321− --inline-code-color: #697C81;
324+ --inline-code-color: #a39e9b;
322325
323326 --theme-popup-bg: #161923;
324327 --theme-popup-border: #737480;
@@ -341,4 +344,5 @@
341344 --searchresults-border-color: #5c5c68;
342345 --searchresults-li-bg: #242430;
343346 --search-mark-bg: #a2cff5;
347+*/
344348}
book/theme/highlight.css+37−64
@@ -1,83 +1,56 @@
1−/*
2− * An increased contrast highlighting scheme loosely based on the
3− * "Base16 Atelier Dune Light" theme by Bram de Haan
4− * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)
5− * Original Base16 color scheme by Chris Kempson
6− * (https://github.com/chriskempson/base16)
7− */
8−
9−/* Comment */
1+pre code.hljs {
2+ display:block;
3+ overflow-x:auto;
4+ padding:1em
5+}
6+code.hljs {
7+ padding:3px 5px
8+}
9+.hljs {
10+ background:#2f1e2e;
11+ color:#a39e9b
12+}
1013.hljs-comment,
1114.hljs-quote {
12− color: #575757;
15+ color:#8d8687
1316}
14−
15−/* Red */
16−.hljs-variable,
17−.hljs-template-variable,
18−.hljs-attribute,
19−.hljs-tag,
20−.hljs-name,
21−.hljs-regexp,
2217.hljs-link,
18+.hljs-meta,
2319.hljs-name,
20+.hljs-regexp,
21+.hljs-selector-class,
2422.hljs-selector-id,
25−.hljs-selector-class {
26− color: #d70025;
23+.hljs-tag,
24+.hljs-template-variable,
25+.hljs-variable {
26+ color:#ef6155
2727}
28−
29−/* Orange */
30−.hljs-number,
31−.hljs-meta,
3228.hljs-built_in,
33−.hljs-builtin-name,
29+.hljs-deletion,
3430.hljs-literal,
35−.hljs-type,
36−.hljs-params {
37− color: #b21e00;
31+.hljs-number,
32+.hljs-params,
33+.hljs-type {
34+ color:#f99b15
3835}
39−
40−/* Green */
41−.hljs-string,
42−.hljs-symbol,
43−.hljs-bullet {
44− color: #008200;
36+.hljs-attribute,
37+.hljs-section,
38+.hljs-title {
39+ color:#fec418
4540}
46−
47−/* Blue */
48−.hljs-title,
49−.hljs-section {
50− color: #0030f2;
41+.hljs-addition,
42+.hljs-bullet,
43+.hljs-string,
44+.hljs-symbol {
45+ color:#48b685
5146}
52−
53−/* Purple */
5447.hljs-keyword,
5548.hljs-selector-tag {
56− color: #9d00ec;
57−}
58−
59−.hljs {
60− display: block;
61− overflow-x: auto;
62− background: #f6f7f6;
63− color: #000;
64− padding: 0.5em;
49+ color:#815ba4
6550}
66−
6751.hljs-emphasis {
68− font-style: italic;
52+ font-style:italic
6953}
70−
7154.hljs-strong {
72− font-weight: bold;
73−}
74−
75−.hljs-addition {
76− color: #22863a;
77− background-color: #f0fff4;
78−}
79−
80−.hljs-deletion {
81− color: #b31d28;
82− background-color: #ffeef0;
55+ font-weight:700
8356}
flake.lock+12−12
@@ -2,11 +2,11 @@
22 "nodes": {
33 "devshell": {
44 "locked": {
5− "lastModified": 1625086391,
6− "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=",
5+ "lastModified": 1630239564,
6+ "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
77 "owner": "numtide",
88 "repo": "devshell",
9− "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7",
9+ "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
1010 "type": "github"
1111 },
1212 "original": {
@@ -40,11 +40,11 @@
4040 "rustOverlay": "rustOverlay"
4141 },
4242 "locked": {
43− "lastModified": 1628489367,
44− "narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=",
43+ "lastModified": 1631254163,
44+ "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
4545 "owner": "yusdacra",
4646 "repo": "nix-cargo-integration",
47− "rev": "0dc8383aae5f791a48e34120edb04670b947dc0b",
47+ "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
4848 "type": "github"
4949 },
5050 "original": {
@@ -55,11 +55,11 @@
5555 },
5656 "nixpkgs": {
5757 "locked": {
58− "lastModified": 1628465643,
59− "narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=",
58+ "lastModified": 1631206977,
59+ "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
6060 "owner": "nixos",
6161 "repo": "nixpkgs",
62− "rev": "6ef4f522d63f22b40004319778761040d3197390",
62+ "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
6363 "type": "github"
6464 },
6565 "original": {
@@ -79,11 +79,11 @@
7979 "rustOverlay": {
8080 "flake": false,
8181 "locked": {
82− "lastModified": 1628475192,
83− "narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=",
82+ "lastModified": 1631240108,
83+ "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
8484 "owner": "oxalica",
8585 "repo": "rust-overlay",
86− "rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468",
86+ "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
8787 "type": "github"
8888 },
8989 "original": {
flake.nix+1−16
@@ -30,22 +30,7 @@
3030 };
3131 # link languages and theme toml files since helix-view expects them
3232 helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
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− '';
33+ helix-syntax = _prev: {
4934 preConfigure = "mkdir -p ../runtime/grammars";
5035 postInstall = "cp -r ../runtime $out/runtime";
5136 };
helix-core/Cargo.toml+2−2
@@ -23,7 +23,7 @@ unicode-segmentation = "1.8"
2323unicode-width = "0.1"
2424unicode-general-category = "0.4"
2525# slab = "0.4.2"
26−tree-sitter = "0.19"
26+tree-sitter = "0.20"
2727once_cell = "1.8"
2828arc-swap = "1"
2929regex = "1"
@@ -31,7 +31,7 @@ regex = "1"
3131serde = { version = "1.0", features = ["derive"] }
3232toml = "0.5"
3333
34−similar = "1.3"
34+similar = "2.0"
3535
3636etcetera = "0.3"
3737
helix-core/src/indent.rs+9−3
@@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos(
316316pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
317317 let mut scopes = Vec::new();
318318 if let Some(syntax) = syntax {
319− let byte_start = text.char_to_byte(pos);
320− let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) {
319+ let pos = text.char_to_byte(pos);
320+ let mut node = match syntax
321+ .tree()
322+ .root_node()
323+ .descendant_for_byte_range(pos, pos)
324+ {
321325 Some(node) => node,
322326 None => return scopes,
323327 };
@@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&
325329 scopes.push(node.kind());
326330
327331 while let Some(parent) = node.parent() {
328− scopes.push(parent.kind())
332+ scopes.push(parent.kind());
333+ node = parent;
329334 }
330335 }
331336
@@ -449,6 +454,7 @@ where
449454 highlight_config: OnceCell::new(),
450455 config: None,
451456 //
457+ injection_regex: None,
452458 roots: vec![],
453459 comment_token: None,
454460 auto_format: false,
helix-core/src/selection.rs+9
@@ -360,6 +360,15 @@ impl Selection {
360360 self.normalize()
361361 }
362362
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+
363372 /// Map selections over a set of changes. Useful for adjusting the selection position after
364373 /// applying changes to a document.
365374 pub fn map(self, changes: &ChangeSet) -> Self {
helix-core/src/syntax.rs+83−44
@@ -21,6 +21,15 @@ use std::{
2121use once_cell::sync::{Lazy, OnceCell};
2222use serde::{Deserialize, Serialize};
2323
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+
2433#[derive(Debug, Serialize, Deserialize)]
2534pub struct Configuration {
2635 pub language: Vec<LanguageConfiguration>,
@@ -42,7 +51,8 @@ pub struct LanguageConfiguration {
4251 pub auto_format: bool,
4352
4453 // content_regex
45− // injection_regex
54+ #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
55+ pub injection_regex: Option<Regex>,
4656 // first_line_regex
4757 //
4858 #[serde(skip)]
@@ -182,8 +192,12 @@ impl LanguageConfiguration {
182192 &highlights_query,
183193 &injections_query,
184194 &locals_query,
185− )
186− .unwrap(); // TODO: no unwrap
195+ );
196+
197+ let config = match config {
198+ Ok(config) => config,
199+ Err(err) => panic!("{}", err),
200+ }; // TODO: avoid panic
187201 config.configure(scopes);
188202 Some(Arc::new(config))
189203 }
@@ -277,6 +291,30 @@ impl Loader {
277291 .cloned()
278292 }
279293
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+ }
280318 pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
281319 self.language_configs.iter()
282320 }
@@ -314,16 +352,6 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
314352 Cow::from(source.slice(start_char..end_char))
315353}
316354
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−
327355impl Syntax {
328356 // buffer, grammar, config, grammars, sync_timeout?
329357 pub fn new(
@@ -416,16 +444,11 @@ impl Syntax {
416444 let config_ref =
417445 unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
418446
419− // TODO: if reusing cursors this might need resetting
420− if let Some(range) = &range {
421− cursor_ref.set_byte_range(range.start, range.end);
422− }
447+ // if reusing cursors & no range this resets to whole range
448+ cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
423449
424450 let captures = cursor_ref
425− .captures(query_ref, tree_ref.root_node(), move |n: Node| {
426− // &source[n.byte_range()]
427− node_to_bytes(n, source)
428− })
451+ .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
429452 .peekable();
430453
431454 // manually craft the root layer based on the existing tree
@@ -539,10 +562,7 @@ impl LanguageLayer {
539562 // let mut injections_by_pattern_index =
540563 // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
541564 // let matches =
542− // cursor.matches(combined_injections_query, tree.root_node(), |n: Node| {
543− // // &source[n.byte_range()]
544− // node_to_bytes(n, source)
545− // });
565+ // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
546566 // for mat in matches {
547567 // let entry = &mut injections_by_pattern_index[mat.pattern_index];
548568 // let (language_name, content_node, include_children) =
@@ -754,7 +774,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
754774use std::{iter, mem, ops, str, usize};
755775use tree_sitter::{
756776 Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
757− QueryMatch, Range, Tree,
777+ QueryMatch, Range, TextProvider, Tree,
758778};
759779
760780const CANCELLATION_CHECK_INTERVAL: usize = 100;
@@ -814,7 +834,7 @@ struct LocalScope<'a> {
814834}
815835
816836#[derive(Debug)]
817−struct HighlightIter<'a, 'tree: 'a, F>
837+struct HighlightIter<'a, F>
818838where
819839 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
820840{
@@ -822,16 +842,41 @@ where
822842 byte_offset: usize,
823843 injection_callback: F,
824844 cancellation_flag: Option<&'a AtomicUsize>,
825− layers: Vec<HighlightIterLayer<'a, 'tree>>,
845+ layers: Vec<HighlightIterLayer<'a>>,
826846 iter_count: usize,
827847 next_event: Option<HighlightEvent>,
828848 last_highlight_range: Option<(usize, usize, usize)>,
829849}
830850
831−struct HighlightIterLayer<'a, 'tree: 'a> {
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+
876+struct HighlightIterLayer<'a> {
832877 _tree: Option<Tree>,
833878 cursor: QueryCursor,
834− captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
879+ captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>,
835880 config: &'a HighlightConfiguration,
836881 highlight_end_stack: Vec<usize>,
837882 scope_stack: Vec<LocalScope<'a>>,
@@ -839,7 +884,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
839884 depth: usize,
840885}
841886
842−impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
887+impl<'a> fmt::Debug for HighlightIterLayer<'a> {
843888 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
844889 f.debug_struct("HighlightIterLayer").finish()
845890 }
@@ -1010,7 +1055,7 @@ impl HighlightConfiguration {
10101055 }
10111056}
10121057
1013−impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
1058+impl<'a> HighlightIterLayer<'a> {
10141059 /// Create a new 'layer' of highlighting for this document.
10151060 ///
10161061 /// In the even that the new layer contains "combined injections" (injections where multiple
@@ -1067,10 +1112,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
10671112 let matches = cursor.matches(
10681113 combined_injections_query,
10691114 tree.root_node(),
1070− |n: Node| {
1071− // &source[n.byte_range()]
1072− node_to_bytes(n, source)
1073− },
1115+ RopeProvider(source),
10741116 );
10751117 for mat in matches {
10761118 let entry = &mut injections_by_pattern_index[mat.pattern_index];
@@ -1117,10 +1159,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
11171159 let cursor_ref =
11181160 unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
11191161 let captures = cursor_ref
1120− .captures(&config.query, tree_ref.root_node(), move |n: Node| {
1121− // &source[n.byte_range()]
1122− node_to_bytes(n, source)
1123− })
1162+ .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
11241163 .peekable();
11251164
11261165 result.push(HighlightIterLayer {
@@ -1274,7 +1313,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
12741313 }
12751314}
12761315
1277−impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
1316+impl<'a, F> HighlightIter<'a, F>
12781317where
12791318 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
12801319{
@@ -1325,7 +1364,7 @@ where
13251364 }
13261365 }
13271366
1328− fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
1367+ fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
13291368 if let Some(sort_key) = layer.sort_key() {
13301369 let mut i = 1;
13311370 while i < self.layers.len() {
@@ -1344,7 +1383,7 @@ where
13441383 }
13451384}
13461385
1347−impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
1386+impl<'a, F> Iterator for HighlightIter<'a, F>
13481387where
13491388 F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
13501389{
@@ -1608,7 +1647,7 @@ where
16081647fn injection_for_match<'a>(
16091648 config: &HighlightConfiguration,
16101649 query: &'a Query,
1611− query_match: &QueryMatch<'a>,
1650+ query_match: &QueryMatch<'a, 'a>,
16121651 source: RopeSlice<'a>,
16131652) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
16141653 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"] }
2323serde = { version = "1.0", features = ["derive"] }
2424serde_json = "1.0"
2525thiserror = "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"] }
2727tokio-stream = "0.1"
helix-lsp/src/client.rs+56−44
@@ -3,17 +3,23 @@ use crate::{
33 Call, Error, OffsetEncoding, Result,
44};
55
6−use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope};
6+use helix_core::{find_root, ChangeSet, Rope};
77use jsonrpc_core as jsonrpc;
88use lsp_types as lsp;
99use serde_json::Value;
1010use std::future::Future;
1111use std::process::Stdio;
12−use std::sync::atomic::{AtomicU64, Ordering};
12+use std::sync::{
13+ atomic::{AtomicU64, Ordering},
14+ Arc,
15+};
1316use tokio::{
1417 io::{BufReader, BufWriter},
1518 process::{Child, Command},
16− sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
19+ sync::{
20+ mpsc::{channel, UnboundedReceiver, UnboundedSender},
21+ Notify, OnceCell,
22+ },
1723};
1824
1925#[derive(Debug)]
@@ -22,18 +28,19 @@ pub struct Client {
2228 _process: Child,
2329 server_tx: UnboundedSender<Payload>,
2430 request_counter: AtomicU64,
25− capabilities: Option<lsp::ServerCapabilities>,
31+ pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
2632 offset_encoding: OffsetEncoding,
2733 config: Option<Value>,
2834}
2935
3036impl Client {
37+ #[allow(clippy::type_complexity)]
3138 pub fn start(
3239 cmd: &str,
3340 args: &[String],
3441 config: Option<Value>,
3542 id: usize,
36− ) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> {
43+ ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
3744 let process = Command::new(cmd)
3845 .args(args)
3946 .stdin(Stdio::piped())
@@ -50,22 +57,20 @@ impl Client {
5057 let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
5158 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
5259
53− let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id);
60+ let (server_rx, server_tx, initialize_notify) =
61+ Transport::start(reader, writer, stderr, id);
5462
5563 let client = Self {
5664 id,
5765 _process: process,
5866 server_tx,
5967 request_counter: AtomicU64::new(0),
60− capabilities: None,
68+ capabilities: OnceCell::new(),
6169 offset_encoding: OffsetEncoding::Utf8,
6270 config,
6371 };
6472
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))
6974 }
7075
7176 pub fn id(&self) -> usize {
@@ -88,9 +93,13 @@ impl Client {
8893 }
8994 }
9095
96+ pub fn is_initialized(&self) -> bool {
97+ self.capabilities.get().is_some()
98+ }
99+
91100 pub fn capabilities(&self) -> &lsp::ServerCapabilities {
92101 self.capabilities
93− .as_ref()
102+ .get()
94103 .expect("language server not yet initialized!")
95104 }
96105
@@ -143,7 +152,8 @@ impl Client {
143152 })
144153 .map_err(|e| Error::Other(e.into()))?;
145154
146− timeout(Duration::from_secs(2), rx.recv())
155+ // TODO: specifiable timeout, delay other calls until initialize success
156+ timeout(Duration::from_secs(20), rx.recv())
147157 .await
148158 .map_err(|_| Error::Timeout)? // return Timeout
149159 .ok_or(Error::StreamClosed)?
@@ -151,7 +161,7 @@ impl Client {
151161 }
152162
153163 /// Send a RPC notification to the language server.
154− fn notify<R: lsp::notification::Notification>(
164+ pub fn notify<R: lsp::notification::Notification>(
155165 &self,
156166 params: R::Params,
157167 ) -> impl Future<Output = Result<()>>
@@ -213,7 +223,7 @@ impl Client {
213223 // General messages
214224 // -------------------------------------------------------------------------------------------
215225
216− pub(crate) async fn initialize(&mut self) -> Result<()> {
226+ pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
217227 // TODO: delay any requests that are triggered prior to initialize
218228 let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
219229
@@ -281,14 +291,7 @@ impl Client {
281291 locale: None, // TODO
282292 };
283293
284− let response = 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(())
294+ self.request::<lsp::request::Initialize>(params).await
292295 }
293296
294297 pub async fn shutdown(&self) -> Result<()> {
@@ -356,7 +359,6 @@ impl Client {
356359 //
357360 // Calculation is therefore a bunch trickier.
358361
359− // TODO: stolen from syntax.rs, share
360362 use helix_core::RopeSlice;
361363 fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
362364 let lsp::Position {
@@ -366,7 +368,12 @@ impl Client {
366368
367369 let mut chars = text.chars().peekable();
368370 while let Some(ch) = chars.next() {
369− if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
371+ // LSP only considers \n, \r or \r\n as line endings
372+ if ch == '\n' || ch == '\r' {
373+ // consume a \r\n
374+ if ch == '\r' && chars.peek() == Some(&'\n') {
375+ chars.next();
376+ }
370377 line += 1;
371378 character = 0;
372379 } else {
@@ -441,7 +448,7 @@ impl Client {
441448 ) -> Option<impl Future<Output = Result<()>>> {
442449 // figure out what kind of sync the server supports
443450
444− let capabilities = self.capabilities.as_ref().unwrap();
451+ let capabilities = self.capabilities.get().unwrap();
445452
446453 let sync_capabilities = match capabilities.text_document_sync {
447454 Some(lsp::TextDocumentSyncCapability::Kind(kind))
@@ -459,7 +466,7 @@ impl Client {
459466 // range = None -> whole document
460467 range: None, //Some(Range)
461468 range_length: None, // u64 apparently deprecated
462− text: "".to_string(),
469+ text: new_text.to_string(),
463470 }]
464471 }
465472 lsp::TextDocumentSyncKind::Incremental => {
@@ -487,12 +494,12 @@ impl Client {
487494
488495 // will_save / will_save_wait_until
489496
490− pub async fn text_document_did_save(
497+ pub fn text_document_did_save(
491498 &self,
492499 text_document: lsp::TextDocumentIdentifier,
493500 text: &Rope,
494− ) -> Result<()> {
495− let capabilities = self.capabilities.as_ref().unwrap();
501+ ) -> Option<impl Future<Output = Result<()>>> {
502+ let capabilities = self.capabilities.get().unwrap();
496503
497504 let include_text = match &capabilities.text_document_sync {
498505 Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
@@ -504,17 +511,18 @@ impl Client {
504511 include_text,
505512 }) => include_text.unwrap_or(false),
506513 // Supported(false)
507− _ => return Ok(()),
514+ _ => return None,
508515 },
509516 // unsupported
510− _ => return Ok(()),
517+ _ => return None,
511518 };
512519
513− self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
514− text_document,
515− text: include_text.then(|| text.into()),
516− })
517− .await
520+ Some(self.notify::<lsp::notification::DidSaveTextDocument>(
521+ lsp::DidSaveTextDocumentParams {
522+ text_document,
523+ text: include_text.then(|| text.into()),
524+ },
525+ ))
518526 }
519527
520528 pub fn completion(
@@ -580,19 +588,19 @@ impl Client {
580588
581589 // formatting
582590
583− pub async fn text_document_formatting(
591+ pub fn text_document_formatting(
584592 &self,
585593 text_document: lsp::TextDocumentIdentifier,
586594 options: lsp::FormattingOptions,
587595 work_done_token: Option<lsp::ProgressToken>,
588− ) -> anyhow::Result<Vec<lsp::TextEdit>> {
589− let capabilities = self.capabilities.as_ref().unwrap();
596+ ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
597+ let capabilities = self.capabilities.get().unwrap();
590598
591599 // check if we're able to format
592600 match capabilities.document_formatting_provider {
593601 Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
594602 // None | Some(false)
595− _ => return Ok(Vec::new()),
603+ _ => return None,
596604 };
597605 // TODO: return err::unavailable so we can fall back to tree sitter formatting
598606
@@ -602,9 +610,13 @@ impl Client {
602610 work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
603611 };
604612
605− let response = self.request::<lsp::request::Formatting>(params).await?;
613+ let request = self.call::<lsp::request::Formatting>(params);
606614
607− Ok(response.unwrap_or_default())
615+ Some(async move {
616+ let json = request.await?;
617+ let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
618+ Ok(response.unwrap_or_default())
619+ })
608620 }
609621
610622 pub async fn text_document_range_formatting(
@@ -614,7 +626,7 @@ impl Client {
614626 options: lsp::FormattingOptions,
615627 work_done_token: Option<lsp::ProgressToken>,
616628 ) -> anyhow::Result<Vec<lsp::TextEdit>> {
617− let capabilities = self.capabilities.as_ref().unwrap();
629+ let capabilities = self.capabilities.get().unwrap();
618630
619631 // check if we're able to format
620632 match capabilities.document_range_formatting_provider {
helix-lsp/src/lib.rs+57−53
@@ -226,6 +226,8 @@ impl MethodCall {
226226
227227#[derive(Debug, PartialEq, Clone)]
228228pub enum Notification {
229+ // we inject this notification to signal the LSP is ready
230+ Initialized,
229231 PublishDiagnostics(lsp::PublishDiagnosticsParams),
230232 ShowMessage(lsp::ShowMessageParams),
231233 LogMessage(lsp::LogMessageParams),
@@ -237,6 +239,7 @@ impl Notification {
237239 use lsp::notification::Notification as _;
238240
239241 let notification = match method {
242+ lsp::notification::Initialized::METHOD => Self::Initialized,
240243 lsp::notification::PublishDiagnostics::METHOD => {
241244 let params: lsp::PublishDiagnosticsParams = params
242245 .parse()
@@ -294,7 +297,7 @@ impl Registry {
294297 }
295298 }
296299
297− pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
300+ pub fn get_by_id(&self, id: usize) -> Option<&Client> {
298301 self.inner
299302 .values()
300303 .find(|(client_id, _)| client_id == &id)
@@ -302,33 +305,60 @@ impl Registry {
302305 }
303306
304307 pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
305− if let Some(config) = &language_config.language_server {
306− // avoid borrow issues
307− let inner = &mut self.inner;
308− let s_incoming = &mut self.incoming;
309−
310− match inner.entry(language_config.scope.clone()) {
311− Entry::Occupied(entry) => Ok(entry.get().1.clone()),
312− Entry::Vacant(entry) => {
313− // initialize a new client
314− let id = self.counter.fetch_add(1, Ordering::Relaxed);
315− let (mut client, incoming) = Client::start(
316− &config.command,
317− &config.args,
318− serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(),
319− id,
320− )?;
321− // TODO: run this async without blocking
322− futures_executor::block_on(client.initialize())?;
323− s_incoming.push(UnboundedReceiverStream::new(incoming));
324− let client = Arc::new(client);
325−
326− entry.insert((id, client.clone()));
327− Ok(client)
328− }
308+ let config = match &language_config.language_server {
309+ Some(config) => config,
310+ None => return Err(Error::LspNotDefined),
311+ };
312+
313+ match self.inner.entry(language_config.scope.clone()) {
314+ Entry::Occupied(entry) => Ok(entry.get().1.clone()),
315+ Entry::Vacant(entry) => {
316+ // initialize a new client
317+ let id = self.counter.fetch_add(1, Ordering::Relaxed);
318+ let (client, incoming, initialize_notify) = Client::start(
319+ &config.command,
320+ &config.args,
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(),
330+ id,
331+ )?;
332+ self.incoming.push(UnboundedReceiverStream::new(incoming));
333+ let client = Arc::new(client);
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+
359+ entry.insert((id, client.clone()));
360+ Ok(client)
329361 }
330− } else {
331− Err(Error::LspNotDefined)
332362 }
333363 }
334364
@@ -415,32 +445,6 @@ impl LspProgressMap {
415445 }
416446}
417447
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−
444448#[cfg(test)]
445449mod tests {
446450 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};
22use anyhow::Context;
33use jsonrpc_core as jsonrpc;
4−use log::{debug, error, info, warn};
4+use log::{error, info};
55use serde::{Deserialize, Serialize};
66use serde_json::Value;
77use std::collections::HashMap;
@@ -11,7 +11,7 @@ use tokio::{
1111 process::{ChildStderr, ChildStdin, ChildStdout},
1212 sync::{
1313 mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
14− Mutex,
14+ Mutex, Notify,
1515 },
1616};
1717
@@ -51,9 +51,11 @@ impl Transport {
5151 ) -> (
5252 UnboundedReceiver<(usize, jsonrpc::Call)>,
5353 UnboundedSender<Payload>,
54+ Arc<Notify>,
5455 ) {
5556 let (client_tx, rx) = unbounded_channel();
5657 let (tx, client_rx) = unbounded_channel();
58+ let notify = Arc::new(Notify::new());
5759
5860 let transport = Self {
5961 id,
@@ -62,11 +64,21 @@ impl Transport {
6264
6365 let transport = Arc::new(transport);
6466
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+ ));
6672 tokio::spawn(Self::err(transport.clone(), server_stderr));
67− tokio::spawn(Self::send(transport, server_stdin, client_rx));
68−
69− (rx, tx)
73+ tokio::spawn(Self::send(
74+ transport,
75+ server_stdin,
76+ client_tx,
77+ client_rx,
78+ notify.clone(),
79+ ));
80+
81+ (rx, tx, notify)
7082 }
7183
7284 async fn recv_server_message(
@@ -76,14 +88,18 @@ impl Transport {
7688 let mut content_length = None;
7789 loop {
7890 buffer.truncate(0);
79− reader.read_line(buffer).await?;
80− let header = buffer.trim();
91+ if reader.read_line(buffer).await? == 0 {
92+ return Err(Error::StreamClosed);
93+ };
94+
95+ // debug!("<- header {:?}", buffer);
8196
82− if header.is_empty() {
97+ if buffer == "\r\n" {
98+ // look for an empty CRLF line
8399 break;
84100 }
85101
86− debug!("<- header {}", header);
102+ let header = buffer.trim();
87103
88104 let parts = header.split_once(": ");
89105
@@ -96,7 +112,8 @@ impl Transport {
96112 // Workaround: Some non-conformant language servers will output logging and other garbage
97113 // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
98114 // the server. Skip such lines and log a warning.
99− warn!("Failed to parse header: {:?}", header);
115+
116+ // warn!("Failed to parse header: {:?}", header);
100117 }
101118 }
102119 }
@@ -121,8 +138,10 @@ impl Transport {
121138 buffer: &mut String,
122139 ) -> Result<()> {
123140 buffer.truncate(0);
124− err.read_line(buffer).await?;
125− error!("err <- {}", buffer);
141+ if err.read_line(buffer).await? == 0 {
142+ return Err(Error::StreamClosed);
143+ };
144+ error!("err <- {:?}", buffer);
126145
127146 Ok(())
128147 }
@@ -255,16 +274,90 @@ impl Transport {
255274 async fn send(
256275 transport: Arc<Self>,
257276 mut server_stdin: BufWriter<ChildStdin>,
277+ client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
258278 mut client_rx: UnboundedReceiver<Payload>,
279+ initialize_notify: Arc<Notify>,
259280 ) {
260− while let Some(msg) = client_rx.recv().await {
261− match transport
262− .send_payload_to_server(&mut server_stdin, msg)
263− .await
264− {
265− Ok(_) => {}
266− Err(err) => {
267− error!("err: <- {:?}", err);
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+ }
339+ msg = client_rx.recv() => {
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 {
350+ match transport.send_payload_to_server(&mut server_stdin, msg).await {
351+ Ok(_) => {}
352+ Err(err) => {
353+ error!("err: <- {:?}", err);
354+ }
355+ }
356+ }
357+ } else {
358+ // channel closed
359+ break;
360+ }
268361 }
269362 }
270363 }
helix-syntax/Cargo.toml+1−1
@@ -11,7 +11,7 @@ homepage = "https://helix-editor.com"
1111include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
1212
1313[dependencies]
14−tree-sitter = "0.19"
14+tree-sitter = "0.20"
1515libloading = "0.7"
1616anyhow = "1"
1717
helix-syntax/build.rs+1−2
@@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) {
158158 .is_none()
159159 {
160160 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'?",
162162 dir
163163 );
164− eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
165164 std::process::exit(1);
166165 }
167166
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"
5656serde_json = "1.0"
5757serde = { version = "1.0", features = ["derive"] }
5858
59+# ripgrep for global search
60+grep-regex = "0.1.9"
61+grep-searcher = "0.1.8"
62+
5963[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
6064signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
helix-term/src/application.rs+53−54
@@ -434,16 +434,42 @@ impl Application {
434434 };
435435
436436 match notification {
437− Notification::PublishDiagnostics(params) => {
438− let path = Some(params.uri.to_file_path().unwrap());
437+ Notification::Initialized => {
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+ };
439446
440− let doc = self
441− .editor
442− .documents
443− .iter_mut()
444− .find(|(_, doc)| doc.path() == path.as_ref());
447+ let docs = self.editor.documents().filter(|doc| {
448+ doc.language_server().map(|server| server.id()) == Some(server_id)
449+ });
445450
446− if let Some((_, doc)) = doc {
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+
472+ if let Some(doc) = doc {
447473 let text = doc.text();
448474
449475 let diagnostics = params
@@ -506,7 +532,7 @@ impl Application {
506532 log::warn!("unhandled window/showMessage: {:?}", params);
507533 }
508534 Notification::LogMessage(params) => {
509− log::warn!("unhandled window/logMessage: {:?}", params);
535+ log::info!("window/logMessage: {:?}", params);
510536 }
511537 Notification::ProgressMessage(params) => {
512538 let lsp::ProgressParams { token, value } = params;
@@ -588,10 +614,27 @@ impl Application {
588614 Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
589615 method, params, id, ..
590616 }) => {
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+
591625 let call = match MethodCall::parse(&method, params) {
592626 Some(call) => call,
593627 None => {
594628 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+ // );
595638 return;
596639 }
597640 };
@@ -604,53 +647,9 @@ impl Application {
604647 if spinner.is_stopped() {
605648 spinner.start();
606649 }
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)));
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− }
650+ tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
643651 }
644652 }
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− // );
654653 }
655654 e => unreachable!("{:?}", e),
656655 }
helix-term/src/commands.rs+435−227
@@ -9,7 +9,7 @@ use helix_core::{
99 match_brackets,
1010 movement::{self, Direction},
1111 object, pos_at_coords,
12− regex::{self, Regex},
12+ regex::{self, Regex, RegexBuilder},
1313 register::Register,
1414 search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
1515 RopeSlice, Selection, SmallVec, Tendril, Transaction,
@@ -35,7 +35,7 @@ use crate::{
3535};
3636
3737use crate::job::{self, Job, Jobs};
38−use futures_util::FutureExt;
38+use futures_util::{FutureExt, StreamExt};
3939use std::num::NonZeroUsize;
4040use std::{collections::HashMap, fmt, future::Future};
4141
@@ -47,8 +47,13 @@ use std::{
4747use once_cell::sync::Lazy;
4848use serde::de::{self, Deserialize, Deserializer};
4949
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+
5055pub struct Context<'a> {
51− pub selected_register: helix_view::RegisterSelection,
56+ pub register: Option<char>,
5257 pub count: Option<NonZeroUsize>,
5358 pub editor: &'a mut Editor,
5459
@@ -166,6 +171,7 @@ impl Command {
166171
167172 #[rustfmt::skip]
168173 commands!(
174+ no_op, "Do nothing",
169175 move_char_left, "Move left",
170176 move_char_right, "Move right",
171177 move_line_up, "Move up",
@@ -184,6 +190,9 @@ impl Command {
184190 move_next_long_word_end, "Move to end of next long word",
185191 extend_next_word_start, "Extend to beginning of next word",
186192 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",
187196 extend_next_word_end, "Extend to end of next word",
188197 find_till_char, "Move till next occurance of char",
189198 find_next_char, "Move to next occurance of char",
@@ -209,6 +218,7 @@ impl Command {
209218 search_next, "Select next search match",
210219 extend_search_next, "Add next search match to selection",
211220 search_selection, "Use current selection as search pattern",
221+ global_search, "Global Search in workspace folder",
212222 extend_line, "Select current line, if already selected, extend to next line",
213223 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
214224 delete_selection, "Delete selection",
@@ -253,6 +263,9 @@ impl Command {
253263 // TODO: different description ?
254264 goto_line_end_newline, "Goto line end",
255265 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",
256269 signature_help, "Show signature help",
257270 insert_tab, "Insert tab char",
258271 insert_newline, "Insert newline char",
@@ -281,6 +294,7 @@ impl Command {
281294 join_selections, "Join lines inside selection",
282295 keep_selections, "Keep selections matching regex",
283296 keep_primary_selection, "Keep primary selection",
297+ remove_primary_selection, "Remove primary selection",
284298 completion, "Invoke completion popup",
285299 hover, "Show docs for item under cursor",
286300 toggle_comments, "Comment/uncomment selections",
@@ -372,52 +386,58 @@ impl PartialEq for Command {
372386 }
373387}
374388
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+{
376395 let count = cx.count();
377396 let (view, doc) = current!(cx.editor);
378397 let text = doc.text().slice(..);
379398
380− let selection = doc.selection(view.id).clone().transform(|range| {
381− movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
382− });
399+ let selection = doc
400+ .selection(view.id)
401+ .clone()
402+ .transform(|range| move_fn(text, range, dir, count, behaviour));
383403 doc.set_selection(view.id, selection);
384404}
385405
386−fn move_char_right(cx: &mut Context) {
387− let count = cx.count();
388− let (view, doc) = current!(cx.editor);
389− let text = doc.text().slice(..);
406+use helix_core::movement::{move_horizontally, move_vertically};
390407
391− let selection = doc.selection(view.id).clone().transform(|range| {
392− movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
393− });
394− doc.set_selection(view.id, selection);
408+fn move_char_left(cx: &mut Context) {
409+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
395410}
396411
397−fn move_line_up(cx: &mut Context) {
398− let count = cx.count();
399− let (view, doc) = current!(cx.editor);
400− let text = doc.text().slice(..);
412+fn move_char_right(cx: &mut Context) {
413+ move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
414+}
401415
402− let selection = doc.selection(view.id).clone().transform(|range| {
403− movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
404− });
405− doc.set_selection(view.id, selection);
416+fn move_line_up(cx: &mut Context) {
417+ move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
406418}
407419
408420fn move_line_down(cx: &mut Context) {
409− let count = cx.count();
410− let (view, doc) = current!(cx.editor);
411− let text = doc.text().slice(..);
421+ move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
422+}
412423
413− let selection = doc.selection(view.id).clone().transform(|range| {
414− movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
415− });
416− doc.set_selection(view.id, selection);
424+fn extend_char_left(cx: &mut Context) {
425+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
417426}
418427
419−fn goto_line_end(cx: &mut Context) {
420− let (view, doc) = current!(cx.editor);
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+
436+fn extend_line_down(cx: &mut Context) {
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) {
421441 let text = doc.text().slice(..);
422442
423443 let selection = doc.selection(view.id).clone().transform(|range| {
@@ -427,26 +447,60 @@ fn goto_line_end(cx: &mut Context) {
427447 let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
428448 .max(line_start);
429449
430− range.put_cursor(text, pos, doc.mode == Mode::Select)
450+ range.put_cursor(text, pos, movement == Movement::Extend)
431451 });
432452 doc.set_selection(view.id, selection);
433453}
434454
435−fn goto_line_end_newline(cx: &mut Context) {
455+fn goto_line_end(cx: &mut Context) {
436456 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) {
437474 let text = doc.text().slice(..);
438475
439476 let selection = doc.selection(view.id).clone().transform(|range| {
440477 let line = range.cursor_line(text);
441478 let pos = line_end_char_index(&text, line);
442479
443− range.put_cursor(text, pos, doc.mode == Mode::Select)
480+ range.put_cursor(text, pos, movement == Movement::Extend)
444481 });
445482 doc.set_selection(view.id, selection);
446483}
447484
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) {
449499 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) {
450504 let text = doc.text().slice(..);
451505
452506 let selection = doc.selection(view.id).clone().transform(|range| {
@@ -454,11 +508,29 @@ fn goto_line_start(cx: &mut Context) {
454508
455509 // adjust to start of the line
456510 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)
458512 });
459513 doc.set_selection(view.id, selection);
460514}
461515
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+
462534fn goto_first_nonwhitespace(cx: &mut Context) {
463535 let (view, doc) = current!(cx.editor);
464536 let text = doc.text().slice(..);
@@ -512,7 +584,10 @@ fn goto_window_bottom(cx: &mut Context) {
512584 goto_window(cx, Align::Bottom)
513585}
514586
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+{
516591 let count = cx.count();
517592 let (view, doc) = current!(cx.editor);
518593 let text = doc.text().slice(..);
@@ -520,68 +595,32 @@ fn move_next_word_start(cx: &mut Context) {
520595 let selection = doc
521596 .selection(view.id)
522597 .clone()
523− .transform(|range| movement::move_next_word_start(text, range, count));
598+ .transform(|range| move_fn(text, range, count));
524599 doc.set_selection(view.id, selection);
525600}
526601
527−fn move_prev_word_start(cx: &mut Context) {
528− let count = cx.count();
529− let (view, doc) = current!(cx.editor);
530− let text = doc.text().slice(..);
602+fn move_next_word_start(cx: &mut Context) {
603+ move_word_impl(cx, movement::move_next_word_start)
604+}
531605
532− let selection = doc
533− .selection(view.id)
534− .clone()
535− .transform(|range| movement::move_prev_word_start(text, range, count));
536− doc.set_selection(view.id, selection);
606+fn move_prev_word_start(cx: &mut Context) {
607+ move_word_impl(cx, movement::move_prev_word_start)
537608}
538609
539610fn 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));
548− doc.set_selection(view.id, selection);
611+ move_word_impl(cx, movement::move_next_word_end)
549612}
550613
551614fn 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));
560− doc.set_selection(view.id, selection);
615+ move_word_impl(cx, movement::move_next_long_word_start)
561616}
562617
563618fn 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));
572− doc.set_selection(view.id, selection);
619+ move_word_impl(cx, movement::move_prev_long_word_start)
573620}
574621
575622fn 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));
584− doc.set_selection(view.id, selection);
623+ move_word_impl(cx, movement::move_next_long_word_end)
585624}
586625
587626fn goto_file_start(cx: &mut Context) {
@@ -600,43 +639,44 @@ fn goto_file_end(cx: &mut Context) {
600639 doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
601640}
602641
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+{
604646 let count = cx.count();
605647 let (view, doc) = current!(cx.editor);
606648 let text = doc.text().slice(..);
607649
608650 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);
610652 let pos = word.cursor(text);
611653 range.put_cursor(text, pos, true)
612654 });
613655 doc.set_selection(view.id, selection);
614656}
615657
616−fn extend_prev_word_start(cx: &mut Context) {
617− let count = cx.count();
618− let (view, doc) = current!(cx.editor);
619− let text = doc.text().slice(..);
658+fn extend_next_word_start(cx: &mut Context) {
659+ extend_word_impl(cx, movement::move_next_word_start)
660+}
620661
621− let selection = doc.selection(view.id).clone().transform(|range| {
622− let word = movement::move_prev_word_start(text, range, count);
623− let pos = word.cursor(text);
624− range.put_cursor(text, pos, true)
625− });
626− doc.set_selection(view.id, selection);
662+fn extend_prev_word_start(cx: &mut Context) {
663+ extend_word_impl(cx, movement::move_prev_word_start)
627664}
628665
629666fn extend_next_word_end(cx: &mut Context) {
630− let count = cx.count();
631− let (view, doc) = current!(cx.editor);
632− let text = doc.text().slice(..);
667+ extend_word_impl(cx, movement::move_next_word_end)
668+}
633669
634− let selection = doc.selection(view.id).clone().transform(|range| {
635− let word = movement::move_next_word_end(text, range, count);
636− let pos = word.cursor(text);
637− range.put_cursor(text, pos, true)
638− });
639− doc.set_selection(view.id, selection);
670+fn extend_next_long_word_start(cx: &mut Context) {
671+ extend_word_impl(cx, movement::move_next_long_word_start)
672+}
673+
674+fn extend_prev_long_word_start(cx: &mut Context) {
675+ extend_word_impl(cx, movement::move_prev_long_word_start)
676+}
677+
678+fn extend_next_long_word_end(cx: &mut Context) {
679+ extend_word_impl(cx, movement::move_next_long_word_end)
640680}
641681
642682#[inline]
@@ -852,12 +892,25 @@ fn replace(cx: &mut Context) {
852892 })
853893}
854894
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+{
856899 let (view, doc) = current!(cx.editor);
857900 let selection = doc.selection(view.id);
858901 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
861914 .chars()
862915 .flat_map(|ch| {
863916 if ch.is_lowercase() {
@@ -868,39 +921,16 @@ fn switch_case(cx: &mut Context) {
868921 vec![ch]
869922 }
870923 })
871− .collect();
872−
873− (range.from(), range.to(), Some(text))
924+ .collect()
874925 });
875−
876− doc.apply(&transaction, view.id);
877− doc.append_changes_to_history(view.id);
878926}
879927
880928fn switch_to_uppercase(cx: &mut Context) {
881− let (view, doc) = current!(cx.editor);
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);
929+ switch_case_impl(cx, |string| string.to_uppercase().into());
891930}
892931
893932fn switch_to_lowercase(cx: &mut Context) {
894− let (view, doc) = current!(cx.editor);
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);
933+ switch_case_impl(cx, |string| string.to_lowercase().into());
904934}
905935
906936pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
@@ -976,28 +1006,6 @@ fn half_page_down(cx: &mut Context) {
9761006 scroll(cx, offset, Direction::Forward);
9771007}
9781008
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−
10011009fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
10021010 let count = cx.count();
10031011 let (view, doc) = current!(cx.editor);
@@ -1068,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) {
10681076 copy_selection_on_line(cx, Direction::Forward)
10691077}
10701078
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−
10931079fn select_all(cx: &mut Context) {
10941080 let (view, doc) = current!(cx.editor);
10951081
@@ -1098,23 +1084,42 @@ fn select_all(cx: &mut Context) {
10981084}
10991085
11001086fn select_regex(cx: &mut Context) {
1101− let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| {
1102− let text = doc.text().slice(..);
1103− if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), &regex)
1104− {
1105− doc.set_selection(view.id, selection);
1106− }
1107− });
1087+ let reg = cx.register.unwrap_or('/');
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+ }
1096+ let text = doc.text().slice(..);
1097+ if let Some(selection) =
1098+ selection::select_on_matches(text, doc.selection(view.id), &regex)
1099+ {
1100+ doc.set_selection(view.id, selection);
1101+ }
1102+ },
1103+ );
11081104
11091105 cx.push_layer(Box::new(prompt));
11101106}
11111107
11121108fn split_selection(cx: &mut Context) {
1113− let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| {
1114− let text = doc.text().slice(..);
1115− let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
1116− doc.set_selection(view.id, selection);
1117− });
1109+ let reg = cx.register.unwrap_or('/');
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+ }
1118+ let text = doc.text().slice(..);
1119+ let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
1120+ doc.set_selection(view.id, selection);
1121+ },
1122+ );
11181123
11191124 cx.push_layer(Box::new(prompt));
11201125}
@@ -1168,6 +1173,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
11681173
11691174// TODO: use one function for search vs extend
11701175fn search(cx: &mut Context) {
1176+ let reg = cx.register.unwrap_or('/');
11711177 let (_, doc) = current!(cx.editor);
11721178
11731179 // TODO: could probably share with select_on_matches?
@@ -1176,23 +1182,44 @@ fn search(cx: &mut Context) {
11761182 // feed chunks into the regex yet
11771183 let contents = doc.text().slice(..).to_string();
11781184
1179− let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| {
1180− search_impl(doc, view, &contents, &regex, false);
1181− // TODO: only store on enter (accept), not update
1182− registers.write('/', vec![regex.as_str().to_string()]);
1183− });
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+ }
1193+ search_impl(doc, view, &contents, &regex, false);
1194+ },
1195+ );
11841196
11851197 cx.push_layer(Box::new(prompt));
11861198}
11871199
11881200fn search_next_impl(cx: &mut Context, extend: bool) {
11891201 let (view, doc) = current!(cx.editor);
1190− let registers = &mut cx.editor.registers;
1202+ let registers = &cx.editor.registers;
11911203 if let Some(query) = registers.read('/') {
1192− let query = query.first().unwrap();
1204+ let query = query.last().unwrap();
11931205 let contents = doc.text().slice(..).to_string();
1194− let regex = Regex::new(query).unwrap();
1195− search_impl(doc, view, &contents, &regex, extend);
1206+ let case_insensitive = if cx.editor.config.smart_case {
1207+ !query.chars().any(char::is_uppercase)
1208+ } else {
1209+ false
1210+ };
1211+ if let Ok(regex) = RegexBuilder::new(query)
1212+ .case_insensitive(case_insensitive)
1213+ .build()
1214+ {
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+ }
11961223 }
11971224}
11981225
@@ -1209,8 +1236,119 @@ fn search_selection(cx: &mut Context) {
12091236 let contents = doc.text().slice(..);
12101237 let query = doc.selection(view.id).primary().fragment(contents);
12111238 let regex = regex::escape(&query);
1212− cx.editor.registers.write('/', vec![regex]);
1213− search_next(cx);
1239+ cx.editor.registers.get_mut('/').push(regex);
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);
12141352}
12151353
12161354fn extend_line(cx: &mut Context) {
@@ -1268,7 +1406,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
12681406}
12691407
12701408fn delete_selection(cx: &mut Context) {
1271− let reg_name = cx.selected_register.name();
1409+ let reg_name = cx.register.unwrap_or('"');
12721410 let (view, doc) = current!(cx.editor);
12731411 let registers = &mut cx.editor.registers;
12741412 let reg = registers.get_mut(reg_name);
@@ -1281,7 +1419,7 @@ fn delete_selection(cx: &mut Context) {
12811419}
12821420
12831421fn change_selection(cx: &mut Context) {
1284− let reg_name = cx.selected_register.name();
1422+ let reg_name = cx.register.unwrap_or('"');
12851423 let (view, doc) = current!(cx.editor);
12861424 let registers = &mut cx.editor.registers;
12871425 let reg = registers.get_mut(reg_name);
@@ -1403,8 +1541,11 @@ mod cmd {
14031541 args: &[&str],
14041542 _event: PromptEvent,
14051543 ) -> anyhow::Result<()> {
1544+ use helix_core::path::expand_tilde;
14061545 let path = args.get(0).context("wrong argument count")?;
1407− let _ = cx.editor.open(path.into(), Action::Replace)?;
1546+ let _ = cx
1547+ .editor
1548+ .open(expand_tilde(Path::new(path)), Action::Replace)?;
14081549 Ok(())
14091550 }
14101551
@@ -1604,7 +1745,7 @@ mod cmd {
16041745
16051746 /// Results an error if there are modified buffers remaining and sets editor error,
16061747 /// 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<()> {
16081749 let modified: Vec<_> = editor
16091750 .documents()
16101751 .filter(|doc| doc.is_modified())
@@ -2577,7 +2718,7 @@ fn apply_workspace_edit(
25772718) {
25782719 if let Some(ref changes) = workspace_edit.changes {
25792720 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"));
25812722 return;
25822723 // 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
25832724 // TODO: find some example that uses workspace changes, and test it
@@ -2595,8 +2736,30 @@ fn apply_workspace_edit(
25952736 match document_changes {
25962737 lsp::DocumentChanges::Edits(document_edits) => {
25972738 for document_edit in document_edits {
2598− let (view, doc) = current!(editor);
2599− assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
2739+ let path = document_edit
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+
26002763 let edits = document_edit
26012764 .edits
26022765 .iter()
@@ -2614,8 +2777,8 @@ fn apply_workspace_edit(
26142777 edits,
26152778 offset_encoding,
26162779 );
2617− doc.apply(&transaction, view.id);
2618− doc.append_changes_to_history(view.id);
2780+ doc.apply(&transaction, view_id);
2781+ doc.append_changes_to_history(view_id);
26192782 }
26202783 }
26212784 lsp::DocumentChanges::Operations(operations) => {
@@ -2779,6 +2942,10 @@ fn open_above(cx: &mut Context) {
27792942fn normal_mode(cx: &mut Context) {
27802943 let (view, doc) = current!(cx.editor);
27812944
2945+ if doc.mode == Mode::Normal {
2946+ return;
2947+ }
2948+
27822949 doc.mode = Mode::Normal;
27832950
27842951 doc.append_changes_to_history(view.id);
@@ -3325,17 +3492,20 @@ pub mod insert {
33253492 }
33263493
33273494 use helix_core::auto_pairs;
3328− const HOOKS: &[Hook] = &[auto_pairs::hook, insert];
3329− const POST_HOOKS: &[PostHook] = &[completion, signature_help];
33303495
33313496 pub fn insert_char(cx: &mut Context, c: char) {
33323497 let (view, doc) = current!(cx.editor);
33333498
3499+ let hooks: &[Hook] = match cx.editor.config.auto_pairs {
3500+ true => &[auto_pairs::hook, insert],
3501+ false => &[insert],
3502+ };
3503+
33343504 let text = doc.text();
33353505 let selection = doc.selection(view.id).clone().cursors(text.slice(..));
33363506
33373507 // run through insert hooks, stopping on the first one that returns Some(t)
3338− for hook in HOOKS {
3508+ for hook in hooks {
33393509 if let Some(transaction) = hook(text, &selection, c) {
33403510 doc.apply(&transaction, view.id);
33413511 break;
@@ -3345,7 +3515,7 @@ pub mod insert {
33453515 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
33463516 // this could also generically look at Transaction, but it's a bit annoying to look at
33473517 // Operation instead of Change.
3348− for hook in POST_HOOKS {
3518+ for hook in &[completion, signature_help] {
33493519 hook(cx, c);
33503520 }
33513521 }
@@ -3510,12 +3680,12 @@ fn yank(cx: &mut Context) {
35103680 let msg = format!(
35113681 "yanked {} selection(s) to register {}",
35123682 values.len(),
3513− cx.selected_register.name()
3683+ cx.register.unwrap_or('"')
35143684 );
35153685
35163686 cx.editor
35173687 .registers
3518− .write(cx.selected_register.name(), values);
3688+ .write(cx.register.unwrap_or('"'), values);
35193689
35203690 cx.editor.set_status(msg);
35213691 exit_select_mode(cx);
@@ -3624,7 +3794,14 @@ fn paste_impl(
36243794 .iter()
36253795 .any(|value| get_line_ending_of_str(value).is_some());
36263796
3627− let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
3797+ // Only compiled once.
3798+ #[allow(clippy::trivial_regex)]
3799+ static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
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);
36283805
36293806 let text = doc.text();
36303807 let selection = doc.selection(view.id);
@@ -3688,7 +3865,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) {
36883865}
36893866
36903867fn replace_with_yanked(cx: &mut Context) {
3691− let reg_name = cx.selected_register.name();
3868+ let reg_name = cx.register.unwrap_or('"');
36923869 let (view, doc) = current!(cx.editor);
36933870 let registers = &mut cx.editor.registers;
36943871
@@ -3739,7 +3916,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) {
37393916}
37403917
37413918fn paste_after(cx: &mut Context) {
3742− let reg_name = cx.selected_register.name();
3919+ let reg_name = cx.register.unwrap_or('"');
37433920 let (view, doc) = current!(cx.editor);
37443921 let registers = &mut cx.editor.registers;
37453922
@@ -3753,7 +3930,7 @@ fn paste_after(cx: &mut Context) {
37533930}
37543931
37553932fn paste_before(cx: &mut Context) {
3756− let reg_name = cx.selected_register.name();
3933+ let reg_name = cx.register.unwrap_or('"');
37573934 let (view, doc) = current!(cx.editor);
37583935 let registers = &mut cx.editor.registers;
37593936
@@ -3934,24 +4111,49 @@ fn join_selections(cx: &mut Context) {
39344111
39354112fn keep_selections(cx: &mut Context) {
39364113 // keep selections matching regex
3937− let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| {
3938− let text = doc.text().slice(..);
4114+ let reg = cx.register.unwrap_or('/');
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+ }
4123+ let text = doc.text().slice(..);
39394124
3940− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
3941− doc.set_selection(view.id, selection);
3942− }
3943− });
4125+ if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
4126+ doc.set_selection(view.id, selection);
4127+ }
4128+ },
4129+ );
39444130
39454131 cx.push_layer(Box::new(prompt));
39464132}
39474133
39484134fn keep_primary_selection(cx: &mut Context) {
39494135 let (view, doc) = current!(cx.editor);
4136+ // TODO: handle count
39504137
39514138 let range = doc.selection(view.id).primary();
39524139 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
39534140}
39544141
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+
39554157fn completion(cx: &mut Context) {
39564158 // trigger on trigger char, or if user calls it
39574159 // (or on word char typing??)
@@ -4259,6 +4461,12 @@ fn vsplit(cx: &mut Context) {
42594461}
42604462
42614463fn 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+ }
42624470 let view_id = view!(cx.editor).id;
42634471 // close current split
42644472 cx.editor.close(view_id, /* close_buffer */ false);
@@ -4267,7 +4475,7 @@ fn wclose(cx: &mut Context) {
42674475fn select_register(cx: &mut Context) {
42684476 cx.on_next_key(move |cx, event| {
42694477 if let Some(ch) = event.char() {
4270− cx.editor.selected_register.select(ch);
4478+ cx.editor.selected_register = Some(ch);
42714479 }
42724480 })
42734481}
helix-term/src/job.rs+2−2
@@ -61,7 +61,7 @@ impl Jobs {
6161 }
6262
6363 pub fn handle_callback(
64− &mut self,
64+ &self,
6565 editor: &mut Editor,
6666 compositor: &mut Compositor,
6767 call: anyhow::Result<Option<Callback>>,
@@ -84,7 +84,7 @@ impl Jobs {
8484 }
8585 }
8686
87− pub fn add(&mut self, j: Job) {
87+ pub fn add(&self, j: Job) {
8888 if j.wait {
8989 self.wait_futures.push(j.future);
9090 } else {
helix-term/src/keymap.rs+110−37
@@ -4,6 +4,7 @@ use helix_core::hashmap;
44use helix_view::{document::Mode, info::Info, input::KeyEvent};
55use serde::Deserialize;
66use std::{
7+ borrow::Cow,
78 collections::HashMap,
89 ops::{Deref, DerefMut},
910};
@@ -47,13 +48,13 @@ macro_rules! keymap {
4748 };
4849
4950 (@trie
50− { $label:literal $($($key:literal)|+ => $value:tt,)+ }
51+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
5152 ) => {
52− keymap!({ $label $($($key)|+ => $value,)+ })
53+ keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
5354 };
5455
5556 (
56− { $label:literal $($($key:literal)|+ => $value:tt,)+ }
57+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
5758 ) => {
5859 // modified from the hashmap! macro
5960 {
@@ -70,7 +71,9 @@ macro_rules! keymap {
7071 _order.push(_key);
7172 )+
7273 )*
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)
7477 }
7578 };
7679}
@@ -84,6 +87,8 @@ pub struct KeyTrieNode {
8487 map: HashMap<KeyEvent, KeyTrie>,
8588 #[serde(skip)]
8689 order: Vec<KeyEvent>,
90+ #[serde(skip)]
91+ pub is_sticky: bool,
8792}
8893
8994impl KeyTrieNode {
@@ -92,6 +97,7 @@ impl KeyTrieNode {
9297 name: name.to_string(),
9398 map,
9499 order,
100+ is_sticky: false,
95101 }
96102 }
97103
@@ -119,12 +125,10 @@ impl KeyTrieNode {
119125 }
120126 }
121127 }
122−}
123128
124−impl From<KeyTrieNode> for Info {
125− fn from(node: KeyTrieNode) -> Self {
126− let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
127− for (&key, trie) in node.iter() {
129+ pub fn infobox(&self) -> Info {
130+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
131+ for (&key, trie) in self.iter() {
128132 let desc = match trie {
129133 KeyTrie::Leaf(cmd) => cmd.doc(),
130134 KeyTrie::Node(n) => n.name(),
@@ -136,16 +140,16 @@ impl From<KeyTrieNode> for Info {
136140 }
137141 }
138142 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()
140144 });
141− let prefix = format!("{} ", node.name());
145+ let prefix = format!("{} ", self.name());
142146 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
143147 body = body
144148 .into_iter()
145149 .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
146150 .collect();
147151 }
148− Info::new(node.name(), body)
152+ Info::new(self.name(), body)
149153 }
150154}
151155
@@ -218,7 +222,7 @@ impl KeyTrie {
218222}
219223
220224#[derive(Debug, Clone, PartialEq)]
221−pub enum KeymapResult {
225+pub enum KeymapResultKind {
222226 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
223227 Pending(KeyTrieNode),
224228 Matched(Command),
@@ -229,14 +233,31 @@ pub enum KeymapResult {
229233 Cancelled(Vec<KeyEvent>),
230234}
231235
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+
232249#[derive(Debug, Clone, PartialEq, Deserialize)]
233250pub struct Keymap {
234251 /// Always a Node
235252 #[serde(flatten)]
236253 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.
238256 #[serde(skip)]
239257 state: Vec<KeyEvent>,
258+ /// Stores the sticky node if one is activated.
259+ #[serde(skip)]
260+ sticky: Option<KeyTrieNode>,
240261}
241262
242263impl Keymap {
@@ -244,6 +265,7 @@ impl Keymap {
244265 Keymap {
245266 root,
246267 state: Vec::new(),
268+ sticky: None,
247269 }
248270 }
249271
@@ -251,27 +273,61 @@ impl Keymap {
251273 &self.root
252274 }
253275
276+ pub fn sticky(&self) -> Option<&KeyTrieNode> {
277+ self.sticky.as_ref()
278+ }
279+
254280 /// Returns list of keys waiting to be disambiguated.
255281 pub fn pending(&self) -> &[KeyEvent] {
256282 &self.state
257283 }
258284
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.
260288 pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
261− let &first = self.state.get(0).unwrap_or(&key);
262− let trie = match self.root.search(&[first]) {
263− Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
264− None => return KeymapResult::NotFound,
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+
300+ let first = self.state.get(0).unwrap_or(&key);
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]) {
307+ Some(&KeyTrie::Leaf(cmd)) => {
308+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
309+ }
310+ None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
265311 Some(t) => t,
266312 };
313+
267314 self.state.push(key);
268315 match trie.search(&self.state[1..]) {
269− Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
270− Some(&KeyTrie::Leaf(command)) => {
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+ }
323+ Some(&KeyTrie::Leaf(cmd)) => {
271324 self.state.clear();
272− KeymapResult::Matched(command)
325+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
273326 }
274− None => KeymapResult::Cancelled(self.state.drain(..).collect()),
327+ None => KeymapResult::new(
328+ KeymapResultKind::Cancelled(self.state.drain(..).collect()),
329+ self.sticky(),
330+ ),
275331 }
276332 }
277333
@@ -380,7 +436,6 @@ impl Default for Keymaps {
380436 "A" => append_to_line,
381437 "o" => open_below,
382438 "O" => open_above,
383− // [<space> ]<space> equivalents too (add blank new line, no edit)
384439
385440 "d" => delete_selection,
386441 // TODO: also delete without yanking
@@ -440,12 +495,11 @@ impl Default for Keymaps {
440495 "<" => unindent,
441496 "=" => format_selections,
442497 "J" => join_selections,
443− // TODO: conflicts hover/doc
444498 "K" => keep_selections,
445499 // TODO: and another method for inverse
446500
447− // TODO: clashes with space mode
448− "space" => keep_primary_selection,
501+ "," => keep_primary_selection,
502+ "A-," => remove_primary_selection,
449503
450504 // "q" => record_macro,
451505 // "Q" => replay_macro,
@@ -473,7 +527,6 @@ impl Default for Keymaps {
473527
474528 // move under <space>c
475529 "C-c" => toggle_comments,
476− "K" => hover,
477530
478531 // z family for save/restore/combine from/to sels from register
479532
@@ -516,7 +569,8 @@ impl Default for Keymaps {
516569 "p" => paste_clipboard_after,
517570 "P" => paste_clipboard_before,
518571 "R" => replace_selections_with_clipboard,
519− "space" => keep_primary_selection,
572+ "/" => global_search,
573+ "k" => hover,
520574 },
521575 "z" => { "View"
522576 "z" | "c" => align_view_center,
@@ -525,6 +579,22 @@ impl Default for Keymaps {
525579 "m" => align_view_middle,
526580 "k" => scroll_up,
527581 "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,
528598 },
529599
530600 "\"" => select_register,
@@ -545,14 +615,17 @@ impl Default for Keymaps {
545615 "w" => extend_next_word_start,
546616 "b" => extend_prev_word_start,
547617 "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,
548621
549622 "t" => extend_till_char,
550623 "f" => extend_next_char,
551624 "T" => extend_till_prev_char,
552625 "F" => extend_prev_char,
553626
554− "home" => goto_line_start,
555− "end" => goto_line_end,
627+ "home" => extend_to_line_start,
628+ "end" => extend_to_line_end,
556629 "esc" => exit_select_mode,
557630
558631 "v" => normal_mode,
@@ -617,19 +690,19 @@ fn merge_partial_keys() {
617690
618691 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
619692 assert_eq!(
620− keymap.get(key!('i')),
621− KeymapResult::Matched(Command::normal_mode),
693+ keymap.get(key!('i')).kind,
694+ KeymapResultKind::Matched(Command::normal_mode),
622695 "Leaf should replace leaf"
623696 );
624697 assert_eq!(
625− keymap.get(key!('无')),
626− KeymapResult::Matched(Command::insert_mode),
698+ keymap.get(key!('无')).kind,
699+ KeymapResultKind::Matched(Command::insert_mode),
627700 "New leaf should be present in merged keymap"
628701 );
629702 // Assumes that z is a node in the default keymap
630703 assert_eq!(
631− keymap.get(key!('z')),
632− KeymapResult::Matched(Command::jump_backward),
704+ keymap.get(key!('z')).kind,
705+ KeymapResultKind::Matched(Command::jump_backward),
633706 "Leaf should replace node"
634707 );
635708 // 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 {
262262 .cursor(doc.text().slice(..));
263263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
264264 - view.offset.row) as u16;
265−
266− let mut doc = match &option.documentation {
265+ let mut markdown_doc = match &option.documentation {
267266 Some(lsp::Documentation::String(contents))
268267 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
269268 kind: lsp::MarkupKind::PlainText,
@@ -311,24 +310,42 @@ impl Component for Completion {
311310 None => return,
312311 };
313312
314− let half = area.height / 2;
315− let height = 15.min(half);
316− // we want to make sure the cursor is visible (not hidden behind the documentation)
317− let y = if cursor_pos + area.y
318− >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
319− {
320− 0
313+ let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
314+ let (popup_width, _popup_height) = self.popup.get_size();
315+ let mut width = area
316+ .width
317+ .saturating_sub(popup_x)
318+ .saturating_sub(popup_width);
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)
321329 } else {
322− // -2 to subtract command line + statusline. a bit of a hack, because of splits.
323− area.height.saturating_sub(height).saturating_sub(2)
324− };
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 {
338+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
339+ area.height.saturating_sub(height).saturating_sub(2)
340+ };
325341
326− let area = Rect::new(0, y, area.width, height);
342+ Rect::new(0, y, area.width, height)
343+ };
327344
328345 // clear area
329346 let background = cx.editor.theme.get("ui.popup");
330347 surface.clear_with(area, background);
331− doc.render(area, surface, cx);
348+ markdown_doc.render(area, surface, cx);
332349 }
333350 }
334351}
helix-term/src/ui/editor.rs+23−22
@@ -3,7 +3,7 @@ use crate::{
33 compositor::{Component, Compositor, Context, EventResult},
44 job::Callback,
55 key,
6− keymap::{KeymapResult, Keymaps},
6+ keymap::{KeymapResult, KeymapResultKind, Keymaps},
77 ui::{Completion, ProgressSpinners},
88};
99
@@ -165,8 +165,7 @@ impl EditorView {
165165 let scopes = theme.scopes();
166166 syntax
167167 .highlight_iter(text.slice(..), Some(range), None, |language| {
168− loader
169− .language_config_for_scope(&format!("source.{}", language))
168+ loader.language_configuration_for_injection_string(language)
170169 .and_then(|language_config| {
171170 let config = language_config.highlight_config(scopes)?;
172171 let config_ref = config.as_ref();
@@ -852,7 +851,7 @@ impl EditorView {
852851
853852 /// Handle events by looking them up in `self.keymaps`. Returns None
854853 /// 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
856855 /// otherwise.
857856 fn handle_keymap_event(
858857 &mut self,
@@ -860,8 +859,6 @@ impl EditorView {
860859 cxt: &mut commands::Context,
861860 event: KeyEvent,
862861 ) -> Option<KeymapResult> {
863− self.autoinfo = None;
864−
865862 if let Some(picker) = cxt.editor.debug_config_picker.clone() {
866863 match event {
867864 KeyEvent {
@@ -912,29 +909,32 @@ impl EditorView {
912909 return None;
913910 }
914911
915− match self.keymaps.get_mut(&mode).unwrap().get(event) {
916− KeymapResult::Matched(command) => command.execute(cxt),
917− KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
918− k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
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+ KeymapResultKind::Matched(command) => command.execute(cxt),
917+ KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
918+ KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
919919 }
920920 None
921921 }
922922
923923 fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
924924 if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
925− match keyresult {
926− KeymapResult::NotFound => {
925+ match keyresult.kind {
926+ KeymapResultKind::NotFound => {
927927 if let Some(ch) = event.char() {
928928 commands::insert::insert_char(cx, ch)
929929 }
930930 }
931− KeymapResult::Cancelled(pending) => {
931+ KeymapResultKind::Cancelled(pending) => {
932932 for ev in pending {
933933 match ev.char() {
934934 Some(ch) => commands::insert::insert_char(cx, ch),
935935 None => {
936− if let KeymapResult::Matched(command) =
937− self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
936+ if let KeymapResultKind::Matched(command) =
937+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
938938 {
939939 command.execute(cx);
940940 }
@@ -972,7 +972,7 @@ impl EditorView {
972972 // debug_assert!(cxt.count != 0);
973973
974974 // set the register
975− cxt.selected_register = cxt.editor.selected_register.take();
975+ cxt.register = cxt.editor.selected_register.take();
976976
977977 self.handle_keymap_event(mode, cxt, event);
978978 if self.keymaps.pending().is_empty() {
@@ -1196,9 +1196,9 @@ impl EditorView {
11961196impl Component for EditorView {
11971197 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
11981198 let mut cxt = commands::Context {
1199− selected_register: helix_view::RegisterSelection::default(),
12001199 editor: &mut cx.editor,
12011200 count: None,
1201+ register: None,
12021202 callback: None,
12031203 on_next_key_callback: None,
12041204 jobs: cx.jobs,
@@ -1288,11 +1288,12 @@ impl Component for EditorView {
12881288 // how we entered insert mode is important, and we should track that so
12891289 // we can repeat the side effect.
12901290
1291− self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
1292− KeymapResult::Matched(command) => command,
1293− // FIXME: insert mode can only be entered through single KeyCodes
1294− _ => unimplemented!(),
1295− };
1291+ self.last_insert.0 =
1292+ match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
1293+ KeymapResultKind::Matched(command) => command,
1294+ // FIXME: insert mode can only be entered through single KeyCodes
1295+ _ => unimplemented!(),
1296+ };
12961297 self.last_insert.1.clear();
12971298 }
12981299 (Mode::Insert, Mode::Normal) => {
helix-term/src/ui/markdown.rs+25−5
@@ -88,7 +88,7 @@ fn parse<'a>(
8888 if let Some(theme) = theme {
8989 let rope = Rope::from(text.as_ref());
9090 let syntax = loader
91− .language_config_for_scope(&format!("source.{}", language))
91+ .language_configuration_for_injection_string(language)
9292 .and_then(|config| config.highlight_config(theme.scopes()))
9393 .map(|config| Syntax::new(&rope, config));
9494
@@ -215,10 +215,30 @@ impl Component for Markdown {
215215 }
216216
217217 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
218− let contents = parse(&self.contents, None, &self.config_loader);
219218 let padding = 2;
220− let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
221− let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
222− Some((width, height))
219+ if padding >= viewport.1 || padding >= viewport.0 {
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+
242+ Some((text_width + padding, height))
223243 }
224244}
helix-term/src/ui/menu.rs+43−33
@@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
3333
3434 scroll: usize,
3535 size: (u16, u16),
36+ viewport: (u16, u16),
37+ recalculate: bool,
3638}
3739
3840impl<T: Item> Menu<T> {
@@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
5153 callback_fn: Box::new(callback_fn),
5254 scroll: 0,
5355 size: (0, 0),
56+ viewport: (0, 0),
57+ recalculate: true,
5458 };
5559
5660 // TODO: scoring on empty input should just use a fastpath
@@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
8387 // reset cursor position
8488 self.cursor = None;
8589 self.scroll = 0;
90+ self.recalculate = true;
8691 }
8792
8893 pub fn move_up(&mut self) {
@@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
99104 self.adjust_scroll();
100105 }
101106
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+
102142 fn adjust_scroll(&mut self) {
103143 let win_height = self.size.1 as usize;
104144 if let Some(cursor) = self.cursor {
@@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> {
221261 }
222262
223263 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
224− let n = self
225− .options
226− .first()
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();
264+ if viewport != self.viewport || self.recalculate {
265+ self.recalculate_size(viewport);
266+ }
255267
256268 Some(self.size)
257269 }
258270
259− // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
260−
261271 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
262272 let theme = &cx.editor.theme;
263273 let style = theme
helix-term/src/ui/mod.rs+23−6
@@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner};
2020pub use text::Text;
2121
2222use helix_core::regex::Regex;
23−use helix_core::register::Registers;
23+use helix_core::regex::RegexBuilder;
2424use helix_view::{Document, Editor, View};
2525
2626use std::path::PathBuf;
@@ -28,7 +28,8 @@ use std::path::PathBuf;
2828pub fn regex_prompt(
2929 cx: &mut crate::commands::Context,
3030 prompt: std::borrow::Cow<'static, str>,
31− fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
31+ history_register: Option<char>,
32+ fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
3233) -> Prompt {
3334 let (view, doc) = current!(cx.editor);
3435 let view_id = view.id;
@@ -36,7 +37,7 @@ pub fn regex_prompt(
3637
3738 Prompt::new(
3839 prompt,
39− None,
40+ history_register,
4041 |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
4142 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
4243 match event {
@@ -46,6 +47,14 @@ pub fn regex_prompt(
4647 }
4748 PromptEvent::Validate => {
4849 // 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+ }
4958 }
5059 PromptEvent::Update => {
5160 // skip empty input, TODO: trigger default
@@ -53,15 +62,23 @@ pub fn regex_prompt(
5362 return;
5463 }
5564
56− match Regex::new(input) {
65+ let case_insensitive = if cx.editor.config.smart_case {
66+ !input.chars().any(char::is_uppercase)
67+ } else {
68+ false
69+ };
70+
71+ match RegexBuilder::new(input)
72+ .case_insensitive(case_insensitive)
73+ .build()
74+ {
5775 Ok(regex) => {
5876 let (view, doc) = current!(cx.editor);
59− let registers = &mut cx.editor.registers;
6077
6178 // revert state to what it was before the last update
6279 doc.set_selection(view.id, snapshot.clone());
6380
64− fun(view, doc, registers, regex);
81+ fun(view, doc, regex, event);
6582
6683 view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
6784 }
helix-term/src/ui/picker.rs+13−12
@@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> {
124124 }) {
125125 // align to middle
126126 let first_line = line
127− .map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines())))
128− .map(|(start, _)| start)
129− .unwrap_or(0)
130− .saturating_sub(inner.height as usize / 2);
127+ .map(|(start, end)| {
128+ let height = end.saturating_sub(start) + 1;
129+ let middle = start + (height.saturating_sub(1) / 2);
130+ middle.saturating_sub(inner.height as usize / 2).min(start)
131+ })
132+ .unwrap_or(0);
133+
131134 let offset = Position::new(first_line, 0);
132135
133136 let highlights = EditorView::doc_syntax_highlights(
@@ -268,17 +271,15 @@ impl<T> Picker<T> {
268271 }
269272
270273 pub fn move_up(&mut self) {
271− self.cursor = self.cursor.saturating_sub(1);
274+ let len = self.matches.len();
275+ let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
276+ self.cursor = pos;
272277 }
273278
274279 pub fn move_down(&mut self) {
275− if self.matches.is_empty() {
276− return;
277− }
278−
279− if self.cursor < self.matches.len() - 1 {
280− self.cursor += 1;
281− }
280+ let len = self.matches.len();
281+ let pos = (self.cursor + 1) % len;
282+ self.cursor = pos;
282283 }
283284
284285 pub fn selection(&self) -> Option<&T> {
helix-term/src/ui/popup.rs+38−23
@@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
1616}
1717
1818impl<T: Component> Popup<T> {
19− // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
20− // rendering)
2119 pub fn new(contents: T) -> Self {
2220 Self {
2321 contents,
@@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
3129 self.position = pos;
3230 }
3331
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+
3465 pub fn scroll(&mut self, offset: usize, direction: bool) {
3566 if direction {
3667 self.scroll += offset;
@@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> {
106137 }
107138
108139 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));
110142
111− let position = self
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− };
143+ cx.scroll = Some(self.scroll);
123144
124− // TODO: be able to specify orientation preference. We want above for most popups, below
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− }
145+ let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
131146
132147 // 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));
134149
135150 // clear area
136151 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;
55
66pub struct Text {
77 contents: String,
8+ size: (u16, u16),
9+ viewport: (u16, u16),
810}
911
1012impl Text {
1113 pub fn new(contents: String) -> Self {
12− Self { contents }
14+ Self {
15+ contents,
16+ size: (0, 0),
17+ viewport: (0, 0),
18+ }
1319 }
1420}
1521impl Component for Text {
@@ -24,9 +30,13 @@ impl Component for Text {
2430 }
2531
2632 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
27− let contents = tui::text::Text::from(self.contents.clone());
28− let width = std::cmp::min(contents.width() as u16, viewport.0);
29− let height = std::cmp::min(contents.height() as u16, viewport.1);
30− Some((width, height))
33+ if viewport != self.viewport {
34+ let contents = tui::text::Text::from(self.contents.clone());
35+ let width = std::cmp::min(contents.width() as u16, viewport.0);
36+ let height = std::cmp::min(contents.height() as u16, viewport.1);
37+ self.size = (width, height);
38+ self.viewport = viewport;
39+ }
40+ Some(self.size)
3141 }
3242}
helix-view/src/document.rs+67−16
@@ -386,21 +386,24 @@ impl Document {
386386 /// If supported, returns the changes that should be applied to this document in order
387387 /// to format it nicely.
388388 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() {
390390 let text = self.text.clone();
391− let id = self.identifier();
391+ let offset_encoding = language_server.offset_encoding();
392+ let request = language_server.text_document_formatting(
393+ self.identifier(),
394+ lsp::FormattingOptions::default(),
395+ None,
396+ )?;
397+
392398 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| {
397− log::warn!("LSP formatting failed: {}", e);
398− Default::default()
399− });
399+ let edits = request.await.unwrap_or_else(|e| {
400+ log::warn!("LSP formatting failed: {}", e);
401+ Default::default()
402+ });
400403 LspFormatting {
401404 doc: text,
402405 edits,
403− offset_encoding: language_server.offset_encoding(),
406+ offset_encoding,
404407 }
405408 };
406409 Some(fut)
@@ -469,9 +472,14 @@ impl Document {
469472 to_writer(&mut file, encoding, &text).await?;
470473
471474 if let Some(language_server) = language_server {
472− language_server
473− .text_document_did_save(identifier, &text)
474− .await?;
475+ if !language_server.is_initialized() {
476+ return Ok(());
477+ }
478+ if let Some(notification) =
479+ language_server.text_document_did_save(identifier, &text)
480+ {
481+ notification.await?;
482+ }
475483 }
476484
477485 Ok(())
@@ -646,7 +654,7 @@ impl Document {
646654 // }
647655
648656 // emit lsp notification
649− if let Some(language_server) = &self.language_server {
657+ if let Some(language_server) = self.language_server() {
650658 let notify = language_server.text_document_did_change(
651659 self.versioned_identifier(),
652660 &old_doc,
@@ -795,9 +803,18 @@ impl Document {
795803 self.version
796804 }
797805
798− #[inline]
799806 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+ }
801818 }
802819
803820 #[inline]
@@ -891,6 +908,40 @@ impl Default for Document {
891908mod test {
892909 use super::*;
893910
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+
894945 #[test]
895946 fn changeset_to_changes() {
896947 use helix_lsp::{lsp, Client, OffsetEncoding};
helix-view/src/editor.rs+31−21
@@ -3,7 +3,7 @@ use crate::{
33 graphics::{CursorKind, Rect},
44 theme::{self, Theme},
55 tree::Tree,
6− Document, DocumentId, RegisterSelection, View, ViewId,
6+ Document, DocumentId, View, ViewId,
77};
88
99use futures_util::future;
@@ -44,6 +44,10 @@ pub struct Config {
4444 pub line_number: LineNumber,
4545 /// Middle click paste support. Defaults to true
4646 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,
4751}
4852
4953#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -69,6 +73,8 @@ impl Default for Config {
6973 },
7074 line_number: LineNumber::Absolute,
7175 middle_click_paste: true,
76+ smart_case: true,
77+ auto_pairs: true,
7278 }
7379 }
7480}
@@ -78,7 +84,7 @@ pub struct Editor {
7884 pub tree: Tree,
7985 pub documents: SlotMap<DocumentId, Document>,
8086 pub count: Option<std::num::NonZeroUsize>,
81− pub selected_register: RegisterSelection,
87+ pub selected_register: Option<char>,
8288 pub registers: Registers,
8389 pub theme: Theme,
8490 pub language_servers: helix_lsp::Registry,
@@ -125,7 +131,7 @@ impl Editor {
125131 tree: Tree::new(area),
126132 documents: SlotMap::with_key(),
127133 count: None,
128− selected_register: RegisterSelection::default(),
134+ selected_register: None,
129135 theme: themes.default(),
130136 language_servers,
131137 debugger: None,
@@ -270,26 +276,31 @@ impl Editor {
270276 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
271277
272278 // try to find a language server based on the language name
273− let language_server = doc
274− .language
275− .as_ref()
276− .and_then(|language| self.language_servers.get(language).ok());
279+ let language_server = doc.language.as_ref().and_then(|language| {
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+ });
277287
278288 if let Some(language_server) = language_server {
279− doc.set_language_server(Some(language_server.clone()));
280−
281289 let language_id = doc
282290 .language()
283291 .and_then(|s| s.split('.').last()) // source.rust
284292 .map(ToOwned::to_owned)
285293 .unwrap_or_default();
286294
295+ // TODO: this now races with on_init code if the init happens too quickly
287296 tokio::spawn(language_server.text_document_did_open(
288297 doc.url().unwrap(),
289298 doc.version(),
290299 doc.text(),
291300 language_id,
292301 ));
302+
303+ doc.set_language_server(Some(language_server));
293304 }
294305
295306 let id = self.documents.insert(doc);
@@ -308,14 +319,9 @@ impl Editor {
308319
309320 if close_buffer {
310321 // get around borrowck issues
311− let language_servers = &mut self.language_servers;
312322 let doc = &self.documents[view.doc];
313323
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() {
319325 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
320326 }
321327 self.documents.remove(view.doc);
@@ -345,20 +351,24 @@ impl Editor {
345351 view.ensure_cursor_in_view(doc, self.config.scrolloff)
346352 }
347353
354+ #[inline]
348355 pub fn document(&self, id: DocumentId) -> Option<&Document> {
349356 self.documents.get(id)
350357 }
351358
359+ #[inline]
352360 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
353361 self.documents.get_mut(id)
354362 }
355363
364+ #[inline]
356365 pub fn documents(&self) -> impl Iterator<Item = &Document> {
357− self.documents.iter().map(|(_id, doc)| doc)
366+ self.documents.values()
358367 }
359368
369+ #[inline]
360370 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()
362372 }
363373
364374 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
@@ -366,10 +376,10 @@ impl Editor {
366376 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
367377 }
368378
369− // pub fn current_document(&self) -> Document {
370− // let id = self.view().doc;
371− // let doc = &mut editor.documents[id];
372− // }
379+ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
380+ self.documents_mut()
381+ .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
382+ }
373383
374384 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
375385 let view = view!(self);
helix-view/src/graphics.rs+3−3
@@ -224,13 +224,13 @@ pub enum Color {
224224 Magenta,
225225 Cyan,
226226 Gray,
227− DarkGray,
228227 LightRed,
229228 LightGreen,
230229 LightYellow,
231230 LightBlue,
232231 LightMagenta,
233232 LightCyan,
233+ LightGray,
234234 White,
235235 Rgb(u8, u8, u8),
236236 Indexed(u8),
@@ -250,14 +250,14 @@ impl From<Color> for crossterm::style::Color {
250250 Color::Blue => CColor::DarkBlue,
251251 Color::Magenta => CColor::DarkMagenta,
252252 Color::Cyan => CColor::DarkCyan,
253− Color::Gray => CColor::Grey,
254− Color::DarkGray => CColor::DarkGrey,
253+ Color::Gray => CColor::DarkGrey,
255254 Color::LightRed => CColor::Red,
256255 Color::LightGreen => CColor::Green,
257256 Color::LightBlue => CColor::Blue,
258257 Color::LightYellow => CColor::Yellow,
259258 Color::LightMagenta => CColor::Magenta,
260259 Color::LightCyan => CColor::Cyan,
260+ Color::LightGray => CColor::Grey,
261261 Color::White => CColor::White,
262262 Color::Indexed(i) => CColor::AnsiValue(i),
263263 Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
helix-view/src/lib.rs−2
@@ -8,7 +8,6 @@ pub mod graphics;
88pub mod info;
99pub mod input;
1010pub mod keyboard;
11−pub mod register_selection;
1211pub mod theme;
1312pub mod tree;
1413pub mod view;
@@ -20,6 +19,5 @@ slotmap::new_key_type! {
2019
2120pub use document::Document;
2221pub use editor::Editor;
23−pub use register_selection::RegisterSelection;
2422pub use theme::Theme;
2523pub 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)`
112#[macro_export]
213macro_rules! current {
314 ( $( $editor:ident ).+ ) => {{
@@ -7,6 +18,8 @@ macro_rules! current {
718 }};
819}
920
21+/// Get the current document mutably.
22+/// Returns `&mut Document`
1023#[macro_export]
1124macro_rules! doc_mut {
1225 ( $( $editor:ident ).+ ) => {{
@@ -14,6 +27,8 @@ macro_rules! doc_mut {
1427 }};
1528}
1629
30+/// Get the current view mutably.
31+/// Returns `&mut View`
1732#[macro_export]
1833macro_rules! view_mut {
1934 ( $( $editor:ident ).+ ) => {{
@@ -21,6 +36,8 @@ macro_rules! view_mut {
2136 }};
2237}
2338
39+/// Get the current view immutably
40+/// Returns `&View`
2441#[macro_export]
2542macro_rules! view {
2643 ( $( $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::{
55};
66
77use anyhow::Context;
8+use helix_core::hashmap;
89use log::warn;
910use once_cell::sync::Lazy;
1011use serde::{Deserialize, Deserializer};
@@ -142,13 +143,37 @@ struct ThemePalette {
142143
143144impl Default for ThemePalette {
144145 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+ }
146166 }
147167}
148168
149169impl ThemePalette {
150170 pub fn new(palette: HashMap<String, Color>) -> Self {
151− Self { palette }
171+ let ThemePalette {
172+ palette: mut default,
173+ } = ThemePalette::default();
174+
175+ default.extend(palette);
176+ Self { palette: default }
152177 }
153178
154179 pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
languages.toml+38−1
@@ -65,6 +65,7 @@ file-types = ["ex", "exs"]
6565roots = []
6666comment-token = "#"
6767
68+language-server = { command = "elixir-ls" }
6869indent = { tab-width = 2, unit = " " }
6970
7071[[language]]
@@ -198,6 +199,17 @@ roots = []
198199language-server = { command = "typescript-language-server", args = ["--stdio"] }
199200indent = { tab-width = 2, unit = " " }
200201
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+
201213[[language]]
202214name = "css"
203215scope = "source.css"
@@ -236,6 +248,7 @@ file-types = ["nix"]
236248roots = []
237249comment-token = "#"
238250
251+language-server = { command = "rnix-lsp" }
239252indent = { tab-width = 2, unit = " " }
240253
241254[[language]]
@@ -286,7 +299,22 @@ injection-regex = "julia"
286299file-types = ["jl"]
287300roots = []
288301comment-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+ ] }
290318indent = { tab-width = 2, unit = " " }
291319
292320[[language]]
@@ -331,6 +359,15 @@ roots = []
331359comment-token = "--"
332360indent = { tab-width = 2, unit = " " }
333361
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+
334371[[language]]
335372name = "yaml"
336373scope = "source.yaml"
runtime/queries/c/highlights.scm+1−1
@@ -61,7 +61,7 @@
6161
6262(null) @constant
6363(number_literal) @number
64−(char_literal) @number
64+(char_literal) @string
6565
6666(call_expression
6767 function: (identifier) @function)
runtime/queries/go/highlights.scm+35−4
@@ -17,9 +17,18 @@
1717
1818; Identifiers
1919
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+
2027(type_identifier) @type
2128(field_identifier) @property
2229(identifier) @variable
30+(package_identifier) @variable
31+
2332
2433; Operators
2534
@@ -79,10 +88,8 @@
7988 "go"
8089 "goto"
8190 "if"
82− "import"
8391 "interface"
8492 "map"
85− "package"
8693 "range"
8794 "return"
8895 "select"
@@ -92,6 +99,29 @@
9299 "var"
93100] @keyword
94101
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+
95125; Literals
96126
97127[
@@ -111,7 +141,8 @@
111141[
112142 (true)
113143 (false)
114− (nil)
115−] @constant.builtin
144+] @constant.builtin.boolean
145+
146+(nil) @constant.builtin
116147
117148(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 @@
22(operator) @operator
33(exp_name (constructor) @constructor)
44(constructor_operator) @operator
5−(module) @module_name
5+(module) @namespace
66(type) @type
77(type) @class
88(constructor) @constructor
99(pragma) @pragma
1010(comment) @comment
1111(signature name: (variable) @fun_type_name)
12−(function name: (variable) @fun_name)
12+(function name: (variable) @function)
1313(constraint class: (class_name (type)) @class)
1414(class (class_head class: (class_name (type)) @class))
1515(instance (instance_head class: (class_name (type)) @class))
16−(integer) @literal
17−(exp_literal (float)) @literal
16+(integer) @number
17+(exp_literal (float)) @number
1818(char) @literal
1919(con_unit) @literal
2020(con_list) @literal
@@ -39,5 +39,7 @@
3939"do" @keyword
4040"mdo" @keyword
4141"rec" @keyword
42−"(" @paren
43−")" @paren
42+[
43+ "("
44+ ")"
45+] @punctuation.bracket
runtime/queries/javascript/highlights.scm+1−1
@@ -87,7 +87,7 @@
8787 (template_string)
8888] @string
8989
90−(regex) @string.special
90+(regex) @string.regexp
9191(number) @number
9292
9393; 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_]*$"))
71
82[
93 (triple_string)
@@ -28,43 +22,43 @@
2822(call_expression
2923 (identifier) @function)
3024(call_expression
31− (field_expression (identifier) @method .))
25+ (field_expression (identifier) @function.method .))
3226(broadcast_call_expression
3327 (identifier) @function)
3428(broadcast_call_expression
35− (field_expression (identifier) @method .))
29+ (field_expression (identifier) @function.method .))
3630(parameter_list
37− (identifier) @parameter)
31+ (identifier) @variable.parameter)
3832(parameter_list
3933 (optional_parameter .
40− (identifier) @parameter))
34+ (identifier) @variable.parameter))
4135(typed_parameter
42− (identifier) @parameter
36+ (identifier) @variable.parameter
4337 (identifier) @type)
4438(type_parameter_list
4539 (identifier) @type)
4640(typed_parameter
47− (identifier) @parameter
41+ (identifier) @variable.parameter
4842 (parameterized_identifier) @type)
4943(function_expression
50− . (identifier) @parameter)
51−(spread_parameter) @parameter
44+ . (identifier) @variable.parameter)
45+(spread_parameter) @variable.parameter
5246(spread_parameter
53− (identifier) @parameter)
47+ (identifier) @variable.parameter)
5448(named_argument
55− . (identifier) @parameter)
49+ . (identifier) @variable.parameter)
5650(argument_list
5751 (typed_expression
58− (identifier) @parameter
52+ (identifier) @variable.parameter
5953 (identifier) @type))
6054(argument_list
6155 (typed_expression
62− (identifier) @parameter
56+ (identifier) @variable.parameter
6357 (parameterized_identifier) @type))
6458
6559;; Symbol expressions (:my-wanna-be-lisp-keyword)
6660(quote_expression
67− (identifier)) @symbol
61+ (identifier)) @string.special.symbol
6862
6963;; Parsing error! foo (::Type) get's parsed as two quote expressions
7064(argument_list
@@ -76,7 +70,7 @@
7670 (identifier) @type)
7771(parameterized_identifier (_)) @type
7872(argument_list
79− (typed_expression . (identifier) @parameter))
73+ (typed_expression . (identifier) @variable.parameter))
8074
8175(typed_expression
8276 (identifier) @type .)
@@ -113,13 +107,13 @@
113107"end" @keyword
114108
115109(if_statement
116− ["if" "end"] @conditional)
110+ ["if" "end"] @keyword.control.conditional)
117111(elseif_clause
118− ["elseif"] @conditional)
112+ ["elseif"] @keyword.control.conditional)
119113(else_clause
120− ["else"] @conditional)
114+ ["else"] @keyword.control.conditional)
121115(ternary_expression
122− ["?" ":"] @conditional)
116+ ["?" ":"] @keyword.control.conditional)
123117
124118(function_definition ["function" "end"] @keyword.function)
125119
@@ -134,47 +128,57 @@
134128 "type"
135129] @keyword
136130
137−((identifier) @keyword (#any-of? @keyword "global" "local"))
131+((identifier) @keyword (match? @keyword "global|local"))
138132
139133(compound_expression
140134 ["begin" "end"] @keyword)
141135(try_statement
142− ["try" "end" ] @exception)
136+ ["try" "end" ] @keyword.control.exception)
143137(finally_clause
144− "finally" @exception)
138+ "finally" @keyword.control.exception)
145139(catch_clause
146− "catch" @exception)
140+ "catch" @keyword.control.exception)
147141(quote_statement
148142 ["quote" "end"] @keyword)
149143(let_statement
150144 ["let" "end"] @keyword)
151145(for_statement
152− ["for" "end"] @repeat)
146+ ["for" "end"] @keyword.control.repeat)
153147(while_statement
154− ["while" "end"] @repeat)
155−(break_statement) @repeat
156−(continue_statement) @repeat
148+ ["while" "end"] @keyword.control.repeat)
149+(break_statement) @keyword.control.repeat
150+(continue_statement) @keyword.control.repeat
157151(for_binding
158− "in" @repeat)
152+ "in" @keyword.control.repeat)
159153(for_clause
160− "for" @repeat)
154+ "for" @keyword.control.repeat)
161155(do_clause
162156 ["do" "end"] @keyword)
163157
164158(export_statement
165− ["export"] @include)
159+ ["export"] @keyword.control.import)
166160
167161[
168162 "using"
169163 "module"
170164 "import"
171−] @include
165+] @keyword.control.import
172166
173−((identifier) @include (#eq? @include "baremodule"))
167+((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule"))
174168
175169(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
176−(((identifier) @boolean) (eq? @boolean "true"))
177−(((identifier) @boolean) (eq? @boolean "false"))
170+(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true"))
171+(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false"))
172+
178173
179174["::" ":" "." "," "..." "!"] @punctuation.delimiter
180175["[" "]" "(" ")" "{" "}"] @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 @@
259259
260260(comment) @comment
261261
262−(bracket_group) @parameter
262+(bracket_group) @variable.parameter
263263
264264[(math_operator) "="] @operator
265265
@@ -312,7 +312,7 @@
312312 key: (word) @text.reference)
313313
314314(key_val_pair
315− key: (_) @parameter
315+ key: (_) @variable.parameter
316316 value: (_))
317317
318318["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
runtime/queries/ledger/injections.scm+1
@@ -1 +1,2 @@
11(comment) @comment
2+(note) @comment
runtime/queries/lua/highlights.scm+9−9
@@ -23,27 +23,27 @@
2323 "for"
2424 "do"
2525 "end"
26−] @keyword.control.loop)
26+] @keyword.control.repeat)
2727
2828(for_in_statement
2929[
3030 "for"
3131 "do"
3232 "end"
33−] @keyword.control.loop)
33+] @keyword.control.repeat)
3434
3535(while_statement
3636[
3737 "while"
3838 "do"
3939 "end"
40−] @keyword.control.loop)
40+] @keyword.control.repeat)
4141
4242(repeat_statement
4343[
4444 "repeat"
4545 "until"
46−] @keyword.control.loop)
46+] @keyword.control.repeat)
4747
4848(do_statement
4949[
@@ -65,7 +65,7 @@
6565 "not"
6666 "and"
6767 "or"
68−] @keyword.operator
68+] @operator
6969
7070[
7171"="
@@ -108,7 +108,7 @@
108108[
109109(false)
110110(true)
111−] @boolean
111+] @constant.builtin.boolean
112112(nil) @constant.builtin
113113(spread) @constant ;; "..."
114114((identifier) @constant
@@ -116,7 +116,7 @@
116116
117117;; Parameters
118118(parameters
119− (identifier) @parameter)
119+ (identifier) @variable.parameter)
120120
121121; ;; Functions
122122(function [(function_name) (identifier)] @function)
@@ -139,8 +139,8 @@
139139
140140(function_call
141141 [
142− ((identifier) @variable (method) @method)
143− ((_) (method) @method)
142+ ((identifier) @variable (method) @function.method)
143+ ((_) (method) @function.method)
144144 (identifier) @function
145145 (field_expression (property_identifier) @function)
146146 ]
runtime/queries/ocaml/highlights.scm+4−4
@@ -25,12 +25,12 @@
2525
2626(external (value_name) @function)
2727
28−(method_name) @method
28+(method_name) @function.method
2929
3030; Variables
3131;----------
3232
33−(value_pattern) @parameter
33+(value_pattern) @variable.parameter
3434
3535; Application
3636;------------
@@ -60,7 +60,7 @@
6060
6161[(number) (signed_number)] @number
6262
63−(character) @character
63+(character) @constant.character
6464
6565(string) @string
6666
@@ -92,7 +92,7 @@
9292
9393["include" "open"] @include
9494
95−["for" "to" "downto" "while" "do" "done"] @keyword.control.loop
95+["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
9696
9797; Macros
9898;-------
runtime/queries/ruby/highlights.scm+1−1
@@ -100,7 +100,7 @@
100100 (bare_symbol)
101101] @string.special.symbol
102102
103−(regex) @string.special.regex
103+(regex) @string.regexp
104104(escape_sequence) @escape
105105
106106[
runtime/queries/rust/highlights.scm+2−2
@@ -17,7 +17,7 @@
1717
1818(escape_sequence) @escape
1919(primitive_type) @type.builtin
20−(boolean_literal) @constant.builtin
20+(boolean_literal) @constant.builtin.boolean
2121[
2222 (integer_literal)
2323 (float_literal)
@@ -149,7 +149,7 @@
149149
150150(mutable_specifier) @keyword.mut
151151
152−
152+; TODO: variable.mut to highlight mutable identifiers via locals.scm
153153
154154; -------
155155; 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 @@
11(block_mapping_pair key: (_) @property)
22(flow_mapping (_ key: (_) @property))
3−(boolean_scalar) @boolean
3+(boolean_scalar) @constant.builtin.boolean
44(null_scalar) @constant.builtin
55(double_quote_scalar) @string
66(single_quote_scalar) @string
runtime/themes/dark_plus.toml+2−1
@@ -34,6 +34,7 @@
3434"comment" = { fg = "#6A9955" }
3535
3636"string" = { fg = "#ce9178" }
37+"string.regexp" = { fg = "regex" }
3738"number" = { fg = "#b5cea8" }
3839"escape" = { fg = "#d7ba7d" }
3940
@@ -61,7 +62,7 @@
6162"ui.text.focus" = { fg = "#ffffff" }
6263
6364"warning" = { fg = "#cca700" }
64−"error" = { fg = "#f48771" }
65+"error" = { fg = "#ff1212" }
6566"info" = { fg = "#75beff" }
6667"hint" = { fg = "#eeeeeeb3" }
6768
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 @@
3434"comment" = { fg = "#88846F" }
3535
3636"string" = { fg = "#e6db74" }
37+"string.regexp" = { fg = "regex" }
3738"number" = { fg = "#ae81ff" }
3839"escape" = { fg = "#ae81ff" }
3940
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"
99property = "white"
1010variable = "lavender"
1111# variable = "almond" # TODO: metavariables only
12−"variable.parameter" = "lavender"
12+# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }
13+"variable.parameter" = { fg = "lavender" }
1314"variable.builtin" = "mint"
1415type = "white"
1516"type.builtin" = "white" # TODO: distinguish?
@@ -28,9 +29,7 @@ escape = "honey"
2829label = "honey"
2930
3031# TODO: diferentiate doc comment
31−# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors
32−
33−module = "#ff0000"
32+# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
3433
3534"ui.background" = { bg = "midnight" }
3635"ui.linenr" = { fg = "comet" }