Merge branch 'master' into debug

pull/574/head
Dmitry Sharshakov 3 years ago
commit bf53aff27d

4
.gitmodules vendored

@ -118,3 +118,7 @@
path = helix-syntax/languages/tree-sitter-zig path = helix-syntax/languages/tree-sitter-zig
url = https://github.com/maxxnino/tree-sitter-zig url = https://github.com/maxxnino/tree-sitter-zig
shallow = true 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

117
Cargo.lock generated

@ -13,15 +13,15 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.43" version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.3.2" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00" checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -41,9 +41,17 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [ dependencies = [
"lazy_static",
"memchr", "memchr",
"regex-automata",
] ]
[[package]]
name = "bytecount"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.0.1" version = "1.0.1"
@ -58,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.69" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -174,6 +182,15 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[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]] [[package]]
name = "error-code" name = "error-code"
version = "2.3.0" version = "2.3.0"
@ -300,6 +317,45 @@ dependencies = [
"regex", "regex",
] ]
[[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]] [[package]]
name = "helix-core" name = "helix-core"
version = "0.4.1" version = "0.4.1"
@ -375,6 +431,8 @@ dependencies = [
"fern", "fern",
"futures-util", "futures-util",
"fuzzy-matcher", "fuzzy-matcher",
"grep-regex",
"grep-searcher",
"helix-core", "helix-core",
"helix-dap", "helix-dap",
"helix-lsp", "helix-lsp",
@ -570,6 +628,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.7.13" version = "0.7.13"
@ -771,6 +838,12 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.25" version = "0.6.25"
@ -829,9 +902,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.67" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -851,9 +924,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.9" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
dependencies = [ dependencies = [
"libc", "libc",
"signal-hook-registry", "signal-hook-registry",
@ -893,9 +966,9 @@ dependencies = [
[[package]] [[package]]
name = "similar" name = "similar"
version = "1.3.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" checksum = "6bf11003835e462f07851028082d2a1c89d956180ce4b4b50e07fb085ec4131a"
[[package]] [[package]]
name = "slab" name = "slab"
@ -948,18 +1021,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.28" version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.28" version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1001,9 +1074,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.10.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1052,9 +1125,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.19.5" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",
@ -1098,9 +1171,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"

@ -1,12 +1,8 @@
- tree sitter: - tree sitter:
- lua
- markdown - markdown
- zig
- regex - regex
- vue
- kotlin - kotlin
- julia
- clojure - clojure
- erlang - erlang
@ -26,8 +22,6 @@ as you type completion!
- [ ] lsp: signature help - [ ] lsp: signature help
- [ ] search: smart case by default: insensitive unless upper detected
2 2
- [ ] macro recording - [ ] macro recording
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) - [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )

@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"]
language = "en" language = "en"
multilingual = false multilingual = false
src = "src" src = "src"
theme = "colibri"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit" edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html] [output.html]
cname = "docs.helix-editor.com" cname = "docs.helix-editor.com"
default-theme = "colibri"
preferred-dark-theme = "colibri"

@ -5,6 +5,21 @@ To override global configuration parameters, create a `config.toml` file located
* Linux and Mac: `~/.config/helix/config.toml` * Linux and Mac: `~/.config/helix/config.toml`
* Windows: `%AppData%\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 ## LSP
To display all language server messages in the status line add the following to your `config.toml`: To display all language server messages in the status line add the following to your `config.toml`:

@ -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 action itself (delete, change, yank, etc) comes second. A cursor is simply a
single width selection. single width selection.
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
> TODO: Mention texobjects, surround, registers > TODO: Mention texobjects, surround, registers

@ -23,7 +23,9 @@ shell for working on Helix.
### Arch Linux ### 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-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 - [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch

@ -4,7 +4,7 @@
### Movement ### Movement
> NOTE: `f`, `F`, `t` and `T` are not confined to the current line. > NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
@ -28,16 +28,16 @@
| `PageDown` | Move page down | `page_down` | | `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` | | `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A | | `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` | | `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A | | `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 | | `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 | | `Space` | Enter [space mode](#space-mode) | N/A |
| `K` | Show documentation for the item under the cursor | `hover` |
### Changes ### Changes
@ -66,6 +66,16 @@
| `d` | Delete selection | `delete_selection` | | `d` | Delete selection | `delete_selection` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` |
#### Shell
| Key | Description | Command |
| ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| <code>A-&#124;</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 ### Selection manipulation
| Key | Description | Command | | Key | Description | Command |
@ -75,6 +85,7 @@
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `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` | | `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` | | `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
| `(` | Rotate main selection forward | `rotate_selections_backward` | | `(` | Rotate main selection forward | `rotate_selections_backward` |
@ -86,22 +97,13 @@
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `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` | | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` | | `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | | `K` | Keep selections matching the regex | `keep_selections` |
| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | | `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 ### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching > TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
in reverse, or searching via smartcase.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
@ -110,41 +112,17 @@ in reverse, or searching via smartcase.
| `N` | Add next search match to selection | `extend_search_next` | | `N` | Add next search match to selection | `extend_search_next` |
| `*` | Use current selection as the search pattern | `search_selection` | | `*` | Use current selection as the search pattern | `search_selection` |
### Unimpaired ### Minor modes
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
| 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 These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
> implemented for all movement commands yet.
## View mode #### View mode
View mode is intended for scrolling and manipulating the view without changing 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 | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
@ -154,8 +132,12 @@ the selection.
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` | | `j` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` | | `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. Jumps to various locations.
@ -177,7 +159,7 @@ Jumps to various locations.
| `i` | Go to implementation | `goto_implementation` | | `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `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 Enter this mode using `m` from normal mode. See the relavant section
in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) 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` | | `a` `<object>` | Select around textobject | `select_textobject_around` |
| `i` `<object>` | Select inside textobject | `select_textobject_inner` | | `i` `<object>` | Select inside textobject | `select_textobject_inner` |
## Object mode
TODO: Mappings for selecting syntax nodes (a superset of `[`). 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. 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` | | `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
| `q`, `Ctrl-q` | Close current window | `wclose` | | `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 | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `k` | Show documentation for the item under the cursor | `hover` |
| `f` | Open file picker | `file_picker` | | `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` | | `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` | | `s` | Open symbol picker (current document) | `symbol_picker` |
| `a` | Apply code action | `code_action` | | `a` | Apply code action | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` | | `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A | | `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 after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_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 # Picker

@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Null | `"null"` | | Null | `"null"` |
| Escape | `"esc"` | | 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) 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)

@ -30,7 +30,52 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as
"key.key" = "#ffffff" "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 | | Modifier |
| --- | | --- |
@ -38,44 +83,88 @@ Possible modifiers:
| `dim` | | `dim` |
| `italic` | | `italic` |
| `underlined` | | `underlined` |
| `slow\_blink` | | `slow_blink` |
| `rapid\_blink` | | `rapid_blink` |
| `reversed` | | `reversed` |
| `hidden` | | `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 | | 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.background` | |
| `ui.cursor` | | | `ui.cursor` | |
| `ui.cursor.insert` | | | `ui.cursor.insert` | |
@ -84,8 +173,8 @@ Possible keys:
| `ui.cursor.primary` | Cursor with primary selection | | `ui.cursor.primary` | Cursor with primary selection |
| `ui.linenr` | | | `ui.linenr` | |
| `ui.linenr.selected` | | | `ui.linenr.selected` | |
| `ui.statusline` | | | `ui.statusline` | Statusline |
| `ui.statusline.inactive` | | | `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.popup` | | | `ui.popup` | |
| `ui.window` | | | `ui.window` | |
| `ui.help` | | | `ui.help` | |
@ -97,29 +186,9 @@ Possible keys:
| `ui.menu.selected` | | | `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area | | `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | | | `ui.selection.primary` | |
| `warning` | LSP warning | | `warning` | Diagnostics warning (gutter) |
| `error` | LSP error | | `error` | Diagnostics error (gutter) |
| `info` | LSP info | | `info` | Diagnostics info (gutter) |
| `hint` | LSP hint | | `hint` | Diagnostics hint (gutter) |
| `diagnostic` | For text in editing area |
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.
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.

@ -114,6 +114,19 @@ h6:target::before {
margin-bottom: .875em; 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 p { line-height: 1.45em; }
.content ol { line-height: 1.45em; } .content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; } .content ul { line-height: 1.45em; }

@ -69,7 +69,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;
@ -110,7 +110,7 @@
--links: #20609f; --links: #20609f;
--inline-code-color: #301900; --inline-code-color: #a39e9b;
--theme-popup-bg: #fafafa; --theme-popup-bg: #fafafa;
--theme-popup-border: #cccccc; --theme-popup-border: #cccccc;
@ -151,7 +151,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
@ -192,7 +192,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #6e6b5e; --inline-code-color: #c5c8c6;
--theme-popup-bg: #e1e1db; --theme-popup-bg: #e1e1db;
--theme-popup-border: #b38f6b; --theme-popup-border: #b38f6b;
@ -234,7 +234,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #6e6b5e;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;
@ -261,6 +261,7 @@
.colibri { .colibri {
--bg: #3b224c; --bg: #3b224c;
--fg: #bcbdd0; --fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733; --sidebar-bg: #281733;
--sidebar-fg: #c8c9db; --sidebar-fg: #c8c9db;
@ -276,18 +277,19 @@
/* --links: #a4a0e8; */ /* --links: #a4a0e8; */
--links: #ECCDBA; --links: #ECCDBA;
--inline-code-color: #c5c8c6;; --inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2); --theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%); --quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%); --quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%); --table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsl(226, 23%, 31%); --table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%); --table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa; --searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6; --searchbar-bg: #aeaec6;
@ -300,6 +302,7 @@
} }
.colibri { .colibri {
/*
--bg: #ffffff; --bg: #ffffff;
--fg: #452859; --fg: #452859;
--fg: #5a5977; --fg: #5a5977;
@ -318,7 +321,7 @@
--links: #6F44F0; --links: #6F44F0;
--inline-code-color: #697C81; --inline-code-color: #a39e9b;
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
@ -341,4 +344,5 @@
--searchresults-border-color: #5c5c68; --searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430; --searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5; --search-mark-bg: #a2cff5;
*/
} }

@ -1,83 +1,56 @@
/* pre code.hljs {
* An increased contrast highlighting scheme loosely based on the display:block;
* "Base16 Atelier Dune Light" theme by Bram de Haan overflow-x:auto;
* (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) padding:1em
* Original Base16 color scheme by Chris Kempson }
* (https://github.com/chriskempson/base16) code.hljs {
*/ padding:3px 5px
}
/* Comment */ .hljs {
background:#2f1e2e;
color:#a39e9b
}
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #575757; color:#8d8687
} }
/* Red */
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link, .hljs-link,
.hljs-meta,
.hljs-name, .hljs-name,
.hljs-regexp,
.hljs-selector-class,
.hljs-selector-id, .hljs-selector-id,
.hljs-selector-class { .hljs-tag,
color: #d70025; .hljs-template-variable,
.hljs-variable {
color:#ef6155
} }
/* Orange */
.hljs-number,
.hljs-meta,
.hljs-built_in, .hljs-built_in,
.hljs-builtin-name, .hljs-deletion,
.hljs-literal, .hljs-literal,
.hljs-type, .hljs-number,
.hljs-params { .hljs-params,
color: #b21e00; .hljs-type {
color:#f99b15
} }
.hljs-attribute,
/* Green */ .hljs-section,
.hljs-string, .hljs-title {
.hljs-symbol, color:#fec418
.hljs-bullet {
color: #008200;
} }
.hljs-addition,
/* Blue */ .hljs-bullet,
.hljs-title, .hljs-string,
.hljs-section { .hljs-symbol {
color: #0030f2; color:#48b685
} }
/* Purple */
.hljs-keyword, .hljs-keyword,
.hljs-selector-tag { .hljs-selector-tag {
color: #9d00ec; color:#815ba4
} }
.hljs {
display: block;
overflow-x: auto;
background: #f6f7f6;
color: #000;
padding: 0.5em;
}
.hljs-emphasis { .hljs-emphasis {
font-style: italic; font-style:italic
} }
.hljs-strong { .hljs-strong {
font-weight: bold; font-weight:700
}
.hljs-addition {
color: #22863a;
background-color: #f0fff4;
}
.hljs-deletion {
color: #b31d28;
background-color: #ffeef0;
} }

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"devshell": { "devshell": {
"locked": { "locked": {
"lastModified": 1625086391, "lastModified": 1630239564,
"narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=", "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7", "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -40,11 +40,11 @@
"rustOverlay": "rustOverlay" "rustOverlay": "rustOverlay"
}, },
"locked": { "locked": {
"lastModified": 1628489367, "lastModified": 1631254163,
"narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=", "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "0dc8383aae5f791a48e34120edb04670b947dc0b", "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -55,11 +55,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1628465643, "lastModified": 1631206977,
"narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=", "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6ef4f522d63f22b40004319778761040d3197390", "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -79,11 +79,11 @@
"rustOverlay": { "rustOverlay": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1628475192, "lastModified": 1631240108,
"narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=", "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468", "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -30,22 +30,7 @@
}; };
# link languages and theme toml files since helix-view expects them # link languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; }; helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = prev: { 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
'';
preConfigure = "mkdir -p ../runtime/grammars"; preConfigure = "mkdir -p ../runtime/grammars";
postInstall = "cp -r ../runtime $out/runtime"; postInstall = "cp -r ../runtime $out/runtime";
}; };

@ -23,7 +23,7 @@ unicode-segmentation = "1.8"
unicode-width = "0.1" unicode-width = "0.1"
unicode-general-category = "0.4" unicode-general-category = "0.4"
# slab = "0.4.2" # slab = "0.4.2"
tree-sitter = "0.19" tree-sitter = "0.20"
once_cell = "1.8" once_cell = "1.8"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
@ -31,7 +31,7 @@ regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.5" toml = "0.5"
similar = "1.3" similar = "2.0"
etcetera = "0.3" etcetera = "0.3"

@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos(
pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
let mut scopes = Vec::new(); let mut scopes = Vec::new();
if let Some(syntax) = syntax { if let Some(syntax) = syntax {
let byte_start = text.char_to_byte(pos); let pos = text.char_to_byte(pos);
let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) { let mut node = match syntax
.tree()
.root_node()
.descendant_for_byte_range(pos, pos)
{
Some(node) => node, Some(node) => node,
None => return scopes, None => return scopes,
}; };
@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&
scopes.push(node.kind()); scopes.push(node.kind());
while let Some(parent) = node.parent() { 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(), highlight_config: OnceCell::new(),
config: None, config: None,
// //
injection_regex: None,
roots: vec![], roots: vec![],
comment_token: None, comment_token: None,
auto_format: false, auto_format: false,

@ -360,6 +360,15 @@ impl Selection {
self.normalize() 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 /// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. /// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self { pub fn map(self, changes: &ChangeSet) -> Self {

@ -21,6 +21,15 @@ use std::{
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
@ -42,7 +51,8 @@ pub struct LanguageConfiguration {
pub auto_format: bool, pub auto_format: bool,
// content_regex // content_regex
// injection_regex #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
pub injection_regex: Option<Regex>,
// first_line_regex // first_line_regex
// //
#[serde(skip)] #[serde(skip)]
@ -182,8 +192,12 @@ impl LanguageConfiguration {
&highlights_query, &highlights_query,
&injections_query, &injections_query,
&locals_query, &locals_query,
) );
.unwrap(); // TODO: no unwrap
let config = match config {
Ok(config) => config,
Err(err) => panic!("{}", err),
}; // TODO: avoid panic
config.configure(scopes); config.configure(scopes);
Some(Arc::new(config)) Some(Arc::new(config))
} }
@ -277,6 +291,30 @@ impl Loader {
.cloned() .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>> { pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter() 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)) 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 { impl Syntax {
// buffer, grammar, config, grammars, sync_timeout? // buffer, grammar, config, grammars, sync_timeout?
pub fn new( pub fn new(
@ -416,16 +444,11 @@ impl Syntax {
let config_ref = let config_ref =
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) }; unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
// TODO: if reusing cursors this might need resetting // if reusing cursors & no range this resets to whole range
if let Some(range) = &range { cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
cursor_ref.set_byte_range(range.start, range.end);
}
let captures = cursor_ref let captures = cursor_ref
.captures(query_ref, tree_ref.root_node(), move |n: Node| { .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.peekable(); .peekable();
// manually craft the root layer based on the existing tree // manually craft the root layer based on the existing tree
@ -539,10 +562,7 @@ impl LanguageLayer {
// let mut injections_by_pattern_index = // let mut injections_by_pattern_index =
// vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
// let matches = // let matches =
// cursor.matches(combined_injections_query, tree.root_node(), |n: Node| { // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
// // &source[n.byte_range()]
// node_to_bytes(n, source)
// });
// for mat in matches { // for mat in matches {
// let entry = &mut injections_by_pattern_index[mat.pattern_index]; // let entry = &mut injections_by_pattern_index[mat.pattern_index];
// let (language_name, content_node, include_children) = // 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 std::{iter, mem, ops, str, usize};
use tree_sitter::{ use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, Tree, QueryMatch, Range, TextProvider, Tree,
}; };
const CANCELLATION_CHECK_INTERVAL: usize = 100; const CANCELLATION_CHECK_INTERVAL: usize = 100;
@ -814,7 +834,7 @@ struct LocalScope<'a> {
} }
#[derive(Debug)] #[derive(Debug)]
struct HighlightIter<'a, 'tree: 'a, F> struct HighlightIter<'a, F>
where where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{ {
@ -822,16 +842,41 @@ where
byte_offset: usize, byte_offset: usize,
injection_callback: F, injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>, cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a, 'tree>>, layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize, iter_count: usize,
next_event: Option<HighlightEvent>, next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>, 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>, _tree: Option<Tree>,
cursor: QueryCursor, cursor: QueryCursor,
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>, captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>,
config: &'a HighlightConfiguration, config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>, highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>, scope_stack: Vec<LocalScope<'a>>,
@ -839,7 +884,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize, 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HighlightIterLayer").finish() 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. /// Create a new 'layer' of highlighting for this document.
/// ///
/// In the even that the new layer contains "combined injections" (injections where multiple /// 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( let matches = cursor.matches(
combined_injections_query, combined_injections_query,
tree.root_node(), tree.root_node(),
|n: Node| { RopeProvider(source),
// &source[n.byte_range()]
node_to_bytes(n, source)
},
); );
for mat in matches { for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index]; let entry = &mut injections_by_pattern_index[mat.pattern_index];
@ -1117,10 +1159,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
let cursor_ref = let cursor_ref =
unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let captures = cursor_ref let captures = cursor_ref
.captures(&config.query, tree_ref.root_node(), move |n: Node| { .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.peekable(); .peekable();
result.push(HighlightIterLayer { 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 where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, 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() { if let Some(sort_key) = layer.sort_key() {
let mut i = 1; let mut i = 1;
while i < self.layers.len() { 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 where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{ {
@ -1608,7 +1647,7 @@ where
fn injection_for_match<'a>( fn injection_for_match<'a>(
config: &HighlightConfiguration, config: &HighlightConfiguration,
query: &'a Query, query: &'a Query,
query_match: &QueryMatch<'a>, query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>, source: RopeSlice<'a>,
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) { ) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
let content_capture_index = config.injection_content_capture_index; let content_capture_index = config.injection_content_capture_index;

@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "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" tokio-stream = "0.1"

@ -3,17 +3,23 @@ use crate::{
Call, Error, OffsetEncoding, Result, 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 jsonrpc_core as jsonrpc;
use lsp_types as lsp; use lsp_types as lsp;
use serde_json::Value; use serde_json::Value;
use std::future::Future; use std::future::Future;
use std::process::Stdio; use std::process::Stdio;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use tokio::{ use tokio::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
process::{Child, Command}, process::{Child, Command},
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender}, sync::{
mpsc::{channel, UnboundedReceiver, UnboundedSender},
Notify, OnceCell,
},
}; };
#[derive(Debug)] #[derive(Debug)]
@ -22,18 +28,19 @@ pub struct Client {
_process: Child, _process: Child,
server_tx: UnboundedSender<Payload>, server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64, request_counter: AtomicU64,
capabilities: Option<lsp::ServerCapabilities>, pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
config: Option<Value>, config: Option<Value>,
} }
impl Client { impl Client {
#[allow(clippy::type_complexity)]
pub fn start( pub fn start(
cmd: &str, cmd: &str,
args: &[String], args: &[String],
config: Option<Value>, config: Option<Value>,
id: usize, id: usize,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> { ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
let process = Command::new(cmd) let process = Command::new(cmd)
.args(args) .args(args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
@ -50,22 +57,20 @@ impl Client {
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); 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 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 { let client = Self {
id, id,
_process: process, _process: process,
server_tx, server_tx,
request_counter: AtomicU64::new(0), request_counter: AtomicU64::new(0),
capabilities: None, capabilities: OnceCell::new(),
offset_encoding: OffsetEncoding::Utf8, offset_encoding: OffsetEncoding::Utf8,
config, config,
}; };
// TODO: async client.initialize() Ok((client, server_rx, initialize_notify))
// maybe use an arc<atomic> flag
Ok((client, server_rx))
} }
pub fn id(&self) -> usize { 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 { pub fn capabilities(&self) -> &lsp::ServerCapabilities {
self.capabilities self.capabilities
.as_ref() .get()
.expect("language server not yet initialized!") .expect("language server not yet initialized!")
} }
@ -143,7 +152,8 @@ impl Client {
}) })
.map_err(|e| Error::Other(e.into()))?; .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 .await
.map_err(|_| Error::Timeout)? // return Timeout .map_err(|_| Error::Timeout)? // return Timeout
.ok_or(Error::StreamClosed)? .ok_or(Error::StreamClosed)?
@ -151,7 +161,7 @@ impl Client {
} }
/// Send a RPC notification to the language server. /// Send a RPC notification to the language server.
fn notify<R: lsp::notification::Notification>( pub fn notify<R: lsp::notification::Notification>(
&self, &self,
params: R::Params, params: R::Params,
) -> impl Future<Output = Result<()>> ) -> impl Future<Output = Result<()>>
@ -213,7 +223,7 @@ impl Client {
// General messages // 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 // 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()); let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
@ -281,14 +291,7 @@ impl Client {
locale: None, // TODO locale: None, // TODO
}; };
let response = self.request::<lsp::request::Initialize>(params).await?; 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(())
} }
pub async fn shutdown(&self) -> Result<()> { pub async fn shutdown(&self) -> Result<()> {
@ -356,7 +359,6 @@ impl Client {
// //
// Calculation is therefore a bunch trickier. // Calculation is therefore a bunch trickier.
// TODO: stolen from syntax.rs, share
use helix_core::RopeSlice; use helix_core::RopeSlice;
fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position { fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
let lsp::Position { let lsp::Position {
@ -366,7 +368,12 @@ impl Client {
let mut chars = text.chars().peekable(); let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() { 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; line += 1;
character = 0; character = 0;
} else { } else {
@ -441,7 +448,7 @@ impl Client {
) -> Option<impl Future<Output = Result<()>>> { ) -> Option<impl Future<Output = Result<()>>> {
// figure out what kind of sync the server supports // 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 { let sync_capabilities = match capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Kind(kind)) Some(lsp::TextDocumentSyncCapability::Kind(kind))
@ -459,7 +466,7 @@ impl Client {
// range = None -> whole document // range = None -> whole document
range: None, //Some(Range) range: None, //Some(Range)
range_length: None, // u64 apparently deprecated range_length: None, // u64 apparently deprecated
text: "".to_string(), text: new_text.to_string(),
}] }]
} }
lsp::TextDocumentSyncKind::Incremental => { lsp::TextDocumentSyncKind::Incremental => {
@ -487,12 +494,12 @@ impl Client {
// will_save / will_save_wait_until // will_save / will_save_wait_until
pub async fn text_document_did_save( pub fn text_document_did_save(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
text: &Rope, text: &Rope,
) -> Result<()> { ) -> Option<impl Future<Output = Result<()>>> {
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.get().unwrap();
let include_text = match &capabilities.text_document_sync { let include_text = match &capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
@ -504,17 +511,18 @@ impl Client {
include_text, include_text,
}) => include_text.unwrap_or(false), }) => include_text.unwrap_or(false),
// Supported(false) // Supported(false)
_ => return Ok(()), _ => return None,
}, },
// unsupported // unsupported
_ => return Ok(()), _ => return None,
}; };
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams { Some(self.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document, text_document,
text: include_text.then(|| text.into()), text: include_text.then(|| text.into()),
}) },
.await ))
} }
pub fn completion( pub fn completion(
@ -580,19 +588,19 @@ impl Client {
// formatting // formatting
pub async fn text_document_formatting( pub fn text_document_formatting(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> { ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.get().unwrap();
// check if we're able to format // check if we're able to format
match capabilities.document_formatting_provider { match capabilities.document_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false) // None | Some(false)
_ => return Ok(Vec::new()), _ => return None,
}; };
// TODO: return err::unavailable so we can fall back to tree sitter formatting // 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 }, 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);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default()) Ok(response.unwrap_or_default())
})
} }
pub async fn text_document_range_formatting( pub async fn text_document_range_formatting(
@ -614,7 +626,7 @@ impl Client {
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> { ) -> 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 // check if we're able to format
match capabilities.document_range_formatting_provider { match capabilities.document_range_formatting_provider {

@ -226,6 +226,8 @@ impl MethodCall {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum Notification { pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
PublishDiagnostics(lsp::PublishDiagnosticsParams), PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams), ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams), LogMessage(lsp::LogMessageParams),
@ -237,6 +239,7 @@ impl Notification {
use lsp::notification::Notification as _; use lsp::notification::Notification as _;
let notification = match method { let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::PublishDiagnostics::METHOD => { lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params let params: lsp::PublishDiagnosticsParams = params
.parse() .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 self.inner
.values() .values()
.find(|(client_id, _)| client_id == &id) .find(|(client_id, _)| client_id == &id)
@ -302,34 +305,61 @@ impl Registry {
} }
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> { pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
if let Some(config) = &language_config.language_server { let config = match &language_config.language_server {
// avoid borrow issues Some(config) => config,
let inner = &mut self.inner; None => return Err(Error::LspNotDefined),
let s_incoming = &mut self.incoming; };
match inner.entry(language_config.scope.clone()) { match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(entry.get().1.clone()), Entry::Occupied(entry) => Ok(entry.get().1.clone()),
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (mut client, incoming) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&config.command, &config.command,
&config.args, &config.args,
serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), 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, id,
)?; )?;
// TODO: run this async without blocking self.incoming.push(UnboundedReceiverStream::new(incoming));
futures_executor::block_on(client.initialize())?;
s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client); 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())); entry.insert((id, client.clone()));
Ok(client) Ok(client)
} }
} }
} else {
Err(Error::LspNotDefined)
}
} }
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> { pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
@ -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)] #[cfg(test)]
mod tests { mod tests {
use super::{lsp, util::*, OffsetEncoding}; use super::{lsp, util::*, OffsetEncoding};

@ -1,7 +1,7 @@
use crate::Result; use crate::{Error, Result};
use anyhow::Context; use anyhow::Context;
use jsonrpc_core as jsonrpc; use jsonrpc_core as jsonrpc;
use log::{debug, error, info, warn}; use log::{error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
@ -11,7 +11,7 @@ use tokio::{
process::{ChildStderr, ChildStdin, ChildStdout}, process::{ChildStderr, ChildStdin, ChildStdout},
sync::{ sync::{
mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
Mutex, Mutex, Notify,
}, },
}; };
@ -51,9 +51,11 @@ impl Transport {
) -> ( ) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>, UnboundedSender<Payload>,
Arc<Notify>,
) { ) {
let (client_tx, rx) = unbounded_channel(); let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel(); let (tx, client_rx) = unbounded_channel();
let notify = Arc::new(Notify::new());
let transport = Self { let transport = Self {
id, id,
@ -62,11 +64,21 @@ impl Transport {
let transport = Arc::new(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::err(transport.clone(), server_stderr));
tokio::spawn(Self::send(transport, server_stdin, client_rx)); tokio::spawn(Self::send(
transport,
server_stdin,
client_tx,
client_rx,
notify.clone(),
));
(rx, tx) (rx, tx, notify)
} }
async fn recv_server_message( async fn recv_server_message(
@ -76,14 +88,18 @@ impl Transport {
let mut content_length = None; let mut content_length = None;
loop { loop {
buffer.truncate(0); buffer.truncate(0);
reader.read_line(buffer).await?; if reader.read_line(buffer).await? == 0 {
let header = buffer.trim(); return Err(Error::StreamClosed);
};
// debug!("<- header {:?}", buffer);
if header.is_empty() { if buffer == "\r\n" {
// look for an empty CRLF line
break; break;
} }
debug!("<- header {}", header); let header = buffer.trim();
let parts = header.split_once(": "); let parts = header.split_once(": ");
@ -96,7 +112,8 @@ impl Transport {
// Workaround: Some non-conformant language servers will output logging and other garbage // 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 // 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. // 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, buffer: &mut String,
) -> Result<()> { ) -> Result<()> {
buffer.truncate(0); buffer.truncate(0);
err.read_line(buffer).await?; if err.read_line(buffer).await? == 0 {
error!("err <- {}", buffer); return Err(Error::StreamClosed);
};
error!("err <- {:?}", buffer);
Ok(()) Ok(())
} }
@ -255,18 +274,92 @@ impl Transport {
async fn send( async fn send(
transport: Arc<Self>, transport: Arc<Self>,
mut server_stdin: BufWriter<ChildStdin>, mut server_stdin: BufWriter<ChildStdin>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
mut client_rx: UnboundedReceiver<Payload>, mut client_rx: UnboundedReceiver<Payload>,
initialize_notify: Arc<Notify>,
) { ) {
while let Some(msg) = client_rx.recv().await { let mut pending_messages: Vec<Payload> = Vec::new();
match transport let mut is_pending = true;
.send_payload_to_server(&mut server_stdin, msg)
.await // 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(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("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;
}
}
}
} }
} }
} }

@ -11,7 +11,7 @@ homepage = "https://helix-editor.com"
include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"] include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
[dependencies] [dependencies]
tree-sitter = "0.19" tree-sitter = "0.20"
libloading = "0.7" libloading = "0.7"
anyhow = "1" anyhow = "1"

@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) {
.is_none() .is_none()
{ {
eprintln!( 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 dir
); );
eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
std::process::exit(1); std::process::exit(1);
} }

@ -1 +1 @@
Subproject commit 0ba7a24b062b671263ae08e707e9e94383b25bb7 Subproject commit 12ea597262125fc22fd2e91aa953ac69b19c26ca

@ -1 +1 @@
Subproject commit 72319504776f14193472a6ad14abec0af0225cbe Subproject commit 0cdeb0e51411a3ba5493662952c3039de08939ca

@ -0,0 +1 @@
Subproject commit 349a5984513b4a4a9e143a6e746120c6ff6cf6ed

@ -56,5 +56,9 @@ toml = "0.5"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } 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 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

@ -434,16 +434,42 @@ impl Application {
}; };
match notification { match notification {
Notification::PublishDiagnostics(params) => { Notification::Initialized => {
let path = Some(params.uri.to_file_path().unwrap()); 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 let docs = self.editor.documents().filter(|doc| {
.editor doc.language_server().map(|server| server.id()) == Some(server_id)
.documents });
.iter_mut()
.find(|(_, doc)| doc.path() == path.as_ref()); // 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 { if let Some(doc) = doc {
let text = doc.text(); let text = doc.text();
let diagnostics = params let diagnostics = params
@ -506,7 +532,7 @@ impl Application {
log::warn!("unhandled window/showMessage: {:?}", params); log::warn!("unhandled window/showMessage: {:?}", params);
} }
Notification::LogMessage(params) => { Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params); log::info!("window/logMessage: {:?}", params);
} }
Notification::ProgressMessage(params) => { Notification::ProgressMessage(params) => {
let lsp::ProgressParams { token, value } = params; let lsp::ProgressParams { token, value } = params;
@ -588,10 +614,27 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall { Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, .. 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) { let call = match MethodCall::parse(&method, params) {
Some(call) => call, Some(call) => call,
None => { None => {
error!("Method not found {}", method); 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; return;
} }
}; };
@ -604,53 +647,9 @@ impl Application {
if spinner.is_stopped() { if spinner.is_stopped() {
spinner.start(); spinner.start();
} }
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
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
);
}
}
}
} }
} }
// 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), e => unreachable!("{:?}", e),
} }

