Merge branch 'master' into debug

imgbot
Dmitry Sharshakov 3 years ago
commit bf53aff27d

4
.gitmodules vendored

@ -118,3 +118,7 @@
path = helix-syntax/languages/tree-sitter-zig
url = https://github.com/maxxnino/tree-sitter-zig
shallow = true
[submodule "helix-syntax/languages/tree-sitter-svelte"]
path = helix-syntax/languages/tree-sitter-svelte
url = https://github.com/Himujjal/tree-sitter-svelte
shallow = true

117
Cargo.lock generated

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

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

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

@ -5,6 +5,21 @@ To override global configuration parameters, create a `config.toml` file located
* Linux and Mac: `~/.config/helix/config.toml`
* Windows: `%AppData%\helix\config.toml`
## Editor
`[editor]` section of the config.
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` |
| `mouse` | Enable mouse mode. | `true` |
| `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
## LSP
To display all language server messages in the status line add the following to your `config.toml`:

@ -7,4 +7,6 @@ going to act on (a word, a paragraph, a line, etc) is selected first and the
action itself (delete, change, yank, etc) comes second. A cursor is simply a
single width selection.
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
> TODO: Mention texobjects, surround, registers

@ -23,7 +23,9 @@ shell for working on Helix.
### Arch Linux
Binary packages are available on AUR:
Releases are available in the `community` repository.
Packages are also available on AUR:
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch

