diff options
72 files changed, 2126 insertions, 1057 deletions
diff --git a/.gitmodules b/.gitmodules index f905b8c7..d1fc1517 100644 --- a/.gitmodules +++ b/.gitmodules @@ -118,3 +118,7 @@ path = helix-syntax/languages/tree-sitter-zig url = https://github.com/maxxnino/tree-sitter-zig shallow = true +[submodule "helix-syntax/languages/tree-sitter-svelte"] + path = helix-syntax/languages/tree-sitter-svelte + url = https://github.com/Himujjal/tree-sitter-svelte + shallow = true @@ -13,15 +13,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "arc-swap" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00" +checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63" [[package]] name = "autocfg" @@ -41,10 +41,18 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] [[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + +[[package]] name = "bytes" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -58,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cfg-if" @@ -175,6 +183,15 @@ dependencies = [ ] [[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] name = "error-code" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -301,6 +318,45 @@ dependencies = [ ] [[package]] +name = "grep-matcher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-regex" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06" +dependencies = [ + "aho-corasick", + "bstr", + "grep-matcher", + "log", + "regex", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "grep-searcher" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d" +dependencies = [ + "bstr", + "bytecount", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memmap2", +] + +[[package]] name = "helix-core" version = "0.4.1" dependencies = [ @@ -375,6 +431,8 @@ dependencies = [ "fern", "futures-util", "fuzzy-matcher", + "grep-regex", + "grep-searcher", "helix-core", "helix-dap", "helix-lsp", @@ -571,6 +629,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + +[[package]] name = "mio" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -772,6 +839,12 @@ dependencies = [ ] [[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] name = "regex-syntax" version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -829,9 +902,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -851,9 +924,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" dependencies = [ "libc", "signal-hook-registry", @@ -893,9 +966,9 @@ dependencies = [ [[package]] name = "similar" -version = "1.3.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +checksum = "6bf11003835e462f07851028082d2a1c89d956180ce4b4b50e07fb085ec4131a" [[package]] name = "slab" @@ -948,18 +1021,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" dependencies = [ "proc-macro2", "quote", @@ -1001,9 +1074,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" +checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" dependencies = [ "autocfg", "bytes", @@ -1052,9 +1125,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.19.5" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" +checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9" dependencies = [ "cc", "regex", @@ -1098,9 +1171,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -1,12 +1,8 @@ - tree sitter: - - lua - markdown - - zig - regex - - vue - kotlin - - julia - clojure - erlang @@ -26,8 +22,6 @@ as you type completion! - [ ] lsp: signature help -- [ ] search: smart case by default: insensitive unless upper detected - 2 - [ ] macro recording - [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) diff --git a/book/book.toml b/book/book.toml index 3ccaf71e..2277a0bd 100644 --- a/book/book.toml +++ b/book/book.toml @@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"] language = "en" multilingual = false src = "src" -theme = "colibri" edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit" [output.html] cname = "docs.helix-editor.com" +default-theme = "colibri" +preferred-dark-theme = "colibri" diff --git a/book/src/configuration.md b/book/src/configuration.md index 00dfbbd8..60b12bfd 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,6 +5,21 @@ To override global configuration parameters, create a `config.toml` file located * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` +## Editor + +`[editor]` section of the config. + +| Key | Description | Default | +|--|--|---------| +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` | +| `mouse` | Enable mouse mode. | `true` | +| `middle-click-paste` | Middle click paste support. | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | +| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` | +| `line-number` | Line number display (`absolute`, `relative`) | `absolute` | +| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | +| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | + ## LSP To display all language server messages in the status line add the following to your `config.toml`: diff --git a/book/src/from-vim.md b/book/src/from-vim.md index 8e9bbac3..09f33386 100644 --- a/book/src/from-vim.md +++ b/book/src/from-vim.md @@ -7,4 +7,6 @@ going to act on (a word, a paragraph, a line, etc) is selected first and the action itself (delete, change, yank, etc) comes second. A cursor is simply a single width selection. +See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim). + > TODO: Mention texobjects, surround, registers diff --git a/book/src/install.md b/book/src/install.md index cd9c980e..b9febbcc 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -23,7 +23,9 @@ shell for working on Helix. ### Arch Linux -Binary packages are available on AUR: +Releases are available in the `community` repository. + +Packages are also available on AUR: - [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release - [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch diff --git a/book/src/keymap.md b/book/src/keymap.md index 861e46ac..78bac0cf 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -4,40 +4,40 @@ ### Movement -> NOTE: `f`, `F`, `t` and `T` are not confined to the current line. - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `h`, `Left` | Move left | `move_char_left` | -| `j`, `Down` | Move down | `move_char_right` | -| `k`, `Up` | Move up | `move_line_up` | -| `l`, `Right` | Move right | `move_line_down` | -| `w` | Move next word start | `move_next_word_start` | -| `b` | Move previous word start | `move_prev_word_start` | -| `e` | Move next word end | `move_next_word_end` | -| `W` | Move next WORD start | `move_next_long_word_start` | -| `B` | Move previous WORD start | `move_prev_long_word_start` | -| `E` | Move next WORD end | `move_next_long_word_end` | -| `t` | Find 'till next char | `find_till_char` | -| `f` | Find next char | `find_next_char` | -| `T` | Find 'till previous char | `till_prev_char` | -| `F` | Find previous char | `find_prev_char` | -| `Home` | Move to the start of the line | `goto_line_start` | -| `End` | Move to the end of the line | `goto_line_end` | -| `PageUp` | Move page up | `page_up` | -| `PageDown` | Move page down | `page_down` | -| `Ctrl-u` | Move half page up | `half_page_up` | -| `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` | -| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | -| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | -| `g` | Enter [goto mode](#goto-mode) | N/A | -| `m` | Enter [match mode](#match-mode) | N/A | -| `:` | Enter command mode | `command_mode` | -| `z` | Enter [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) | N/A | -| `Space` | Enter [space mode](#space-mode) | N/A | -| `K` | Show documentation for the item under the cursor | `hover` | +> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `h`, `Left` | Move left | `move_char_left` | +| `j`, `Down` | Move down | `move_char_right` | +| `k`, `Up` | Move up | `move_line_up` | +| `l`, `Right` | Move right | `move_line_down` | +| `w` | Move next word start | `move_next_word_start` | +| `b` | Move previous word start | `move_prev_word_start` | +| `e` | Move next word end | `move_next_word_end` | +| `W` | Move next WORD start | `move_next_long_word_start` | +| `B` | Move previous WORD start | `move_prev_long_word_start` | +| `E` | Move next WORD end | `move_next_long_word_end` | +| `t` | Find 'till next char | `find_till_char` | +| `f` | Find next char | `find_next_char` | +| `T` | Find 'till previous char | `till_prev_char` | +| `F` | Find previous char | `find_prev_char` | +| `Home` | Move to the start of the line | `goto_line_start` | +| `End` | Move to the end of the line | `goto_line_end` | +| `PageUp` | Move page up | `page_up` | +| `PageDown` | Move page down | `page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | +| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | +| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | +| `g` | Enter [goto mode](#goto-mode) | N/A | +| `m` | Enter [match mode](#match-mode) | N/A | +| `:` | Enter command mode | `command_mode` | +| `z` | Enter [view mode](#view-mode) | N/A | +| `Z` | Enter sticky [view mode](#view-mode) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | +| `Space` | Enter [space mode](#space-mode) | N/A | ### Changes @@ -66,6 +66,16 @@ | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +#### Shell + +| Key | Description | Command | +| ------ | ----------- | ------- | +| <code>|</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` | +| <code>A-|</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | +| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | +| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | + + ### Selection manipulation | Key | Description | Command | @@ -75,6 +85,7 @@ | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | +| `,` | Keep only the primary selection | `keep_primary_selection` | | `C` | Copy selection onto the next line | `copy_selection_on_next_line` | | `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` | | `(` | Rotate main selection forward | `rotate_selections_backward` | @@ -86,22 +97,13 @@ | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | -| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | -| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | +| `K` | Keep selections matching the regex | `keep_selections` | +| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | -### Insert Mode - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-w` | Delete previous word | `delete_word_backward` | - ### Search -> TODO: The search implementation isn't ideal yet -- we don't support searching -in reverse, or searching via smartcase. +> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse. | Key | Description | Command | | ----- | ----------- | ------- | @@ -110,41 +112,17 @@ in reverse, or searching via smartcase. | `N` | Add next search match to selection | `extend_search_next` | | `*` | Use current selection as the search pattern | `search_selection` | -### Unimpaired - -Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired) - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `[d` | Go to previous diagnostic | `goto_prev_diag` | -| `]d` | Go to next diagnostic | `goto_next_diag` | -| `[D` | Go to first diagnostic in document | `goto_first_diag` | -| `]D` | Go to last diagnostic in document | `goto_last_diag` | -| `[space` | Add newline above | `add_newline_above` | -| `]space` | Add newline below | `add_newline_below` | - -### Shell +### Minor modes -| Key | Description | Command | -| ------ | ----------- | ------- | -| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` | -| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | -| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | -| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | -| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | - -## Select / extend mode - -I'm still pondering whether to keep this mode or not. It changes movement -commands to extend the existing selection instead of replacing it. - -> NOTE: It's a bit confusing at the moment because extend hasn't been -> implemented for all movement commands yet. +These sub-modes are accessible from normal mode and typically switch back to normal mode after a command. -## View mode +#### View mode View mode is intended for scrolling and manipulating the view without changing -the selection. +the selection. The "sticky" variant of this mode is persistent; use the Escape +key to return to normal mode after usage (useful when you're simply looking +over text and not actively editing it). + | Key | Description | Command | | ----- | ----------- | ------- | @@ -154,8 +132,12 @@ the selection. | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `j` | Scroll the view downwards | `scroll_down` | | `k` | Scroll the view upwards | `scroll_up` | +| `f` | Move page down | `page_down` | +| `b` | Move page up | `page_up` | +| `d` | Move half page down | `half_page_down` | +| `u` | Move half page up | `half_page_up` | -## Goto mode +#### Goto mode Jumps to various locations. @@ -177,7 +159,7 @@ Jumps to various locations. | `i` | Go to implementation | `goto_implementation` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | -## Match mode +#### Match mode Enter this mode using `m` from normal mode. See the relavant section in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) @@ -192,11 +174,9 @@ and [textobject](./usage.md#textobject) usage. | `a` `<object>` | Select around textobject | `select_textobject_around` | | `i` `<object>` | Select inside textobject | `select_textobject_inner` | -## Object mode - TODO: Mappings for selecting syntax nodes (a superset of `[`). -## Window mode +#### Window mode This layer is similar to vim keybindings as kakoune does not support window. @@ -207,24 +187,56 @@ This layer is similar to vim keybindings as kakoune does not support window. | `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` | | `q`, `Ctrl-q` | Close current window | `wclose` | -## Space mode +#### Space mode -This layer is a kludge of mappings I had under leader key in neovim. +This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | +| `k` | Show documentation for the item under the cursor | `hover` | | `f` | Open file picker | `file_picker` | | `b` | Open buffer picker | `buffer_picker` | | `s` | Open symbol picker (current document) | `symbol_picker` | | `a` | Apply code action | `code_action` | | `'` | Open last fuzzy picker | `last_picker` | | `w` | Enter [window mode](#window-mode) | N/A | -| `space` | Keep primary selection TODO: it's here because space mode replaced it | `keep_primary_selection` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | + +> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file. + +#### Unimpaired + +Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `[d` | Go to previous diagnostic | `goto_prev_diag` | +| `]d` | Go to next diagnostic | `goto_next_diag` | +| `[D` | Go to first diagnostic in document | `goto_first_diag` | +| `]D` | Go to last diagnostic in document | `goto_last_diag` | +| `[space` | Add newline above | `add_newline_above` | +| `]space` | Add newline below | `add_newline_below` | + +## Insert Mode + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-w` | Delete previous word | `delete_word_backward` | + +## Select / extend mode + +I'm still pondering whether to keep this mode or not. It changes movement +commands (including goto) to extend the existing selection instead of replacing it. + +> NOTE: It's a bit confusing at the moment because extend hasn't been +> implemented for all movement commands yet. # Picker diff --git a/book/src/remapping.md b/book/src/remapping.md index 3f25e364..81f45da3 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes | Null | `"null"` | | Escape | `"esc"` | +Keys can be disabled by binding them to the `no_op` command. + Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) diff --git a/book/src/themes.md b/book/src/themes.md index 0a4d58ad..a99e3a59 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -30,7 +30,52 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as "key.key" = "#ffffff" ``` -Possible modifiers: +### Color palettes + +It's recommended define a palette of named colors, and refer to them from the +configuration values in your theme. To do this, add a table called +`palette` to your theme file: + +```toml +ui.background = "white" +ui.text = "black" + +[palette] +white = "#ffffff" +black = "#000000" +``` + +Remember that the `[palette]` table includes all keys after its header, +so you should define the palette after normal theme options. + +The default palette uses the terminal's default 16 colors, and the colors names +are listed below. The `[palette]` section in the config file takes precedence +over it and is merged into the default palette. + +| Color Name | +| --- | +| `black` | +| `red` | +| `green` | +| `yellow` | +| `blue` | +| `magenta` | +| `cyan` | +| `gray` | +| `light-red` | +| `light-green` | +| `light-yellow` | +| `light-blue` | +| `light-magenta` | +| `light-cyan` | +| `light-gray` | +| `white` | + +### Modifiers + +The following values may be used as modifiers. + +Less common modifiers might not be supported by your terminal emulator. | Modifier | | --- | @@ -38,44 +83,88 @@ Possible modifiers: | `dim` | | `italic` | | `underlined` | -| `slow\_blink` | -| `rapid\_blink` | +| `slow_blink` | +| `rapid_blink` | | `reversed` | | `hidden` | -| `crossed\_out` | +| `crossed_out` | + +### Scopes + +The following is a list of scopes available to use for styling. + +#### Syntax highlighting + +These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). + +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`. + +We use a similar set of scopes as +[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also +[TextMate](https://macromates.com/manual/en/language_grammars) scopes. + +- `escape` (TODO: rename to (constant).character.escape) + +- `type` - Types + - `builtin` - Primitive types provided by the language (`int`, `usize`) + +- `constant` (TODO: constant.other.placeholder for %v) + - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) + - `boolean` + - `character` + +- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex}) +- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)? + - `regexp` - Regular expressions + - `special` + - `path` + - `url` + +- `comment` - Code comments + - `line` - Single line comments (`//`) + - `block` - Block comments (e.g. (`/* */`) + - `documentation` - Documentation comments (e.g. `///` in Rust) + +- `variable` - Variables + - `builtin` - Reserved language variables (`self`, `this`, `super`, etc) + - `parameter` - Function parameters + - `property` + - `function` (TODO: ?) + +- `label` + +- `punctuation` + - `delimiter` - Commas, colons + - `bracket` - Parentheses, angle brackets, etc. + +- `keyword` + - `control` + - `conditional` - `if`, `else` + - `repeat` - `for`, `while`, `loop` + - `import` - `import`, `export` + - (TODO: return?) + - `directive` - Preprocessor directives (`#if` in C) + - `function` - `fn`, `func` + +- `operator` - `||`, `+=`, `>`, `or` + +- `function` + - `builtin` + - `method` + - `macro` + - `special` (preprocesor in C) + +- `tag` - Tags (e.g. `<body>` in HTML) + +- `namespace` + +#### Interface + +These scopes are used for theming the editor interface. -Possible keys: | Key | Notes | | --- | --- | -| `attribute` | | -| `keyword` | | -| `keyword.directive` | Preprocessor directives (\#if in C) | -| `keyword.control` | Control flow | -| `namespace` | | -| `punctuation` | | -| `punctuation.delimiter` | | -| `operator` | | -| `special` | | -| `property` | | -| `variable` | | -| `variable.parameter` | | -| `type` | | -| `type.builtin` | | -| `type.enum.variant` | Enum variants | -| `constructor` | | -| `function` | | -| `function.macro` | | -| `function.builtin` | | -| `comment` | | -| `variable.builtin` | | -| `constant` | | -| `constant.builtin` | | -| `string` | | -| `number` | | -| `escape` | Escaped characters | -| `label` | For lifetimes | -| `module` | | | `ui.background` | | | `ui.cursor` | | | `ui.cursor.insert` | | @@ -84,8 +173,8 @@ Possible keys: | `ui.cursor.primary` | Cursor with primary selection | | `ui.linenr` | | | `ui.linenr.selected` | | -| `ui.statusline` | | -| `ui.statusline.inactive` | | +| `ui.statusline` | Statusline | +| `ui.statusline.inactive` | Statusline (unfocused document) | | `ui.popup` | | | `ui.window` | | | `ui.help` | | @@ -97,29 +186,9 @@ Possible keys: | `ui.menu.selected` | | | `ui.selection` | For selections in the editing area | | `ui.selection.primary` | | -| `warning` | LSP warning | -| `error` | LSP error | -| `info` | LSP info | -| `hint` | LSP hint | - -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. +| `warning` | Diagnostics warning (gutter) | +| `error` | Diagnostics error (gutter) | +| `info` | Diagnostics info (gutter) | +| `hint` | Diagnostics hint (gutter) | +| `diagnostic` | For text in editing area | -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. - -## Color palettes - -You can define a palette of named colors, and refer to them from the -configuration values in your theme. To do this, add a table called -`palette` to your theme file: - -```toml -ui.background = "white" -ui.text = "black" - -[palette] -white = "#ffffff" -black = "#000000" -``` - -Remember that the `[palette]` table includes all keys after its header, -so you should define the palette after normal theme options. diff --git a/book/theme/css/general.css b/book/theme/css/general.css index 7749bded..ddc2387a 100644 --- a/book/theme/css/general.css +++ b/book/theme/css/general.css @@ -114,6 +114,19 @@ h6:target::before { margin-bottom: .875em; } +.content ul li { +margin-bottom: .25rem; +} +.content ul { + list-style-type: square; +} +.content ul ul, .content ol ul { + margin-bottom: .5rem; +} +.content li p { + margin-bottom: .5em; +} + .content p { line-height: 1.45em; } .content ol { line-height: 1.45em; } .content ul { line-height: 1.45em; } diff --git a/book/theme/css/variables.css b/book/theme/css/variables.css index a49d6794..db1a11b8 100644 --- a/book/theme/css/variables.css +++ b/book/theme/css/variables.css @@ -69,7 +69,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -110,7 +110,7 @@ --links: #20609f; - --inline-code-color: #301900; + --inline-code-color: #a39e9b; --theme-popup-bg: #fafafa; --theme-popup-border: #cccccc; @@ -151,7 +151,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -192,7 +192,7 @@ --links: #2b79a2; - --inline-code-color: #6e6b5e; + --inline-code-color: #c5c8c6; --theme-popup-bg: #e1e1db; --theme-popup-border: #b38f6b; @@ -234,7 +234,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #6e6b5e; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -261,6 +261,7 @@ .colibri { --bg: #3b224c; --fg: #bcbdd0; + --heading-fg: #fff; --sidebar-bg: #281733; --sidebar-fg: #c8c9db; @@ -276,18 +277,19 @@ /* --links: #a4a0e8; */ --links: #ECCDBA; - --inline-code-color: #c5c8c6;; + --inline-code-color: hsl(48.7, 7.8%, 70%); --theme-popup-bg: #161923; --theme-popup-border: #737480; --theme-hover: rgba(0,0,0, .2); - --quote-bg: hsl(226, 15%, 17%); + --quote-bg: #281733; --quote-border: hsl(226, 15%, 22%); - --table-border-color: hsl(226, 23%, 16%); - --table-header-bg: hsl(226, 23%, 31%); + --table-border-color: hsl(226, 23%, 76%); + --table-header-bg: hsla(226, 23%, 31%, 0); --table-alternate-bg: hsl(226, 23%, 14%); + --table-border-line: hsla(201deg, 20%, 92%, 0.2); --searchbar-border-color: #aaa; --searchbar-bg: #aeaec6; @@ -300,6 +302,7 @@ } .colibri { +/* --bg: #ffffff; --fg: #452859; --fg: #5a5977; @@ -318,7 +321,7 @@ --links: #6F44F0; - --inline-code-color: #697C81; + --inline-code-color: #a39e9b; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -341,4 +344,5 @@ --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; --search-mark-bg: #a2cff5; +*/ } diff --git a/book/theme/highlight.css b/book/theme/highlight.css index c2343227..8dce7d65 100644 --- a/book/theme/highlight.css +++ b/book/theme/highlight.css @@ -1,83 +1,56 @@ -/* - * An increased contrast highlighting scheme loosely based on the - * "Base16 Atelier Dune Light" theme by Bram de Haan - * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) - * Original Base16 color scheme by Chris Kempson - * (https://github.com/chriskempson/base16) - */ - -/* Comment */ +pre code.hljs { + display:block; + overflow-x:auto; + padding:1em +} +code.hljs { + padding:3px 5px +} +.hljs { + background:#2f1e2e; + color:#a39e9b +} .hljs-comment, .hljs-quote { - color: #575757; + color:#8d8687 } - -/* Red */ -.hljs-variable, -.hljs-template-variable, -.hljs-attribute, -.hljs-tag, -.hljs-name, -.hljs-regexp, .hljs-link, +.hljs-meta, .hljs-name, +.hljs-regexp, +.hljs-selector-class, .hljs-selector-id, -.hljs-selector-class { - color: #d70025; +.hljs-tag, +.hljs-template-variable, +.hljs-variable { + color:#ef6155 } - -/* Orange */ -.hljs-number, -.hljs-meta, .hljs-built_in, -.hljs-builtin-name, +.hljs-deletion, .hljs-literal, -.hljs-type, -.hljs-params { - color: #b21e00; +.hljs-number, +.hljs-params, +.hljs-type { + color:#f99b15 } - -/* Green */ -.hljs-string, -.hljs-symbol, -.hljs-bullet { - color: #008200; +.hljs-attribute, +.hljs-section, +.hljs-title { + color:#fec418 } - -/* Blue */ -.hljs-title, -.hljs-section { - color: #0030f2; +.hljs-addition, +.hljs-bullet, +.hljs-string, +.hljs-symbol { + color:#48b685 } - -/* Purple */ .hljs-keyword, .hljs-selector-tag { - color: #9d00ec; -} - -.hljs { - display: block; - overflow-x: auto; - background: #f6f7f6; - color: #000; - padding: 0.5em; + color:#815ba4 } - .hljs-emphasis { - font-style: italic; + font-style:italic } - .hljs-strong { - font-weight: bold; -} - -.hljs-addition { - color: #22863a; - background-color: #f0fff4; -} - -.hljs-deletion { - color: #b31d28; - background-color: #ffeef0; + font-weight:700 } @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1625086391, - "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=", + "lastModified": 1630239564, + "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=", "owner": "numtide", "repo": "devshell", - "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7", + "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73", "type": "github" }, "original": { @@ -40,11 +40,11 @@ "rustOverlay": "rustOverlay" }, "locked": { - "lastModified": 1628489367, - "narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=", + "lastModified": 1631254163, + "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "0dc8383aae5f791a48e34120edb04670b947dc0b", + "rev": "432d8504a32232e8d74710024d5bf5cc31767651", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1628465643, - "narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=", + "lastModified": 1631206977, + "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6ef4f522d63f22b40004319778761040d3197390", + "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", "type": "github" }, "original": { @@ -79,11 +79,11 @@ "rustOverlay": { "flake": false, "locked": { - "lastModified": 1628475192, - "narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=", + "lastModified": 1631240108, + "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468", + "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289", "type": "github" }, "original": { @@ -30,22 +30,7 @@ }; # link languages and theme toml files since helix-view expects them helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; }; - helix-syntax = prev: { - src = - let - pkgs = common.pkgs; - helix = pkgs.fetchgit { - url = "https://github.com/helix-editor/helix.git"; - rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5"; - fetchSubmodules = true; - sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ="; - }; - in - pkgs.runCommand prev.src.name { } '' - mkdir -p $out - ln -s ${prev.src}/* $out - ln -sf ${helix}/helix-syntax/languages $out - ''; + helix-syntax = _prev: { preConfigure = "mkdir -p ../runtime/grammars"; postInstall = "cp -r ../runtime $out/runtime"; }; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index d26ff953..e6843375 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -23,7 +23,7 @@ unicode-segmentation = "1.8" unicode-width = "0.1" unicode-general-category = "0.4" # slab = "0.4.2" -tree-sitter = "0.19" +tree-sitter = "0.20" once_cell = "1.8" arc-swap = "1" regex = "1" @@ -31,7 +31,7 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" -similar = "1.3" +similar = "2.0" etcetera = "0.3" diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 4b74aa7a..1f32d038 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos( pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { let mut scopes = Vec::new(); if let Some(syntax) = syntax { - let byte_start = text.char_to_byte(pos); - let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) { + let pos = text.char_to_byte(pos); + let mut node = match syntax + .tree() + .root_node() + .descendant_for_byte_range(pos, pos) + { Some(node) => node, None => return scopes, }; @@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<& scopes.push(node.kind()); while let Some(parent) = node.parent() { - scopes.push(parent.kind()) + scopes.push(parent.kind()); + node = parent; } } @@ -449,6 +454,7 @@ where highlight_config: OnceCell::new(), config: None, // + injection_regex: None, roots: vec![], comment_token: None, auto_format: false, diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index a3ea2ed4..755ee679 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -360,6 +360,15 @@ impl Selection { self.normalize() } + /// Adds a new range to the selection and makes it the primary range. + pub fn remove(mut self, index: usize) -> Self { + self.ranges.remove(index); + if index < self.primary_index || self.primary_index == self.ranges.len() { + self.primary_index -= 1; + } + self + } + /// Map selections over a set of changes. Useful for adjusting the selection position after /// applying changes to a document. pub fn map(self, changes: &ChangeSet) -> Self { diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index bbee921b..75c2b179 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -21,6 +21,15 @@ use std::{ use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; +fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::<String>::deserialize(deserializer)? + .map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom)) + .transpose() +} + #[derive(Debug, Serialize, Deserialize)] pub struct Configuration { pub language: Vec<LanguageConfiguration>, @@ -42,7 +51,8 @@ pub struct LanguageConfiguration { pub auto_format: bool, // content_regex - // injection_regex + #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] + pub injection_regex: Option<Regex>, // first_line_regex // #[serde(skip)] @@ -182,8 +192,12 @@ impl LanguageConfiguration { &highlights_query, &injections_query, &locals_query, - ) - .unwrap(); // TODO: no unwrap + ); + + let config = match config { + Ok(config) => config, + Err(err) => panic!("{}", err), + }; // TODO: avoid panic config.configure(scopes); Some(Arc::new(config)) } @@ -277,6 +291,30 @@ impl Loader { .cloned() } + pub fn language_configuration_for_injection_string( + &self, + string: &str, + ) -> Option<Arc<LanguageConfiguration>> { + let mut best_match_length = 0; + let mut best_match_position = None; + for (i, configuration) in self.language_configs.iter().enumerate() { + if let Some(injection_regex) = &configuration.injection_regex { + if let Some(mat) = injection_regex.find(string) { + let length = mat.end() - mat.start(); + if length > best_match_length { + best_match_position = Some(i); + best_match_length = length; + } + } + } + } + + if let Some(i) = best_match_position { + let configuration = &self.language_configs[i]; + return Some(configuration.clone()); + } + None + } pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> { self.language_configs.iter() } @@ -314,16 +352,6 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st Cow::from(source.slice(start_char..end_char)) } -fn node_to_bytes<'a>(node: Node, source: RopeSlice<'a>) -> Cow<'a, [u8]> { - let start_char = source.byte_to_char(node.start_byte()); - let end_char = source.byte_to_char(node.end_byte()); - let fragment = source.slice(start_char..end_char); - match fragment.as_str() { - Some(fragment) => Cow::Borrowed(fragment.as_bytes()), - None => Cow::Owned(String::from(fragment).into_bytes()), - } -} - impl Syntax { // buffer, grammar, config, grammars, sync_timeout? pub fn new( @@ -416,16 +444,11 @@ impl Syntax { let config_ref = unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) }; - // TODO: if reusing cursors this might need resetting - if let Some(range) = &range { - cursor_ref.set_byte_range(range.start, range.end); - } + // if reusing cursors & no range this resets to whole range + cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); let captures = cursor_ref - .captures(query_ref, tree_ref.root_node(), move |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }) + .captures(query_ref, tree_ref.root_node(), RopeProvider(source)) .peekable(); // manually craft the root layer based on the existing tree @@ -539,10 +562,7 @@ impl LanguageLayer { // let mut injections_by_pattern_index = // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; // let matches = - // cursor.matches(combined_injections_query, tree.root_node(), |n: Node| { - // // &source[n.byte_range()] - // node_to_bytes(n, source) - // }); + // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source)); // for mat in matches { // let entry = &mut injections_by_pattern_index[mat.pattern_index]; // let (language_name, content_node, include_children) = @@ -754,7 +774,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::{iter, mem, ops, str, usize}; use tree_sitter::{ Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, - QueryMatch, Range, Tree, + QueryMatch, Range, TextProvider, Tree, }; const CANCELLATION_CHECK_INTERVAL: usize = 100; @@ -814,7 +834,7 @@ struct LocalScope<'a> { } #[derive(Debug)] -struct HighlightIter<'a, 'tree: 'a, F> +struct HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -822,16 +842,41 @@ where byte_offset: usize, injection_callback: F, cancellation_flag: Option<&'a AtomicUsize>, - layers: Vec<HighlightIterLayer<'a, 'tree>>, + layers: Vec<HighlightIterLayer<'a>>, iter_count: usize, next_event: Option<HighlightEvent>, last_highlight_range: Option<(usize, usize, usize)>, } -struct HighlightIterLayer<'a, 'tree: 'a> { +// Adapter to convert rope chunks to bytes +struct ChunksBytes<'a> { + chunks: ropey::iter::Chunks<'a>, +} +impl<'a> Iterator for ChunksBytes<'a> { + type Item = &'a [u8]; + fn next(&mut self) -> Option<Self::Item> { + self.chunks.next().map(str::as_bytes) + } +} + +struct RopeProvider<'a>(RopeSlice<'a>); +impl<'a> TextProvider<'a> for RopeProvider<'a> { + type I = ChunksBytes<'a>; + + fn text(&mut self, node: Node) -> Self::I { + let start_char = self.0.byte_to_char(node.start_byte()); + let end_char = self.0.byte_to_char(node.end_byte()); + let fragment = self.0.slice(start_char..end_char); + ChunksBytes { + chunks: fragment.chunks(), + } + } +} + +struct HighlightIterLayer<'a> { _tree: Option<Tree>, cursor: QueryCursor, - captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>, + captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>, config: &'a HighlightConfiguration, highlight_end_stack: Vec<usize>, scope_stack: Vec<LocalScope<'a>>, @@ -839,7 +884,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> { depth: usize, } -impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> { +impl<'a> fmt::Debug for HighlightIterLayer<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HighlightIterLayer").finish() } @@ -1010,7 +1055,7 @@ impl HighlightConfiguration { } } -impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { +impl<'a> HighlightIterLayer<'a> { /// Create a new 'layer' of highlighting for this document. /// /// In the even that the new layer contains "combined injections" (injections where multiple @@ -1067,10 +1112,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { let matches = cursor.matches( combined_injections_query, tree.root_node(), - |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }, + RopeProvider(source), ); for mat in matches { let entry = &mut injections_by_pattern_index[mat.pattern_index]; @@ -1117,10 +1159,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; let captures = cursor_ref - .captures(&config.query, tree_ref.root_node(), move |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }) + .captures(&config.query, tree_ref.root_node(), RopeProvider(source)) .peekable(); result.push(HighlightIterLayer { @@ -1274,7 +1313,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { } } -impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F> +impl<'a, F> HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -1325,7 +1364,7 @@ where } } - fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) { + fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) { if let Some(sort_key) = layer.sort_key() { let mut i = 1; while i < self.layers.len() { @@ -1344,7 +1383,7 @@ where } } -impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F> +impl<'a, F> Iterator for HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -1608,7 +1647,7 @@ where fn injection_for_match<'a>( config: &HighlightConfiguration, query: &'a Query, - query_match: &QueryMatch<'a>, + query_match: &QueryMatch<'a, 'a>, source: RopeSlice<'a>, ) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) { let content_capture_index = config.injection_content_capture_index; diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 8aecba74..aaa7e8a1 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.10", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.11", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index fd34f45d..4068ae1f 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -3,17 +3,23 @@ use crate::{ Call, Error, OffsetEncoding, Result, }; -use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope}; +use helix_core::{find_root, ChangeSet, Rope}; use jsonrpc_core as jsonrpc; use lsp_types as lsp; use serde_json::Value; use std::future::Future; use std::process::Stdio; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, - sync::mpsc::{channel, UnboundedReceiver, UnboundedSender}, + sync::{ + mpsc::{channel, UnboundedReceiver, UnboundedSender}, + Notify, OnceCell, + }, }; #[derive(Debug)] @@ -22,18 +28,19 @@ pub struct Client { _process: Child, server_tx: UnboundedSender<Payload>, request_counter: AtomicU64, - capabilities: Option<lsp::ServerCapabilities>, + pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>, offset_encoding: OffsetEncoding, config: Option<Value>, } impl Client { + #[allow(clippy::type_complexity)] pub fn start( cmd: &str, args: &[String], config: Option<Value>, id: usize, - ) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> { + ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { let process = Command::new(cmd) .args(args) .stdin(Stdio::piped()) @@ -50,22 +57,20 @@ impl Client { let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); - let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id); + let (server_rx, server_tx, initialize_notify) = + Transport::start(reader, writer, stderr, id); let client = Self { id, _process: process, server_tx, request_counter: AtomicU64::new(0), - capabilities: None, + capabilities: OnceCell::new(), offset_encoding: OffsetEncoding::Utf8, config, }; - // TODO: async client.initialize() - // maybe use an arc<atomic> flag - - Ok((client, server_rx)) + Ok((client, server_rx, initialize_notify)) } pub fn id(&self) -> usize { @@ -88,9 +93,13 @@ impl Client { } } + pub fn is_initialized(&self) -> bool { + self.capabilities.get().is_some() + } + pub fn capabilities(&self) -> &lsp::ServerCapabilities { self.capabilities - .as_ref() + .get() .expect("language server not yet initialized!") } @@ -143,7 +152,8 @@ impl Client { }) .map_err(|e| Error::Other(e.into()))?; - timeout(Duration::from_secs(2), rx.recv()) + // TODO: specifiable timeout, delay other calls until initialize success + timeout(Duration::from_secs(20), rx.recv()) .await .map_err(|_| Error::Timeout)? // return Timeout .ok_or(Error::StreamClosed)? @@ -151,7 +161,7 @@ impl Client { } /// Send a RPC notification to the language server. - fn notify<R: lsp::notification::Notification>( + pub fn notify<R: lsp::notification::Notification>( &self, params: R::Params, ) -> impl Future<Output = Result<()>> @@ -213,7 +223,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&mut self) -> Result<()> { + pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> { // TODO: delay any requests that are triggered prior to initialize let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok()); @@ -281,14 +291,7 @@ impl Client { locale: None, // TODO }; - let response = self.request::<lsp::request::Initialize>(params).await?; - self.capabilities = Some(response.capabilities); - - // next up, notify<initialized> - self.notify::<lsp::notification::Initialized>(lsp::InitializedParams {}) - .await?; - - Ok(()) + self.request::<lsp::request::Initialize>(params).await } pub async fn shutdown(&self) -> Result<()> { @@ -356,7 +359,6 @@ impl Client { // // Calculation is therefore a bunch trickier. - // TODO: stolen from syntax.rs, share use helix_core::RopeSlice; fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position { let lsp::Position { @@ -366,7 +368,12 @@ impl Client { let mut chars = text.chars().peekable(); while let Some(ch) = chars.next() { - if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) { + // LSP only considers \n, \r or \r\n as line endings + if ch == '\n' || ch == '\r' { + // consume a \r\n + if ch == '\r' && chars.peek() == Some(&'\n') { + chars.next(); + } line += 1; character = 0; } else { @@ -441,7 +448,7 @@ impl Client { ) -> Option<impl Future<Output = Result<()>>> { // figure out what kind of sync the server supports - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); let sync_capabilities = match capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Kind(kind)) @@ -459,7 +466,7 @@ impl Client { // range = None -> whole document range: None, //Some(Range) range_length: None, // u64 apparently deprecated - text: "".to_string(), + text: new_text.to_string(), }] } lsp::TextDocumentSyncKind::Incremental => { @@ -487,12 +494,12 @@ impl Client { // will_save / will_save_wait_until - pub async fn text_document_did_save( + pub fn text_document_did_save( &self, text_document: lsp::TextDocumentIdentifier, text: &Rope, - ) -> Result<()> { - let capabilities = self.capabilities.as_ref().unwrap(); + ) -> Option<impl Future<Output = Result<()>>> { + let capabilities = self.capabilities.get().unwrap(); let include_text = match &capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { @@ -504,17 +511,18 @@ impl Client { include_text, }) => include_text.unwrap_or(false), // Supported(false) - _ => return Ok(()), + _ => return None, }, // unsupported - _ => return Ok(()), + _ => return None, }; - self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams { - text_document, - text: include_text.then(|| text.into()), - }) - .await + Some(self.notify::<lsp::notification::DidSaveTextDocument>( + lsp::DidSaveTextDocumentParams { + text_document, + text: include_text.then(|| text.into()), + }, + )) } pub fn completion( @@ -580,19 +588,19 @@ impl Client { // formatting - pub async fn text_document_formatting( + pub fn text_document_formatting( &self, text_document: lsp::TextDocumentIdentifier, options: lsp::FormattingOptions, work_done_token: Option<lsp::ProgressToken>, - ) -> anyhow::Result<Vec<lsp::TextEdit>> { - let capabilities = self.capabilities.as_ref().unwrap(); + ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> { + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_formatting_provider { Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), // None | Some(false) - _ => return Ok(Vec::new()), + _ => return None, }; // TODO: return err::unavailable so we can fall back to tree sitter formatting @@ -602,9 +610,13 @@ impl Client { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, }; - let response = self.request::<lsp::request::Formatting>(params).await?; + let request = self.call::<lsp::request::Formatting>(params); - Ok(response.unwrap_or_default()) + Some(async move { + let json = request.await?; + let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?; + Ok(response.unwrap_or_default()) + }) } pub async fn text_document_range_formatting( @@ -614,7 +626,7 @@ impl Client { options: lsp::FormattingOptions, work_done_token: Option<lsp::ProgressToken>, ) -> anyhow::Result<Vec<lsp::TextEdit>> { - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_range_formatting_provider { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 72606b70..35cff754 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -226,6 +226,8 @@ impl MethodCall { #[derive(Debug, PartialEq, Clone)] pub enum Notification { + // we inject this notification to signal the LSP is ready + Initialized, PublishDiagnostics(lsp::PublishDiagnosticsParams), ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), @@ -237,6 +239,7 @@ impl Notification { use lsp::notification::Notification as _; let notification = match method { + lsp::notification::Initialized::METHOD => Self::Initialized, lsp::notification::PublishDiagnostics::METHOD => { let params: lsp::PublishDiagnosticsParams = params .parse() @@ -294,7 +297,7 @@ impl Registry { } } - pub fn get_by_id(&mut self, id: usize) -> Option<&Client> { + pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() .find(|(client_id, _)| client_id == &id) @@ -302,33 +305,60 @@ impl Registry { } pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> { - if let Some(config) = &language_config.language_server { - // avoid borrow issues - let inner = &mut self.inner; - let s_incoming = &mut self.incoming; - - match inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(entry.get().1.clone()), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (mut client, incoming) = Client::start( - &config.command, - &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), - id, - )?; - // TODO: run this async without blocking - futures_executor::block_on(client.initialize())?; - s_incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - entry.insert((id, client.clone())); - Ok(client) - } + let config = match &language_config.language_server { + Some(config) => config, + None => return Err(Error::LspNotDefined), + }; + + match self.inner.entry(language_config.scope.clone()) { + Entry::Occupied(entry) => Ok(entry.get().1.clone()), + Entry::Vacant(entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + let (client, incoming, initialize_notify) = Client::start( + &config.command, + &config.args, + serde_json::from_str(language_config.config.as_deref().unwrap_or("")) + .map_err(|e| { + log::error!( + "LSP Config, {}, in `languages.toml` for `{}`", + e, + language_config.scope() + ) + }) + .ok(), + id, + )?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + value.expect("failed to initialize capabilities"); + + // next up, notify<initialized> + _client + .notify::<lsp::notification::Initialized>(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + entry.insert((id, client.clone())); + Ok(client) } - } else { - Err(Error::LspNotDefined) } } @@ -415,32 +445,6 @@ impl LspProgressMap { } } -// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>> -// spawn one server per language type, need to spawn one per workspace if server doesn't support -// workspaces -// -// could also be a client per root dir -// -// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily -// accessible during edit/save callbacks -// -// the event loop needs to process all incoming streams, maybe we can just have that be a separate -// task that's continually running and store the state on the client, then use read lock to -// retrieve data during render -// -> PROBLEM: how do you trigger an update on the editor side when data updates? -// -// -> The data updates should pull all events until we run out so we don't frequently re-render -// -// -// v2: -// -// there should be a registry of lsp clients, one per language type (or workspace). -// the clients should lazy init on first access -// the client.initialize() should be called async and we buffer any requests until that completes -// there needs to be a way to process incoming lsp messages from all clients. -// -> notifications need to be dispatched to wherever -// -> requests need to generate a reply and travel back to the same lsp! - #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 67b7b48f..6e28094d 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,7 +1,7 @@ -use crate::Result; +use crate::{Error, Result}; use anyhow::Context; use jsonrpc_core as jsonrpc; -use log::{debug, error, info, warn}; +use log::{error, info}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -11,7 +11,7 @@ use tokio::{ process::{ChildStderr, ChildStdin, ChildStdout}, sync::{ mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, - Mutex, + Mutex, Notify, }, }; @@ -51,9 +51,11 @@ impl Transport { ) -> ( UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedSender<Payload>, + Arc<Notify>, ) { let (client_tx, rx) = unbounded_channel(); let (tx, client_rx) = unbounded_channel(); + let notify = Arc::new(Notify::new()); let transport = Self { id, @@ -62,11 +64,21 @@ impl Transport { let transport = Arc::new(transport); - tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); + tokio::spawn(Self::recv( + transport.clone(), + server_stdout, + client_tx.clone(), + )); tokio::spawn(Self::err(transport.clone(), server_stderr)); - tokio::spawn(Self::send(transport, server_stdin, client_rx)); - - (rx, tx) + tokio::spawn(Self::send( + transport, + server_stdin, + client_tx, + client_rx, + notify.clone(), + )); + + (rx, tx, notify) } async fn recv_server_message( @@ -76,14 +88,18 @@ impl Transport { let mut content_length = None; loop { buffer.truncate(0); - reader.read_line(buffer).await?; - let header = buffer.trim(); + if reader.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + + // debug!("<- header {:?}", buffer); - if header.is_empty() { + if buffer == "\r\n" { + // look for an empty CRLF line break; } - debug!("<- header {}", header); + let header = buffer.trim(); let parts = header.split_once(": "); @@ -96,7 +112,8 @@ impl Transport { // Workaround: Some non-conformant language servers will output logging and other garbage // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn // the server. Skip such lines and log a warning. - warn!("Failed to parse header: {:?}", header); + + // warn!("Failed to parse header: {:?}", header); } } } @@ -121,8 +138,10 @@ impl Transport { buffer: &mut String, ) -> Result<()> { buffer.truncate(0); - err.read_line(buffer).await?; - error!("err <- {}", buffer); + if err.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + error!("err <- {:?}", buffer); Ok(()) } @@ -255,16 +274,90 @@ impl Transport { async fn send( transport: Arc<Self>, mut server_stdin: BufWriter<ChildStdin>, + client_tx: UnboundedSender<(usize, jsonrpc::Call)>, mut client_rx: UnboundedReceiver<Payload>, + initialize_notify: Arc<Notify>, ) { - while let Some(msg) = client_rx.recv().await { - match transport - .send_payload_to_server(&mut server_stdin, msg) - .await - { - Ok(_) => {} - Err(err) => { - error!("err: <- {:?}", err); + let mut pending_messages: Vec<Payload> = Vec::new(); + let mut is_pending = true; + + // Determine if a message is allowed to be sent early + fn is_initialize(payload: &Payload) -> bool { + use lsp_types::{ + notification::{Initialized, Notification}, + request::{Initialize, Request}, + }; + match payload { + Payload::Request { + value: jsonrpc::MethodCall { method, .. }, + .. + } if method == Initialize::METHOD => true, + Payload::Notification(jsonrpc::Notification { method, .. }) + if method == Initialized::METHOD => + { + true + } + _ => false, + } + } + + // TODO: events that use capabilities need to do the right thing + + loop { + tokio::select! { + biased; + _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe + // server successfully initialized + is_pending = false; + + use lsp_types::notification::Notification; + // Hack: inject an initialized notification so we trigger code that needs to happen after init + let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { + jsonrpc: None, + + method: lsp_types::notification::Initialized::METHOD.to_string(), + params: jsonrpc::Params::None, + })); + match transport.process_server_message(&client_tx, notification).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + + // drain the pending queue and send payloads to server + for msg in pending_messages.drain(..) { + log::info!("Draining pending message {:?}", msg); + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } + msg = client_rx.recv() => { + if let Some(msg) = msg { + if is_pending && !is_initialize(&msg) { + // ignore notifications + if let Payload::Notification(_) = msg { + continue; + } + + log::info!("Language server not initialized, delaying request"); + pending_messages.push(msg); + } else { + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } else { + // channel closed + break; + } } } } diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml index 73eda472..9c2b8275 100644 --- a/helix-syntax/Cargo.toml +++ b/helix-syntax/Cargo.toml @@ -11,7 +11,7 @@ homepage = "https://helix-editor.com" include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"] [dependencies] -tree-sitter = "0.19" +tree-sitter = "0.20" libloading = "0.7" anyhow = "1" diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs index 473646fd..28f85e74 100644 --- a/helix-syntax/build.rs +++ b/helix-syntax/build.rs @@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) { .is_none() { eprintln!( - "The directory {} is empty, did you use 'git clone --recursive'?", + "The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?", dir ); - eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'."); std::process::exit(1); } diff --git a/helix-syntax/languages/tree-sitter-julia b/helix-syntax/languages/tree-sitter-julia -Subproject 0ba7a24b062b671263ae08e707e9e94383b25bb +Subproject 12ea597262125fc22fd2e91aa953ac69b19c26c diff --git a/helix-syntax/languages/tree-sitter-ledger b/helix-syntax/languages/tree-sitter-ledger -Subproject 72319504776f14193472a6ad14abec0af0225cb +Subproject 0cdeb0e51411a3ba5493662952c3039de08939c diff --git a/helix-syntax/languages/tree-sitter-svelte b/helix-syntax/languages/tree-sitter-svelte new file mode 160000 +Subproject 349a5984513b4a4a9e143a6e746120c6ff6cf6e diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 6e9c0daf..68ff260d 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -56,5 +56,9 @@ toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +# ripgrep for global search +grep-regex = "0.1.9" +grep-searcher = "0.1.8" + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 7a573adc..b99fccdf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -434,16 +434,42 @@ impl Application { }; match notification { - Notification::PublishDiagnostics(params) => { - let path = Some(params.uri.to_file_path().unwrap()); + Notification::Initialized => { + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; - let doc = self - .editor - .documents - .iter_mut() - .find(|(_, doc)| doc.path() == path.as_ref()); + let docs = self.editor.documents().filter(|doc| { + doc.language_server().map(|server| server.id()) == Some(server_id) + }); - if let Some((_, doc)) = doc { + // trigger textDocument/didOpen for docs that are already open + for doc in docs { + // TODO: extract and share with editor.open + let language_id = doc + .language() + .and_then(|s| s.split('.').last()) // source.rust + .map(ToOwned::to_owned) + .unwrap_or_default(); + + tokio::spawn(language_server.text_document_did_open( + doc.url().unwrap(), + doc.version(), + doc.text(), + language_id, + )); + } + } + Notification::PublishDiagnostics(params) => { + let path = params.uri.to_file_path().unwrap(); + let doc = self.editor.document_by_path_mut(&path); + + if let Some(doc) = doc { let text = doc.text(); let diagnostics = params @@ -506,7 +532,7 @@ impl Application { log::warn!("unhandled window/showMessage: {:?}", params); } Notification::LogMessage(params) => { - log::warn!("unhandled window/logMessage: {:?}", params); + log::info!("window/logMessage: {:?}", params); } Notification::ProgressMessage(params) => { let lsp::ProgressParams { token, value } = params; @@ -588,10 +614,27 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { + let language_server = match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { error!("Method not found {}", method); + // language_server.reply( + // call.id, + // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) + // Err(helix_lsp::jsonrpc::Error { + // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, + // message: "Method not found".to_string(), + // data: None, + // }), + // ); return; } }; @@ -604,53 +647,9 @@ impl Application { if spinner.is_stopped() { spinner.start(); } - - let doc = self.editor.documents().find(|doc| { - doc.language_server() - .map(|server| server.id() == server_id) - .unwrap_or_default() - }); - match doc { - Some(doc) => { - // it's ok to unwrap, we check for the language server before - let server = doc.language_server().unwrap(); - tokio::spawn(server.reply(id, Ok(serde_json::Value::Null))); - } - None => { - if let Some(server) = - self.editor.language_servers.get_by_id(server_id) - { - log::warn!( - "missing document with language server id `{}`", - server_id - ); - tokio::spawn(server.reply( - id, - Err(helix_lsp::jsonrpc::Error { - code: helix_lsp::jsonrpc::ErrorCode::InternalError, - message: "document missing".to_string(), - data: None, - }), - )); - } else { - log::warn!( - "can't find language server with id `{}`", - server_id - ); - } - } - } + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } } - // self.language_server.reply( - // call.id, - // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) - // Err(helix_lsp::jsonrpc::Error { - // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, - // message: "Method not found".to_string(), - // data: None, - // }), - // ); } e => unreachable!("{:?}", e), } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a4dd080b..acaba6d6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -9,7 +9,7 @@ use helix_core::{ match_brackets, movement::{self, Direction}, object, pos_at_coords, - regex::{self, Regex}, + regex::{self, Regex, RegexBuilder}, register::Register, search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -35,7 +35,7 @@ use crate::{ }; use crate::job::{self, Job, Jobs}; -use futures_util::FutureExt; +use futures_util::{FutureExt, StreamExt}; use std::num::NonZeroUsize; use std::{collections::HashMap, fmt, future::Future}; @@ -47,8 +47,13 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; +use grep_regex::RegexMatcherBuilder; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use tokio_stream::wrappers::UnboundedReceiverStream; + pub struct Context<'a> { - pub selected_register: helix_view::RegisterSelection, + pub register: Option<char>, pub count: Option<NonZeroUsize>, pub editor: &'a mut Editor, @@ -166,6 +171,7 @@ impl Command { #[rustfmt::skip] commands!( + no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", move_line_up, "Move up", @@ -184,6 +190,9 @@ impl Command { move_next_long_word_end, "Move to end of next long word", extend_next_word_start, "Extend to beginning of next word", extend_prev_word_start, "Extend to beginning of previous word", + extend_next_long_word_start, "Extend to beginning of next long word", + extend_prev_long_word_start, "Extend to beginning of previous long word", + extend_next_long_word_end, "Extend to end of next long word", extend_next_word_end, "Extend to end of next word", find_till_char, "Move till next occurance of char", find_next_char, "Move to next occurance of char", @@ -209,6 +218,7 @@ impl Command { search_next, "Select next search match", extend_search_next, "Add next search match to selection", search_selection, "Use current selection as search pattern", + global_search, "Global Search in workspace folder", extend_line, "Select current line, if already selected, extend to next line", extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)", delete_selection, "Delete selection", @@ -253,6 +263,9 @@ impl Command { // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", + extend_to_line_start, "Extend to line start", + extend_to_line_end, "Extend to line end", + extend_to_line_end_newline, "Extend to line end", signature_help, "Show signature help", insert_tab, "Insert tab char", insert_newline, "Insert newline char", @@ -281,6 +294,7 @@ impl Command { join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", keep_primary_selection, "Keep primary selection", + remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", hover, "Show docs for item under cursor", toggle_comments, "Comment/uncomment selections", @@ -372,52 +386,58 @@ impl PartialEq for Command { } } -fn move_char_left(cx: &mut Context) { +fn no_op(_cx: &mut Context) {} + +fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) +where + F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move) - }); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| move_fn(text, range, dir, count, behaviour)); doc.set_selection(view.id, selection); } -fn move_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +use helix_core::movement::{move_horizontally, move_vertically}; - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Move) } -fn move_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Move) } fn move_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + move_impl(cx, move_vertically, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn extend_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend) } -fn goto_line_end(cx: &mut Context) { - let (view, doc) = current!(cx.editor); +fn extend_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend) +} + +fn extend_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Extend) +} + +fn extend_line_down(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) +} + +fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -427,26 +447,60 @@ fn goto_line_end(cx: &mut Context) { let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line)) .max(line_start); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_end_newline(cx: &mut Context) { +fn goto_line_end(cx: &mut Context) { let (view, doc) = current!(cx.editor); + goto_line_end_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) +} + +fn extend_to_line_end(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + goto_line_end_impl(view, doc, Movement::Extend) +} + +fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text); let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_start(cx: &mut Context) { +fn goto_line_end_newline(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) +} + +fn extend_to_line_end_newline(cx: &mut Context) { let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl(view, doc, Movement::Extend) +} + +fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -454,11 +508,29 @@ fn goto_line_start(cx: &mut Context) { // adjust to start of the line let pos = text.line_to_char(line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } +fn goto_line_start(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + goto_line_start_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) +} + +fn extend_to_line_start(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + goto_line_start_impl(view, doc, Movement::Extend) +} + fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -512,7 +584,10 @@ fn goto_window_bottom(cx: &mut Context) { goto_window(cx, Align::Bottom) } -fn move_next_word_start(cx: &mut Context) { +fn move_word_impl<F>(cx: &mut Context, move_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -520,68 +595,32 @@ fn move_next_word_start(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| movement::move_next_word_start(text, range, count)); + .transform(|range| move_fn(text, range, count)); doc.set_selection(view.id, selection); } -fn move_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_next_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_next_word_start) +} - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_word_start(text, range, count)); - doc.set_selection(view.id, selection); +fn move_prev_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_word_start) } fn move_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_word_end) } fn move_next_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_start) } fn move_prev_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_prev_long_word_start) } fn move_next_long_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_end) } fn goto_file_start(cx: &mut Context) { @@ -600,43 +639,44 @@ fn goto_file_end(cx: &mut Context) { doc.set_selection(view.id, Selection::point(doc.text().len_chars())); } -fn extend_next_word_start(cx: &mut Context) { +fn extend_word_impl<F>(cx: &mut Context, extend_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_start(text, range, count); + let word = extend_fn(text, range, count); let pos = word.cursor(text); range.put_cursor(text, pos, true) }); doc.set_selection(view.id, selection); } -fn extend_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn extend_next_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_next_word_start) +} - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_prev_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); +fn extend_prev_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_word_start) } fn extend_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + extend_word_impl(cx, movement::move_next_word_end) +} - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_end(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); +fn extend_next_long_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_next_long_word_start) +} + +fn extend_prev_long_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_long_word_start) +} + +fn extend_next_long_word_end(cx: &mut Context) { + extend_word_impl(cx, movement::move_next_long_word_end) } #[inline] @@ -852,12 +892,25 @@ fn replace(cx: &mut Context) { }) } -fn switch_case(cx: &mut Context) { +fn switch_case_impl<F>(cx: &mut Context, change_fn: F) +where + F: Fn(Cow<str>) -> Tendril, +{ let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range - .fragment(doc.text().slice(..)) + let text: Tendril = change_fn(range.fragment(doc.text().slice(..))); + + (range.from(), range.to(), Some(text)) + }); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); +} + +fn switch_case(cx: &mut Context) { + switch_case_impl(cx, |string| { + string .chars() .flat_map(|ch| { if ch.is_lowercase() { @@ -868,39 +921,16 @@ fn switch_case(cx: &mut Context) { vec![ch] } }) - .collect(); - - (range.from(), range.to(), Some(text)) + .collect() }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn switch_to_uppercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_uppercase().into()); } fn switch_to_lowercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_lowercase().into()); } pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { @@ -976,28 +1006,6 @@ fn half_page_down(cx: &mut Context) { scroll(cx, offset, Direction::Forward); } -fn extend_char_left(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn copy_selection_on_line(cx: &mut Context, direction: Direction) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -1068,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) { copy_selection_on_line(cx, Direction::Forward) } -fn extend_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn select_all(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -1098,23 +1084,42 @@ fn select_all(cx: &mut Context) { } fn select_regex(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| { - let text = doc.text().slice(..); - if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) - { - doc.set_selection(view.id, selection); - } - }); + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt( + cx, + "select:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + if let Some(selection) = + selection::select_on_matches(text, doc.selection(view.id), ®ex) + { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } fn split_selection(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| { - let text = doc.text().slice(..); - let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); - doc.set_selection(view.id, selection); - }); + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt( + cx, + "split:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); + doc.set_selection(view.id, selection); + }, + ); cx.push_layer(Box::new(prompt)); } @@ -1168,6 +1173,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege // TODO: use one function for search vs extend fn search(cx: &mut Context) { + let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1176,23 +1182,44 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| { - search_impl(doc, view, &contents, ®ex, false); - // TODO: only store on enter (accept), not update - registers.write('/', vec![regex.as_str().to_string()]); - }); + let prompt = ui::regex_prompt( + cx, + "search:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + search_impl(doc, view, &contents, ®ex, false); + }, + ); cx.push_layer(Box::new(prompt)); } fn search_next_impl(cx: &mut Context, extend: bool) { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; + let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { - let query = query.first().unwrap(); + let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - let regex = Regex::new(query).unwrap(); - search_impl(doc, view, &contents, ®ex, extend); + let case_insensitive = if cx.editor.config.smart_case { + !query.chars().any(char::is_uppercase) + } else { + false + }; + if let Ok(regex) = RegexBuilder::new(query) + .case_insensitive(case_insensitive) + .build() + { + search_impl(doc, view, &contents, ®ex, extend); + } else { + // get around warning `mutable_borrow_reservation_conflict` + // which will be a hard error in the future + // see: https://github.com/rust-lang/rust/issues/59159 + let query = query.clone(); + cx.editor.set_error(format!("Invalid regex: {}", query)); + } } } @@ -1209,8 +1236,119 @@ fn search_selection(cx: &mut Context) { let contents = doc.text().slice(..); let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); - cx.editor.registers.write('/', vec![regex]); - search_next(cx); + cx.editor.registers.get_mut('/').push(regex); + let msg = format!("register '{}' set to '{}'", '\\', query); + cx.editor.set_status(msg); +} + +fn global_search(cx: &mut Context) { + let (all_matches_sx, all_matches_rx) = + tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); + let smart_case = cx.editor.config.smart_case; + let prompt = ui::regex_prompt( + cx, + "global search:".into(), + None, + move |_view, _doc, regex, event| { + if event != PromptEvent::Validate { + return; + } + + if let Ok(matcher) = RegexMatcherBuilder::new() + .case_smart(smart_case) + .build(regex.as_str()) + { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + + let search_root = std::env::current_dir() + .expect("Global search error: Failed to get current dir"); + WalkBuilder::new(search_root).build_parallel().run(|| { + let mut searcher_cl = searcher.clone(); + let matcher_cl = matcher.clone(); + let all_matches_sx_cl = all_matches_sx.clone(); + Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState { + let dent = match dent { + Ok(dent) => dent, + Err(_) => return WalkState::Continue, + }; + + match dent.file_type() { + Some(fi) => { + if !fi.is_file() { + return WalkState::Continue; + } + } + None => return WalkState::Continue, + } + + let result_sink = sinks::UTF8(|line_num, _| { + match all_matches_sx_cl + .send((line_num as usize - 1, dent.path().to_path_buf())) + { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + }); + let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink); + + if let Err(err) = result { + log::error!("Global search error: {}, {}", dent.path().display(), err); + } + WalkState::Continue + }) + }); + } else { + // Otherwise do nothing + // log::warn!("Global Search Invalid Pattern") + } + }, + ); + + cx.push_layer(Box::new(prompt)); + + let show_picker = async move { + let all_matches: Vec<(usize, PathBuf)> = + UnboundedReceiverStream::new(all_matches_rx).collect().await; + let call: job::Callback = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + if all_matches.is_empty() { + editor.set_status("No matches found".to_string()); + return; + } + let picker = FilePicker::new( + all_matches, + move |(_line_num, path)| path.to_str().unwrap().into(), + move |editor: &mut Editor, (line_num, path), action| { + match editor.open(path.into(), action) { + Ok(_) => {} + Err(e) => { + editor.set_error(format!( + "Failed to open file '{}': {}", + path.display(), + e + )); + return; + } + } + + let line_num = *line_num; + let (view, doc) = current!(editor); + let text = doc.text(); + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); + + doc.set_selection(view.id, Selection::single(start, end)); + align_view(doc, view, Align::Center); + }, + |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))), + ); + compositor.push(Box::new(picker)); + }); + Ok(call) + }; + cx.jobs.callback(show_picker); } fn extend_line(cx: &mut Context) { @@ -1268,7 +1406,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId } fn delete_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -1281,7 +1419,7 @@ fn delete_selection(cx: &mut Context) { } fn change_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -1403,8 +1541,11 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { + use helix_core::path::expand_tilde; let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + let _ = cx + .editor + .open(expand_tilde(Path::new(path)), Action::Replace)?; Ok(()) } @@ -1604,7 +1745,7 @@ mod cmd { /// Results an error if there are modified buffers remaining and sets editor error, /// otherwise returns `Ok(())` - fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { + pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { let modified: Vec<_> = editor .documents() .filter(|doc| doc.is_modified()) @@ -2577,7 +2718,7 @@ fn apply_workspace_edit( ) { if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); - editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); return; // 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 // TODO: find some example that uses workspace changes, and test it @@ -2595,8 +2736,30 @@ fn apply_workspace_edit( match document_changes { lsp::DocumentChanges::Edits(document_edits) => { for document_edit in document_edits { - let (view, doc) = current!(editor); - assert_eq!(doc.url().unwrap(), document_edit.text_document.uri); + let path = document_edit + .text_document + .uri + .to_file_path() + .expect("unable to convert URI to filepath"); + let current_view_id = view!(editor).id; + let doc = editor + .document_by_path_mut(path) + .expect("Document for document_changes not found"); + + // Need to determine a view for apply/append_changes_to_history + let selections = doc.selections(); + let view_id = if selections.contains_key(¤t_view_id) { + // use current if possible + current_view_id + } else { + // Hack: we take the first available view_id + selections + .keys() + .next() + .copied() + .expect("No view_id available") + }; + let edits = document_edit .edits .iter() @@ -2614,8 +2777,8 @@ fn apply_workspace_edit( edits, offset_encoding, ); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + doc.apply(&transaction, view_id); + doc.append_changes_to_history(view_id); } } lsp::DocumentChanges::Operations(operations) => { @@ -2779,6 +2942,10 @@ fn open_above(cx: &mut Context) { fn normal_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); + if doc.mode == Mode::Normal { + return; + } + doc.mode = Mode::Normal; doc.append_changes_to_history(view.id); @@ -3325,17 +3492,20 @@ pub mod insert { } use helix_core::auto_pairs; - const HOOKS: &[Hook] = &[auto_pairs::hook, insert]; - const POST_HOOKS: &[PostHook] = &[completion, signature_help]; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current!(cx.editor); + let hooks: &[Hook] = match cx.editor.config.auto_pairs { + true => &[auto_pairs::hook, insert], + false => &[insert], + }; + let text = doc.text(); let selection = doc.selection(view.id).clone().cursors(text.slice(..)); // run through insert hooks, stopping on the first one that returns Some(t) - for hook in HOOKS { + for hook in hooks { if let Some(transaction) = hook(text, &selection, c) { doc.apply(&transaction, view.id); break; @@ -3345,7 +3515,7 @@ pub mod insert { // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // this could also generically look at Transaction, but it's a bit annoying to look at // Operation instead of Change. - for hook in POST_HOOKS { + for hook in &[completion, signature_help] { hook(cx, c); } } @@ -3510,12 +3680,12 @@ fn yank(cx: &mut Context) { let msg = format!( "yanked {} selection(s) to register {}", values.len(), - cx.selected_register.name() + cx.register.unwrap_or('"') ); cx.editor .registers - .write(cx.selected_register.name(), values); + .write(cx.register.unwrap_or('"'), values); cx.editor.set_status(msg); exit_select_mode(cx); @@ -3624,7 +3794,14 @@ fn paste_impl( .iter() .any(|value| get_line_ending_of_str(value).is_some()); - let mut values = values.iter().cloned().map(Tendril::from).chain(repeat); + // Only compiled once. + #[allow(clippy::trivial_regex)] + static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); + let mut values = values + .iter() + .map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) + .map(|value| Tendril::from(value.as_ref())) + .chain(repeat); let text = doc.text(); let selection = doc.selection(view.id); @@ -3688,7 +3865,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) { } fn replace_with_yanked(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3739,7 +3916,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) { } fn paste_after(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3753,7 +3930,7 @@ fn paste_after(cx: &mut Context) { } fn paste_before(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3934,24 +4111,49 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex - let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| { - let text = doc.text().slice(..); + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt( + cx, + "keep:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); - if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { - doc.set_selection(view.id, selection); - } - }); + if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); + // TODO: handle count let range = doc.selection(view.id).primary(); doc.set_selection(view.id, Selection::single(range.anchor, range.head)); } +fn remove_primary_selection(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + // TODO: handle count + + let selection = doc.selection(view.id); + if selection.len() == 1 { + cx.editor.set_error("no selections remaining".to_owned()); + return; + } + let index = selection.primary_index(); + let selection = selection.clone().remove(index); + + doc.set_selection(view.id, selection); +} + fn completion(cx: &mut Context) { // trigger on trigger char, or if user calls it // (or on word char typing??) @@ -4259,6 +4461,12 @@ fn vsplit(cx: &mut Context) { } fn wclose(cx: &mut Context) { + if cx.editor.tree.views().count() == 1 { + if let Err(err) = cmd::buffers_remaining_impl(cx.editor) { + cx.editor.set_error(err.to_string()); + return; + } + } let view_id = view!(cx.editor).id; // close current split cx.editor.close(view_id, /* close_buffer */ false); @@ -4267,7 +4475,7 @@ fn wclose(cx: &mut Context) { fn select_register(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - cx.editor.selected_register.select(ch); + cx.editor.selected_register = Some(ch); } }) } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 2ac41926..4fa38174 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -61,7 +61,7 @@ impl Jobs { } pub fn handle_callback( - &mut self, + &self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result<Option<Callback>>, @@ -84,7 +84,7 @@ impl Jobs { } } - pub fn add(&mut self, j: Job) { + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a0f2be94..77bb187c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -4,6 +4,7 @@ use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ + borrow::Cow, collections::HashMap, ops::{Deref, DerefMut}, }; @@ -47,13 +48,13 @@ macro_rules! keymap { }; (@trie - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { - keymap!({ $label $($($key)|+ => $value,)+ }) + keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) }; ( - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { // modified from the hashmap! macro { @@ -70,7 +71,9 @@ macro_rules! keymap { _order.push(_key); )+ )* - $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order)) + let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + $( _node.is_sticky = $sticky; )? + $crate::keymap::KeyTrie::Node(_node) } }; } @@ -84,6 +87,8 @@ pub struct KeyTrieNode { map: HashMap<KeyEvent, KeyTrie>, #[serde(skip)] order: Vec<KeyEvent>, + #[serde(skip)] + pub is_sticky: bool, } impl KeyTrieNode { @@ -92,6 +97,7 @@ impl KeyTrieNode { name: name.to_string(), map, order, + is_sticky: false, } } @@ -119,12 +125,10 @@ impl KeyTrieNode { } } } -} -impl From<KeyTrieNode> for Info { - fn from(node: KeyTrieNode) -> Self { - let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len()); - for (&key, trie) in node.iter() { + pub fn infobox(&self) -> Info { + let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len()); + for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => cmd.doc(), KeyTrie::Node(n) => n.name(), @@ -136,16 +140,16 @@ impl From<KeyTrieNode> for Info { } } body.sort_unstable_by_key(|(_, keys)| { - node.order.iter().position(|&k| k == keys[0]).unwrap() + self.order.iter().position(|&k| k == keys[0]).unwrap() }); - let prefix = format!("{} ", node.name()); + let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body .into_iter() .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .collect(); } - Info::new(node.name(), body) + Info::new(self.name(), body) } } @@ -218,7 +222,7 @@ impl KeyTrie { } #[derive(Debug, Clone, PartialEq)] -pub enum KeymapResult { +pub enum KeymapResultKind { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), Matched(Command), @@ -229,14 +233,31 @@ pub enum KeymapResult { Cancelled(Vec<KeyEvent>), } +/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a +/// reference to the sticky node if one is currently active. +pub struct KeymapResult<'a> { + pub kind: KeymapResultKind, + pub sticky: Option<&'a KeyTrieNode>, +} + +impl<'a> KeymapResult<'a> { + pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self { + Self { kind, sticky } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Keymap { /// Always a Node #[serde(flatten)] root: KeyTrie, - /// Stores pending keys waiting for the next key + /// Stores pending keys waiting for the next key. This is relative to a + /// sticky node if one is in use. #[serde(skip)] state: Vec<KeyEvent>, + /// Stores the sticky node if one is activated. + #[serde(skip)] + sticky: Option<KeyTrieNode>, } impl Keymap { @@ -244,6 +265,7 @@ impl Keymap { Keymap { root, state: Vec::new(), + sticky: None, } } @@ -251,27 +273,61 @@ impl Keymap { &self.root } + pub fn sticky(&self) -> Option<&KeyTrieNode> { + self.sticky.as_ref() + } + /// Returns list of keys waiting to be disambiguated. pub fn pending(&self) -> &[KeyEvent] { &self.state } - /// Lookup `key` in the keymap to try and find a command to execute + /// Lookup `key` in the keymap to try and find a command to execute. Escape + /// key cancels pending keystrokes. If there are no pending keystrokes but a + /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { - let &first = self.state.get(0).unwrap_or(&key); - let trie = match self.root.search(&[first]) { - Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), - None => return KeymapResult::NotFound, + if let key!(Esc) = key { + if !self.state.is_empty() { + return KeymapResult::new( + // Note that Esc is not included here + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ); + } + self.sticky = None; + } + + let first = self.state.get(0).unwrap_or(&key); + let trie_node = match self.sticky { + Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), + None => Cow::Borrowed(&self.root), + }; + + let trie = match trie_node.search(&[*first]) { + Some(&KeyTrie::Leaf(cmd)) => { + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) + } + None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), Some(t) => t, }; + self.state.push(key); match trie.search(&self.state[1..]) { - Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), - Some(&KeyTrie::Leaf(command)) => { + Some(&KeyTrie::Node(ref map)) => { + if map.is_sticky { + self.state.clear(); + self.sticky = Some(map.clone()); + } + KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) + } + Some(&KeyTrie::Leaf(cmd)) => { self.state.clear(); - KeymapResult::Matched(command) + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), + None => KeymapResult::new( + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ), } } @@ -380,7 +436,6 @@ impl Default for Keymaps { "A" => append_to_line, "o" => open_below, "O" => open_above, - // [<space> ]<space> equivalents too (add blank new line, no edit) "d" => delete_selection, // TODO: also delete without yanking @@ -440,12 +495,11 @@ impl Default for Keymaps { "<" => unindent, "=" => format_selections, "J" => join_selections, - // TODO: conflicts hover/doc "K" => keep_selections, // TODO: and another method for inverse - // TODO: clashes with space mode - "space" => keep_primary_selection, + "," => keep_primary_selection, + "A-," => remove_primary_selection, // "q" => record_macro, // "Q" => replay_macro, @@ -473,7 +527,6 @@ impl Default for Keymaps { // move under <space>c "C-c" => toggle_comments, - "K" => hover, // z family for save/restore/combine from/to sels from register @@ -516,7 +569,8 @@ impl Default for Keymaps { "p" => paste_clipboard_after, "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, - "space" => keep_primary_selection, + "/" => global_search, + "k" => hover, }, "z" => { "View" "z" | "c" => align_view_center, @@ -525,6 +579,22 @@ impl Default for Keymaps { "m" => align_view_middle, "k" => scroll_up, "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, + }, + "Z" => { "View" sticky=true + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" => scroll_up, + "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, }, "\"" => select_register, @@ -545,14 +615,17 @@ impl Default for Keymaps { "w" => extend_next_word_start, "b" => extend_prev_word_start, "e" => extend_next_word_end, + "W" => extend_next_long_word_start, + "B" => extend_prev_long_word_start, + "E" => extend_next_long_word_end, "t" => extend_till_char, "f" => extend_next_char, "T" => extend_till_prev_char, "F" => extend_prev_char, - "home" => goto_line_start, - "end" => goto_line_end, + "home" => extend_to_line_start, + "end" => extend_to_line_end, "esc" => exit_select_mode, "v" => normal_mode, @@ -617,19 +690,19 @@ fn merge_partial_keys() { let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( - keymap.get(key!('i')), - KeymapResult::Matched(Command::normal_mode), + keymap.get(key!('i')).kind, + KeymapResultKind::Matched(Command::normal_mode), "Leaf should replace leaf" ); assert_eq!( - keymap.get(key!('无')), - KeymapResult::Matched(Command::insert_mode), + keymap.get(key!('无')).kind, + KeymapResultKind::Matched(Command::insert_mode), "New leaf should be present in merged keymap" ); // Assumes that z is a node in the default keymap assert_eq!( - keymap.get(key!('z')), - KeymapResult::Matched(Command::jump_backward), + keymap.get(key!('z')).kind, + KeymapResultKind::Matched(Command::jump_backward), "Leaf should replace node" ); // Assumes that `g` is a node in default keymap diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 90657764..6c9e3a80 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -262,8 +262,7 @@ impl Component for Completion { .cursor(doc.text().slice(..)); let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - view.offset.row) as u16; - - let mut doc = match &option.documentation { + let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::PlainText, @@ -311,24 +310,42 @@ impl Component for Completion { None => return, }; - let half = area.height / 2; - let height = 15.min(half); - // we want to make sure the cursor is visible (not hidden behind the documentation) - let y = if cursor_pos + area.y - >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) - { - 0 + let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); + let (popup_width, _popup_height) = self.popup.get_size(); + let mut width = area + .width + .saturating_sub(popup_x) + .saturating_sub(popup_width); + let area = if width > 30 { + let mut height = area.height.saturating_sub(popup_y); + let x = popup_x + popup_width; + let y = popup_y; + + if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { + width = rel_width; + height = rel_height; + } + Rect::new(x, y, width, height) } else { - // -2 to subtract command line + statusline. a bit of a hack, because of splits. - area.height.saturating_sub(height).saturating_sub(2) - }; + let half = area.height / 2; + let height = 15.min(half); + // we want to make sure the cursor is visible (not hidden behind the documentation) + let y = if cursor_pos + area.y + >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) + { + 0 + } else { + // -2 to subtract command line + statusline. a bit of a hack, because of splits. + area.height.saturating_sub(height).saturating_sub(2) + }; - let area = Rect::new(0, y, area.width, height); + Rect::new(0, y, area.width, height) + }; // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - doc.render(area, surface, cx); + markdown_doc.render(area, surface, cx); } } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 63694d0b..128fe948 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Compositor, Context, EventResult}, job::Callback, key, - keymap::{KeymapResult, Keymaps}, + keymap::{KeymapResult, KeymapResultKind, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -165,8 +165,7 @@ impl EditorView { let scopes = theme.scopes(); syntax .highlight_iter(text.slice(..), Some(range), None, |language| { - loader - .language_config_for_scope(&format!("source.{}", language)) + loader.language_configuration_for_injection_string(language) .and_then(|language_config| { let config = language_config.highlight_config(scopes)?; let config_ref = config.as_ref(); @@ -852,7 +851,7 @@ impl EditorView { /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was - /// activated). Only KeymapResult::{NotFound, Cancelled} is returned + /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned /// otherwise. fn handle_keymap_event( &mut self, @@ -860,8 +859,6 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option<KeymapResult> { - self.autoinfo = None; - if let Some(picker) = cxt.editor.debug_config_picker.clone() { match event { KeyEvent { @@ -912,29 +909,32 @@ impl EditorView { return None; } - match self.keymaps.get_mut(&mode).unwrap().get(event) { - KeymapResult::Matched(command) => command.execute(cxt), - KeymapResult::Pending(node) => self.autoinfo = Some(node.into()), - k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), + let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); + self.autoinfo = key_result.sticky.map(|node| node.infobox()); + + match &key_result.kind { + KeymapResultKind::Matched(command) => command.execute(cxt), + KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), } None } fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { - match keyresult { - KeymapResult::NotFound => { + match keyresult.kind { + KeymapResultKind::NotFound => { if let Some(ch) = event.char() { commands::insert::insert_char(cx, ch) } } - KeymapResult::Cancelled(pending) => { + KeymapResultKind::Cancelled(pending) => { for ev in pending { match ev.char() { Some(ch) => commands::insert::insert_char(cx, ch), None => { - if let KeymapResult::Matched(command) = - self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) + if let KeymapResultKind::Matched(command) = + self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind { command.execute(cx); } @@ -972,7 +972,7 @@ impl EditorView { // debug_assert!(cxt.count != 0); // set the register - cxt.selected_register = cxt.editor.selected_register.take(); + cxt.register = cxt.editor.selected_register.take(); self.handle_keymap_event(mode, cxt, event); if self.keymaps.pending().is_empty() { @@ -1196,9 +1196,9 @@ impl EditorView { impl Component for EditorView { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let mut cxt = commands::Context { - selected_register: helix_view::RegisterSelection::default(), editor: &mut cx.editor, count: None, + register: None, callback: None, on_next_key_callback: None, jobs: cx.jobs, @@ -1288,11 +1288,12 @@ impl Component for EditorView { // how we entered insert mode is important, and we should track that so // we can repeat the side effect. - self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { - KeymapResult::Matched(command) => command, - // FIXME: insert mode can only be entered through single KeyCodes - _ => unimplemented!(), - }; + self.last_insert.0 = + match self.keymaps.get_mut(&mode).unwrap().get(key).kind { + KeymapResultKind::Matched(command) => command, + // FIXME: insert mode can only be entered through single KeyCodes + _ => unimplemented!(), + }; self.last_insert.1.clear(); } (Mode::Insert, Mode::Normal) => { diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 28542cdc..4144ed3c 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -88,7 +88,7 @@ fn parse<'a>( if let Some(theme) = theme { let rope = Rope::from(text.as_ref()); let syntax = loader - .language_config_for_scope(&format!("source.{}", language)) + .language_configuration_for_injection_string(language) .and_then(|config| config.highlight_config(theme.scopes())) .map(|config| Syntax::new(&rope, config)); @@ -215,10 +215,30 @@ impl Component for Markdown { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = parse(&self.contents, None, &self.config_loader); let padding = 2; - let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); - let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); - Some((width, height)) + if padding >= viewport.1 || padding >= viewport.0 { + return None; + } + let contents = parse(&self.contents, None, &self.config_loader); + let max_text_width = (viewport.0 - padding).min(120); + let mut text_width = 0; + let mut height = padding; + for content in contents { + height += 1; + let content_width = content.width() as u16; + if content_width > max_text_width { + text_width = max_text_width; + height += content_width / max_text_width; + } else if content_width > text_width { + text_width = content_width; + } + + if height >= viewport.1 { + height = viewport.1; + break; + } + } + + Some((text_width + padding, height)) } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 24dd3e61..dab0c34f 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -33,6 +33,8 @@ pub struct Menu<T: Item> { scroll: usize, size: (u16, u16), + viewport: (u16, u16), + recalculate: bool, } impl<T: Item> Menu<T> { @@ -51,6 +53,8 @@ impl<T: Item> Menu<T> { callback_fn: Box::new(callback_fn), scroll: 0, size: (0, 0), + viewport: (0, 0), + recalculate: true, }; // TODO: scoring on empty input should just use a fastpath @@ -83,6 +87,7 @@ impl<T: Item> Menu<T> { // reset cursor position self.cursor = None; self.scroll = 0; + self.recalculate = true; } pub fn move_up(&mut self) { @@ -99,6 +104,41 @@ impl<T: Item> Menu<T> { self.adjust_scroll(); } + fn recalculate_size(&mut self, viewport: (u16, u16)) { + let n = self + .options + .first() + .map(|option| option.row().cells.len()) + .unwrap_or_default(); + let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.row(); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + + acc + }); + let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar + let width = len.min(viewport.0 as usize); + + self.widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + + let height = self.matches.len().min(10).min(viewport.1 as usize); + + self.size = (width as u16, height as u16); + + // adjust scroll offsets if size changed + self.adjust_scroll(); + self.recalculate = false; + } + fn adjust_scroll(&mut self) { let win_height = self.size.1 as usize; if let Some(cursor) = self.cursor { @@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let n = self - .options - .first() - .map(|option| option.row().cells.len()) - .unwrap_or_default(); - let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.row(); - // maintain max for each column - for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { - let width = cell.content.width(); - if width > *acc { - *acc = width; - } - } - - acc - }); - let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar - let width = len.min(viewport.0 as usize); - - self.widths = max_lens - .into_iter() - .map(|len| Constraint::Length(len as u16)) - .collect(); - - let height = self.options.len().min(10).min(viewport.1 as usize); - - self.size = (width as u16, height as u16); - - // adjust scroll offsets if size changed - self.adjust_scroll(); + if viewport != self.viewport || self.recalculate { + self.recalculate_size(viewport); + } Some(self.size) } - // TODO: required size should re-trigger when we filter items so we can draw a smaller menu - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let theme = &cx.editor.theme; let style = theme diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 37148ae2..e66673ca 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_core::regex::Regex; -use helix_core::register::Registers; +use helix_core::regex::RegexBuilder; use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -28,7 +28,8 @@ use std::path::PathBuf; pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, - fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static, + history_register: Option<char>, + fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); let view_id = view.id; @@ -36,7 +37,7 @@ pub fn regex_prompt( Prompt::new( prompt, - None, + history_register, |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { @@ -46,6 +47,14 @@ pub fn regex_prompt( } PromptEvent::Validate => { // TODO: push_jump to store selection just before jump + + match Regex::new(input) { + Ok(regex) => { + let (view, doc) = current!(cx.editor); + fun(view, doc, regex, event); + } + Err(_err) => (), // TODO: mark command line as error + } } PromptEvent::Update => { // skip empty input, TODO: trigger default @@ -53,15 +62,23 @@ pub fn regex_prompt( return; } - match Regex::new(input) { + let case_insensitive = if cx.editor.config.smart_case { + !input.chars().any(char::is_uppercase) + } else { + false + }; + + match RegexBuilder::new(input) + .case_insensitive(case_insensitive) + .build() + { Ok(regex) => { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; // revert state to what it was before the last update doc.set_selection(view.id, snapshot.clone()); - fun(view, doc, registers, regex); + fun(view, doc, regex, event); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 84b8dd72..ee1ec177 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> { }) { // align to middle let first_line = line - .map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines()))) - .map(|(start, _)| start) - .unwrap_or(0) - .saturating_sub(inner.height as usize / 2); + .map(|(start, end)| { + let height = end.saturating_sub(start) + 1; + let middle = start + (height.saturating_sub(1) / 2); + middle.saturating_sub(inner.height as usize / 2).min(start) + }) + .unwrap_or(0); + let offset = Position::new(first_line, 0); let highlights = EditorView::doc_syntax_highlights( @@ -268,17 +271,15 @@ impl<T> Picker<T> { } pub fn move_up(&mut self) { - self.cursor = self.cursor.saturating_sub(1); + let len = self.matches.len(); + let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; + self.cursor = pos; } pub fn move_down(&mut self) { - if self.matches.is_empty() { - return; - } - - if self.cursor < self.matches.len() - 1 { - self.cursor += 1; - } + let len = self.matches.len(); + let pos = (self.cursor + 1) % len; + self.cursor = pos; } pub fn selection(&self) -> Option<&T> { diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index e126c845..1bab1eae 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -16,8 +16,6 @@ pub struct Popup<T: Component> { } impl<T: Component> Popup<T> { - // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different - // rendering) pub fn new(contents: T) -> Self { Self { contents, @@ -31,6 +29,39 @@ impl<T: Component> Popup<T> { self.position = pos; } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { + let position = self + .position + .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); + + let (width, height) = self.size; + + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above + + // -- make sure frame doesn't stick out of bounds + let mut rel_x = position.col as u16; + let mut rel_y = position.row as u16; + if viewport.width <= rel_x + width { + rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); + } + + // TODO: be able to specify orientation preference. We want above for most popups, below + // for menus/autocomplete. + if viewport.height > rel_y + height { + rel_y += 1 // position below point + } else { + rel_y = rel_y.saturating_sub(height) // position above point + } + + (rel_x, rel_y) + } + + pub fn get_size(&self) -> (u16, u16) { + (self.size.0, self.size.1) + } + pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; @@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - cx.scroll = Some(self.scroll); + // trigger required_size so we recalculate if the child changed + self.required_size((viewport.width, viewport.height)); - let position = self - .position - .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); - - let (width, height) = self.size; - - // -- make sure frame doesn't stick out of bounds - let mut rel_x = position.col as u16; - let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - }; + cx.scroll = Some(self.scroll); - // TODO: be able to specify orientation preference. We want above for most popups, below - // for menus/autocomplete. - if height <= rel_y { - rel_y = rel_y.saturating_sub(height) // position above point - } else { - rel_y += 1 // position below point - } + let (rel_x, rel_y) = self.get_rel_position(viewport, cx); // clip to viewport - let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height)); + let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)); // clear area let background = cx.editor.theme.get("ui.popup"); diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index 65a75a4a..4641fae1 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -5,11 +5,17 @@ use helix_view::graphics::Rect; pub struct Text { contents: String, + size: (u16, u16), + viewport: (u16, u16), } impl Text { pub fn new(contents: String) -> Self { - Self { contents } + Self { + contents, + size: (0, 0), + viewport: (0, 0), + } } } impl Component for Text { @@ -24,9 +30,13 @@ impl Component for Text { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = tui::text::Text::from(self.contents.clone()); - let width = std::cmp::min(contents.width() as u16, viewport.0); - let height = std::cmp::min(contents.height() as u16, viewport.1); - Some((width, height)) + if viewport != self.viewport { + let contents = tui::text::Text::from(self.contents.clone()); + let width = std::cmp::min(contents.width() as u16, viewport.0); + let height = std::cmp::min(contents.height() as u16, viewport.1); + self.size = (width, height); + self.viewport = viewport; + } + Some(self.size) } } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e890a336..1f1b1f5f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -386,21 +386,24 @@ impl Document { /// If supported, returns the changes that should be applied to this document in order /// to format it nicely. pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { - if let Some(language_server) = self.language_server.clone() { + if let Some(language_server) = self.language_server() { let text = self.text.clone(); - let id = self.identifier(); + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions::default(), + None, + )?; + let fut = async move { - let edits = language_server - .text_document_formatting(id, lsp::FormattingOptions::default(), None) - .await - .unwrap_or_else(|e| { - log::warn!("LSP formatting failed: {}", e); - Default::default() - }); + let edits = request.await.unwrap_or_else(|e| { + log::warn!("LSP formatting failed: {}", e); + Default::default() + }); LspFormatting { doc: text, edits, - offset_encoding: language_server.offset_encoding(), + offset_encoding, } }; Some(fut) @@ -469,9 +472,14 @@ impl Document { to_writer(&mut file, encoding, &text).await?; if let Some(language_server) = language_server { - language_server - .text_document_did_save(identifier, &text) - .await?; + if !language_server.is_initialized() { + return Ok(()); + } + if let Some(notification) = + language_server.text_document_did_save(identifier, &text) + { + notification.await?; + } } Ok(()) @@ -646,7 +654,7 @@ impl Document { // } // emit lsp notification - if let Some(language_server) = &self.language_server { + if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, @@ -795,9 +803,18 @@ impl Document { self.version } - #[inline] pub fn language_server(&self) -> Option<&helix_lsp::Client> { - self.language_server.as_deref() + let server = self.language_server.as_deref(); + let initialized = server + .map(|server| server.is_initialized()) + .unwrap_or(false); + + // only resolve language_server if it's initialized + if initialized { + server + } else { + None + } } #[inline] @@ -892,6 +909,40 @@ mod test { use super::*; #[test] + fn changeset_to_changes_ignore_line_endings() { + use helix_lsp::{lsp, Client, OffsetEncoding}; + let text = Rope::from("hello\r\nworld"); + let mut doc = Document::from(text, None); + let view = ViewId::default(); + doc.set_selection(view, Selection::single(0, 0)); + + let transaction = + Transaction::change(doc.text(), vec![(5, 7, Some("\n".into()))].into_iter()); + let old_doc = doc.text().clone(); + doc.apply(&transaction, view); + let changes = Client::changeset_to_changes( + &old_doc, + doc.text(), + transaction.changes(), + OffsetEncoding::Utf8, + ); + + assert_eq!(doc.text(), "hello\nworld"); + + assert_eq!( + changes, + &[lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(1, 0) + )), + text: "\n".into(), + range_length: None, + }] + ); + } + + #[test] fn changeset_to_changes() { use helix_lsp::{lsp, Client, OffsetEncoding}; let text = Rope::from("hello"); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index adc40eb4..72140ea8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -3,7 +3,7 @@ use crate::{ graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::Tree, - Document, DocumentId, RegisterSelection, View, ViewId, + Document, DocumentId, View, ViewId, }; use futures_util::future; @@ -44,6 +44,10 @@ pub struct Config { pub line_number: LineNumber, /// Middle click paste support. Defaults to true pub middle_click_paste: bool, + /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. + pub smart_case: bool, + /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. + pub auto_pairs: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -69,6 +73,8 @@ impl Default for Config { }, line_number: LineNumber::Absolute, middle_click_paste: true, + smart_case: true, + auto_pairs: true, } } } @@ -78,7 +84,7 @@ pub struct Editor { pub tree: Tree, pub documents: SlotMap<DocumentId, Document>, pub count: Option<std::num::NonZeroUsize>, - pub selected_register: RegisterSelection, + pub selected_register: Option<char>, pub registers: Registers, pub theme: Theme, pub language_servers: helix_lsp::Registry, @@ -125,7 +131,7 @@ impl Editor { tree: Tree::new(area), documents: SlotMap::with_key(), count: None, - selected_register: RegisterSelection::default(), + selected_register: None, theme: themes.default(), language_servers, debugger: None, @@ -270,26 +276,31 @@ impl Editor { let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; // try to find a language server based on the language name - let language_server = doc - .language - .as_ref() - .and_then(|language| self.language_servers.get(language).ok()); + let language_server = doc.language.as_ref().and_then(|language| { + self.language_servers + .get(language) + .map_err(|e| { + log::error!("Failed to get LSP, {}, for `{}`", e, language.scope()) + }) + .ok() + }); if let Some(language_server) = language_server { - doc.set_language_server(Some(language_server.clone())); - let language_id = doc .language() .and_then(|s| s.split('.').last()) // source.rust .map(ToOwned::to_owned) .unwrap_or_default(); + // TODO: this now races with on_init code if the init happens too quickly tokio::spawn(language_server.text_document_did_open( doc.url().unwrap(), doc.version(), doc.text(), language_id, )); + + doc.set_language_server(Some(language_server)); } let id = self.documents.insert(doc); @@ -308,14 +319,9 @@ impl Editor { if close_buffer { // get around borrowck issues - let language_servers = &mut self.language_servers; let doc = &self.documents[view.doc]; - let language_server = doc - .language - .as_ref() - .and_then(|language| language_servers.get(language).ok()); - if let Some(language_server) = language_server { + if let Some(language_server) = doc.language_server() { tokio::spawn(language_server.text_document_did_close(doc.identifier())); } self.documents.remove(view.doc); @@ -345,20 +351,24 @@ impl Editor { view.ensure_cursor_in_view(doc, self.config.scrolloff) } + #[inline] pub fn document(&self, id: DocumentId) -> Option<&Document> { self.documents.get(id) } + #[inline] pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { self.documents.get_mut(id) } + #[inline] pub fn documents(&self) -> impl Iterator<Item = &Document> { - self.documents.iter().map(|(_id, doc)| doc) + self.documents.values() } + #[inline] pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> { - self.documents.iter_mut().map(|(_id, doc)| doc) + self.documents.values_mut() } pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> { @@ -366,10 +376,10 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } - // pub fn current_document(&self) -> Document { - // let id = self.view().doc; - // let doc = &mut editor.documents[id]; - // } + pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> { + self.documents_mut() + .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) + } pub fn cursor(&self) -> (Option<Position>, CursorKind) { let view = view!(self); diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 66013ee5..0bfca04a 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -224,13 +224,13 @@ pub enum Color { Magenta,
Cyan,
Gray,
- DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
+ LightGray,
White,
Rgb(u8, u8, u8),
Indexed(u8),
@@ -250,14 +250,14 @@ impl From<Color> for crossterm::style::Color { Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
- Color::Gray => CColor::Grey,
- Color::DarkGray => CColor::DarkGrey,
+ Color::Gray => CColor::DarkGrey,
Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan,
+ Color::LightGray => CColor::Grey,
Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9bcc0b7d..c37474d6 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -8,7 +8,6 @@ pub mod graphics; pub mod info; pub mod input; pub mod keyboard; -pub mod register_selection; pub mod theme; pub mod tree; pub mod view; @@ -20,6 +19,5 @@ slotmap::new_key_type! { pub use document::Document; pub use editor::Editor; -pub use register_selection::RegisterSelection; pub use theme::Theme; pub use view::View; diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index a06d37e7..c9a04270 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -1,3 +1,14 @@ +//! These are macros to make getting very nested fields in the `Editor` struct easier +//! These are macros instead of functions because functions will have to take `&mut self` +//! However, rust doesn't know that you only want a partial borrow instead of borrowing the +//! entire struct which `&mut self` says. This makes it impossible to do other mutable +//! stuff to the struct because it is already borrowed. Because macros are expanded, +//! this circumvents the problem because it is just like indexing fields by hand and then +//! putting a `&mut` in front of it. This way rust can see that we are only borrowing a +//! part of the struct and not the entire thing. + +/// Get the current view and document mutably as a tuple. +/// Returns `(&mut View, &mut Document)` #[macro_export] macro_rules! current { ( $( $editor:ident ).+ ) => {{ @@ -7,6 +18,8 @@ macro_rules! current { }}; } +/// Get the current document mutably. +/// Returns `&mut Document` #[macro_export] macro_rules! doc_mut { ( $( $editor:ident ).+ ) => {{ @@ -14,6 +27,8 @@ macro_rules! doc_mut { }}; } +/// Get the current view mutably. +/// Returns `&mut View` #[macro_export] macro_rules! view_mut { ( $( $editor:ident ).+ ) => {{ @@ -21,6 +36,8 @@ macro_rules! view_mut { }}; } +/// Get the current view immutably +/// Returns `&View` #[macro_export] macro_rules! view { ( $( $editor:ident ).+ ) => {{ diff --git a/helix-view/src/register_selection.rs b/helix-view/src/register_selection.rs deleted file mode 100644 index a2b78f72..00000000 --- a/helix-view/src/register_selection.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// Register selection and configuration -/// -/// This is a kind a of specialized `Option<char>` for register selection. -/// Point is to keep whether the register selection has been explicitely -/// set or not while being convenient by knowing the default register name. -#[derive(Debug)] -pub struct RegisterSelection { - selected: char, - default_name: char, -} - -impl RegisterSelection { - pub fn new(default_name: char) -> Self { - Self { - selected: default_name, - default_name, - } - } - - pub fn select(&mut self, name: char) { - self.selected = name; - } - - pub fn take(&mut self) -> Self { - Self { - selected: std::mem::replace(&mut self.selected, self.default_name), - default_name: self.default_name, - } - } - - pub fn is_default(&self) -> bool { - self.selected == self.default_name - } - - pub fn name(&self) -> char { - self.selected - } -} - -impl Default for RegisterSelection { - fn default() -> Self { - let default_name = '"'; - Self { - selected: default_name, - default_name, - } - } -} diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 74b817d0..9c33685b 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::Context; +use helix_core::hashmap; use log::warn; use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; @@ -142,13 +143,37 @@ struct ThemePalette { impl Default for ThemePalette { fn default() -> Self { - Self::new(HashMap::new()) + Self { + palette: hashmap! { + "black".to_string() => Color::Black, + "red".to_string() => Color::Red, + "green".to_string() => Color::Green, + "yellow".to_string() => Color::Yellow, + "blue".to_string() => Color::Blue, + "magenta".to_string() => Color::Magenta, + "cyan".to_string() => Color::Cyan, + "gray".to_string() => Color::Gray, + "light-red".to_string() => Color::LightRed, + "light-green".to_string() => Color::LightGreen, + "light-yellow".to_string() => Color::LightYellow, + "light-blue".to_string() => Color::LightBlue, + "light-magenta".to_string() => Color::LightMagenta, + "light-cyan".to_string() => Color::LightCyan, + "light-gray".to_string() => Color::LightGray, + "white".to_string() => Color::White, + }, + } } } impl ThemePalette { pub fn new(palette: HashMap<String, Color>) -> Self { - Self { palette } + let ThemePalette { + palette: mut default, + } = ThemePalette::default(); + + default.extend(palette); + Self { palette: default } } pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> { diff --git a/languages.toml b/languages.toml index 6ec5302d..eb49a70c 100644 --- a/languages.toml +++ b/languages.toml @@ -65,6 +65,7 @@ file-types = ["ex", "exs"] roots = [] comment-token = "#" +language-server = { command = "elixir-ls" } indent = { tab-width = 2, unit = " " } [[language]] @@ -199,6 +200,17 @@ language-server = { command = "typescript-language-server", args = ["--stdio"] } indent = { tab-width = 2, unit = " " } [[language]] +name = "tsx" +scope = "source.tsx" +injection-regex = "^(tsx)$" # |typescript +file-types = ["tsx"] +roots = [] +# TODO: highlights-jsx, highlights-params + +language-server = { command = "typescript-language-server", args = ["--stdio"] } +indent = { tab-width = 2, unit = " " } + +[[language]] name = "css" scope = "source.css" injection-regex = "css" @@ -236,6 +248,7 @@ file-types = ["nix"] roots = [] comment-token = "#" +language-server = { command = "rnix-lsp" } indent = { tab-width = 2, unit = " " } [[language]] @@ -286,7 +299,22 @@ injection-regex = "julia" file-types = ["jl"] roots = [] comment-token = "#" -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);" ] } +language-server = { command = "julia", args = [ + "--startup-file=no", + "--history-file=no", + "--quiet", + "-e", + """ + using LanguageServer; + using Pkg; + import StaticLint; + env_path = dirname(Pkg.Types.Context().env.project_file); + + server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); + server.runlinter = true; + run(server); + """, + ] } indent = { tab-width = 2, unit = " " } [[language]] @@ -332,6 +360,15 @@ comment-token = "--" indent = { tab-width = 2, unit = " " } [[language]] +name = "svelte" +scope = "source.svelte" +injection-regex = "svelte" +file-types = ["svelte"] +roots = [] +indent = { tab-width = 2, unit = " " } +language-server = { command = "svelteserver", args = ["--stdio"] } + +[[language]] name = "yaml" scope = "source.yaml" file-types = ["yml", "yaml"] diff --git a/runtime/queries/c/highlights.scm b/runtime/queries/c/highlights.scm index 258e07e7..2c42710f 100644 --- a/runtime/queries/c/highlights.scm +++ b/runtime/queries/c/highlights.scm @@ -61,7 +61,7 @@ (null) @constant (number_literal) @number -(char_literal) @number +(char_literal) @string (call_expression function: (identifier) @function) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 224c8b78..3129c4b2 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -17,9 +17,18 @@ ; Identifiers +((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$")) +(const_spec + name: (identifier) @constant) + +(parameter_declaration (identifier) @variable.parameter) +(variadic_parameter_declaration (identifier) @variable.parameter) + (type_identifier) @type (field_identifier) @property (identifier) @variable +(package_identifier) @variable + ; Operators @@ -79,10 +88,8 @@ "go" "goto" "if" - "import" "interface" "map" - "package" "range" "return" "select" @@ -92,6 +99,29 @@ "var" ] @keyword +[ + "import" + "package" +] @keyword.control.import + +; Delimiters + +[ + ":" + "." + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + ; Literals [ @@ -111,7 +141,8 @@ [ (true) (false) - (nil) -] @constant.builtin +] @constant.builtin.boolean + +(nil) @constant.builtin (comment) @comment diff --git a/runtime/queries/go/locals.scm b/runtime/queries/go/locals.scm new file mode 100644 index 00000000..d240e2b7 --- /dev/null +++ b/runtime/queries/go/locals.scm @@ -0,0 +1,30 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter_declaration (identifier) @local.definition) +(variadic_parameter_declaration (identifier) @local.definition) + +(short_var_declaration + left: (expression_list + (identifier) @local.definition)) + +(var_spec + name: (identifier) @local.definition) + +(for_statement + (range_clause + left: (expression_list + (identifier) @local.definition))) + +(const_declaration + (const_spec + name: (identifier) @local.definition)) + +; References + +(identifier) @local.reference +(field_identifier) @local.reference + diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index ecaa2d2c..dada80b6 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -2,19 +2,19 @@ (operator) @operator (exp_name (constructor) @constructor) (constructor_operator) @operator -(module) @module_name +(module) @namespace (type) @type (type) @class (constructor) @constructor (pragma) @pragma (comment) @comment (signature name: (variable) @fun_type_name) -(function name: (variable) @fun_name) +(function name: (variable) @function) (constraint class: (class_name (type)) @class) (class (class_head class: (class_name (type)) @class)) (instance (instance_head class: (class_name (type)) @class)) -(integer) @literal -(exp_literal (float)) @literal +(integer) @number +(exp_literal (float)) @number (char) @literal (con_unit) @literal (con_list) @literal @@ -39,5 +39,7 @@ "do" @keyword "mdo" @keyword "rec" @keyword -"(" @paren -")" @paren +[ + "(" + ")" +] @punctuation.bracket diff --git a/runtime/queries/javascript/highlights.scm b/runtime/queries/javascript/highlights.scm index a18c38d9..e29829bf 100644 --- a/runtime/queries/javascript/highlights.scm +++ b/runtime/queries/javascript/highlights.scm @@ -87,7 +87,7 @@ (template_string) ] @string -(regex) @string.special +(regex) @string.regexp (number) @number ; Tokens diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm index a53dabe5..7b7d426c 100644 --- a/runtime/queries/julia/highlights.scm +++ b/runtime/queries/julia/highlights.scm @@ -1,9 +1,3 @@ -(identifier) @variable -;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) -;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables - ;(match? @type "^[A-Z][^_]")) -((identifier) @constant - (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) [ (triple_string) @@ -28,43 +22,43 @@ (call_expression (identifier) @function) (call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (broadcast_call_expression (identifier) @function) (broadcast_call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (parameter_list - (identifier) @parameter) + (identifier) @variable.parameter) (parameter_list (optional_parameter . - (identifier) @parameter)) + (identifier) @variable.parameter)) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type) (type_parameter_list (identifier) @type) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type) (function_expression - . (identifier) @parameter) -(spread_parameter) @parameter + . (identifier) @variable.parameter) +(spread_parameter) @variable.parameter (spread_parameter - (identifier) @parameter) + (identifier) @variable.parameter) (named_argument - . (identifier) @parameter) + . (identifier) @variable.parameter) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type)) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type)) ;; Symbol expressions (:my-wanna-be-lisp-keyword) (quote_expression - (identifier)) @symbol + (identifier)) @string.special.symbol ;; Parsing error! foo (::Type) get's parsed as two quote expressions (argument_list @@ -76,7 +70,7 @@ (identifier) @type) (parameterized_identifier (_)) @type (argument_list - (typed_expression . (identifier) @parameter)) + (typed_expression . (identifier) @variable.parameter)) (typed_expression (identifier) @type .) @@ -113,13 +107,13 @@ "end" @keyword (if_statement - ["if" "end"] @conditional) + ["if" "end"] @keyword.control.conditional) (elseif_clause - ["elseif"] @conditional) + ["elseif"] @keyword.control.conditional) (else_clause - ["else"] @conditional) + ["else"] @keyword.control.conditional) (ternary_expression - ["?" ":"] @conditional) + ["?" ":"] @keyword.control.conditional) (function_definition ["function" "end"] @keyword.function) @@ -134,47 +128,57 @@ "type" ] @keyword -((identifier) @keyword (#any-of? @keyword "global" "local")) +((identifier) @keyword (match? @keyword "global|local")) (compound_expression ["begin" "end"] @keyword) (try_statement - ["try" "end" ] @exception) + ["try" "end" ] @keyword.control.exception) (finally_clause - "finally" @exception) + "finally" @keyword.control.exception) (catch_clause - "catch" @exception) + "catch" @keyword.control.exception) (quote_statement ["quote" "end"] @keyword) (let_statement ["let" "end"] @keyword) (for_statement - ["for" "end"] @repeat) + ["for" "end"] @keyword.control.repeat) (while_statement - ["while" "end"] @repeat) -(break_statement) @repeat -(continue_statement) @repeat + ["while" "end"] @keyword.control.repeat) +(break_statement) @keyword.control.repeat +(continue_statement) @keyword.control.repeat (for_binding - "in" @repeat) + "in" @keyword.control.repeat) (for_clause - "for" @repeat) + "for" @keyword.control.repeat) (do_clause ["do" "end"] @keyword) (export_statement - ["export"] @include) + ["export"] @keyword.control.import) [ "using" "module" "import" -] @include +] @keyword.control.import -((identifier) @include (#eq? @include "baremodule")) +((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule")) (((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$")) -(((identifier) @boolean) (eq? @boolean "true")) -(((identifier) @boolean) (eq? @boolean "false")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false")) + ["::" ":" "." "," "..." "!"] @punctuation.delimiter ["[" "]" "(" ")" "{" "}"] @punctuation.bracket + +["="] @operator + +(identifier) @variable +;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) +;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables + ;(match? @type "^[A-Z][^_]")) +((identifier) @constant + (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index cd04a62c..f045c82d 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -259,7 +259,7 @@ (comment) @comment -(bracket_group) @parameter +(bracket_group) @variable.parameter [(math_operator) "="] @operator @@ -312,7 +312,7 @@ key: (word) @text.reference) (key_val_pair - key: (_) @parameter + key: (_) @variable.parameter value: (_)) ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX diff --git a/runtime/queries/ledger/injections.scm b/runtime/queries/ledger/injections.scm index 4bb7d675..2d948141 100644 --- a/runtime/queries/ledger/injections.scm +++ b/runtime/queries/ledger/injections.scm @@ -1 +1,2 @@ (comment) @comment +(note) @comment diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 8e27a39a..40c2be70 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -23,27 +23,27 @@ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (for_in_statement [ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (while_statement [ "while" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (repeat_statement [ "repeat" "until" -] @keyword.control.loop) +] @keyword.control.repeat) (do_statement [ @@ -65,7 +65,7 @@ "not" "and" "or" -] @keyword.operator +] @operator [ "=" @@ -108,7 +108,7 @@ [ (false) (true) -] @boolean +] @constant.builtin.boolean (nil) @constant.builtin (spread) @constant ;; "..." ((identifier) @constant @@ -116,7 +116,7 @@ ;; Parameters (parameters - (identifier) @parameter) + (identifier) @variable.parameter) ; ;; Functions (function [(function_name) (identifier)] @function) @@ -139,8 +139,8 @@ (function_call [ - ((identifier) @variable (method) @method) - ((_) (method) @method) + ((identifier) @variable (method) @function.method) + ((_) (method) @function.method) (identifier) @function (field_expression (property_identifier) @function) ] diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 093b3cce..160f2cb4 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -25,12 +25,12 @@ (external (value_name) @function) -(method_name) @method +(method_name) @function.method ; Variables ;---------- -(value_pattern) @parameter +(value_pattern) @variable.parameter ; Application ;------------ @@ -60,7 +60,7 @@ [(number) (signed_number)] @number -(character) @character +(character) @constant.character (string) @string @@ -92,7 +92,7 @@ ["include" "open"] @include -["for" "to" "downto" "while" "do" "done"] @keyword.control.loop +["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat ; Macros ;------- diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm index 7f296f3b..8617d6f0 100644 --- a/runtime/queries/ruby/highlights.scm +++ b/runtime/queries/ruby/highlights.scm @@ -100,7 +100,7 @@ (bare_symbol) ] @string.special.symbol -(regex) @string.special.regex +(regex) @string.regexp (escape_sequence) @escape [ diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 6b14d74d..956a5dac 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -17,7 +17,7 @@ (escape_sequence) @escape (primitive_type) @type.builtin -(boolean_literal) @constant.builtin +(boolean_literal) @constant.builtin.boolean [ (integer_literal) (float_literal) @@ -149,7 +149,7 @@ (mutable_specifier) @keyword.mut - +; TODO: variable.mut to highlight mutable identifiers via locals.scm ; ------- ; Guess Other Types diff --git a/runtime/queries/rust/locals.scm b/runtime/queries/rust/locals.scm new file mode 100644 index 00000000..6428f9b4 --- /dev/null +++ b/runtime/queries/rust/locals.scm @@ -0,0 +1,17 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter + (identifier) @local.definition) + +(let_declaration + pattern: (identifier) @local.definition) + +(closure_parameters (identifier)) @local.definition + +; References +(identifier) @local.reference + diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm new file mode 100644 index 00000000..4c6f5f35 --- /dev/null +++ b/runtime/queries/svelte/highlights.scm @@ -0,0 +1,68 @@ +; Special identifiers +;-------------------- + +; TODO: +((element (start_tag (tag_name) @_tag) (text) @markup.heading) + (#match? @_tag "^(h[0-9]|title)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.bold) + (#match? @_tag "^(strong|b)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.italic) + (#match? @_tag "^(em|i)$")) + +; ((element (start_tag (tag_name) @_tag) (text) @markup.strike) +; (#match? @_tag "^(s|del)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.underline) + (#eq? @_tag "u")) + +((element (start_tag (tag_name) @_tag) (text) @markup.inline) + (#match? @_tag "^(code|kbd)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.underline.link) + (#eq? @_tag "a")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @markup.undeline.link)) + (#match? @_attr "^(href|src)$")) + +(tag_name) @tag +(attribute_name) @property +(erroneous_end_tag_name) @error +(comment) @comment + +[ + (attribute_value) + (quoted_attribute_value) +] @string + +[ + (text) + (raw_text_expr) +] @none + +[ + (special_block_keyword) + (then) + (as) +] @keyword + +[ + "{" + "}" +] @punctuation.brackets + +"=" @operator + +[ + "<" + ">" + "</" + "/>" + "#" + ":" + "/" + "@" +] @punctuation.definition.tag diff --git a/runtime/queries/svelte/indents.toml b/runtime/queries/svelte/indents.toml new file mode 100644 index 00000000..693db8e3 --- /dev/null +++ b/runtime/queries/svelte/indents.toml @@ -0,0 +1,18 @@ +indent = [ + "element" + "if_statement" + "each_statement" + "await_statement" +] + +outdent = [ + "end_tag" + "else_statement" + "if_end_expr" + "each_end_expr" + "await_end_expr" + ">" + "/>" +] + +ignore = "comment" diff --git a/runtime/queries/svelte/injections.scm b/runtime/queries/svelte/injections.scm new file mode 100644 index 00000000..266f4701 --- /dev/null +++ b/runtime/queries/svelte/injections.scm @@ -0,0 +1,30 @@ +; injections.scm +; -------------- +((style_element + (raw_text) @injection.content) + (#set! injection.language "css")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @css)) + (#eq? @_attr "style")) + +((script_element + (raw_text) @injection.content) + (#set! injection.language "javascript")) + +((raw_text_expr) @injection.content + (#set! injection.language "javascript")) + +( + (script_element + (start_tag + (attribute + (quoted_attribute_value (attribute_value) @_lang))) + (raw_text) @injection.content) + (#match? @_lang "(ts|typescript)") + (#set! injection.language "typescript") +) + +(comment) @comment + diff --git a/runtime/queries/tsx/highlights.scm b/runtime/queries/tsx/highlights.scm new file mode 100644 index 00000000..1b61e36d --- /dev/null +++ b/runtime/queries/tsx/highlights.scm @@ -0,0 +1 @@ +; inherits: typescript diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm index 4ebb4440..2955a4ce 100644 --- a/runtime/queries/yaml/highlights.scm +++ b/runtime/queries/yaml/highlights.scm @@ -1,6 +1,6 @@ (block_mapping_pair key: (_) @property) (flow_mapping (_ key: (_) @property)) -(boolean_scalar) @boolean +(boolean_scalar) @constant.builtin.boolean (null_scalar) @constant.builtin (double_quote_scalar) @string (single_quote_scalar) @string diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index c105d52b..c48a7e28 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#6A9955" } "string" = { fg = "#ce9178" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#b5cea8" } "escape" = { fg = "#d7ba7d" } @@ -61,7 +62,7 @@ "ui.text.focus" = { fg = "#ffffff" } "warning" = { fg = "#cca700" } -"error" = { fg = "#f48771" } +"error" = { fg = "#ff1212" } "info" = { fg = "#75beff" } "hint" = { fg = "#eeeeeeb3" } diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml new file mode 100644 index 00000000..462c8265 --- /dev/null +++ b/runtime/themes/everforest_dark.toml @@ -0,0 +1,87 @@ +# Everforest (Dark Hard) +# Author: CptPotato + +# Original Author: +# URL: https://github.com/sainnhe/everforest +# Filename: autoload/everforest.vim +# Author: sainnhe +# Email: sainnhe@gmail.com +# License: MIT License + +"escape" = "orange" +"type" = "yellow" +"constant" = "purple" +"number" = "purple" +"string" = "grey2" +"comment" = "grey0" +"variable" = "fg" +"variable.builtin" = "blue" +"variable.parameter" = "fg" +"variable.property" = "fg" +"label" = "aqua" +"punctuation" = "grey2" +"punctuation.delimiter" = "grey2" +"punctuation.bracket" = "fg" +"keyword" = "red" +"operator" = "orange" +"function" = "green" +"function.builtin" = "blue" +"function.macro" = "aqua" +"tag" = "yellow" +"namespace" = "aqua" +"attribute" = "aqua" +"constructor" = "yellow" +"module" = "blue" +"property" = "fg" +"special" = "orange" + +"ui.background" = { bg = "bg0" } +"ui.cursor" = { fg = "bg0", bg = "fg" } +"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" } +"ui.cursor.insert" = { fg = "bg0", bg = "grey1" } +"ui.cursor.select" = { fg = "bg0", bg = "blue" } +"ui.linenr" = "grey0" +"ui.linenr.selected" = "fg" +"ui.statusline" = { fg = "grey2", bg = "bg2" } +"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" } +"ui.popup" = { fg = "grey2", bg = "bg1" } +"ui.window" = { fg = "grey2", bg = "bg1" } +"ui.help" = { fg = "fg", bg = "bg1" } +"ui.text" = "fg" +"ui.text.focus" = "fg" +"ui.menu" = { fg = "fg", bg = "bg2" } +"ui.menu.selected" = { fg = "bg0", bg = "green" } +"ui.selection" = { bg = "bg3" } + +"hint" = "blue" +"info" = "aqua" +"warning" = "yellow" +"error" = "red" +"diagnostic" = { modifiers = ["underlined"] } + + +[palette] + +bg0 = "#2b3339" +bg1 = "#323c41" +bg2 = "#3a454a" +bg3 = "#445055" +bg4 = "#4c555b" +bg5 = "#53605c" +bg_visual = "#503946" +bg_red = "#4e3e43" +bg_green = "#404d44" +bg_blue = "#394f5a" +bg_yellow = "#4a4940" + +fg = "#d3c6aa" +red = "#e67e80" +orange = "#e69875" +yellow = "#dbbc7f" +green = "#a7c080" +aqua = "#83c092" +blue = "#7fbbb3" +purple = "#d699b6" +grey0 = "#7a8478" +grey1 = "#859289" +grey2 = "#9da9a0" diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 2407591a..a8f03ff3 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#88846F" } "string" = { fg = "#e6db74" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#ae81ff" } "escape" = { fg = "#ae81ff" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..79f6f8f6 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "rust-src"] @@ -9,7 +9,8 @@ special = "honey" property = "white" variable = "lavender" # variable = "almond" # TODO: metavariables only -"variable.parameter" = "lavender" +# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } +"variable.parameter" = { fg = "lavender" } "variable.builtin" = "mint" type = "white" "type.builtin" = "white" # TODO: distinguish? @@ -28,9 +29,7 @@ escape = "honey" label = "honey" # TODO: diferentiate doc comment -# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors - -module = "#ff0000" +# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.background" = { bg = "midnight" } "ui.linenr" = { fg = "comet" } |