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