@ -4,40 +4,40 @@
### Movement
> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_char_right` |
| `k`, `Up` | Move up | `move_line_up` |
| `l`, `Right` | Move right | `move_line_down` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` |
| `W` | Move next WORD start | `move_next_long_word_start` |
| `B` | Move previous WORD start | `move_prev_long_word_start` |
| `E` | Move next WORD end | `move_next_long_word_end` |
| `t` | Find 'till next char | `find_till_char` |
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` |
| `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A |
| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) | N/A |
| `Space` | Enter [space mode](#space-mode) | N/A |
| `K` | Show documentation for the item under the cursor | `hover` |
> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_char_right` |
| `k`, `Up` | Move up | `move_line_up` |
| `l`, `Right` | Move right | `move_line_down` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` |
| `W` | Move next WORD start | `move_next_long_word_start` |
| `B` | Move previous WORD start | `move_prev_long_word_start` |
| `E` | Move next WORD end | `move_next_long_word_end` |
| `t` | Find 'till next char | `find_till_char` |
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` |
| `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A |
| `Z` | Enter sticky [view mode](#view-mode) | N/A |
| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
| `Space` | Enter [space mode](#space-mode) | N/A |
### Changes
@ -66,6 +66,16 @@
| `d` | Delete selection | `delete_selection` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
#### Shell
| Key | Description | Command |
| ------ | ----------- | ------- |
| <code>&#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
| Key | Description | Command |
@ -75,6 +85,7 @@
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` |
| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
| `(` | Rotate main selection forward | `rotate_selections_backward` |
@ -86,22 +97,13 @@
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` |
| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` |
| `K` | Keep selections matching the regex | `keep_selections` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
### Insert Mode
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching
in reverse, or searching via smartcase.
> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
| Key | Description | Command |
| ----- | ----------- | ------- |
@ -110,41 +112,17 @@ in reverse, or searching via smartcase.
| `N` | Add next search match to selection | `extend_search_next` |
| `*` | Use current selection as the search pattern | `search_selection` |
### Unimpaired
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
| Key | Description | Command |
| ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
### Shell
### Minor modes
| Key | Description | Command |
| ------ | ----------- | ------- |
| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
## Select / extend mode
I'm still pondering whether to keep this mode or not. It changes movement
commands to extend the existing selection instead of replacing it.
> NOTE: It's a bit confusing at the moment because extend hasn't been
> implemented for all movement commands yet.
These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
## View mode
#### View mode
View mode is intended for scrolling and manipulating the view without changing
the selection.
the selection. The "sticky" variant of this mode is persistent; use the Escape
key to return to normal mode after usage (useful when you're simply looking
over text and not actively editing it).
| Key | Description | Command |
| ----- | ----------- | ------- |
@ -154,8 +132,12 @@ the selection.
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` |
## Goto mode
#### Goto mode
Jumps to various locations.
@ -177,7 +159,7 @@ Jumps to various locations.
| `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
## Match mode
#### Match mode
Enter this mode using `m` from normal mode. See the relavant section
in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
@ -192,11 +174,9 @@ and [textobject](./usage.md#textobject) usage.
| `a` `<object>` | Select around textobject | `select_textobject_around` |
| `i` `<object>` | Select inside textobject | `select_textobject_inner` |
## Object mode
TODO: Mappings for selecting syntax nodes (a superset of `[`).
## Window mode
#### Window mode
This layer is similar to vim keybindings as kakoune does not support window.
@ -207,24 +187,56 @@ This layer is similar to vim keybindings as kakoune does not support window.
| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
| `q`, `Ctrl-q` | Close current window | `wclose` |
## Space mode
#### Space mode
This layer is a kludge of mappings I had under leader key in neovim.
This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `k` | Show documentation for the item under the cursor | `hover` |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` |
| `a` | Apply code action | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `space` | Keep primary selection TODO: it's here because space mode replaced it | `keep_primary_selection` |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
#### Unimpaired
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
## Insert Mode
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
## Select / extend mode
I'm still pondering whether to keep this mode or not. It changes movement
commands (including goto) to extend the existing selection instead of replacing it.
> NOTE: It's a bit confusing at the moment because extend hasn't been
> implemented for all movement commands yet.
# Picker

@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Null | `"null"` |
| Escape | `"esc"` |
Keys can be disabled by binding them to the `no_op` command.
Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)

@ -30,7 +30,52 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as
"key.key" = "#ffffff"
```
Possible modifiers:
### Color palettes
It's recommended define a palette of named colors, and refer to them from the
configuration values in your theme. To do this, add a table called
`palette` to your theme file:
```toml
ui.background = "white"
ui.text = "black"
[palette]
white = "#ffffff"
black = "#000000"
```
Remember that the `[palette]` table includes all keys after its header,
so you should define the palette after normal theme options.
The default palette uses the terminal's default 16 colors, and the colors names
are listed below. The `[palette]` section in the config file takes precedence
over it and is merged into the default palette.
| Color Name |
| --- |
| `black` |
| `red` |
| `green` |
| `yellow` |
| `blue` |
| `magenta` |
| `cyan` |
| `gray` |
| `light-red` |
| `light-green` |
| `light-yellow` |
| `light-blue` |
| `light-magenta` |
| `light-cyan` |
| `light-gray` |
| `white` |
### Modifiers
The following values may be used as modifiers.
Less common modifiers might not be supported by your terminal emulator.
| Modifier |
| --- |
@ -38,44 +83,88 @@ Possible modifiers:
| `dim` |
| `italic` |
| `underlined` |
| `slow\_blink` |
| `rapid\_blink` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed\_out` |
| `crossed_out` |
### Scopes
The following is a list of scopes available to use for styling.
#### Syntax highlighting
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme).
For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`.
We use a similar set of scopes as
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
- `escape` (TODO: rename to (constant).character.escape)
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
- `constant` (TODO: constant.other.placeholder for %v)
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
- `boolean`
- `character`
- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
- `regexp` - Regular expressions
- `special`
- `path`
- `url`
- `comment` - Code comments
- `line` - Single line comments (`//`)
- `block` - Block comments (e.g. (`/* */`)
- `documentation` - Documentation comments (e.g. `///` in Rust)
- `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
- `parameter` - Function parameters
- `property`
- `function` (TODO: ?)
- `label`
- `punctuation`
- `delimiter` - Commas, colons
- `bracket` - Parentheses, angle brackets, etc.
- `keyword`
- `control`
- `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- (TODO: return?)
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
- `operator` - `||`, `+=`, `>`, `or`
- `function`
- `builtin`
- `method`
- `macro`
- `special` (preprocesor in C)
- `tag` - Tags (e.g. `<body>` in HTML)
- `namespace`
#### Interface
These scopes are used for theming the editor interface.
Possible keys:
| Key | Notes |
| --- | --- |
| `attribute` | |
| `keyword` | |
| `keyword.directive` | Preprocessor directives (\#if in C) |
| `keyword.control` | Control flow |
| `namespace` | |
| `punctuation` | |
| `punctuation.delimiter` | |
| `operator` | |
| `special` | |
| `property` | |
| `variable` | |
| `variable.parameter` | |
| `type` | |
| `type.builtin` | |
| `type.enum.variant` | Enum variants |
| `constructor` | |
| `function` | |
| `function.macro` | |
| `function.builtin` | |
| `comment` | |
| `variable.builtin` | |
| `constant` | |
| `constant.builtin` | |
| `string` | |
| `number` | |
| `escape` | Escaped characters |
| `label` | For lifetimes |
| `module` | |
| `ui.background` | |
| `ui.cursor` | |
| `ui.cursor.insert` | |
@ -84,8 +173,8 @@ Possible keys:
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.linenr` | |
| `ui.linenr.selected` | |
| `ui.statusline` | |
| `ui.statusline.inactive` | |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
@ -97,29 +186,9 @@ Possible keys:
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `warning` | LSP warning |
| `error` | LSP error |
| `info` | LSP info |
| `hint` | LSP hint |
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | For text in editing area |
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
## Color palettes
You can define a palette of named colors, and refer to them from the
configuration values in your theme. To do this, add a table called
`palette` to your theme file:
```toml
ui.background = "white"
ui.text = "black"
[palette]
white = "#ffffff"
black = "#000000"
```
Remember that the `[palette]` table includes all keys after its header,
so you should define the palette after normal theme options.

@ -114,6 +114,19 @@ h6:target::before {
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul, .content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }

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

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

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

@ -30,22 +30,7 @@
};
# link languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = prev: {
src =
let
pkgs = common.pkgs;
helix = pkgs.fetchgit {
url = "https://github.com/helix-editor/helix.git";
rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5";
fetchSubmodules = true;
sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ=";
};
in
pkgs.runCommand prev.src.name { } ''
mkdir -p $out
ln -s ${prev.src}/* $out
ln -sf ${helix}/helix-syntax/languages $out
'';
helix-syntax = _prev: {
preConfigure = "mkdir -p ../runtime/grammars";
postInstall = "cp -r ../runtime $out/runtime";
};

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

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

@ -360,6 +360,15 @@ impl Selection {
self.normalize()
}
/// Adds a new range to the selection and makes it the primary range.
pub fn remove(mut self, index: usize) -> Self {
self.ranges.remove(index);
if index < self.primary_index || self.primary_index == self.ranges.len() {
self.primary_index -= 1;
}
self
}
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self {

@ -21,6 +21,15 @@ use std::{
use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize};
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom))
.transpose()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
@ -42,7 +51,8 @@ pub struct LanguageConfiguration {
pub auto_format: bool,
// content_regex
// injection_regex
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
pub injection_regex: Option<Regex>,
// first_line_regex
//
#[serde(skip)]
@ -182,8 +192,12 @@ impl LanguageConfiguration {
&highlights_query,
&injections_query,
&locals_query,
)
.unwrap(); // TODO: no unwrap
);
let config = match config {
Ok(config) => config,
Err(err) => panic!("{}", err),
}; // TODO: avoid panic
config.configure(scopes);
Some(Arc::new(config))
}
@ -277,6 +291,30 @@ impl Loader {
.cloned()
}
pub fn language_configuration_for_injection_string(
&self,
string: &str,
) -> Option<Arc<LanguageConfiguration>> {
let mut best_match_length = 0;
let mut best_match_position = None;
for (i, configuration) in self.language_configs.iter().enumerate() {
if let Some(injection_regex) = &configuration.injection_regex {
if let Some(mat) = injection_regex.find(string) {
let length = mat.end() - mat.start();
if length > best_match_length {
best_match_position = Some(i);
best_match_length = length;
}
}
}
}
if let Some(i) = best_match_position {
let configuration = &self.language_configs[i];
return Some(configuration.clone());
}
None
}
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
@ -314,16 +352,6 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
Cow::from(source.slice(start_char..end_char))
}
fn node_to_bytes<'a>(node: Node, source: RopeSlice<'a>) -> Cow<'a, [u8]> {
let start_char = source.byte_to_char(node.start_byte());
let end_char = source.byte_to_char(node.end_byte());
let fragment = source.slice(start_char..end_char);
match fragment.as_str() {
Some(fragment) => Cow::Borrowed(fragment.as_bytes()),
None => Cow::Owned(String::from(fragment).into_bytes()),
}
}
impl Syntax {
// buffer, grammar, config, grammars, sync_timeout?
pub fn new(
@ -416,16 +444,11 @@ impl Syntax {
let config_ref =
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
// TODO: if reusing cursors this might need resetting
if let Some(range) = &range {
cursor_ref.set_byte_range(range.start, range.end);
}
// if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
let captures = cursor_ref
.captures(query_ref, tree_ref.root_node(), move |n: Node| {
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.captures(query_ref, tree_ref.root_node(), RopeProvider(source))
.peekable();
// manually craft the root layer based on the existing tree
@ -539,10 +562,7 @@ impl LanguageLayer {
// let mut injections_by_pattern_index =
// vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
// let matches =
// cursor.matches(combined_injections_query, tree.root_node(), |n: Node| {
// // &source[n.byte_range()]
// node_to_bytes(n, source)
// });
// cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
// for mat in matches {
// let entry = &mut injections_by_pattern_index[mat.pattern_index];
// let (language_name, content_node, include_children) =
@ -754,7 +774,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, Tree,
QueryMatch, Range, TextProvider, Tree,
};
const CANCELLATION_CHECK_INTERVAL: usize = 100;
@ -814,7 +834,7 @@ struct LocalScope<'a> {
}
#[derive(Debug)]
struct HighlightIter<'a, 'tree: 'a, F>
struct HighlightIter<'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@ -822,16 +842,41 @@ where
byte_offset: usize,
injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a, 'tree>>,
layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize,
next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>,
}
struct HighlightIterLayer<'a, 'tree: 'a> {
// Adapter to convert rope chunks to bytes
struct ChunksBytes<'a> {
chunks: ropey::iter::Chunks<'a>,
}
impl<'a> Iterator for ChunksBytes<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
self.chunks.next().map(str::as_bytes)
}
}
struct RopeProvider<'a>(RopeSlice<'a>);
impl<'a> TextProvider<'a> for RopeProvider<'a> {
type I = ChunksBytes<'a>;
fn text(&mut self, node: Node) -> Self::I {
let start_char = self.0.byte_to_char(node.start_byte());
let end_char = self.0.byte_to_char(node.end_byte());
let fragment = self.0.slice(start_char..end_char);
ChunksBytes {
chunks: fragment.chunks(),
}
}
}
struct HighlightIterLayer<'a> {
_tree: Option<Tree>,
cursor: QueryCursor,
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>,
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@ -839,7 +884,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize,
}
impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
impl<'a> fmt::Debug for HighlightIterLayer<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HighlightIterLayer").finish()
}
@ -1010,7 +1055,7 @@ impl HighlightConfiguration {
}
}
impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
impl<'a> HighlightIterLayer<'a> {
/// Create a new 'layer' of highlighting for this document.
///
/// In the even that the new layer contains "combined injections" (injections where multiple
@ -1067,10 +1112,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
let matches = cursor.matches(
combined_injections_query,
tree.root_node(),
|n: Node| {
// &source[n.byte_range()]
node_to_bytes(n, source)
},
RopeProvider(source),
);
for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index];
@ -1117,10 +1159,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
let cursor_ref =
unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let captures = cursor_ref
.captures(&config.query, tree_ref.root_node(), move |n: Node| {
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.captures(&config.query, tree_ref.root_node(), RopeProvider(source))
.peekable();
result.push(HighlightIterLayer {
@ -1274,7 +1313,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
}
}
impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
impl<'a, F> HighlightIter<'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@ -1325,7 +1364,7 @@ where
}
}
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
if let Some(sort_key) = layer.sort_key() {
let mut i = 1;
while i < self.layers.len() {
@ -1344,7 +1383,7 @@ where
}
}
impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
impl<'a, F> Iterator for HighlightIter<'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@ -1608,7 +1647,7 @@ where
fn injection_for_match<'a>(
config: &HighlightConfiguration,
query: &'a Query,
query_match: &QueryMatch<'a>,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
let content_capture_index = config.injection_content_capture_index;

@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.10", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio = { version = "1.11", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1"

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

@ -226,6 +226,8 @@ impl MethodCall {
#[derive(Debug, PartialEq, Clone)]
pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
@ -237,6 +239,7 @@ impl Notification {
use lsp::notification::Notification as _;
let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params
.parse()
@ -294,7 +297,7 @@ impl Registry {
}
}
pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
self.inner
.values()
.find(|(client_id, _)| client_id == &id)
@ -302,33 +305,60 @@ impl Registry {
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &mut self.incoming;
match inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(entry.get().1.clone()),
Entry::Vacant(entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (mut client, incoming) = Client::start(
&config.command,
&config.args,
serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(),
id,
)?;
// TODO: run this async without blocking
futures_executor::block_on(client.initialize())?;
s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
entry.insert((id, client.clone()));
Ok(client)
}
let config = match &language_config.language_server {
Some(config) => config,
None => return Err(Error::LspNotDefined),
};
match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(entry.get().1.clone()),
Entry::Vacant(entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (client, incoming, initialize_notify) = Client::start(
&config.command,
&config.args,
serde_json::from_str(language_config.config.as_deref().unwrap_or(""))
.map_err(|e| {
log::error!(
"LSP Config, {}, in `languages.toml` for `{}`",
e,
language_config.scope()
)
})
.ok(),
id,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
// Initialize the client asynchronously
let _client = client.clone();
tokio::spawn(async move {
use futures_util::TryFutureExt;
let value = _client
.capabilities
.get_or_try_init(|| {
_client
.initialize()
.map_ok(|response| response.capabilities)
})
.await;
value.expect("failed to initialize capabilities");
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
initialize_notify.notify_one();
});
entry.insert((id, client.clone()));
Ok(client)
}
} else {
Err(Error::LspNotDefined)
}
}
@ -415,32 +445,6 @@ impl LspProgressMap {
}
}
// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>>
// spawn one server per language type, need to spawn one per workspace if server doesn't support
// workspaces
//
// could also be a client per root dir
//
// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily
// accessible during edit/save callbacks
//
// the event loop needs to process all incoming streams, maybe we can just have that be a separate
// task that's continually running and store the state on the client, then use read lock to
// retrieve data during render
// -> PROBLEM: how do you trigger an update on the editor side when data updates?
//
// -> The data updates should pull all events until we run out so we don't frequently re-render
//
//
// v2:
//
// there should be a registry of lsp clients, one per language type (or workspace).
// the clients should lazy init on first access
// the client.initialize() should be called async and we buffer any requests until that completes
// there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp!
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};

@ -1,7 +1,7 @@
use crate::Result;
use crate::{Error, Result};
use anyhow::Context;
use jsonrpc_core as jsonrpc;
use log::{debug, error, info, warn};
use log::{error, info};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
@ -11,7 +11,7 @@ use tokio::{
process::{ChildStderr, ChildStdin, ChildStdout},
sync::{
mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
Mutex,
Mutex, Notify,
},
};
@ -51,9 +51,11 @@ impl Transport {
) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>,
Arc<Notify>,
) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
let notify = Arc::new(Notify::new());
let transport = Self {
id,
@ -62,11 +64,21 @@ impl Transport {
let transport = Arc::new(transport);
tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx));
tokio::spawn(Self::recv(
transport.clone(),
server_stdout,
client_tx.clone(),
));
tokio::spawn(Self::err(transport.clone(), server_stderr));
tokio::spawn(Self::send(transport, server_stdin, client_rx));
(rx, tx)
tokio::spawn(Self::send(
transport,
server_stdin,
client_tx,
client_rx,
notify.clone(),
));
(rx, tx, notify)
}
async fn recv_server_message(
@ -76,14 +88,18 @@ impl Transport {
let mut content_length = None;
loop {
buffer.truncate(0);
reader.read_line(buffer).await?;
let header = buffer.trim();
if reader.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
// debug!("<- header {:?}", buffer);
if header.is_empty() {
if buffer == "\r\n" {
// look for an empty CRLF line
break;
}
debug!("<- header {}", header);
let header = buffer.trim();
let parts = header.split_once(": ");
@ -96,7 +112,8 @@ impl Transport {
// Workaround: Some non-conformant language servers will output logging and other garbage
// into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
// the server. Skip such lines and log a warning.
warn!("Failed to parse header: {:?}", header);
// warn!("Failed to parse header: {:?}", header);
}
}
}
@ -121,8 +138,10 @@ impl Transport {
buffer: &mut String,
) -> Result<()> {
buffer.truncate(0);
err.read_line(buffer).await?;
error!("err <- {}", buffer);
if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
error!("err <- {:?}", buffer);
Ok(())
}
@ -255,16 +274,90 @@ impl Transport {
async fn send(
transport: Arc<Self>,
mut server_stdin: BufWriter<ChildStdin>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
mut client_rx: UnboundedReceiver<Payload>,
initialize_notify: Arc<Notify>,
) {
while let Some(msg) = client_rx.recv().await {
match transport
.send_payload_to_server(&mut server_stdin, msg)
.await
{
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
let mut pending_messages: Vec<Payload> = Vec::new();
let mut is_pending = true;
// Determine if a message is allowed to be sent early
fn is_initialize(payload: &Payload) -> bool {
use lsp_types::{
notification::{Initialized, Notification},
request::{Initialize, Request},
};
match payload {
Payload::Request {
value: jsonrpc::MethodCall { method, .. },
..
} if method == Initialize::METHOD => true,
Payload::Notification(jsonrpc::Notification { method, .. })
if method == Initialized::METHOD =>
{
true
}
_ => false,
}
}
// TODO: events that use capabilities need to do the right thing
loop {
tokio::select! {
biased;
_ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe
// server successfully initialized
is_pending = false;
use lsp_types::notification::Notification;
// Hack: inject an initialized notification so we trigger code that needs to happen after init
let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification {
jsonrpc: None,
method: lsp_types::notification::Initialized::METHOD.to_string(),
params: jsonrpc::Params::None,
}));
match transport.process_server_message(&client_tx, notification).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
// drain the pending queue and send payloads to server
for msg in pending_messages.drain(..) {
log::info!("Draining pending message {:?}", msg);
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
}
}
msg = client_rx.recv() => {
if let Some(msg) = msg {
if is_pending && !is_initialize(&msg) {
// ignore notifications
if let Payload::Notification(_) = msg {
continue;
}
log::info!("Language server not initialized, delaying request");
pending_messages.push(msg);
} else {
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
}
} else {
// channel closed
break;
}
}
}
}

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

@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) {
.is_none()
{
eprintln!(
"The directory {} is empty, did you use 'git clone --recursive'?",
"The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?",
dir
);
eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
std::process::exit(1);
}

@ -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 = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

@ -434,16 +434,42 @@ impl Application {
};
match notification {
Notification::PublishDiagnostics(params) => {
let path = Some(params.uri.to_file_path().unwrap());
Notification::Initialized => {
let language_server =
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let doc = self
.editor
.documents
.iter_mut()
.find(|(_, doc)| doc.path() == path.as_ref());
let docs = self.editor.documents().filter(|doc| {
doc.language_server().map(|server| server.id()) == Some(server_id)
});
if let Some((_, doc)) = doc {
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
// TODO: extract and share with editor.open
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
}
}
Notification::PublishDiagnostics(params) => {
let path = params.uri.to_file_path().unwrap();
let doc = self.editor.document_by_path_mut(&path);
if let Some(doc) = doc {
let text = doc.text();
let diagnostics = params
@ -506,7 +532,7 @@ impl Application {
log::warn!("unhandled window/showMessage: {:?}", params);
}
Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params);
log::info!("window/logMessage: {:?}", params);
}
Notification::ProgressMessage(params) => {
let lsp::ProgressParams { token, value } = params;
@ -588,10 +614,27 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, ..
}) => {
let language_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
error!("Method not found {}", method);
// language_server.reply(
// call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
// Err(helix_lsp::jsonrpc::Error {
// code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
// message: "Method not found".to_string(),
// data: None,
// }),
// );
return;
}
};
@ -604,53 +647,9 @@ impl Application {
if spinner.is_stopped() {
spinner.start();
}
let doc = self.editor.documents().find(|doc| {
doc.language_server()
.map(|server| server.id() == server_id)
.unwrap_or_default()
});
match doc {
Some(doc) => {
// it's ok to unwrap, we check for the language server before
let server = doc.language_server().unwrap();
tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
}
None => {
if let Some(server) =
self.editor.language_servers.get_by_id(server_id)
{
log::warn!(
"missing document with language server id `{}`",
server_id
);
tokio::spawn(server.reply(
id,
Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InternalError,
message: "document missing".to_string(),
data: None,
}),
));
} else {
log::warn!(
"can't find language server with id `{}`",
server_id
);
}
}
}
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
}
}
// self.language_server.reply(
// call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
// Err(helix_lsp::jsonrpc::Error {
// code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
// message: "Method not found".to_string(),
// data: None,
// }),
// );
}
e => unreachable!("{:?}", e),
}

File diff suppressed because it is too large Load Diff

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

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

@ -262,8 +262,7 @@ impl Component for Completion {
.cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16;
let mut doc = match &option.documentation {
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
@ -311,24 +310,42 @@ impl Component for Completion {
None => return,
};
let half = area.height / 2;
let height = 15.min(half);
// we want to make sure the cursor is visible (not hidden behind the documentation)
let y = if cursor_pos + area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
let (popup_width, _popup_height) = self.popup.get_size();
let mut width = area
.width
.saturating_sub(popup_x)
.saturating_sub(popup_width);
let area = if width > 30 {
let mut height = area.height.saturating_sub(popup_y);
let x = popup_x + popup_width;
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width;
height = rel_height;
}
Rect::new(x, y, width, height)
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
};
let half = area.height / 2;
let height = 15.min(half);
// we want to make sure the cursor is visible (not hidden behind the documentation)
let y = if cursor_pos + area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
};
let area = Rect::new(0, y, area.width, height);
Rect::new(0, y, area.width, height)
};
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
markdown_doc.render(area, surface, cx);
}
}
}

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

@ -88,7 +88,7 @@ fn parse<'a>(
if let Some(theme) = theme {
let rope = Rope::from(text.as_ref());
let syntax = loader
.language_config_for_scope(&format!("source.{}", language))
.language_configuration_for_injection_string(language)
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config));
@ -215,10 +215,30 @@ impl Component for Markdown {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
Some((width, height))
if padding >= viewport.1 || padding >= viewport.0 {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
for content in contents {
height += 1;
let content_width = content.width() as u16;
if content_width > max_text_width {
text_width = max_text_width;
height += content_width / max_text_width;
} else if content_width > text_width {
text_width = content_width;
}
if height >= viewport.1 {
height = viewport.1;
break;
}
}
Some((text_width + padding, height))
}
}

@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
scroll: usize,
size: (u16, u16),
viewport: (u16, u16),
recalculate: bool,
}
impl<T: Item> Menu<T> {
@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
viewport: (0, 0),
recalculate: true,
};
// TODO: scoring on empty input should just use a fastpath
@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
// reset cursor position
self.cursor = None;
self.scroll = 0;
self.recalculate = true;
}
pub fn move_up(&mut self) {
@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
self.adjust_scroll();
}
fn recalculate_size(&mut self, viewport: (u16, u16)) {
let n = self
.options
.first()
.map(|option| option.row().cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.row();
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
if width > *acc {
*acc = width;
}
}
acc
});
let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
let width = len.min(viewport.0 as usize);
self.widths = max_lens
.into_iter()
.map(|len| Constraint::Length(len as u16))
.collect();
let height = self.matches.len().min(10).min(viewport.1 as usize);
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
self.adjust_scroll();
self.recalculate = false;
}
fn adjust_scroll(&mut self) {
let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor {
@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let n = self
.options
.first()
.map(|option| option.row().cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.row();
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
if width > *acc {
*acc = width;
}
}
acc
});
let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
let width = len.min(viewport.0 as usize);
self.widths = max_lens
.into_iter()
.map(|len| Constraint::Length(len as u16))
.collect();
let height = self.options.len().min(10).min(viewport.1 as usize);
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
self.adjust_scroll();
if viewport != self.viewport || self.recalculate {
self.recalculate_size(viewport);
}
Some(self.size)
}
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let style = theme

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

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

@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
}
impl<T: Component> Popup<T> {
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(contents: T) -> Self {
Self {
contents,
@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
self.position = pos;
}
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
.get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
let (width, height) = self.size;
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if viewport.height > rel_y + height {
rel_y += 1 // position below point
} else {
rel_y = rel_y.saturating_sub(height) // position above point
}
(rel_x, rel_y)
}
pub fn get_size(&self) -> (u16, u16) {
(self.size.0, self.size.1)
}
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> {
}
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
cx.scroll = Some(self.scroll);
// trigger required_size so we recalculate if the child changed
self.required_size((viewport.width, viewport.height));
let position = self
.position
.get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
let (width, height) = self.size;
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
};
cx.scroll = Some(self.scroll);
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if height <= rel_y {
rel_y = rel_y.saturating_sub(height) // position above point
} else {
rel_y += 1 // position below point
}
let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
// clip to viewport
let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));
let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
// clear area
let background = cx.editor.theme.get("ui.popup");

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

@ -386,21 +386,24 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
if let Some(language_server) = self.language_server.clone() {
if let Some(language_server) = self.language_server() {
let text = self.text.clone();
let id = self.identifier();
let offset_encoding = language_server.offset_encoding();
let request = language_server.text_document_formatting(
self.identifier(),
lsp::FormattingOptions::default(),
None,
)?;
let fut = async move {
let edits = language_server
.text_document_formatting(id, lsp::FormattingOptions::default(), None)
.await
.unwrap_or_else(|e| {
log::warn!("LSP formatting failed: {}", e);
Default::default()
});
let edits = request.await.unwrap_or_else(|e| {
log::warn!("LSP formatting failed: {}", e);
Default::default()
});
LspFormatting {
doc: text,
edits,
offset_encoding: language_server.offset_encoding(),
offset_encoding,
}
};
Some(fut)
@ -469,9 +472,14 @@ impl Document {
to_writer(&mut file, encoding, &text).await?;
if let Some(language_server) = language_server {
language_server
.text_document_did_save(identifier, &text)
.await?;
if !language_server.is_initialized() {
return Ok(());
}
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
{
notification.await?;
}
}
Ok(())
@ -646,7 +654,7 @@ impl Document {
// }
// emit lsp notification
if let Some(language_server) = &self.language_server {
if let Some(language_server) = self.language_server() {
let notify = language_server.text_document_did_change(
self.versioned_identifier(),
&old_doc,
@ -795,9 +803,18 @@ impl Document {
self.version
}
#[inline]
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
self.language_server.as_deref()
let server = self.language_server.as_deref();
let initialized = server
.map(|server| server.is_initialized())
.unwrap_or(false);
// only resolve language_server if it's initialized
if initialized {
server
} else {
None
}
}
#[inline]
@ -891,6 +908,40 @@ impl Default for Document {
mod test {
use super::*;
#[test]
fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello\r\nworld");
let mut doc = Document::from(text, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(0, 0));
let transaction =
Transaction::change(doc.text(), vec![(5, 7, Some("\n".into()))].into_iter());
let old_doc = doc.text().clone();
doc.apply(&transaction, view);
let changes = Client::changeset_to_changes(
&old_doc,
doc.text(),
transaction.changes(),
OffsetEncoding::Utf8,
);
assert_eq!(doc.text(), "hello\nworld");
assert_eq!(
changes,
&[lsp::TextDocumentContentChangeEvent {
range: Some(lsp::Range::new(
lsp::Position::new(0, 5),
lsp::Position::new(1, 0)
)),
text: "\n".into(),
range_length: None,
}]
);
}
#[test]
fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding};

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

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

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

@ -1,3 +1,14 @@
//! These are macros to make getting very nested fields in the `Editor` struct easier
//! These are macros instead of functions because functions will have to take `&mut self`
//! However, rust doesn't know that you only want a partial borrow instead of borrowing the
//! entire struct which `&mut self` says. This makes it impossible to do other mutable
//! stuff to the struct because it is already borrowed. Because macros are expanded,
//! this circumvents the problem because it is just like indexing fields by hand and then
//! putting a `&mut` in front of it. This way rust can see that we are only borrowing a
//! part of the struct and not the entire thing.
/// Get the current view and document mutably as a tuple.
/// Returns `(&mut View, &mut Document)`
#[macro_export]
macro_rules! current {
( $( $editor:ident ).+ ) => {{
@ -7,6 +18,8 @@ macro_rules! current {
}};
}
/// Get the current document mutably.
/// Returns `&mut Document`
#[macro_export]
macro_rules! doc_mut {
( $( $editor:ident ).+ ) => {{
@ -14,6 +27,8 @@ macro_rules! doc_mut {
}};
}
/// Get the current view mutably.
/// Returns `&mut View`
#[macro_export]
macro_rules! view_mut {
( $( $editor:ident ).+ ) => {{
@ -21,6 +36,8 @@ macro_rules! view_mut {
}};
}
/// Get the current view immutably
/// Returns `&View`
#[macro_export]
macro_rules! view {
( $( $editor:ident ).+ ) => {{

@ -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 helix_core::hashmap;
use log::warn;
use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer};
@ -142,13 +143,37 @@ struct ThemePalette {
impl Default for ThemePalette {
fn default() -> Self {
Self::new(HashMap::new())
Self {
palette: hashmap! {
"black".to_string() => Color::Black,
"red".to_string() => Color::Red,
"green".to_string() => Color::Green,
"yellow".to_string() => Color::Yellow,
"blue".to_string() => Color::Blue,
"magenta".to_string() => Color::Magenta,
"cyan".to_string() => Color::Cyan,
"gray".to_string() => Color::Gray,
"light-red".to_string() => Color::LightRed,
"light-green".to_string() => Color::LightGreen,
"light-yellow".to_string() => Color::LightYellow,
"light-blue".to_string() => Color::LightBlue,
"light-magenta".to_string() => Color::LightMagenta,
"light-cyan".to_string() => Color::LightCyan,
"light-gray".to_string() => Color::LightGray,
"white".to_string() => Color::White,
},
}
}
}
impl ThemePalette {
pub fn new(palette: HashMap<String, Color>) -> Self {
Self { palette }
let ThemePalette {
palette: mut default,
} = ThemePalette::default();
default.extend(palette);
Self { palette: default }
}
pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {

@ -65,6 +65,7 @@ file-types = ["ex", "exs"]
roots = []
comment-token = "#"
language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " }
[[language]]
@ -198,6 +199,17 @@ roots = []
language-server = { command = "typescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "tsx"
scope = "source.tsx"
injection-regex = "^(tsx)$" # |typescript
file-types = ["tsx"]
roots = []
# TODO: highlights-jsx, highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "css"
scope = "source.css"
@ -236,6 +248,7 @@ file-types = ["nix"]
roots = []
comment-token = "#"
language-server = { command = "rnix-lsp" }
indent = { tab-width = 2, unit = " " }
[[language]]
@ -286,7 +299,22 @@ injection-regex = "julia"
file-types = ["jl"]
roots = []
comment-token = "#"
language-server = { command = "julia", args = [ "--startup-file=no", "--history-file=no", "-e", "using LanguageServer;using Pkg;import StaticLint;import SymbolServer;env_path = dirname(Pkg.Types.Context().env.project_file);server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, \"\");server.runlinter = true;run(server);" ] }
language-server = { command = "julia", args = [
"--startup-file=no",
"--history-file=no",
"--quiet",
"-e",
"""
using LanguageServer;
using Pkg;
import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file);
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true;
run(server);
""",
] }
indent = { tab-width = 2, unit = " " }
[[language]]
@ -331,6 +359,15 @@ roots = []
comment-token = "--"
indent = { tab-width = 2, unit = " " }
[[language]]
name = "svelte"
scope = "source.svelte"
injection-regex = "svelte"
file-types = ["svelte"]
roots = []
indent = { tab-width = 2, unit = " " }
language-server = { command = "svelteserver", args = ["--stdio"] }
[[language]]
name = "yaml"
scope = "source.yaml"

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

@ -17,9 +17,18 @@
; Identifiers
((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$"))
(const_spec
name: (identifier) @constant)
(parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter)
(type_identifier) @type
(field_identifier) @property
(identifier) @variable
(package_identifier) @variable
; Operators
@ -79,10 +88,8 @@
"go"
"goto"
"if"
"import"
"interface"
"map"
"package"
"range"
"return"
"select"
@ -92,6 +99,29 @@
"var"
] @keyword
[
"import"
"package"
] @keyword.control.import
; Delimiters
[
":"
"."
","
";"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Literals
[
@ -111,7 +141,8 @@
[
(true)
(false)
(nil)
] @constant.builtin
] @constant.builtin.boolean
(nil) @constant.builtin
(comment) @comment

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

@ -87,7 +87,7 @@
(template_string)
] @string
(regex) @string.special
(regex) @string.regexp
(number) @number
; 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)
@ -28,43 +22,43 @@
(call_expression
(identifier) @function)
(call_expression
(field_expression (identifier) @method .))
(field_expression (identifier) @function.method .))
(broadcast_call_expression
(identifier) @function)
(broadcast_call_expression
(field_expression (identifier) @method .))
(field_expression (identifier) @function.method .))
(parameter_list
(identifier) @parameter)
(identifier) @variable.parameter)
(parameter_list
(optional_parameter .
(identifier) @parameter))
(identifier) @variable.parameter))
(typed_parameter
(identifier) @parameter
(identifier) @variable.parameter
(identifier) @type)
(type_parameter_list
(identifier) @type)
(typed_parameter
(identifier) @parameter
(identifier) @variable.parameter
(parameterized_identifier) @type)
(function_expression
. (identifier) @parameter)
(spread_parameter) @parameter
. (identifier) @variable.parameter)
(spread_parameter) @variable.parameter
(spread_parameter
(identifier) @parameter)
(identifier) @variable.parameter)
(named_argument
. (identifier) @parameter)
. (identifier) @variable.parameter)
(argument_list
(typed_expression
(identifier) @parameter
(identifier) @variable.parameter
(identifier) @type))
(argument_list
(typed_expression
(identifier) @parameter
(identifier) @variable.parameter
(parameterized_identifier) @type))
;; Symbol expressions (:my-wanna-be-lisp-keyword)
(quote_expression
(identifier)) @symbol
(identifier)) @string.special.symbol
;; Parsing error! foo (::Type) get's parsed as two quote expressions
(argument_list
@ -76,7 +70,7 @@
(identifier) @type)
(parameterized_identifier (_)) @type
(argument_list
(typed_expression . (identifier) @parameter))
(typed_expression . (identifier) @variable.parameter))
(typed_expression
(identifier) @type .)
@ -113,13 +107,13 @@
"end" @keyword
(if_statement
["if" "end"] @conditional)
["if" "end"] @keyword.control.conditional)
(elseif_clause
["elseif"] @conditional)
["elseif"] @keyword.control.conditional)
(else_clause
["else"] @conditional)
["else"] @keyword.control.conditional)
(ternary_expression
["?" ":"] @conditional)
["?" ":"] @keyword.control.conditional)
(function_definition ["function" "end"] @keyword.function)
@ -134,47 +128,57 @@
"type"
] @keyword
((identifier) @keyword (#any-of? @keyword "global" "local"))
((identifier) @keyword (match? @keyword "global|local"))
(compound_expression
["begin" "end"] @keyword)
(try_statement
["try" "end" ] @exception)
["try" "end" ] @keyword.control.exception)
(finally_clause
"finally" @exception)
"finally" @keyword.control.exception)
(catch_clause
"catch" @exception)
"catch" @keyword.control.exception)
(quote_statement
["quote" "end"] @keyword)
(let_statement
["let" "end"] @keyword)
(for_statement
["for" "end"] @repeat)
["for" "end"] @keyword.control.repeat)
(while_statement
["while" "end"] @repeat)
(break_statement) @repeat
(continue_statement) @repeat
["while" "end"] @keyword.control.repeat)
(break_statement) @keyword.control.repeat
(continue_statement) @keyword.control.repeat
(for_binding
"in" @repeat)
"in" @keyword.control.repeat)
(for_clause
"for" @repeat)
"for" @keyword.control.repeat)
(do_clause
["do" "end"] @keyword)
(export_statement
["export"] @include)
["export"] @keyword.control.import)
[
"using"
"module"
"import"
] @include
] @keyword.control.import
((identifier) @include (#eq? @include "baremodule"))
((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule"))
(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
(((identifier) @boolean) (eq? @boolean "true"))
(((identifier) @boolean) (eq? @boolean "false"))
(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true"))
(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false"))
["::" ":" "." "," "..." "!"] @punctuation.delimiter
["[" "]" "(" ")" "{" "}"] @punctuation.bracket
["="] @operator
(identifier) @variable
;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation)
;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables
;(match? @type "^[A-Z][^_]"))
((identifier) @constant
(match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$"))

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

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

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

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

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

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

@ -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)
(flow_mapping (_ key: (_) @property))
(boolean_scalar) @boolean
(boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin
(double_quote_scalar) @string
(single_quote_scalar) @string

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

@ -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" }
"string" = { fg = "#e6db74" }
"string.regexp" = { fg = "regex" }
"number" = { fg = "#ae81ff" }
"escape" = { fg = "#ae81ff" }

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

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

Loading…
Cancel
Save