File diff suppressed because it is too large Load Diff

@ -61,7 +61,7 @@ impl Jobs {
} }
pub fn handle_callback( pub fn handle_callback(
&mut self, &self,
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, compositor: &mut Compositor,
call: anyhow::Result<Option<Callback>>, 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 { if j.wait {
self.wait_futures.push(j.future); self.wait_futures.push(j.future);
} else { } else {

@ -4,6 +4,7 @@ use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent}; use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
borrow::Cow,
collections::HashMap, collections::HashMap,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -47,13 +48,13 @@ macro_rules! keymap {
}; };
(@trie (@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 // modified from the hashmap! macro
{ {
@ -70,7 +71,9 @@ macro_rules! keymap {
_order.push(_key); _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>, map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)] #[serde(skip)]
order: Vec<KeyEvent>, order: Vec<KeyEvent>,
#[serde(skip)]
pub is_sticky: bool,
} }
impl KeyTrieNode { impl KeyTrieNode {
@ -92,6 +97,7 @@ impl KeyTrieNode {
name: name.to_string(), name: name.to_string(),
map, map,
order, order,
is_sticky: false,
} }
} }
@ -119,12 +125,10 @@ impl KeyTrieNode {
} }
} }
} }
}
impl From<KeyTrieNode> for Info { pub fn infobox(&self) -> Info {
fn from(node: KeyTrieNode) -> Self { let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len()); for (&key, trie) in self.iter() {
for (&key, trie) in node.iter() {
let desc = match trie { let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(), KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Node(n) => n.name(), KeyTrie::Node(n) => n.name(),
@ -136,16 +140,16 @@ impl From<KeyTrieNode> for Info {
} }
} }
body.sort_unstable_by_key(|(_, keys)| { 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)) { if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body body = body
.into_iter() .into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect(); .collect();
} }
Info::new(node.name(), body) Info::new(self.name(), body)
} }
} }
@ -218,7 +222,7 @@ impl KeyTrie {
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum KeymapResult { pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke. /// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode), Pending(KeyTrieNode),
Matched(Command), Matched(Command),
@ -229,14 +233,31 @@ pub enum KeymapResult {
Cancelled(Vec<KeyEvent>), 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)] #[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Keymap { pub struct Keymap {
/// Always a Node /// Always a Node
#[serde(flatten)] #[serde(flatten)]
root: KeyTrie, 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)] #[serde(skip)]
state: Vec<KeyEvent>, state: Vec<KeyEvent>,
/// Stores the sticky node if one is activated.
#[serde(skip)]
sticky: Option<KeyTrieNode>,
} }
impl Keymap { impl Keymap {
@ -244,6 +265,7 @@ impl Keymap {
Keymap { Keymap {
root, root,
state: Vec::new(), state: Vec::new(),
sticky: None,
} }
} }
@ -251,27 +273,61 @@ impl Keymap {
&self.root &self.root
} }
pub fn sticky(&self) -> Option<&KeyTrieNode> {
self.sticky.as_ref()
}
/// Returns list of keys waiting to be disambiguated. /// Returns list of keys waiting to be disambiguated.
pub fn pending(&self) -> &[KeyEvent] { pub fn pending(&self) -> &[KeyEvent] {
&self.state &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 { pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
let &first = self.state.get(0).unwrap_or(&key); if let key!(Esc) = key {
let trie = match self.root.search(&[first]) { if !self.state.is_empty() {
Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), return KeymapResult::new(
None => return KeymapResult::NotFound, // 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, Some(t) => t,
}; };
self.state.push(key); self.state.push(key);
match trie.search(&self.state[1..]) { match trie.search(&self.state[1..]) {
Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), Some(&KeyTrie::Node(ref map)) => {
Some(&KeyTrie::Leaf(command)) => { 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(); 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, "A" => append_to_line,
"o" => open_below, "o" => open_below,
"O" => open_above, "O" => open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit)
"d" => delete_selection, "d" => delete_selection,
// TODO: also delete without yanking // TODO: also delete without yanking
@ -440,12 +495,11 @@ impl Default for Keymaps {
"<" => unindent, "<" => unindent,
"=" => format_selections, "=" => format_selections,
"J" => join_selections, "J" => join_selections,
// TODO: conflicts hover/doc
"K" => keep_selections, "K" => keep_selections,
// TODO: and another method for inverse // TODO: and another method for inverse
// TODO: clashes with space mode "," => keep_primary_selection,
"space" => keep_primary_selection, "A-," => remove_primary_selection,
// "q" => record_macro, // "q" => record_macro,
// "Q" => replay_macro, // "Q" => replay_macro,
@ -473,7 +527,6 @@ impl Default for Keymaps {
// move under <space>c // move under <space>c
"C-c" => toggle_comments, "C-c" => toggle_comments,
"K" => hover,
// z family for save/restore/combine from/to sels from register // 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_after,
"P" => paste_clipboard_before, "P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard, "R" => replace_selections_with_clipboard,
"space" => keep_primary_selection, "/" => global_search,
"k" => hover,
}, },
"z" => { "View" "z" => { "View"
"z" | "c" => align_view_center, "z" | "c" => align_view_center,
@ -525,6 +579,22 @@ impl Default for Keymaps {
"m" => align_view_middle, "m" => align_view_middle,
"k" => scroll_up, "k" => scroll_up,
"j" => scroll_down, "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, "\"" => select_register,
@ -545,14 +615,17 @@ impl Default for Keymaps {
"w" => extend_next_word_start, "w" => extend_next_word_start,
"b" => extend_prev_word_start, "b" => extend_prev_word_start,
"e" => extend_next_word_end, "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, "t" => extend_till_char,
"f" => extend_next_char, "f" => extend_next_char,
"T" => extend_till_prev_char, "T" => extend_till_prev_char,
"F" => extend_prev_char, "F" => extend_prev_char,
"home" => goto_line_start, "home" => extend_to_line_start,
"end" => goto_line_end, "end" => extend_to_line_end,
"esc" => exit_select_mode, "esc" => exit_select_mode,
"v" => normal_mode, "v" => normal_mode,
@ -617,19 +690,19 @@ fn merge_partial_keys() {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
keymap.get(key!('i')), keymap.get(key!('i')).kind,
KeymapResult::Matched(Command::normal_mode), KeymapResultKind::Matched(Command::normal_mode),
"Leaf should replace leaf" "Leaf should replace leaf"
); );
assert_eq!( assert_eq!(
keymap.get(key!('无')), keymap.get(key!('无')).kind,
KeymapResult::Matched(Command::insert_mode), KeymapResultKind::Matched(Command::insert_mode),
"New leaf should be present in merged keymap" "New leaf should be present in merged keymap"
); );
// Assumes that z is a node in the default keymap // Assumes that z is a node in the default keymap
assert_eq!( assert_eq!(
keymap.get(key!('z')), keymap.get(key!('z')).kind,
KeymapResult::Matched(Command::jump_backward), KeymapResultKind::Matched(Command::jump_backward),
"Leaf should replace node" "Leaf should replace node"
); );
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap

@ -262,8 +262,7 @@ impl Component for Completion {
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16; - view.offset.row) as u16;
let mut markdown_doc = match &option.documentation {
let mut doc = match &option.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText, kind: lsp::MarkupKind::PlainText,
@ -311,6 +310,23 @@ impl Component for Completion {
None => return, None => return,
}; };
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 {
let half = area.height / 2; let half = area.height / 2;
let height = 15.min(half); let height = 15.min(half);
// we want to make sure the cursor is visible (not hidden behind the documentation) // we want to make sure the cursor is visible (not hidden behind the documentation)
@ -323,12 +339,13 @@ impl Component for Completion {
area.height.saturating_sub(height).saturating_sub(2) 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 // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background); surface.clear_with(area, background);
doc.render(area, surface, cx); markdown_doc.render(area, surface, cx);
} }
} }
} }

@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Compositor, Context, EventResult}, compositor::{Component, Compositor, Context, EventResult},
job::Callback, job::Callback,
key, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, KeymapResultKind, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{Completion, ProgressSpinners},
}; };
@ -165,8 +165,7 @@ impl EditorView {
let scopes = theme.scopes(); let scopes = theme.scopes();
syntax syntax
.highlight_iter(text.slice(..), Some(range), None, |language| { .highlight_iter(text.slice(..), Some(range), None, |language| {
loader loader.language_configuration_for_injection_string(language)
.language_config_for_scope(&format!("source.{}", language))
.and_then(|language_config| { .and_then(|language_config| {
let config = language_config.highlight_config(scopes)?; let config = language_config.highlight_config(scopes)?;
let config_ref = config.as_ref(); let config_ref = config.as_ref();
@ -852,7 +851,7 @@ impl EditorView {
/// Handle events by looking them up in `self.keymaps`. Returns None /// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was /// 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. /// otherwise.
fn handle_keymap_event( fn handle_keymap_event(
&mut self, &mut self,
@ -860,8 +859,6 @@ impl EditorView {
cxt: &mut commands::Context, cxt: &mut commands::Context,
event: KeyEvent, event: KeyEvent,
) -> Option<KeymapResult> { ) -> Option<KeymapResult> {
self.autoinfo = None;
if let Some(picker) = cxt.editor.debug_config_picker.clone() { if let Some(picker) = cxt.editor.debug_config_picker.clone() {
match event { match event {
KeyEvent { KeyEvent {
@ -912,29 +909,32 @@ impl EditorView {
return None; return None;
} }
match self.keymaps.get_mut(&mode).unwrap().get(event) { let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
KeymapResult::Matched(command) => command.execute(cxt), self.autoinfo = key_result.sticky.map(|node| node.infobox());
KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), 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 None
} }
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
match keyresult { match keyresult.kind {
KeymapResult::NotFound => { KeymapResultKind::NotFound => {
if let Some(ch) = event.char() { if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch) commands::insert::insert_char(cx, ch)
} }
} }
KeymapResult::Cancelled(pending) => { KeymapResultKind::Cancelled(pending) => {
for ev in pending { for ev in pending {
match ev.char() { match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch), Some(ch) => commands::insert::insert_char(cx, ch),
None => { None => {
if let KeymapResult::Matched(command) = if let KeymapResultKind::Matched(command) =
self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
{ {
command.execute(cx); command.execute(cx);
} }
@ -972,7 +972,7 @@ impl EditorView {
// debug_assert!(cxt.count != 0); // debug_assert!(cxt.count != 0);
// set the register // 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); self.handle_keymap_event(mode, cxt, event);
if self.keymaps.pending().is_empty() { if self.keymaps.pending().is_empty() {
@ -1196,9 +1196,9 @@ impl EditorView {
impl Component for EditorView { impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context { let mut cxt = commands::Context {
selected_register: helix_view::RegisterSelection::default(),
editor: &mut cx.editor, editor: &mut cx.editor,
count: None, count: None,
register: None,
callback: None, callback: None,
on_next_key_callback: None, on_next_key_callback: None,
jobs: cx.jobs, jobs: cx.jobs,
@ -1288,8 +1288,9 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so // how we entered insert mode is important, and we should track that so
// we can repeat the side effect. // we can repeat the side effect.
self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { self.last_insert.0 =
KeymapResult::Matched(command) => command, match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
KeymapResultKind::Matched(command) => command,
// FIXME: insert mode can only be entered through single KeyCodes // FIXME: insert mode can only be entered through single KeyCodes
_ => unimplemented!(), _ => unimplemented!(),
}; };

@ -88,7 +88,7 @@ fn parse<'a>(
if let Some(theme) = theme { if let Some(theme) = theme {
let rope = Rope::from(text.as_ref()); let rope = Rope::from(text.as_ref());
let syntax = loader let syntax = loader
.language_config_for_scope(&format!("source.{}", language)) .language_configuration_for_injection_string(language)
.and_then(|config| config.highlight_config(theme.scopes())) .and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config)); .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)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2; let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); if padding >= viewport.1 || padding >= viewport.0 {
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); return None;
Some((width, height)) }
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))
} }
} }

@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
scroll: usize, scroll: usize,
size: (u16, u16), size: (u16, u16),
viewport: (u16, u16),
recalculate: bool,
} }
impl<T: Item> Menu<T> { impl<T: Item> Menu<T> {
@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
scroll: 0, scroll: 0,
size: (0, 0), size: (0, 0),
viewport: (0, 0),
recalculate: true,
}; };
// TODO: scoring on empty input should just use a fastpath // TODO: scoring on empty input should just use a fastpath
@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;
self.scroll = 0; self.scroll = 0;
self.recalculate = true;
} }
pub fn move_up(&mut self) { pub fn move_up(&mut self) {
@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
self.adjust_scroll(); 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) { fn adjust_scroll(&mut self) {
let win_height = self.size.1 as usize; let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor { 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)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let n = self if viewport != self.viewport || self.recalculate {
.options self.recalculate_size(viewport);
.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();
Some(self.size) 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) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme; let theme = &cx.editor.theme;
let style = theme let style = theme

@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text; pub use text::Text;
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::register::Registers; use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View}; use helix_view::{Document, Editor, View};
use std::path::PathBuf; use std::path::PathBuf;
@ -28,7 +28,8 @@ use std::path::PathBuf;
pub fn regex_prompt( pub fn regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, 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 { ) -> Prompt {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id; let view_id = view.id;
@ -36,7 +37,7 @@ pub fn regex_prompt(
Prompt::new( Prompt::new(
prompt, prompt,
None, history_register,
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event { match event {
@ -46,6 +47,14 @@ pub fn regex_prompt(
} }
PromptEvent::Validate => { PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump // 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 => { PromptEvent::Update => {
// skip empty input, TODO: trigger default // skip empty input, TODO: trigger default
@ -53,15 +62,23 @@ pub fn regex_prompt(
return; 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) => { Ok(regex) => {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
// revert state to what it was before the last update // revert state to what it was before the last update
doc.set_selection(view.id, snapshot.clone()); 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); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
} }

@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> {
}) { }) {
// align to middle // align to middle
let first_line = line let first_line = line
.map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines()))) .map(|(start, end)| {
.map(|(start, _)| start) let height = end.saturating_sub(start) + 1;
.unwrap_or(0) let middle = start + (height.saturating_sub(1) / 2);
.saturating_sub(inner.height as usize / 2); middle.saturating_sub(inner.height as usize / 2).min(start)
})
.unwrap_or(0);
let offset = Position::new(first_line, 0); let offset = Position::new(first_line, 0);
let highlights = EditorView::doc_syntax_highlights( let highlights = EditorView::doc_syntax_highlights(
@ -268,17 +271,15 @@ impl<T> Picker<T> {
} }
pub fn move_up(&mut self) { 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) { pub fn move_down(&mut self) {
if self.matches.is_empty() { let len = self.matches.len();
return; let pos = (self.cursor + 1) % len;
} self.cursor = pos;
if self.cursor < self.matches.len() - 1 {
self.cursor += 1;
}
} }
pub fn selection(&self) -> Option<&T> { pub fn selection(&self) -> Option<&T> {

@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
} }
impl<T: Component> Popup<T> { 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 { pub fn new(contents: T) -> Self {
Self { Self {
contents, contents,
@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
self.position = pos; 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) { pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction { if direction {
self.scroll += offset; 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) { 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 cx.scroll = Some(self.scroll);
.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));
};
// TODO: be able to specify orientation preference. We want above for most popups, below let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
// for menus/autocomplete.
if height <= rel_y {
rel_y = rel_y.saturating_sub(height) // position above point
} else {
rel_y += 1 // position below point
}
// clip to viewport // 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 // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");

@ -5,11 +5,17 @@ use helix_view::graphics::Rect;
pub struct Text { pub struct Text {
contents: String, contents: String,
size: (u16, u16),
viewport: (u16, u16),
} }
impl Text { impl Text {
pub fn new(contents: String) -> Self { pub fn new(contents: String) -> Self {
Self { contents } Self {
contents,
size: (0, 0),
viewport: (0, 0),
}
} }
} }
impl Component for Text { impl Component for Text {
@ -24,9 +30,13 @@ impl Component for Text {
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
if viewport != self.viewport {
let contents = tui::text::Text::from(self.contents.clone()); let contents = tui::text::Text::from(self.contents.clone());
let width = std::cmp::min(contents.width() as u16, viewport.0); let width = std::cmp::min(contents.width() as u16, viewport.0);
let height = std::cmp::min(contents.height() as u16, viewport.1); let height = std::cmp::min(contents.height() as u16, viewport.1);
Some((width, height)) self.size = (width, height);
self.viewport = viewport;
}
Some(self.size)
} }
} }

@ -386,21 +386,24 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order /// If supported, returns the changes that should be applied to this document in order
/// to format it nicely. /// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { 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 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 fut = async move {
let edits = language_server let edits = request.await.unwrap_or_else(|e| {
.text_document_formatting(id, lsp::FormattingOptions::default(), None)
.await
.unwrap_or_else(|e| {
log::warn!("LSP formatting failed: {}", e); log::warn!("LSP formatting failed: {}", e);
Default::default() Default::default()
}); });
LspFormatting { LspFormatting {
doc: text, doc: text,
edits, edits,
offset_encoding: language_server.offset_encoding(), offset_encoding,
} }
}; };
Some(fut) Some(fut)
@ -469,9 +472,14 @@ impl Document {
to_writer(&mut file, encoding, &text).await?; to_writer(&mut file, encoding, &text).await?;
if let Some(language_server) = language_server { if let Some(language_server) = language_server {
language_server if !language_server.is_initialized() {
.text_document_did_save(identifier, &text) return Ok(());
.await?; }
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
{
notification.await?;
}
} }
Ok(()) Ok(())
@ -646,7 +654,7 @@ impl Document {
// } // }
// emit lsp notification // 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( let notify = language_server.text_document_did_change(
self.versioned_identifier(), self.versioned_identifier(),
&old_doc, &old_doc,
@ -795,9 +803,18 @@ impl Document {
self.version self.version
} }
#[inline]
pub fn language_server(&self) -> Option<&helix_lsp::Client> { 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] #[inline]
@ -891,6 +908,40 @@ impl Default for Document {
mod test { mod test {
use super::*; 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] #[test]
fn changeset_to_changes() { fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding}; use helix_lsp::{lsp, Client, OffsetEncoding};

@ -3,7 +3,7 @@ use crate::{
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
theme::{self, Theme}, theme::{self, Theme},
tree::Tree, tree::Tree,
Document, DocumentId, RegisterSelection, View, ViewId, Document, DocumentId, View, ViewId,
}; };
use futures_util::future; use futures_util::future;
@ -44,6 +44,10 @@ pub struct Config {
pub line_number: LineNumber, pub line_number: LineNumber,
/// Middle click paste support. Defaults to true /// Middle click paste support. Defaults to true
pub middle_click_paste: bool, 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)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@ -69,6 +73,8 @@ impl Default for Config {
}, },
line_number: LineNumber::Absolute, line_number: LineNumber::Absolute,
middle_click_paste: true, middle_click_paste: true,
smart_case: true,
auto_pairs: true,
} }
} }
} }
@ -78,7 +84,7 @@ pub struct Editor {
pub tree: Tree, pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>, pub documents: SlotMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>, pub count: Option<std::num::NonZeroUsize>,
pub selected_register: RegisterSelection, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
pub theme: Theme, pub theme: Theme,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
@ -125,7 +131,7 @@ impl Editor {
tree: Tree::new(area), tree: Tree::new(area),
documents: SlotMap::with_key(), documents: SlotMap::with_key(),
count: None, count: None,
selected_register: RegisterSelection::default(), selected_register: None,
theme: themes.default(), theme: themes.default(),
language_servers, language_servers,
debugger: None, debugger: None,
@ -270,26 +276,31 @@ impl Editor {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; 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 // try to find a language server based on the language name
let language_server = doc let language_server = doc.language.as_ref().and_then(|language| {
.language self.language_servers
.as_ref() .get(language)
.and_then(|language| self.language_servers.get(language).ok()); .map_err(|e| {
log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
})
.ok()
});
if let Some(language_server) = language_server { if let Some(language_server) = language_server {
doc.set_language_server(Some(language_server.clone()));
let language_id = doc let language_id = doc
.language() .language()
.and_then(|s| s.split('.').last()) // source.rust .and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.unwrap_or_default(); .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( tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(), doc.url().unwrap(),
doc.version(), doc.version(),
doc.text(), doc.text(),
language_id, language_id,
)); ));
doc.set_language_server(Some(language_server));
} }
let id = self.documents.insert(doc); let id = self.documents.insert(doc);
@ -308,14 +319,9 @@ impl Editor {
if close_buffer { if close_buffer {
// get around borrowck issues // get around borrowck issues
let language_servers = &mut self.language_servers;
let doc = &self.documents[view.doc]; let doc = &self.documents[view.doc];
let language_server = doc if let Some(language_server) = doc.language_server() {
.language
.as_ref()
.and_then(|language| language_servers.get(language).ok());
if let Some(language_server) = language_server {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
self.documents.remove(view.doc); self.documents.remove(view.doc);
@ -345,20 +351,24 @@ impl Editor {
view.ensure_cursor_in_view(doc, self.config.scrolloff) view.ensure_cursor_in_view(doc, self.config.scrolloff)
} }
#[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> { pub fn document(&self, id: DocumentId) -> Option<&Document> {
self.documents.get(id) self.documents.get(id)
} }
#[inline]
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
self.documents.get_mut(id) self.documents.get_mut(id)
} }
#[inline]
pub fn documents(&self) -> impl Iterator<Item = &Document> { 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> { 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> { 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)) .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
} }
// pub fn current_document(&self) -> Document { pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
// let id = self.view().doc; self.documents_mut()
// let doc = &mut editor.documents[id]; .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
// } }
pub fn cursor(&self) -> (Option<Position>, CursorKind) { pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self); let view = view!(self);

@ -224,13 +224,13 @@ pub enum Color {
Magenta, Magenta,
Cyan, Cyan,
Gray, Gray,
DarkGray,
LightRed, LightRed,
LightGreen, LightGreen,
LightYellow, LightYellow,
LightBlue, LightBlue,
LightMagenta, LightMagenta,
LightCyan, LightCyan,
LightGray,
White, White,
Rgb(u8, u8, u8), Rgb(u8, u8, u8),
Indexed(u8), Indexed(u8),
@ -250,14 +250,14 @@ impl From<Color> for crossterm::style::Color {
Color::Blue => CColor::DarkBlue, Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta, Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan, Color::Cyan => CColor::DarkCyan,
Color::Gray => CColor::Grey, Color::Gray => CColor::DarkGrey,
Color::DarkGray => CColor::DarkGrey,
Color::LightRed => CColor::Red, Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green, Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue, Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow, Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta, Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan, Color::LightCyan => CColor::Cyan,
Color::LightGray => CColor::Grey,
Color::White => CColor::White, Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i), Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },

@ -8,7 +8,6 @@ pub mod graphics;
pub mod info; pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;
pub mod register_selection;
pub mod theme; pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
@ -20,6 +19,5 @@ slotmap::new_key_type! {
pub use document::Document; pub use document::Document;
pub use editor::Editor; pub use editor::Editor;
pub use register_selection::RegisterSelection;
pub use theme::Theme; pub use theme::Theme;
pub use view::View; pub use view::View;

@ -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_export]
macro_rules! current { macro_rules! current {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{
@ -7,6 +18,8 @@ macro_rules! current {
}}; }};
} }
/// Get the current document mutably.
/// Returns `&mut Document`
#[macro_export] #[macro_export]
macro_rules! doc_mut { macro_rules! doc_mut {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{
@ -14,6 +27,8 @@ macro_rules! doc_mut {
}}; }};
} }
/// Get the current view mutably.
/// Returns `&mut View`
#[macro_export] #[macro_export]
macro_rules! view_mut { macro_rules! view_mut {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{
@ -21,6 +36,8 @@ macro_rules! view_mut {
}}; }};
} }
/// Get the current view immutably
/// Returns `&View`
#[macro_export] #[macro_export]
macro_rules! view { macro_rules! view {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{

@ -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,
}
}
}

@ -5,6 +5,7 @@ use std::{
}; };
use anyhow::Context; use anyhow::Context;
use helix_core::hashmap;
use log::warn; use log::warn;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -142,13 +143,37 @@ struct ThemePalette {
impl Default for ThemePalette { impl Default for ThemePalette {
fn default() -> Self { 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 { impl ThemePalette {
pub fn new(palette: HashMap<String, Color>) -> Self { 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> { pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {

@ -65,6 +65,7 @@ file-types = ["ex", "exs"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]] [[language]]
@ -198,6 +199,17 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"] } language-server = { command = "typescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " } 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]] [[language]]
name = "css" name = "css"
scope = "source.css" scope = "source.css"
@ -236,6 +248,7 @@ file-types = ["nix"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
language-server = { command = "rnix-lsp" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]] [[language]]
@ -286,7 +299,22 @@ injection-regex = "julia"
file-types = ["jl"] file-types = ["jl"]
roots = [] roots = []
comment-token = "#" 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 = " " } indent = { tab-width = 2, unit = " " }
[[language]] [[language]]
@ -331,6 +359,15 @@ roots = []
comment-token = "--" comment-token = "--"
indent = { tab-width = 2, unit = " " } 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]] [[language]]
name = "yaml" name = "yaml"
scope = "source.yaml" scope = "source.yaml"

@ -61,7 +61,7 @@
(null) @constant (null) @constant
(number_literal) @number (number_literal) @number
(char_literal) @number (char_literal) @string
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)

@ -17,9 +17,18 @@
; Identifiers ; 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 (type_identifier) @type
(field_identifier) @property (field_identifier) @property
(identifier) @variable (identifier) @variable
(package_identifier) @variable
; Operators ; Operators
@ -79,10 +88,8 @@
"go" "go"
"goto" "goto"
"if" "if"
"import"
"interface" "interface"
"map" "map"
"package"
"range" "range"
"return" "return"
"select" "select"
@ -92,6 +99,29 @@
"var" "var"
] @keyword ] @keyword
[
"import"
"package"
] @keyword.control.import
; Delimiters
[
":"
"."
","
";"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Literals ; Literals
[ [
@ -111,7 +141,8 @@
[ [
(true) (true)
(false) (false)
(nil) ] @constant.builtin.boolean
] @constant.builtin
(nil) @constant.builtin
(comment) @comment (comment) @comment

@ -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

@ -2,19 +2,19 @@
(operator) @operator (operator) @operator
(exp_name (constructor) @constructor) (exp_name (constructor) @constructor)
(constructor_operator) @operator (constructor_operator) @operator
(module) @module_name (module) @namespace
(type) @type (type) @type
(type) @class (type) @class
(constructor) @constructor (constructor) @constructor
(pragma) @pragma (pragma) @pragma
(comment) @comment (comment) @comment
(signature name: (variable) @fun_type_name) (signature name: (variable) @fun_type_name)
(function name: (variable) @fun_name) (function name: (variable) @function)
(constraint class: (class_name (type)) @class) (constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class)) (class (class_head class: (class_name (type)) @class))
(instance (instance_head class: (class_name (type)) @class)) (instance (instance_head class: (class_name (type)) @class))
(integer) @literal (integer) @number
(exp_literal (float)) @literal (exp_literal (float)) @number
(char) @literal (char) @literal
(con_unit) @literal (con_unit) @literal
(con_list) @literal (con_list) @literal
@ -39,5 +39,7 @@
"do" @keyword "do" @keyword
"mdo" @keyword "mdo" @keyword
"rec" @keyword "rec" @keyword
"(" @paren [
")" @paren "("
")"
] @punctuation.bracket

@ -87,7 +87,7 @@
(template_string) (template_string)
] @string ] @string
(regex) @string.special (regex) @string.regexp
(number) @number (number) @number
; Tokens ; Tokens

@ -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) (triple_string)
@ -28,43 +22,43 @@
(call_expression (call_expression
(identifier) @function) (identifier) @function)
(call_expression (call_expression
(field_expression (identifier) @method .)) (field_expression (identifier) @function.method .))
(broadcast_call_expression (broadcast_call_expression
(identifier) @function) (identifier) @function)
(broadcast_call_expression (broadcast_call_expression
(field_expression (identifier) @method .)) (field_expression (identifier) @function.method .))
(parameter_list (parameter_list
(identifier) @parameter) (identifier) @variable.parameter)
(parameter_list (parameter_list
(optional_parameter . (optional_parameter .
(identifier) @parameter)) (identifier) @variable.parameter))
(typed_parameter (typed_parameter
(identifier) @parameter (identifier) @variable.parameter
(identifier) @type) (identifier) @type)
(type_parameter_list (type_parameter_list
(identifier) @type) (identifier) @type)
(typed_parameter (typed_parameter
(identifier) @parameter (identifier) @variable.parameter
(parameterized_identifier) @type) (parameterized_identifier) @type)
(function_expression (function_expression
. (identifier) @parameter) . (identifier) @variable.parameter)
(spread_parameter) @parameter (spread_parameter) @variable.parameter
(spread_parameter (spread_parameter
(identifier) @parameter) (identifier) @variable.parameter)
(named_argument (named_argument
. (identifier) @parameter) . (identifier) @variable.parameter)
(argument_list (argument_list
(typed_expression (typed_expression
(identifier) @parameter (identifier) @variable.parameter
(identifier) @type)) (identifier) @type))
(argument_list (argument_list
(typed_expression (typed_expression
(identifier) @parameter (identifier) @variable.parameter
(parameterized_identifier) @type)) (parameterized_identifier) @type))
;; Symbol expressions (:my-wanna-be-lisp-keyword) ;; Symbol expressions (:my-wanna-be-lisp-keyword)
(quote_expression (quote_expression
(identifier)) @symbol (identifier)) @string.special.symbol
;; Parsing error! foo (::Type) get's parsed as two quote expressions ;; Parsing error! foo (::Type) get's parsed as two quote expressions
(argument_list (argument_list
@ -76,7 +70,7 @@
(identifier) @type) (identifier) @type)
(parameterized_identifier (_)) @type (parameterized_identifier (_)) @type
(argument_list (argument_list
(typed_expression . (identifier) @parameter)) (typed_expression . (identifier) @variable.parameter))
(typed_expression (typed_expression
(identifier) @type .) (identifier) @type .)
@ -113,13 +107,13 @@
"end" @keyword "end" @keyword
(if_statement (if_statement
["if" "end"] @conditional) ["if" "end"] @keyword.control.conditional)
(elseif_clause (elseif_clause
["elseif"] @conditional) ["elseif"] @keyword.control.conditional)
(else_clause (else_clause
["else"] @conditional) ["else"] @keyword.control.conditional)
(ternary_expression (ternary_expression
["?" ":"] @conditional) ["?" ":"] @keyword.control.conditional)
(function_definition ["function" "end"] @keyword.function) (function_definition ["function" "end"] @keyword.function)
@ -134,47 +128,57 @@
"type" "type"
] @keyword ] @keyword
((identifier) @keyword (#any-of? @keyword "global" "local")) ((identifier) @keyword (match? @keyword "global|local"))
(compound_expression (compound_expression
["begin" "end"] @keyword) ["begin" "end"] @keyword)
(try_statement (try_statement
["try" "end" ] @exception) ["try" "end" ] @keyword.control.exception)
(finally_clause (finally_clause
"finally" @exception) "finally" @keyword.control.exception)
(catch_clause (catch_clause
"catch" @exception) "catch" @keyword.control.exception)
(quote_statement (quote_statement
["quote" "end"] @keyword) ["quote" "end"] @keyword)
(let_statement (let_statement
["let" "end"] @keyword) ["let" "end"] @keyword)
(for_statement (for_statement
["for" "end"] @repeat) ["for" "end"] @keyword.control.repeat)
(while_statement (while_statement
["while" "end"] @repeat) ["while" "end"] @keyword.control.repeat)
(break_statement) @repeat (break_statement) @keyword.control.repeat
(continue_statement) @repeat (continue_statement) @keyword.control.repeat
(for_binding (for_binding
"in" @repeat) "in" @keyword.control.repeat)
(for_clause (for_clause
"for" @repeat) "for" @keyword.control.repeat)
(do_clause (do_clause
["do" "end"] @keyword) ["do" "end"] @keyword)
(export_statement (export_statement
["export"] @include) ["export"] @keyword.control.import)
[ [
"using" "using"
"module" "module"
"import" "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) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
(((identifier) @boolean) (eq? @boolean "true")) (((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true"))
(((identifier) @boolean) (eq? @boolean "false")) (((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false"))
["::" ":" "." "," "..." "!"] @punctuation.delimiter ["::" ":" "." "," "..." "!"] @punctuation.delimiter
["[" "]" "(" ")" "{" "}"] @punctuation.bracket ["[" "]" "(" ")" "{" "}"] @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_]*$"))

@ -259,7 +259,7 @@
(comment) @comment (comment) @comment
(bracket_group) @parameter (bracket_group) @variable.parameter
[(math_operator) "="] @operator [(math_operator) "="] @operator
@ -312,7 +312,7 @@
key: (word) @text.reference) key: (word) @text.reference)
(key_val_pair (key_val_pair
key: (_) @parameter key: (_) @variable.parameter
value: (_)) value: (_))
["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX

@ -1 +1,2 @@
(comment) @comment (comment) @comment
(note) @comment

@ -23,27 +23,27 @@
"for" "for"
"do" "do"
"end" "end"
] @keyword.control.loop) ] @keyword.control.repeat)
(for_in_statement (for_in_statement
[ [
"for" "for"
"do" "do"
"end" "end"
] @keyword.control.loop) ] @keyword.control.repeat)
(while_statement (while_statement
[ [
"while" "while"
"do" "do"
"end" "end"
] @keyword.control.loop) ] @keyword.control.repeat)
(repeat_statement (repeat_statement
[ [
"repeat" "repeat"
"until" "until"
] @keyword.control.loop) ] @keyword.control.repeat)
(do_statement (do_statement
[ [
@ -65,7 +65,7 @@
"not" "not"
"and" "and"
"or" "or"
] @keyword.operator ] @operator
[ [
"=" "="
@ -108,7 +108,7 @@
[ [
(false) (false)
(true) (true)
] @boolean ] @constant.builtin.boolean
(nil) @constant.builtin (nil) @constant.builtin
(spread) @constant ;; "..." (spread) @constant ;; "..."
((identifier) @constant ((identifier) @constant
@ -116,7 +116,7 @@
;; Parameters ;; Parameters
(parameters (parameters
(identifier) @parameter) (identifier) @variable.parameter)
; ;; Functions ; ;; Functions
(function [(function_name) (identifier)] @function) (function [(function_name) (identifier)] @function)
@ -139,8 +139,8 @@
(function_call (function_call
[ [
((identifier) @variable (method) @method) ((identifier) @variable (method) @function.method)
((_) (method) @method) ((_) (method) @function.method)
(identifier) @function (identifier) @function
(field_expression (property_identifier) @function) (field_expression (property_identifier) @function)
] ]

@ -25,12 +25,12 @@
(external (value_name) @function) (external (value_name) @function)
(method_name) @method (method_name) @function.method
; Variables ; Variables
;---------- ;----------
(value_pattern) @parameter (value_pattern) @variable.parameter
; Application ; Application
;------------ ;------------
@ -60,7 +60,7 @@
[(number) (signed_number)] @number [(number) (signed_number)] @number
(character) @character (character) @constant.character
(string) @string (string) @string
@ -92,7 +92,7 @@
["include" "open"] @include ["include" "open"] @include
["for" "to" "downto" "while" "do" "done"] @keyword.control.loop ["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
; Macros ; Macros
;------- ;-------

@ -100,7 +100,7 @@
(bare_symbol) (bare_symbol)
] @string.special.symbol ] @string.special.symbol
(regex) @string.special.regex (regex) @string.regexp
(escape_sequence) @escape (escape_sequence) @escape
[ [

@ -17,7 +17,7 @@
(escape_sequence) @escape (escape_sequence) @escape
(primitive_type) @type.builtin (primitive_type) @type.builtin
(boolean_literal) @constant.builtin (boolean_literal) @constant.builtin.boolean
[ [
(integer_literal) (integer_literal)
(float_literal) (float_literal)
@ -149,7 +149,7 @@
(mutable_specifier) @keyword.mut (mutable_specifier) @keyword.mut
; TODO: variable.mut to highlight mutable identifiers via locals.scm
; ------- ; -------
; Guess Other Types ; Guess Other Types

@ -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

@ -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

@ -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"

@ -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

@ -0,0 +1 @@
; inherits: typescript

@ -1,6 +1,6 @@
(block_mapping_pair key: (_) @property) (block_mapping_pair key: (_) @property)
(flow_mapping (_ key: (_) @property)) (flow_mapping (_ key: (_) @property))
(boolean_scalar) @boolean (boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin (null_scalar) @constant.builtin
(double_quote_scalar) @string (double_quote_scalar) @string
(single_quote_scalar) @string (single_quote_scalar) @string

@ -34,6 +34,7 @@
"comment" = { fg = "#6A9955" } "comment" = { fg = "#6A9955" }
"string" = { fg = "#ce9178" } "string" = { fg = "#ce9178" }
"string.regexp" = { fg = "regex" }
"number" = { fg = "#b5cea8" } "number" = { fg = "#b5cea8" }
"escape" = { fg = "#d7ba7d" } "escape" = { fg = "#d7ba7d" }
@ -61,7 +62,7 @@
"ui.text.focus" = { fg = "#ffffff" } "ui.text.focus" = { fg = "#ffffff" }
"warning" = { fg = "#cca700" } "warning" = { fg = "#cca700" }
"error" = { fg = "#f48771" } "error" = { fg = "#ff1212" }
"info" = { fg = "#75beff" } "info" = { fg = "#75beff" }
"hint" = { fg = "#eeeeeeb3" } "hint" = { fg = "#eeeeeeb3" }

@ -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"

@ -34,6 +34,7 @@
"comment" = { fg = "#88846F" } "comment" = { fg = "#88846F" }
"string" = { fg = "#e6db74" } "string" = { fg = "#e6db74" }
"string.regexp" = { fg = "regex" }
"number" = { fg = "#ae81ff" } "number" = { fg = "#ae81ff" }
"escape" = { fg = "#ae81ff" } "escape" = { fg = "#ae81ff" }

@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "rust-src"]

@ -9,7 +9,8 @@ special = "honey"
property = "white" property = "white"
variable = "lavender" variable = "lavender"
# variable = "almond" # TODO: metavariables only # variable = "almond" # TODO: metavariables only
"variable.parameter" = "lavender" # "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }
"variable.parameter" = { fg = "lavender" }
"variable.builtin" = "mint" "variable.builtin" = "mint"
type = "white" type = "white"
"type.builtin" = "white" # TODO: distinguish? "type.builtin" = "white" # TODO: distinguish?
@ -28,9 +29,7 @@ escape = "honey"
label = "honey" label = "honey"
# TODO: diferentiate doc comment # TODO: diferentiate doc comment
# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors # concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
module = "#ff0000"
"ui.background" = { bg = "midnight" } "ui.background" = { bg = "midnight" }
"ui.linenr" = { fg = "comet" } "ui.linenr" = { fg = "comet" }

Loading…
Cancel
Save