summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-11-06 15:28:19 +0000
committerBlaž Hrastnik2021-11-06 15:28:19 +0000
commitf2b709a3c3a9cc036bfea46734efd7e4100eb34b (patch)
treead5f921f13659e5ba395442e13389af317ee81b0
parentcde57dae356021c6ca8c2a2ed68777bd9d0bc0b2 (diff)
parentf979bdc442ab3150a369ff8bee0703e90e32e2a4 (diff)
Merge branch 'master' into debug
-rw-r--r--.github/workflows/build.yml18
-rw-r--r--.gitmodules6
-rw-r--r--CHANGELOG.md87
-rw-r--r--Cargo.lock91
-rw-r--r--README.md6
-rw-r--r--TODO.md6
-rw-r--r--book/src/SUMMARY.md2
-rw-r--r--book/src/configuration.md2
-rw-r--r--book/src/guides/README.md4
-rw-r--r--book/src/guides/textobject.md30
-rw-r--r--book/src/keymap.md145
-rw-r--r--book/src/remapping.md2
-rw-r--r--book/src/themes.md11
-rw-r--r--book/src/usage.md15
-rw-r--r--flake.lock68
-rw-r--r--flake.nix4
-rw-r--r--helix-core/Cargo.toml7
-rw-r--r--helix-core/src/auto_pairs.rs3
-rw-r--r--helix-core/src/chars.rs2
-rw-r--r--helix-core/src/comment.rs5
-rw-r--r--helix-core/src/diagnostic.rs7
-rw-r--r--helix-core/src/graphemes.rs4
-rw-r--r--helix-core/src/history.rs105
-rw-r--r--helix-core/src/indent.rs1
-rw-r--r--helix-core/src/lib.rs3
-rw-r--r--helix-core/src/line_ending.rs6
-rw-r--r--helix-core/src/movement.rs4
-rw-r--r--helix-core/src/object.rs9
-rw-r--r--helix-core/src/position.rs81
-rw-r--r--helix-core/src/register.rs4
-rw-r--r--helix-core/src/selection.rs11
-rw-r--r--helix-core/src/syntax.rs90
-rw-r--r--helix-core/src/textobject.rs51
-rw-r--r--helix-core/src/transaction.rs10
-rw-r--r--helix-dap/Cargo.toml2
-rw-r--r--helix-lsp/Cargo.toml12
-rw-r--r--helix-lsp/src/client.rs7
-rw-r--r--helix-syntax/Cargo.toml4
m---------helix-syntax/languages/tree-sitter-cmake0
m---------helix-syntax/languages/tree-sitter-cpp0
m---------helix-syntax/languages/tree-sitter-elixir0
-rw-r--r--helix-term/Cargo.toml18
-rw-r--r--helix-term/src/application.rs26
-rw-r--r--helix-term/src/args.rs2
-rw-r--r--helix-term/src/commands.rs630
-rw-r--r--helix-term/src/compositor.rs2
-rw-r--r--helix-term/src/keymap.rs228
-rw-r--r--helix-term/src/main.rs8
-rw-r--r--helix-term/src/ui/completion.rs134
-rw-r--r--helix-term/src/ui/editor.rs134
-rw-r--r--helix-term/src/ui/menu.rs43
-rw-r--r--helix-term/src/ui/mod.rs23
-rw-r--r--helix-term/src/ui/picker.rs157
-rw-r--r--helix-term/src/ui/prompt.rs37
-rw-r--r--helix-tui/Cargo.toml10
-rw-r--r--helix-tui/src/buffer.rs78
-rw-r--r--helix-view/Cargo.toml12
-rw-r--r--helix-view/src/clipboard.rs22
-rw-r--r--helix-view/src/document.rs81
-rw-r--r--helix-view/src/editor.rs124
-rw-r--r--helix-view/src/info.rs4
-rw-r--r--helix-view/src/input.rs2
-rw-r--r--helix-view/src/keyboard.rs2
-rw-r--r--helix-view/src/lib.rs4
-rw-r--r--helix-view/src/macros.rs5
-rw-r--r--helix-view/src/theme.rs1
-rw-r--r--helix-view/src/tree.rs191
-rw-r--r--helix-view/src/view.rs8
-rw-r--r--languages.toml11
-rw-r--r--runtime/queries/bash/highlights.scm4
-rw-r--r--runtime/queries/c-sharp/highlights.scm10
-rw-r--r--runtime/queries/c/highlights.scm6
-rw-r--r--runtime/queries/cmake/highlights.scm97
-rw-r--r--runtime/queries/cpp/highlights.scm9
-rw-r--r--runtime/queries/css/highlights.scm14
-rw-r--r--runtime/queries/elixir/highlights.scm323
-rw-r--r--runtime/queries/go/highlights.scm6
-rw-r--r--runtime/queries/go/textobjects.scm21
-rw-r--r--runtime/queries/haskell/highlights.scm6
-rw-r--r--runtime/queries/java/highlights.scm11
-rw-r--r--runtime/queries/javascript/highlights.scm4
-rw-r--r--runtime/queries/json/highlights.scm19
-rw-r--r--runtime/queries/julia/highlights.scm12
-rw-r--r--runtime/queries/ledger/highlights.scm4
-rw-r--r--runtime/queries/lua/highlights.scm6
-rw-r--r--runtime/queries/nix/highlights.scm12
-rw-r--r--runtime/queries/ocaml/highlights.scm8
-rw-r--r--runtime/queries/php/highlights.scm12
-rw-r--r--runtime/queries/php/indents.toml17
-rw-r--r--runtime/queries/protobuf/highlights.scm10
-rw-r--r--runtime/queries/python/highlights.scm11
-rw-r--r--runtime/queries/python/indents.toml39
-rw-r--r--runtime/queries/python/textobjects.scm14
-rw-r--r--runtime/queries/ruby/highlights.scm6
-rw-r--r--runtime/queries/rust/highlights.scm35
-rw-r--r--runtime/queries/rust/textobjects.scm26
-rw-r--r--runtime/queries/svelte/highlights.scm2
-rw-r--r--runtime/queries/toml/highlights.scm8
-rw-r--r--runtime/queries/tsq/highlights.scm4
-rw-r--r--runtime/queries/yaml/highlights.scm10
-rw-r--r--runtime/queries/zig/highlights.scm8
-rw-r--r--runtime/themes/base16_default_dark.toml59
-rw-r--r--runtime/themes/bogster.toml6
-rw-r--r--runtime/themes/dark_plus.toml53
-rw-r--r--runtime/themes/everforest_dark.toml7
-rw-r--r--runtime/themes/gruvbox.toml7
-rw-r--r--runtime/themes/ingrid.toml6
-rw-r--r--runtime/themes/monokai.toml7
-rw-r--r--runtime/themes/nord.toml104
-rw-r--r--runtime/themes/onedark.toml103
-rw-r--r--runtime/themes/rose_pine.toml61
-rw-r--r--theme.toml2
112 files changed, 2908 insertions, 1203 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3d82c609..d4822f70 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,19 +28,19 @@ jobs:
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
- key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
- key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
- key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check
uses: actions-rs/cargo@v1
@@ -67,19 +67,19 @@ jobs:
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
- key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
- key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
- key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
uses: actions-rs/cargo@v1
@@ -112,19 +112,19 @@ jobs:
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
- key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
- key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
- key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo fmt
uses: actions-rs/cargo@v1
diff --git a/.gitmodules b/.gitmodules
index a8e6481e..7ed34ad3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -84,7 +84,7 @@
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
- url = https://github.com/IceDragon200/tree-sitter-elixir
+ url = https://github.com/elixir-lang/tree-sitter-elixir
shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix
@@ -130,3 +130,7 @@
path = helix-syntax/languages/tree-sitter-tsq
url = https://github.com/tree-sitter/tree-sitter-tsq
shallow = true
+[submodule "helix-syntax/languages/tree-sitter-cmake"]
+ path = helix-syntax/languages/tree-sitter-cmake
+ url = https://github.com/uyha/tree-sitter-cmake
+ shallow = true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03f57307..52ca2d60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,85 @@
+# 0.5.0 (2021-10-28)
+
+A big shout out to all the contributors! We had 46 contributors in this release.
+
+Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)!
+
+The following is a quick rundown of the larger changes, there were many more
+(check the git history for more details).
+
+Breaking changes:
+
+- A couple of keymaps moved to resolve a few conflicting keybinds.
+ - Documentation popups were moved from `K` to `space+k`
+ - `K` is now `keep_selections` which filters selections to only keeps ones matching the regex
+ - `keep_primary_selection` moved from `space+space` to `,`
+ - `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one
+ - Opening files in a split moved from `C-h` to `C-s`
+- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html)
+
+Features:
+
+- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript)
+- Autocompletion! Completion now triggers automatically after a set idle timeout
+- Completion documentation is now displayed next to the popup ([#691](https://github.com/helix-editor/helix/pull/691))
+- Treesitter textobjects (select a function via `mf`, class via `mc`) ([#728](https://github.com/helix-editor/helix/pull/728))
+- Global search across entire workspace `space+/` ([#651](https://github.com/helix-editor/helix/pull/651))
+- Relative line number support ([#485](https://github.com/helix-editor/helix/pull/485))
+- Prompts now store a history (72cf86e)
+- `:vsplit` and `:hsplit` commands ([#639](https://github.com/helix-editor/helix/pull/639))
+- `C-w h/j/k/l` can now be used to navigate between splits ([#860](https://github.com/helix-editor/helix/pull/860))
+- `C-j` and `C-k` are now alternative keybindings to `C-n` and `C-p` in the UI ([#876](https://github.com/helix-editor/helix/pull/876))
+- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) ([#547](https://github.com/helix-editor/helix/pull/547))
+- Searching now defaults to smart case search (case insensitive unless uppercase is used) ([#761](https://github.com/helix-editor/helix/pull/761))
+- The preview pane was improved to highlight and center line ranges
+- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc)
+- Show hidden files in completions ([#648](https://github.com/helix-editor/helix/pull/648))
+- Grammar injections are now properly handled (dd0b15e)
+- `v` in select mode now switches back to normal mode ([#660](https://github.com/helix-editor/helix/pull/660))
+- View mode can now be triggered as a "sticky" mode ([#719](https://github.com/helix-editor/helix/pull/719))
+- `f`/`t` and object selection motions can now be repeated via `Alt-.` ([#891](https://github.com/helix-editor/helix/pull/891))
+- Statusline now displays total selection count and diagnostics counts for both errors and warnings ([#916](https://github.com/helix-editor/helix/pull/916))
+
+New grammars:
+
+- Ledger ([#572](https://github.com/helix-editor/helix/pull/572))
+- Protobuf ([#614](https://github.com/helix-editor/helix/pull/614))
+- Zig ([#631](https://github.com/helix-editor/helix/pull/631))
+- YAML ([#667](https://github.com/helix-editor/helix/pull/667))
+- Lua ([#665](https://github.com/helix-editor/helix/pull/665))
+- OCaml ([#666](https://github.com/helix-editor/helix/pull/666))
+- Svelte ([#733](https://github.com/helix-editor/helix/pull/733))
+- Vue ([#787](https://github.com/helix-editor/helix/pull/787))
+- Tree-sitter queries ([#845](https://github.com/helix-editor/helix/pull/845))
+- CMake ([#888](https://github.com/helix-editor/helix/pull/888))
+- Elixir (we switched over to the official grammar) (6c0786e)
+- Language server definitions for Nix and Elixir ([#725](https://github.com/helix-editor/helix/pull/725))
+- Python now uses `pylsp` instead of `pyls`
+- Python now supports indentation
+
+New themes:
+
+- Monokai ([#628](https://github.com/helix-editor/helix/pull/628))
+- Everforest Dark ([#760](https://github.com/helix-editor/helix/pull/760))
+- Nord ([#799](https://github.com/helix-editor/helix/pull/799))
+- Base16 Default Dark ([#833](https://github.com/helix-editor/helix/pull/833))
+- Rose Pine ([#897](https://github.com/helix-editor/helix/pull/897))
+
+Fixes:
+
+- Fix crash on empty rust file ([#592](https://github.com/helix-editor/helix/pull/592))
+- Exit select mode after toggle comment ([#598](https://github.com/helix-editor/helix/pull/598))
+- Pin popups with no positioning to the initial position (12ea3888)
+- xsel copy should not freeze the editor (6dd7dc4)
+- `*` now only sets the search register and doesn't jump to the next occurrence (3426285)
+- Goto line start/end commands extend when in select mode ([#739](https://github.com/helix-editor/helix/pull/739))
+- Fix documentation popups sometimes not getting fully highlighted (066367c)
+- Refactor apply_workspace_edit to remove assert (b02d872)
+- Wrap around the top of the picker menu when scrolling (c7d6e44)
+- Don't allow closing the last split if there's unsaved changes (3ff5b00)
+- Indentation used different default on hx vs hx new_file.txt (c913bad)
+
# 0.4.1 (2021-08-14)
A minor release that includes:
@@ -7,6 +88,8 @@ A minor release that includes:
# 0.4.0 (2021-08-13)
+A big shout out to all the contributors! We had 28 contributors in this release.
+
Two months have passed, so this is another big release. A big thank you to all
the contributors and package maintainers!
@@ -44,6 +127,8 @@ selections in the future as well as resolves many bugs and edge cases.
# 0.3.0 (2021-06-27)
+A big shout out to all the contributors! We had 24 contributors in this release.
+
Another big release.
Highlights:
@@ -90,6 +175,8 @@ Includes a fix where wq/wqa could exit before file saving completed.
# 0.2.0
+A big shout out to all the contributors! We had 18 contributors in this release.
+
Enough has changed to bump the version. We're skipping 0.1.x because
previously the CLI would always report version as 0.1.0, and we'd like
to distinguish it in bug reports..
diff --git a/Cargo.lock b/Cargo.lock
index ef9d74bd..d200cc27 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chardetng"
-version = "0.1.14"
+version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c"
+checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
dependencies = [
"cfg-if",
"encoding_rs",
@@ -101,9 +101,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
-version = "4.2.1"
+version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
+checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
dependencies = [
"error-code",
"str-buf",
@@ -111,6 +111,15 @@ dependencies = [
]
[[package]]
+name = "content_inspector"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -122,9 +131,9 @@ dependencies = [
[[package]]
name = "crossterm"
-version = "0.21.0"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
+checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
dependencies = [
"bitflags",
"crossterm_winapi",
@@ -139,9 +148,9 @@ dependencies = [
[[package]]
name = "crossterm_winapi"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
+checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [
"winapi",
]
@@ -175,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
-version = "0.8.28"
+version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [
"cfg-if",
]
@@ -358,11 +367,12 @@ dependencies = [
[[package]]
name = "helix-core"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"arc-swap",
"etcetera",
"helix-syntax",
+ "log",
"once_cell",
"quickcheck",
"regex",
@@ -395,7 +405,7 @@ dependencies = [
[[package]]
name = "helix-lsp"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"anyhow",
"futures-executor",
@@ -413,7 +423,7 @@ dependencies = [
[[package]]
name = "helix-syntax"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"anyhow",
"cc",
@@ -424,10 +434,11 @@ dependencies = [
[[package]]
name = "helix-term"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"anyhow",
"chrono",
+ "content_inspector",
"crossterm",
"fern",
"futures-util",
@@ -455,7 +466,7 @@ dependencies = [
[[package]]
name = "helix-tui"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"bitflags",
"cassowary",
@@ -468,7 +479,7 @@ dependencies = [
[[package]]
name = "helix-view"
-version = "0.4.1"
+version = "0.5.0"
dependencies = [
"anyhow",
"bitflags",
@@ -532,9 +543,9 @@ dependencies = [
[[package]]
name = "instant"
-version = "0.1.11"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
@@ -566,9 +577,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.103"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
+checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
[[package]]
name = "libloading"
@@ -600,9 +611,9 @@ dependencies = [
[[package]]
name = "lsp-types"
-version = "0.90.1"
+version = "0.91.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3"
+checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
dependencies = [
"bitflags",
"serde",
@@ -640,9 +651,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.7.13"
+version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
@@ -755,9 +766,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
-version = "1.0.29"
+version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
+checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [
"unicode-xid",
]
@@ -784,9 +795,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.9"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
@@ -973,9 +984,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
[[package]]
name = "slab"
-version = "0.4.4"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "slotmap"
@@ -1000,9 +1011,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]]
name = "syn"
-version = "1.0.78"
+version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
+checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [
"proc-macro2",
"quote",
@@ -1075,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
+checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
dependencies = [
"autocfg",
"bytes",
@@ -1095,9 +1106,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "1.4.1"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb"
+checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
dependencies = [
"proc-macro2",
"quote",
@@ -1106,9 +1117,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
+checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -1145,9 +1156,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
-version = "0.3.6"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-general-category"
diff --git a/README.md b/README.md
index 7cd58d59..faf5851e 100644
--- a/README.md
+++ b/README.md
@@ -27,11 +27,11 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
It's a terminal-based editor first, but I'd like to explore a custom renderer
(similar to emacs) in wgpu or skulpin.
-# Installation
-
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.toml`.
+# Installation
+
We provide packaging for various distributions, but here's a quick method to
build from source.
@@ -71,7 +71,7 @@ Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
- Help with packaging on various distributions needed!
-- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
+- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
diff --git a/TODO.md b/TODO.md
index 90e7e450..80a9be05 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,10 +6,6 @@
- clojure
- erlang
-as you type completion!
-- [ ] use signature_help_provider and completion_provider trigger characters in
- a hook to trigger signature help text / autocompletion
-- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support
1
@@ -18,8 +14,6 @@ as you type completion!
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers
-- [ ] repeat selection
-
- [ ] lsp: signature help
2
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 3fa8e067..56f50e21 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -8,3 +8,5 @@
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
+- [Guides](./guides/README.md)
+ - [Adding Textobject Queries](./guides/textobject.md)
diff --git a/book/src/configuration.md b/book/src/configuration.md
index d47f95d9..be25441f 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -21,6 +21,8 @@ To override global configuration parameters, create a `config.toml` file located
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
+| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
+| `auto-info` | Whether to display infoboxes | `true` |
## LSP
diff --git a/book/src/guides/README.md b/book/src/guides/README.md
new file mode 100644
index 00000000..96e62978
--- /dev/null
+++ b/book/src/guides/README.md
@@ -0,0 +1,4 @@
+# Guides
+
+This section contains guides for adding new language server configurations,
+tree-sitter grammers, textobject queries, etc.
diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md
new file mode 100644
index 00000000..50b3b574
--- /dev/null
+++ b/book/src/guides/textobject.md
@@ -0,0 +1,30 @@
+# Adding Textobject Queries
+
+Textobjects that are language specific ([like functions, classes, etc][textobjects])
+require an accompanying tree-sitter grammar and a `textobjects.scm` query file
+to work properly. Tree-sitter allows us to query the source code syntax tree
+and capture specific parts of it. The queries are written in a lisp dialect.
+More information on how to write queries can be found in the [official tree-sitter
+documentation](tree-sitter-queries).
+
+Query files should be placed in `runtime/queries/{language}/textobjects.scm`
+when contributing. Note that to test the query files locally you should put
+them under your local runtime directory (`~/.config/helix/runtime` on Linux
+for example).
+
+The following [captures][tree-sitter-captures] are recognized:
+
+| Capture Name |
+| --- |
+| `function.inside` |
+| `function.around` |
+| `class.inside` |
+| `class.around` |
+| `parameter.inside` |
+
+[Example query files][textobject-examples] can be found in the helix GitHub repository.
+
+[textobjects]: ../usage.md#textobjects
+[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
+[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
+[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=
diff --git a/book/src/keymap.md b/book/src/keymap.md
index 156b1d99..5a6aee41 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -6,38 +6,39 @@
> 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 |
+| Key | Description | Command |
+| ----- | ----------- | ------- |
+| `h`/`Left` | Move left | `move_char_left` |
+| `j`/`Down` | Move down | `move_line_down` |
+| `k`/`Up` | Move up | `move_line_up` |
+| `l`/`Right` | Move right | `move_char_right` |
+| `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` |
+| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
+| `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
@@ -54,6 +55,7 @@
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
+| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `y` | Yank selection | `yank` |
@@ -86,8 +88,9 @@
| `;` | 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` |
+| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
+| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
+| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
| `(` | Rotate main selection backward | `rotate_selections_backward` |
| `)` | Rotate main selection forward | `rotate_selections_forward` |
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
@@ -103,13 +106,13 @@
### Search
-> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `/` | Search for regex pattern | `search` |
+| `?` | Search for previous pattern | `rsearch` |
| `n` | Select next search match | `search_next` |
-| `N` | Add next search match to selection | `extend_search_next` |
+| `N` | Select previous search match | `search_prev` |
| `*` | Use current selection as the search pattern | `search_selection` |
### Minor modes
@@ -158,6 +161,8 @@ Jumps to various locations.
| `r` | Go to references | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
+| `n` | Go to next buffer | `goto_next_buffer` |
+| `p` | Go to previous buffer | `goto_previous_buffer` |
#### Match mode
@@ -180,12 +185,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
This layer is similar to vim keybindings as kakoune does not support window.
-| Key | Description | Command |
-| ----- | ------------- | ------- |
-| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
-| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
-| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
-| `q`, `Ctrl-q` | Close current window | `wclose` |
+| Key | Description | Command |
+| ----- | ------------- | ------- |
+| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
+| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
+| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
+| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
+| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
+| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
+| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
+| `q`, `Ctrl-q` | Close current window | `wclose` |
#### Space mode
@@ -213,12 +222,12 @@ This layer is a kludge of mappings, mostly pickers.
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` |
+| 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` |
@@ -242,12 +251,34 @@ commands (including goto) to extend the existing selection instead of replacing
Keys to use within picker. Remapping currently not supported.
-| Key | Description |
-| ----- | ------------- |
-| `Up`, `Ctrl-p` | Previous entry |
-| `Down`, `Ctrl-n` | Next entry |
-| `Ctrl-space` | Filter options |
-| `Enter` | Open selected |
-| `Ctrl-h` | Open horizontally |
-| `Ctrl-v` | Open vertically |
-| `Escape`, `Ctrl-c` | Close picker |
+| Key | Description |
+| ----- | ------------- |
+| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
+| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
+| `Ctrl-space` | Filter options |
+| `Enter` | Open selected |
+| `Ctrl-s` | Open horizontally |
+| `Ctrl-v` | Open vertically |
+| `Escape`, `Ctrl-c` | Close picker |
+
+# Prompt
+Keys to use within prompt, Remapping currently not supported.
+| Key | Description |
+| ----- | ------------- |
+| `Escape`, `Ctrl-c` | Close prompt |
+| `Alt-b`, `Alt-Left` | Backward a word |
+| `Ctrl-b`, `Left` | Backward a char |
+| `Alt-f`, `Alt-Right` | Forward a word |
+| `Ctrl-f`, `Right` | Forward a char |
+| `Ctrl-e`, `End` | move prompt end |
+| `Ctrl-a`, `Home` | move prompt start |
+| `Ctrl-w` | delete previous word |
+| `Ctrl-k` | delete to end of line |
+| `backspace` | delete previous char |
+| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
+| `Ctrl-p`, `Up` | select previous history |
+| `Ctrl-n`, `Down` | select next history |
+| `Tab` | slect next completion item |
+| `BackTab` | slect previous completion item |
+| `Enter` | Open selected |
+
diff --git a/book/src/remapping.md b/book/src/remapping.md
index 81f45da3..3369f031 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -2,7 +2,7 @@
One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be
-available in the feature).
+available in the future).
To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like
diff --git a/book/src/themes.md b/book/src/themes.md
index a99e3a59..ecbbb6e9 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -103,8 +103,6 @@ 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`)
@@ -112,13 +110,17 @@ We use a similar set of scopes as
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
- `boolean`
- `character`
+ - `escape`
+ - `numeric` (numbers)
+ - `integer`
+ - `float`
-- `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`
+ - `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
- `comment` - Code comments
- `line` - Single line comments (`//`)
@@ -128,7 +130,8 @@ We use a similar set of scopes as
- `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
- `parameter` - Function parameters
- - `property`
+ - `other`
+ - `member` - Fields of composite data types (e.g. structs, unions)
- `function` (TODO: ?)
- `label`
diff --git a/book/src/usage.md b/book/src/usage.md
index 9ee8634c..71730fa8 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -2,6 +2,8 @@
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
+See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction.
+
## Registers
Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
@@ -49,9 +51,10 @@ Multiple characters are currently not supported, but planned.
## Textobjects
-Currently supported: `word`, `surround`.
+Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
+![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
@@ -60,5 +63,11 @@ Currently supported: `word`, `surround`.
| --- | --- |
| `w` | Word |
| `(`, `[`, `'`, etc | Specified surround pairs |
-
-Textobjects based on treesitter, like `function`, `class`, etc are planned.
+| `f` | Function |
+| `c` | Class |
+| `p` | Parameter |
+
+Note: `f`, `c`, etc need a tree-sitter grammar active for the current
+document and a special tree-sitter query file to work properly. [Only
+some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=)
+currently have the query file implemented. Contributions are welcome !
diff --git a/flake.lock b/flake.lock
index 21e44c6e..2029d580 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"devshell": {
"locked": {
- "lastModified": 1630239564,
- "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
+ "lastModified": 1632436039,
+ "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"owner": "numtide",
"repo": "devshell",
- "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
+ "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"type": "github"
},
"original": {
@@ -15,6 +15,21 @@
"type": "github"
}
},
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1623875721,
+ "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
"flakeCompat": {
"flake": false,
"locked": {
@@ -37,14 +52,16 @@
"nixpkgs": [
"nixpkgs"
],
- "rustOverlay": "rustOverlay"
+ "rustOverlay": [
+ "rust-overlay"
+ ]
},
"locked": {
- "lastModified": 1631254163,
- "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
+ "lastModified": 1634796585,
+ "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
- "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
+ "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"type": "github"
},
"original": {
@@ -55,11 +72,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1631206977,
- "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
+ "lastModified": 1634782485,
+ "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
+ "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"type": "github"
},
"original": {
@@ -69,21 +86,40 @@
"type": "github"
}
},
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1628186154,
+ "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "06552b72346632b6943c8032e57e702ea12413bf",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay"
}
},
- "rustOverlay": {
- "flake": false,
+ "rust-overlay": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs_2"
+ },
"locked": {
- "lastModified": 1631240108,
- "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
+ "lastModified": 1634869268,
+ "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
+ "rev": "c02c2d86354327317546501af001886fbb53d374",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index bcc9383e..296a68d5 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,9 +3,11 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+ rust-overlay.url = "github:oxalica/rust-overlay";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
+ inputs.rustOverlay.follows = "rust-overlay";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
@@ -61,7 +63,7 @@
'';
};
shell = common: prev: {
- packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
+ packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 51096453..ea695d34 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "helix-core"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
@@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
[features]
[dependencies]
-helix-syntax = { version = "0.4", path = "../helix-syntax" }
+helix-syntax = { version = "0.5", path = "../helix-syntax" }
ropey = "1.3"
smallvec = "1.7"
@@ -27,6 +27,7 @@ once_cell = "1.8"
arc-swap = "1"
regex = "1"
+log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index 9b901e9b..cc966852 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,3 +1,6 @@
+//! When typing the opening character of one of the possible pairs defined below,
+//! this module provides the functionality to insert the paired closing character.
+
use crate::{Range, Rope, Selection, Tendril, Transaction};
use smallvec::SmallVec;
diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs
index 24133dd3..c8e5efbd 100644
--- a/helix-core/src/chars.rs
+++ b/helix-core/src/chars.rs
@@ -1,3 +1,5 @@
+//! Utility functions to categorize a `char`.
+
use crate::LineEnding;
#[derive(Debug, Eq, PartialEq)]
diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs
index 3d8e1ce3..b22a95a6 100644
--- a/helix-core/src/comment.rs
+++ b/helix-core/src/comment.rs
@@ -1,3 +1,6 @@
+//! This module contains the functionality toggle comments on lines over the selection
+//! using the comment character defined in the user's `languages.toml`
+
use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
@@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token));
- let mut lines: Vec<usize> = Vec::new();
+ let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
let mut min_next_line = 0;
for selection in selection {
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index e08a71e7..ad1ba16a 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,3 +1,6 @@
+//! LSP diagnostic utility types.
+
+/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)]
pub enum Severity {
Error,
@@ -6,12 +9,14 @@ pub enum Severity {
Hint,
}
-#[derive(Debug)]
+/// A range of `char`s within the text.
+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct Range {
pub start: usize,
pub end: usize,
}
+/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug)]
pub struct Diagnostic {
pub range: Range,
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index 0465fe51..c6398875 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -1,4 +1,6 @@
-// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
+//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
+//!
+//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs
index 67ded166..b53c01fe 100644
--- a/helix-core/src/history.rs
+++ b/helix-core/src/history.rs
@@ -4,48 +4,50 @@ use regex::Regex;
use std::num::NonZeroUsize;
use std::time::{Duration, Instant};
-// Stores the history of changes to a buffer.
-//
-// Currently the history is represented as a vector of revisions. The vector
-// always has at least one element: the empty root revision. Each revision
-// with the exception of the root has a parent revision, a [Transaction]
-// that can be applied to its parent to transition from the parent to itself,
-// and an inversion of that transaction to transition from the parent to its
-// latest child.
-//
-// When using `u` to undo a change, an inverse of the stored transaction will
-// be applied which will transition the buffer to the parent state.
-//
-// Each revision with the exception of the last in the vector also has a
-// last child revision. When using `U` to redo a change, the last child transaction
-// will be applied to the current state of the buffer.
-//
-// The current revision is the one currently displayed in the buffer.
-//
-// Commiting a new revision to the history will update the last child of the
-// current revision, and push a new revision to the end of the vector.
-//
-// Revisions are commited with a timestamp. :earlier and :later can be used
-// to jump to the closest revision to a moment in time relative to the timestamp
-// of the current revision plus (:later) or minus (:earlier) the duration
-// given to the command. If a single integer is given, the editor will instead
-// jump the given number of revisions in the vector.
-//
-// Limitations:
-// * Changes in selections currently don't commit history changes. The selection
-// will only be updated to the state after a commited buffer change.
-// * The vector of history revisions is currently unbounded. This might
-// cause the memory consumption to grow significantly large during long
-// editing sessions.
-// * Because delete transactions currently don't store the text that they
-// delete, we also store an inversion of the transaction.
+/// Stores the history of changes to a buffer.
+///
+/// Currently the history is represented as a vector of revisions. The vector
+/// always has at least one element: the empty root revision. Each revision
+/// with the exception of the root has a parent revision, a [Transaction]
+/// that can be applied to its parent to transition from the parent to itself,
+/// and an inversion of that transaction to transition from the parent to its
+/// latest child.
+///
+/// When using `u` to undo a change, an inverse of the stored transaction will
+/// be applied which will transition the buffer to the parent state.
+///
+/// Each revision with the exception of the last in the vector also has a
+/// last child revision. When using `U` to redo a change, the last child transaction
+/// will be applied to the current state of the buffer.
+///
+/// The current revision is the one currently displayed in the buffer.
+///
+/// Commiting a new revision to the history will update the last child of the
+/// current revision, and push a new revision to the end of the vector.
+///
+/// Revisions are commited with a timestamp. :earlier and :later can be used
+/// to jump to the closest revision to a moment in time relative to the timestamp
+/// of the current revision plus (:later) or minus (:earlier) the duration
+/// given to the command. If a single integer is given, the editor will instead
+/// jump the given number of revisions in the vector.
+///
+/// Limitations:
+/// * Changes in selections currently don't commit history changes. The selection
+/// will only be updated to the state after a commited buffer change.
+/// * The vector of history revisions is currently unbounded. This might
+/// cause the memory consumption to grow significantly large during long
+/// editing sessions.
+/// * Because delete transactions currently don't store the text that they
+/// delete, we also store an inversion of the transaction.
+///
+/// Using time to navigate the history: <https://github.com/helix-editor/helix/pull/194>
#[derive(Debug)]
pub struct History {
revisions: Vec<Revision>,
current: usize,
}
-// A single point in history. See [History] for more information.
+/// A single point in history. See [History] for more information.
#[derive(Debug)]
struct Revision {
parent: usize,
@@ -111,6 +113,7 @@ impl History {
self.current == 0
}
+ /// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
return None;
@@ -121,6 +124,7 @@ impl History {
Some(&current_revision.inversion)
}
+ /// Redo the last edit.
pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.current];
let last_child = current_revision.last_child?;
@@ -147,8 +151,8 @@ impl History {
}
}
- // List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
- // Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
+ /// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
+ /// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
let mut path = Vec::new();
while n != a {
@@ -158,6 +162,7 @@ impl History {
path
}
+ /// Create a [`Transaction`] that will jump to a specific revision in the history.
fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
let lca = self.lowest_common_ancestor(self.current, to);
let up = self.path_up(self.current, lca);
@@ -171,10 +176,12 @@ impl History {
up_txns.chain(down_txns).collect()
}
+ /// Creates a [`Transaction`] that will undo `delta` revisions.
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(self.current.saturating_sub(delta))
}
+ /// Creates a [`Transaction`] that will redo `delta` revisions.
fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(
self.current
@@ -183,7 +190,7 @@ impl History {
)
}
- // Helper for a binary search case below.
+ /// Helper for a binary search case below.
fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
let dur_i = self.revisions[i].timestamp.duration_since(instant);
@@ -194,6 +201,8 @@ impl History {
}
}
+ /// Creates a [`Transaction`] that will match a revision created at around
+ /// `instant`.
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
let search_result = self
.revisions
@@ -209,6 +218,8 @@ impl History {
self.jump_to(revision)
}
+ /// Creates a [`Transaction`] that will match a revision created `duration` ago
+ /// from the timestamp of current revision.
fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_sub(duration) {
Some(instant) => self.jump_instant(instant),
@@ -216,6 +227,8 @@ impl History {
}
}
+ /// Creates a [`Transaction`] that will match a revision created `duration` in
+ /// the future from the timestamp of the current revision.
fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_add(duration) {
Some(instant) => self.jump_instant(instant),
@@ -223,6 +236,7 @@ impl History {
}
}
+ /// Creates an undo [`Transaction`].
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*;
match uk {
@@ -231,6 +245,7 @@ impl History {
}
}
+ /// Creates a redo [`Transaction`].
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*;
match uk {
@@ -240,13 +255,14 @@ impl History {
}
}
+/// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq)]
pub enum UndoKind {
Steps(usize),
TimePeriod(std::time::Duration),
}
-// A subset of sytemd.time time span syntax units.
+/// A subset of sytemd.time time span syntax units.
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["seconds", "second", "sec", "s"], "seconds", 1),
(&["minutes", "minute", "min", "m"], "minutes", 60),
@@ -254,11 +270,20 @@ const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["days", "day", "d"], "days", 24 * 60 * 60),
];
+/// Checks if the duration input can be turned into a valid duration. It must be a
+/// positive integer and denote the [unit of time.](`TIME_UNITS`)
+/// Examples of valid durations:
+/// * `5 sec`
+/// * `5 min`
+/// * `5 hr`
+/// * `5 days`
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
+/// Captures both the number and unit as separate capture groups.
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
+/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`].
fn parse_human_duration(s: &str) -> Result<Duration, String> {
if !DURATION_VALIDATION_REGEX.is_match(s) {
return Err("duration should be composed \
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 1f32d038..df158363 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -464,6 +464,7 @@ where
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
+ textobject_query: OnceCell::new(),
debugger: None,
}],
});
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 0854eb04..f4284139 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
+/// Find `.git` root.
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
@@ -193,7 +194,7 @@ pub use tendril::StrTendril as Tendril;
pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes;
-pub use position::{coords_at_pos, pos_at_coords, Position};
+pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position};
pub use selection::{Range, Selection};
pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax;
diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs
index 18ea5f9f..3541305c 100644
--- a/helix-core/src/line_ending.rs
+++ b/helix-core/src/line_ending.rs
@@ -20,7 +20,7 @@ pub enum LineEnding {
impl LineEnding {
#[inline]
- pub fn len_chars(&self) -> usize {
+ pub const fn len_chars(&self) -> usize {
match self {
Self::Crlf => 2,
_ => 1,
@@ -28,7 +28,7 @@ impl LineEnding {
}
#[inline]
- pub fn as_str(&self) -> &'static str {
+ pub const fn as_str(&self) -> &'static str {
match self {
Self::Crlf => "\u{000D}\u{000A}",
Self::LF => "\u{000A}",
@@ -42,7 +42,7 @@ impl LineEnding {
}
#[inline]
- pub fn from_char(ch: char) -> Option<LineEnding> {
+ pub const fn from_char(ch: char) -> Option<LineEnding> {
match ch {
'\u{000A}' => Some(LineEnding::LF),
'\u{000B}' => Some(LineEnding::VT),
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 5d080545..9e85bd21 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -53,6 +53,10 @@ pub fn move_vertically(
let pos = range.cursor(slice);
// Compute the current position's 2d coordinates.
+ // TODO: switch this to use `visual_coords_at_pos` rather than
+ // `coords_at_pos` as this will cause a jerky movement when the visual
+ // position does not match, like moving from a line with tabs/CJK to
+ // a line without
let Position { row, col } = coords_at_pos(slice, pos);
let horiz = range.horiz.unwrap_or(col as u32);
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index d9558dd8..717c5994 100644
--- a/helix-core/src/object.rs
+++ b/helix-core/src/object.rs
@@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
let parent = match tree
.root_node()
.descendant_for_byte_range(from, to)
- .and_then(|node| node.parent())
- {
+ .and_then(|node| {
+ if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
+ node.parent()
+ } else {
+ Some(node)
+ }
+ }) {
Some(parent) => parent,
None => return range,
};
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
index 08a8aed5..c6018ce6 100644
--- a/helix-core/src/position.rs
+++ b/helix-core/src/position.rs
@@ -2,6 +2,7 @@ use crate::{
chars::char_is_line_ending,
graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
line_ending::line_end_char_index,
+ unicode::width::UnicodeWidthChar,
RopeSlice,
};
@@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
}
/// Convert a character index to (line, column) coordinates.
///
-/// TODO: this should be split into two methods: one for visual
-/// row/column, and one for "objective" row/column (possibly with
-/// the column specified in `char`s). The former would be used
-/// for cursor movement, and the latter would be used for e.g. the
-/// row:column display in the status line.
+/// column in `char` count which can be used for row:column display in
+/// status line. See [`visual_coords_at_pos`] for a visual one.
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line = text.char_to_line(pos);
@@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
Position::new(line, col)
}
+/// Convert a character index to (line, column) coordinates visually.
+///
+/// Takes \t, double-width characters (CJK) into account as well as text
+/// not in the document in the future.
+/// See [`coords_at_pos`] for an "objective" one.
+pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
+ let line = text.char_to_line(pos);
+
+ let line_start = text.line_to_char(line);
+ let pos = ensure_grapheme_boundary_prev(text, pos);
+ let col = text
+ .slice(line_start..pos)
+ .chars()
+ .flat_map(|c| match c {
+ '\t' => Some(tab_width),
+ c => UnicodeWidthChar::width(c),
+ })
+ .sum();
+
+ Position::new(line, col)
+}
+
/// Convert (line, column) coordinates to a character index.
///
/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -130,7 +150,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
// Test with wide characters.
- // TODO: account for character width.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -151,7 +170,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
// Test with wide-character grapheme clusters.
- // TODO: account for character width.
let text = Rope::from("किमपि\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -161,7 +179,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
// Test with tabs.
- // Todo: account for tab stops.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -170,6 +187,54 @@ mod test {
}
#[test]
+ fn test_visual_coords_at_pos() {
+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
+ let slice = text.slice(..);
+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n
+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w
+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o
+ assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d
+
+ // Test with wide characters.
+ let text = Rope::from("今日はいい\n");
+ let slice = text.slice(..);
+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into());
+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into());
+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into());
+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into());
+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into());
+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
+
+ // Test with grapheme clusters.
+ let text = Rope::from("a̐éö̲\r\n");
+ let slice = text.slice(..);
+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into());
+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into());
+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into());
+ assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into());
+
+ // Test with wide-character grapheme clusters.
+ // TODO: account for cluster.
+ let text = Rope::from("किमपि\n");
+ let slice = text.slice(..);
+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into());
+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into());
+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into());
+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
+
+ // Test with tabs.
+ let text = Rope::from("\tHello\n");
+ let slice = text.slice(..);
+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into());
+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
+ }
+
+ #[test]
fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..);
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index c3e6652e..c5444eb7 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -7,7 +7,7 @@ pub struct Register {
}
impl Register {
- pub fn new(name: char) -> Self {
+ pub const fn new(name: char) -> Self {
Self {
name,
values: Vec::new(),
@@ -18,7 +18,7 @@ impl Register {
Self { name, values }
}
- pub fn name(&self) -> char {
+ pub const fn name(&self) -> char {
self.name
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 18af4d08..f3b5d2c8 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -362,6 +362,11 @@ impl Selection {
/// Adds a new range to the selection and makes it the primary range.
pub fn remove(mut self, index: usize) -> Self {
+ assert!(
+ self.ranges.len() > 1,
+ "can't remove the last range from a selection!"
+ );
+
self.ranges.remove(index);
if index < self.primary_index || self.primary_index == self.ranges.len() {
self.primary_index -= 1;
@@ -369,6 +374,12 @@ impl Selection {
self
}
+ /// Replace a range in the selection with a new range.
+ pub fn replace(mut self, index: usize, range: Range) -> Self {
+ self.ranges[index] = range;
+ self.normalize()
+ }
+
/// 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 441802a5..18504c21 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -49,7 +49,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
- pub(crate) language_id: String,
+ pub language_id: String,
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
+ #[serde(skip)]
+ pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>,
}
@@ -160,6 +162,32 @@ pub struct IndentQuery {
pub outdent: HashSet<String>,
}
+#[derive(Debug)]
+pub struct TextObjectQuery {
+ pub query: Query,
+}
+
+impl TextObjectQuery {
+ /// Run the query on the given node and return sub nodes which match given
+ /// capture ("function.inside", "class.around", etc).
+ pub fn capture_nodes<'a>(
+ &'a self,
+ capture_name: &str,
+ node: Node<'a>,
+ slice: RopeSlice<'a>,
+ cursor: &'a mut QueryCursor,
+ ) -> Option<impl Iterator<Item = Node<'a>>> {
+ let capture_idx = self.query.capture_index_for_name(capture_name)?;
+ let captures = cursor.captures(&self.query, node, RopeProvider(slice));
+
+ captures
+ .filter_map(move |(mat, idx)| {
+ (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
+ })
+ .into()
+ }
+}
+
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
@@ -208,13 +236,14 @@ impl LanguageConfiguration {
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
-
let locals_query = read_query(&language, "locals.scm");
if highlights_query.is_empty() {
None
} else {
- let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
+ let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
+ .map_err(|e| log::info!("{}", e))
+ .ok()?;
let config = HighlightConfiguration::new(
language,
&highlights_query,
@@ -258,6 +287,18 @@ impl LanguageConfiguration {
.as_ref()
}
+ pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
+ self.textobject_query
+ .get_or_init(|| -> Option<TextObjectQuery> {
+ let lang_name = self.language_id.to_ascii_lowercase();
+ let query_text = read_query(&lang_name, "textobjects.scm");
+ let lang = self.highlight_config.get()?.as_ref()?.language;
+ let query = Query::new(lang, &query_text).ok()?;
+ Some(TextObjectQuery { query })
+ })
+ .as_ref()
+ }
+
pub fn scope(&self) -> &str {
&self.scope
}
@@ -451,7 +492,7 @@ impl Syntax {
/// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>(
- &self,
+ &'a self,
source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>,
@@ -466,11 +507,10 @@ impl Syntax {
let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
});
- let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
+ let tree_ref = self.tree();
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
- let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
- let config_ref =
- unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
+ let query_ref = &self.config.query;
+ let config_ref = self.config.as_ref();
// if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@@ -582,39 +622,7 @@ impl LanguageLayer {
self.tree.as_ref(),
)
.ok_or(Error::Cancelled)?;
- // unsafe { syntax.parser.set_cancellation_flag(None) };
- // let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new);
-
- // Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526)
- // if let Some(combined_injections_query) = &config.combined_injections_query {
- // 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(), RopeProvider(source));
- // for mat in matches {
- // let entry = &mut injections_by_pattern_index[mat.pattern_index];
- // let (language_name, content_node, include_children) =
- // injection_for_match(config, combined_injections_query, &mat, source);
- // if language_name.is_some() {
- // entry.0 = language_name;
- // }
- // if let Some(content_node) = content_node {
- // entry.1.push(content_node);
- // }
- // entry.2 = include_children;
- // }
- // for (lang_name, content_nodes, includes_children) in injections_by_pattern_index {
- // if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
- // if let Some(next_config) = (injection_callback)(lang_name) {
- // let ranges =
- // Self::intersect_ranges(&ranges, &content_nodes, includes_children);
- // if !ranges.is_empty() {
- // queue.push((next_config, depth + 1, ranges));
- // }
- // }
- // }
- // }
- // }
+
self.tree = Some(tree)
}
Ok(())
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index b965f6df..975ed115 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -1,9 +1,13 @@
+use std::fmt::Display;
+
use ropey::RopeSlice;
+use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction;
use crate::surround;
+use crate::syntax::LanguageConfiguration;
use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
@@ -51,6 +55,15 @@ pub enum TextObject {
Inside,
}
+impl Display for TextObject {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Self::Around => "around",
+ Self::Inside => "inside",
+ })
+ }
+}
+
// count doesn't do anything yet
pub fn textobject_word(
slice: RopeSlice,
@@ -108,6 +121,44 @@ pub fn textobject_surround(
.unwrap_or(range)
}
+/// Transform the given range to select text objects based on tree-sitter.
+/// `object_name` is a query capture base name like "function", "class", etc.
+/// `slice_tree` is the tree-sitter node corresponding to given text slice.
+pub fn textobject_treesitter(
+ slice: RopeSlice,
+ range: Range,
+ textobject: TextObject,
+ object_name: &str,
+ slice_tree: Node,
+ lang_config: &LanguageConfiguration,
+ _count: usize,
+) -> Range {
+ let get_range = move || -> Option<Range> {
+ let byte_pos = slice.char_to_byte(range.cursor(slice));
+
+ let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
+ let mut cursor = QueryCursor::new();
+ let node = lang_config
+ .textobject_query()?
+ .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
+ .filter(|node| node.byte_range().contains(&byte_pos))
+ .min_by_key(|node| node.byte_range().len())?;
+
+ let len = slice.len_bytes();
+ let start_byte = node.start_byte();
+ let end_byte = node.end_byte();
+ if start_byte >= len || end_byte >= len {
+ return None;
+ }
+
+ let start_char = slice.byte_to_char(start_byte);
+ let end_char = slice.byte_to_char(end_byte);
+
+ Some(Range::new(start_char, end_char))
+ };
+ get_range().unwrap_or(range)
+}
+
#[cfg(test)]
mod test {
use super::TextObject::*;
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index d682f058..dfc18fbe 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -132,6 +132,9 @@ impl ChangeSet {
if self.changes.is_empty() {
return other;
}
+ if other.changes.is_empty() {
+ return self;
+ }
let len = self.changes.len();
@@ -465,6 +468,13 @@ impl Transaction {
}
}
+ pub fn compose(mut self, other: Self) -> Self {
+ self.changes = self.changes.compose(other.changes);
+ // Other selection takes precedence
+ self.selection = other.selection;
+ self
+ }
+
pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = Some(selection);
self
diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml
index 2b922e84..6870adb6 100644
--- a/helix-dap/Cargo.toml
+++ b/helix-dap/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-dap"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 1252172a..0192ba1e 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "helix-lsp"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
description = "LSP client implementation for Helix project"
categories = ["editor"]
@@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-helix-core = { version = "0.4", path = "../helix-core" }
+helix-core = { version = "0.5", path = "../helix-core" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
log = "0.4"
-lsp-types = { version = "0.90", features = ["proposed"] }
+lsp-types = { version = "0.91", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
-tokio = { version = "1.12", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
-tokio-stream = "0.1.7"
+tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
+tokio-stream = "0.1.8"
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 4068ae1f..b810feef 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -461,7 +461,7 @@ impl Client {
};
let changes = match sync_capabilities {
- lsp::TextDocumentSyncKind::Full => {
+ lsp::TextDocumentSyncKind::FULL => {
vec![lsp::TextDocumentContentChangeEvent {
// range = None -> whole document
range: None, //Some(Range)
@@ -469,10 +469,11 @@ impl Client {
text: new_text.to_string(),
}]
}
- lsp::TextDocumentSyncKind::Incremental => {
+ lsp::TextDocumentSyncKind::INCREMENTAL => {
Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding)
}
- lsp::TextDocumentSyncKind::None => return None,
+ lsp::TextDocumentSyncKind::NONE => return None,
+ kind => unimplemented!("{:?}", kind),
};
Some(self.notify::<lsp::notification::DidChangeTextDocument>(
diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml
index 9c2b8275..cceec412 100644
--- a/helix-syntax/Cargo.toml
+++ b/helix-syntax/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "helix-syntax"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
description = "Tree-sitter grammars support"
categories = ["editor"]
diff --git a/helix-syntax/languages/tree-sitter-cmake b/helix-syntax/languages/tree-sitter-cmake
new file mode 160000
+Subproject f6616f1e417ee8b62daf251aa1daa5d73781c59
diff --git a/helix-syntax/languages/tree-sitter-cpp b/helix-syntax/languages/tree-sitter-cpp
-Subproject c61212414a3e95b5f7507f98e83de1d638044ad
+Subproject e8dcc9d2b404c542fd236ea5f7208f90be8a6e8
diff --git a/helix-syntax/languages/tree-sitter-elixir b/helix-syntax/languages/tree-sitter-elixir
-Subproject 295e62a43b92cea909cfabe57e8818d177f4857
+Subproject f5d7bda543da788bd507b05bd722627dde66c9e
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 68ff260d..43268291 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -1,9 +1,9 @@
[package]
name = "helix-term"
-version = "0.4.1"
+version = "0.5.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
@@ -21,10 +21,10 @@ name = "hx"
path = "src/main.rs"
[dependencies]
-helix-core = { version = "0.4", path = "../helix-core" }
-helix-view = { version = "0.4", path = "../helix-view" }
-helix-lsp = { version = "0.4", path = "../helix-lsp" }
-helix-dap = { version = "0.4", path = "../helix-dap" }
+helix-core = { version = "0.5", path = "../helix-core" }
+helix-view = { version = "0.5", path = "../helix-view" }
+helix-lsp = { version = "0.5", path = "../helix-lsp" }
+helix-dap = { version = "0.5", path = "../helix-dap" }
anyhow = "1"
once_cell = "1.8"
@@ -32,7 +32,7 @@ once_cell = "1.8"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
-crossterm = { version = "0.21", features = ["event-stream"] }
+crossterm = { version = "0.22", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -45,10 +45,10 @@ log = "0.4"
# File picker
fuzzy-matcher = "0.3"
ignore = "0.4"
-# shellexpand = "2.1"
-# dirs-next = "2.0"
# markdown doc rendering
pulldown-cmark = { version = "0.8", default-features = false }
+# file type detection
+content_inspector = "0.2.4"
# config
toml = "0.5"
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 27062a36..0fb4e479 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -99,12 +99,17 @@ impl Application {
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view);
- if !args.files.is_empty() {
+ if args.load_tutor {
+ let path = helix_core::runtime_dir().join("tutor.txt");
+ editor.open(path, Action::VerticalSplit)?;
+ // Unset path to prevent accidentally saving to the original tutor file.
+ doc_mut!(editor).set_path(None)?;
+ } else if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
- compositor.push(Box::new(ui::file_picker(first.clone())));
+ compositor.push(Box::new(ui::file_picker(".".into())));
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -240,7 +245,7 @@ impl Application {
}
pub fn handle_idle_timeout(&mut self) {
- use crate::commands::{completion, Context};
+ use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
@@ -267,7 +272,7 @@ impl Application {
callback: None,
on_next_key_callback: None,
};
- completion(&mut cx);
+ idle_completion(&mut cx);
self.render();
}
@@ -548,10 +553,11 @@ impl Application {
message: diagnostic.message,
severity: diagnostic.severity.map(
|severity| match severity {
- DiagnosticSeverity::Error => Error,
- DiagnosticSeverity::Warning => Warning,
- DiagnosticSeverity::Information => Info,
- DiagnosticSeverity::Hint => Hint,
+ DiagnosticSeverity::ERROR => Error,
+ DiagnosticSeverity::WARNING => Warning,
+ DiagnosticSeverity::INFORMATION => Info,
+ DiagnosticSeverity::HINT => Hint,
+ severity => unimplemented!("{:?}", severity),
},
),
// code
@@ -727,7 +733,9 @@ impl Application {
let mut stdout = stdout();
// reset cursor shape
write!(stdout, "\x1B[2 q")?;
- execute!(stdout, DisableMouseCapture)?;
+ // Ignore errors on disabling, this might trigger on windows if we call
+ // disable without calling enable previously
+ let _ = execute!(stdout, DisableMouseCapture);
execute!(stdout, terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
Ok(())
diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs
index f0ef09eb..40113db9 100644
--- a/helix-term/src/args.rs
+++ b/helix-term/src/args.rs
@@ -5,6 +5,7 @@ use std::path::PathBuf;
pub struct Args {
pub display_help: bool,
pub display_version: bool,
+ pub load_tutor: bool,
pub verbosity: u64,
pub files: Vec<PathBuf>,
}
@@ -22,6 +23,7 @@ impl Args {
"--" => break, // stop parsing at this point treat the remaining as files
"--version" => args.display_version = true,
"--help" => args.display_help = true,
+ "--tutor" => args.load_tutor = true,
arg if arg.starts_with("--") => {
return Err(Error::msg(format!(
"unexpected double dash argument: {}",
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index f3761d7d..3616d6a8 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -16,8 +16,13 @@ use helix_core::{
};
use helix_view::{
- clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
- view::View, Document, DocumentId, Editor, ViewId,
+ clipboard::ClipboardType,
+ document::Mode,
+ editor::{Action, Motion},
+ input::KeyEvent,
+ keyboard::KeyCode,
+ view::View,
+ Document, DocumentId, Editor, ViewId,
};
use anyhow::{anyhow, bail, Context as _};
@@ -202,6 +207,7 @@ impl Command {
find_prev_char, "Move to previous occurance of char",
extend_till_prev_char, "Extend till previous occurance of char",
extend_prev_char, "Extend to previous occurance of char",
+ repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)",
replace, "Replace with new char",
switch_case, "Switch (toggle) case",
switch_to_uppercase, "Switch to uppercase",
@@ -215,8 +221,11 @@ impl Command {
split_selection, "Split selection into subselections on regex matches",
split_selection_on_newline, "Split selection on newlines",
search, "Search for regex pattern",
+ rsearch, "Reverse search for regex pattern",
search_next, "Select next search match",
+ search_prev, "Select previous search match",
extend_search_next, "Add next search match to selection",
+ extend_search_prev, "Add previous 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",
@@ -260,6 +269,8 @@ impl Command {
goto_prev_diag, "Goto previous diagnostic",
goto_line_start, "Goto line start",
goto_line_end, "Goto line end",
+ goto_next_buffer, "Goto next buffer",
+ goto_previous_buffer, "Goto previous buffer",
// TODO: different description ?
goto_line_end_newline, "Goto line end",
goto_first_nonwhitespace, "Goto first non-blank in line",
@@ -305,6 +316,10 @@ impl Command {
expand_selection, "Expand selection to parent syntax node",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
+ jump_view_right, "Jump to the split to the right",
+ jump_view_left, "Jump to the split to the left",
+ jump_view_up, "Jump to the split above",
+ jump_view_down, "Jump to the split below",
rotate_view, "Goto next window",
hsplit, "Horizontal bottom split",
vsplit, "Vertical right split",
@@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) {
)
}
+fn goto_next_buffer(cx: &mut Context) {
+ goto_buffer(cx, Direction::Forward);
+}
+
+fn goto_previous_buffer(cx: &mut Context) {
+ goto_buffer(cx, Direction::Backward);
+}
+
+fn goto_buffer(cx: &mut Context, direction: Direction) {
+ let current = view!(cx.editor).doc;
+
+ let id = match direction {
+ Direction::Forward => {
+ let iter = cx.editor.documents.keys();
+ let mut iter = iter.skip_while(|id| *id != &current);
+ iter.next(); // skip current item
+ iter.next().or_else(|| cx.editor.documents.keys().next())
+ }
+ Direction::Backward => {
+ let iter = cx.editor.documents.keys();
+ let mut iter = iter.rev().skip_while(|id| *id != &current);
+ iter.next(); // skip current item
+ iter.next()
+ .or_else(|| cx.editor.documents.keys().rev().next())
+ }
+ }
+ .unwrap();
+
+ let id = *id;
+
+ cx.editor.switch(id, Action::Replace);
+}
+
fn extend_to_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
goto_line_start_impl(view, doc, Movement::Extend)
@@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) {
} else {
push_jump(cx.editor);
let (view, doc) = current!(cx.editor);
- doc.set_selection(view.id, Selection::point(0));
+ let text = doc.text().slice(..);
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
}
}
fn goto_file_end(cx: &mut Context) {
push_jump(cx.editor);
let (view, doc) = current!(cx.editor);
- doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
+ let text = doc.text().slice(..);
+ let pos = doc.text().len_chars();
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
}
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
@@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
extend_word_impl(cx, movement::move_next_long_word_end)
}
-#[inline]
-fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
+fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
where
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
{
@@ -704,13 +762,7 @@ where
// usually mix line endings. But we should fix it eventually
// anyway.
{
- current!(cx.editor)
- .1
- .line_ending
- .as_str()
- .chars()
- .next()
- .unwrap()
+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
}
KeyEvent {
@@ -720,29 +772,48 @@ where
_ => return,
};
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
+ })));
+ })
+}
- let selection = doc.selection(view.id).clone().transform(|range| {
- // TODO: use `Range::cursor()` here instead. However, that works in terms of
- // graphemes, whereas this function doesn't yet. So we're doing the same logic
- // here, but just in terms of chars instead.
- let search_start_pos = if range.anchor < range.head {
- range.head - 1
- } else {
- range.head
- };
+//
- search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
- if extend {
- range.put_cursor(text, pos, true)
- } else {
- Range::point(range.cursor(text)).put_cursor(text, pos, true)
- }
- })
- });
- doc.set_selection(view.id, selection);
- })
+#[inline]
+fn find_char_impl<F>(
+ editor: &mut Editor,
+ search_fn: &F,
+ inclusive: bool,
+ extend: bool,
+ ch: char,
+ count: usize,
+) where
+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
+{
+ let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
+ // here, but just in terms of chars instead.
+ let search_start_pos = if range.anchor < range.head {
+ range.head - 1
+ } else {
+ range.head
+ };
+
+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
+ if extend {
+ range.put_cursor(text, pos, true)
+ } else {
+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
+ }
+ })
+ });
+ doc.set_selection(view.id, selection);
}
fn find_next_char_impl(
@@ -756,6 +827,10 @@ fn find_next_char_impl(
if inclusive {
search::find_nth_next(text, ch, pos, n)
} else {
+ let n = match text.get_char(pos) {
+ Some(next_ch) if next_ch == ch => n + 1,
+ _ => n,
+ };
search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
}
}
@@ -770,80 +845,52 @@ fn find_prev_char_impl(
if inclusive {
search::find_nth_prev(text, ch, pos, n)
} else {
+ let n = match text.get_char(pos.saturating_sub(1)) {
+ Some(next_ch) if next_ch == ch => n + 1,
+ _ => n,
+ };
search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
}
}
fn find_till_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_next_char_impl,
- false, /* inclusive */
- false, /* extend */
- )
+ will_find_char(cx, find_next_char_impl, false, false)
}
fn find_next_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_next_char_impl,
- true, /* inclusive */
- false, /* extend */
- )
+ will_find_char(cx, find_next_char_impl, true, false)
}
fn extend_till_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_next_char_impl,
- false, /* inclusive */
- true, /* extend */
- )
+ will_find_char(cx, find_next_char_impl, false, true)
}
fn extend_next_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_next_char_impl,
- true, /* inclusive */
- true, /* extend */
- )
+ will_find_char(cx, find_next_char_impl, true, true)
}
fn till_prev_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_prev_char_impl,
- false, /* inclusive */
- false, /* extend */
- )
+ will_find_char(cx, find_prev_char_impl, false, false)
}
fn find_prev_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_prev_char_impl,
- true, /* inclusive */
- false, /* extend */
- )
+ will_find_char(cx, find_prev_char_impl, true, false)
}
fn extend_till_prev_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_prev_char_impl,
- false, /* inclusive */
- true, /* extend */
- )
+ will_find_char(cx, find_prev_char_impl, false, true)
}
fn extend_prev_char(cx: &mut Context) {
- find_char_impl(
- cx,
- find_prev_char_impl,
- true, /* inclusive */
- true, /* extend */
- )
+ will_find_char(cx, find_prev_char_impl, true, true)
+}
+
+fn repeat_last_motion(cx: &mut Context) {
+ let last_motion = cx.editor.last_motion.take();
+ if let Some(m) = &last_motion {
+ m.run(cx.editor);
+ cx.editor.last_motion = last_motion;
+ }
}
fn replace(cx: &mut Context) {
@@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) {
cx,
"select:".into(),
Some(reg),
+ |_input: &str| Vec::new(),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
@@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) {
cx,
"split:".into(),
Some(reg),
+ |_input: &str| Vec::new(),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
@@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
+fn search_impl(
+ doc: &mut Document,
+ view: &mut View,
+ contents: &str,
+ regex: &Regex,
+ movement: Movement,
+ direction: Direction,
+) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
- // Get the right side of the primary block cursor.
- let start = text.char_to_byte(graphemes::next_grapheme_boundary(
- text,
- selection.primary().cursor(text),
- ));
+ // Get the right side of the primary block cursor for forward search, or the
+ //grapheme before the start of the selection for reverse search.
+ let start = match direction {
+ Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary(
+ text,
+ selection.primary().to(),
+ )),
+ Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary(
+ text,
+ selection.primary().from(),
+ )),
+ };
+
+ //A regex::Match returns byte-positions in the str. In the case where we
+ //do a reverse search and wraparound to the end, we don't need to search
+ //the text before the current cursor position for matches, but by slicing
+ //it out, we need to add it back to the position of the selection.
+ let mut offset = 0;
// use find_at to find the next match after the cursor, loop around the end
// Careful, `Regex` uses `bytes` as offsets, not character indices!
- let mat = regex
- .find_at(contents, start)
- .or_else(|| regex.find(contents));
+ let mat = match direction {
+ Direction::Forward => regex
+ .find_at(contents, start)
+ .or_else(|| regex.find(contents)),
+ Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
+ offset = start;
+ regex.find_iter(&contents[start..]).last()
+ }),
+ };
// TODO: message on wraparound
if let Some(mat) = mat {
- let start = text.byte_to_char(mat.start());
- let end = text.byte_to_char(mat.end());
+ let start = text.byte_to_char(mat.start() + offset);
+ let end = text.byte_to_char(mat.end() + offset);
if end == 0 {
// skip empty matches that don't make sense
return;
}
- let selection = if extend {
- selection.clone().push(Range::new(start, end))
+ // Determine range direction based on the primary range
+ let primary = selection.primary();
+ let range = if primary.head < primary.anchor {
+ Range::new(end, start)
} else {
- Selection::single(start, end)
+ Range::new(start, end)
+ };
+
+ let selection = match movement {
+ Movement::Extend => selection.clone().push(range),
+ Movement::Move => selection.clone().replace(selection.primary_index(), range),
};
doc.set_selection(view.id, selection);
@@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
};
}
+fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
+ let mut items = reg
+ .and_then(|reg| cx.editor.registers.get(reg))
+ .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
+ items.sort_unstable();
+ items.dedup();
+ items.into_iter().cloned().collect()
+}
+
// TODO: use one function for search vs extend
fn search(cx: &mut Context) {
+ searcher(cx, Direction::Forward)
+}
+
+fn rsearch(cx: &mut Context) {
+ searcher(cx, Direction::Backward)
+}
+// TODO: use one function for search vs extend
+fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
let (_, doc) = current!(cx.editor);
@@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) {
// HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
// feed chunks into the regex yet
let contents = doc.text().slice(..).to_string();
+ let completions = search_completions(cx, Some(reg));
let prompt = ui::regex_prompt(
cx,
"search:".into(),
Some(reg),
+ move |input: &str| {
+ completions
+ .iter()
+ .filter(|comp| comp.starts_with(input))
+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
+ .collect()
+ },
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
}
- search_impl(doc, view, &contents, &regex, false);
+ search_impl(doc, view, &contents, &regex, Movement::Move, direction);
},
);
cx.push_layer(Box::new(prompt));
}
-fn search_next_impl(cx: &mut Context, extend: bool) {
+fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
let (view, doc) = current!(cx.editor);
let registers = &cx.editor.registers;
if let Some(query) = registers.read('/') {
@@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
.case_insensitive(case_insensitive)
.build()
{
- search_impl(doc, view, &contents, &regex, extend);
+ search_impl(doc, view, &contents, &regex, movement, direction);
} else {
// get around warning `mutable_borrow_reservation_conflict`
// which will be a hard error in the future
@@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
}
fn search_next(cx: &mut Context) {
- search_next_impl(cx, false);
+ search_next_or_prev_impl(cx, Movement::Move, Direction::Forward);
}
+fn search_prev(cx: &mut Context) {
+ search_next_or_prev_impl(cx, Movement::Move, Direction::Backward);
+}
fn extend_search_next(cx: &mut Context) {
- search_next_impl(cx, true);
+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward);
+}
+
+fn extend_search_prev(cx: &mut Context) {
+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward);
}
fn search_selection(cx: &mut Context) {
@@ -1247,10 +1361,19 @@ 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 completions = search_completions(cx, None);
let prompt = ui::regex_prompt(
cx,
"global search:".into(),
None,
+ move |input: &str| {
+ completions
+ .iter()
+ .filter(|comp| comp.starts_with(input))
+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
+ .collect()
+ },
move |_view, _doc, regex, event| {
if event != PromptEvent::Validate {
return;
@@ -1572,7 +1695,8 @@ mod cmd {
let (_, doc) = current!(cx.editor);
if let Some(path) = path {
- doc.set_path(path.as_ref()).context("invalid filepath")?;
+ doc.set_path(Some(path.as_ref()))
+ .context("invalid filepath")?;
}
if doc.path().is_none() {
bail!("cannot write a buffer without a filename");
@@ -1635,7 +1759,7 @@ mod cmd {
// If no argument, report current indent style.
if args.is_empty() {
- let style = current!(cx.editor).1.indent_style;
+ let style = doc!(cx.editor).indent_style;
cx.editor.set_status(match style {
Tabs => "tabs".into(),
Spaces(1) => "1 space".into(),
@@ -1674,7 +1798,7 @@ mod cmd {
// If no argument, report current line ending setting.
if args.is_empty() {
- let line_ending = current!(cx.editor).1.line_ending;
+ let line_ending = doc!(cx.editor).line_ending;
cx.editor.set_status(match line_ending {
Crlf => "crlf".into(),
LF => "line feed".into(),
@@ -1790,7 +1914,7 @@ mod cmd {
let mut errors = String::new();
// save all documents
- for (_, doc) in &mut cx.editor.documents {
+ for doc in &mut cx.editor.documents.values_mut() {
if doc.path().is_none() {
errors.push_str("cannot write a buffer without a filename\n");
continue;
@@ -2085,8 +2209,7 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let id = doc.id();
+ let id = view!(cx.editor).doc;
if let Some(path) = args.get(0) {
cx.editor.open(path.into(), Action::VerticalSplit)?;
@@ -2102,8 +2225,7 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let id = doc.id();
+ let id = view!(cx.editor).doc;
if let Some(path) = args.get(0) {
cx.editor.open(path.into(), Action::HorizontalSplit)?;
@@ -2188,6 +2310,18 @@ mod cmd {
Ok(())
}
+ fn tutor(
+ cx: &mut compositor::Context,
+ _args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let path = helix_core::runtime_dir().join("tutor.txt");
+ cx.editor.open(path, Action::Replace)?;
+ // Unset path to prevent accidentally saving to the original tutor file.
+ doc_mut!(cx.editor).set_path(None)?;
+ Ok(())
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2199,7 +2333,7 @@ mod cmd {
TypableCommand {
name: "quit!",
aliases: &["q!"],
- doc: "Close the current view.",
+ doc: "Close the current view forcefully (ignoring unsaved changes).",
fun: force_quit,
completer: None,
},
@@ -2262,35 +2396,35 @@ mod cmd {
TypableCommand {
name: "write-quit",
aliases: &["wq", "x"],
- doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)",
+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
fun: write_quit,
completer: Some(completers::filename),
},
TypableCommand {
name: "write-quit!",
aliases: &["wq!", "x!"],
- doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
fun: force_write_quit,
completer: Some(completers::filename),
},
TypableCommand {
name: "write-all",
aliases: &["wa"],
- doc: "Writes changes from all views to disk.",
+ doc: "Write changes from all views to disk.",
fun: write_all,
completer: None,
},
TypableCommand {
name: "write-quit-all",
aliases: &["wqa", "xa"],
- doc: "Writes changes from all views to disk and close all views.",
+ doc: "Write changes from all views to disk and close all views.",
fun: write_all_quit,
completer: None,
},
TypableCommand {
name: "write-quit-all!",
aliases: &["wqa!", "xa!"],
- doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
+ doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
fun: force_write_all_quit,
completer: None,
},
@@ -2461,7 +2595,14 @@ mod cmd {
doc: "Open the file in a horizontal split.",
fun: hsplit,
completer: Some(completers::filename),
- }
+ },
+ TypableCommand {
+ name: "tutor",
+ aliases: &[],
+ doc: "Open the tutorial.",
+ fun: tutor,
+ completer: None,
+ },
];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) {
cx.editor
.documents
.iter()
- .map(|(id, doc)| (id, doc.path().cloned()))
+ .map(|(id, doc)| (*id, doc.path().cloned()))
.collect(),
move |(id, path): &(DocumentId, Option<PathBuf>)| {
let path = path.as_deref().map(helix_core::path::get_relative_path);
@@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) {
editor.switch(*id, Action::Replace);
},
|editor, (id, path)| {
- let doc = &editor.documents.get(*id)?;
+ let doc = &editor.documents.get(id)?;
let &view_id = doc.selections().keys().next()?;
let line = doc
.selection(view_id)
@@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) {
doc.text().len_lines() - 1
};
let line_idx = std::cmp::min(count.get() - 1, max_line);
+ let text = doc.text().slice(..);
let pos = doc.text().line_to_char(line_idx);
- doc.set_selection(view.id, Selection::point(pos));
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
}
}
@@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) {
} else {
doc.text().len_lines() - 1
};
+ let text = doc.text().slice(..);
let pos = doc.text().line_to_char(line_idx);
- doc.set_selection(view.id, Selection::point(pos));
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
}
fn goto_last_accessed_file(cx: &mut Context) {
@@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (_, doc) = current!(editor);
- let diag = if let Some(diag) = doc.diagnostics().first() {
- diag.range.start
- } else {
- return;
+ let pos = match doc.diagnostics().first() {
+ Some(diag) => diag.range.start,
+ None => return,
};
- goto_pos(editor, diag);
+ goto_pos(editor, pos);
}
fn goto_last_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (_, doc) = current!(editor);
- let diag = if let Some(diag) = doc.diagnostics().last() {
- diag.range.start
- } else {
- return;
+ let pos = match doc.diagnostics().last() {
+ Some(diag) => diag.range.start,
+ None => return,
};
- goto_pos(editor, diag);
+ goto_pos(editor, pos);
}
fn goto_next_diag(cx: &mut Context) {
@@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
- let diag = if let Some(diag) = doc
+
+ let diag = doc
.diagnostics()
.iter()
- .map(|diag| diag.range.start)
- .find(|&pos| pos > cursor_pos)
- {
- diag
- } else if let Some(diag) = doc.diagnostics().first() {
- diag.range.start
- } else {
- return;
+ .find(|diag| diag.range.start > cursor_pos)
+ .or_else(|| doc.diagnostics().first());
+
+ let pos = match diag {
+ Some(diag) => diag.range.start,
+ None => return,
};
- goto_pos(editor, diag);
+ goto_pos(editor, pos);
}
fn goto_prev_diag(cx: &mut Context) {
@@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
- let diag = if let Some(diag) = doc
+
+ let diag = doc
.diagnostics()
.iter()
.rev()
- .map(|diag| diag.range.start)
- .find(|&pos| pos < cursor_pos)
- {
- diag
- } else if let Some(diag) = doc.diagnostics().last() {
- diag.range.start
- } else {
- return;
+ .find(|diag| diag.range.start < cursor_pos)
+ .or_else(|| doc.diagnostics().last());
+
+ let pos = match diag {
+ Some(diag) => diag.range.start,
+ None => return,
};
- goto_pos(editor, diag);
+ goto_pos(editor, pos);
}
fn signature_help(cx: &mut Context) {
@@ -3423,7 +3570,26 @@ pub mod insert {
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
pub type PostHook = fn(&mut Context, char);
- fn completion(cx: &mut Context, ch: char) {
+ // It trigger completion when idle timer reaches deadline
+ // Only trigger completion if the word under cursor is longer than n characters
+ pub fn idle_completion(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let cursor = doc.selection(view.id).primary().cursor(text);
+
+ use helix_core::chars::char_is_word;
+ let mut iter = text.chars_at(cursor);
+ iter.reverse();
+ for _ in 0..cx.editor.config.completion_trigger_len {
+ match iter.next() {
+ Some(c) if char_is_word(c) => {}
+ _ => return,
+ }
+ }
+ super::completion(cx);
+ }
+
+ fn language_server_completion(cx: &mut Context, ch: char) {
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() {
@@ -3433,19 +3599,14 @@ pub mod insert {
let capabilities = language_server.capabilities();
- if let lsp::ServerCapabilities {
- completion_provider:
- Some(lsp::CompletionOptions {
- trigger_characters: Some(triggers),
- ..
- }),
+ if let Some(lsp::CompletionOptions {
+ trigger_characters: Some(triggers),
..
- } = capabilities
+ }) = &capabilities.completion_provider
{
// TODO: what if trigger is multiple chars long
- let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
-
- if is_trigger {
+ if triggers.iter().any(|trigger| trigger.contains(ch)) {
+ cx.editor.clear_idle_timer();
super::completion(cx);
}
}
@@ -3527,7 +3688,8 @@ 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 &[completion, signature_help] {
+ for hook in &[language_server_completion, signature_help] {
+ // for hook in &[signature_help] {
hook(cx, c);
}
}
@@ -3668,13 +3830,19 @@ pub mod insert {
fn undo(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let view_id = view.id;
- doc.undo(view_id);
+ let success = doc.undo(view_id);
+ if !success {
+ cx.editor.set_status("Already at oldest change".to_owned());
+ }
}
fn redo(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let view_id = view.id;
- doc.redo(view_id);
+ let success = doc.redo(view_id);
+ if !success {
+ cx.editor.set_status("Already at newest change".to_owned());
+ }
}
// Yank / Paste
@@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl(
}
fn yank_joined_to_clipboard(cx: &mut Context) {
- let line_ending = current!(cx.editor).1.line_ending;
+ let line_ending = doc!(cx.editor).line_ending;
let _ = yank_joined_to_clipboard_impl(
&mut cx.editor,
line_ending.as_str(),
@@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) {
}
fn yank_joined_to_primary_clipboard(cx: &mut Context) {
- let line_ending = current!(cx.editor).1.line_ending;
+ let line_ending = doc!(cx.editor).line_ending;
let _ = yank_joined_to_clipboard_impl(
&mut cx.editor,
line_ending.as_str(),
@@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) {
let registers = &mut cx.editor.registers;
if let Some(values) = registers.read(reg_name) {
- if let Some(yank) = values.first() {
+ if !values.is_empty() {
+ let repeat = std::iter::repeat(
+ values
+ .last()
+ .map(|value| Tendril::from_slice(value))
+ .unwrap(),
+ );
+ let mut values = values
+ .iter()
+ .map(|value| Tendril::from_slice(value))
+ .chain(repeat);
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
if !range.is_empty() {
- (range.from(), range.to(), Some(yank.as_str().into()))
+ (range.from(), range.to(), Some(values.next().unwrap()))
} else {
(range.from(), range.to(), None)
}
@@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) {
cx,
"keep:".into(),
Some(reg),
+ |_input: &str| Vec::new(),
move |view, doc, regex, event| {
if event != PromptEvent::Update {
return;
@@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) {
iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset);
+ let prefix = text.slice(start_offset..cursor).to_string();
cx.callback(
future,
@@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) {
return;
}
- let items = match response {
+ let mut items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
@@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) {
None => Vec::new(),
};
+ if !prefix.is_empty() {
+ items = items
+ .into_iter()
+ .filter(|item| {
+ item.filter_text
+ .as_ref()
+ .unwrap_or(&item.label)
+ .starts_with(&prefix)
+ })
+ .collect();
+ }
+
if items.is_empty() {
// editor.set_error("No completion available".to_string());
return;
@@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
// tree sitter node selection
fn expand_selection(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
- if let Some(syntax) = doc.syntax() {
- let text = doc.text().slice(..);
- let selection = object::expand_selection(syntax, text, doc.selection(view.id));
- doc.set_selection(view.id, selection);
- }
+ if let Some(syntax) = doc.syntax() {
+ let text = doc.text().slice(..);
+ let selection = object::expand_selection(syntax, text, doc.selection(view.id));
+ doc.set_selection(view.id, selection);
+ }
+ };
+ motion(&mut cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
}
fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
if let Some(syntax) = doc.syntax() {
- let pos = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
- let selection = Selection::point(pos);
- doc.set_selection(view.id, selection);
- };
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
+ range.put_cursor(text, pos, doc.mode == Mode::Select)
+ } else {
+ range
+ }
+ });
+ doc.set_selection(view.id, selection);
}
}
@@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) {
fn jump_forward(cx: &mut Context) {
let count = cx.count();
- let (view, _doc) = current!(cx.editor);
+ let view = view_mut!(cx.editor);
if let Some((id, selection)) = view.jumps.forward(count) {
view.doc = *id;
@@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) {
cx.editor.focus_next()
}
+fn jump_view_right(cx: &mut Context) {
+ cx.editor.focus_right()
+}
+
+fn jump_view_left(cx: &mut Context) {
+ cx.editor.focus_left()
+}
+
+fn jump_view_up(cx: &mut Context) {
+ cx.editor.focus_up()
+}
+
+fn jump_view_down(cx: &mut Context) {
+ cx.editor.focus_down()
+}
+
// split helper, clear it later
fn split(cx: &mut Context, action: Action) {
let (view, doc) = current!(cx.editor);
@@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
let count = cx.count();
cx.on_next_key(move |cx, event| {
if let Some(ch) = event.char() {
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+ let textobject = move |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
+
+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
+ Some(t) => t,
+ None => return range,
+ };
+ textobject::textobject_treesitter(
+ text,
+ range,
+ objtype,
+ obj_name,
+ syntax.tree().root_node(),
+ lang_config,
+ count,
+ )
+ };
- let selection = doc.selection(view.id).clone().transform(|range| {
- match ch {
- 'w' => textobject::textobject_word(text, range, objtype, count),
- // TODO: cancel new ranges if inconsistent surround matches across lines
- ch if !ch.is_ascii_alphanumeric() => {
- textobject::textobject_surround(text, range, objtype, ch, count)
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ match ch {
+ 'w' => textobject::textobject_word(text, range, objtype, count),
+ 'c' => textobject_treesitter("class", range),
+ 'f' => textobject_treesitter("function", range),
+ 'p' => textobject_treesitter("parameter", range),
+ // TODO: cancel new ranges if inconsistent surround matches across lines
+ ch if !ch.is_ascii_alphanumeric() => {
+ textobject::textobject_surround(text, range, objtype, ch, count)
+ }
+ _ => range,
}
- _ => range,
- }
- });
- doc.set_selection(view.id, selection);
+ });
+ doc.set_selection(view.id, selection);
+ };
+ textobject(&mut cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
}
})
}
@@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) {
let selection = doc.selection(view.id);
let (open, close) = surround::get_pair(ch);
- let mut changes = Vec::new();
+ let mut changes = Vec::with_capacity(selection.len() * 2);
for range in selection.iter() {
changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index cad1df05..dc8b91d7 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -207,7 +207,7 @@ pub trait AnyComponent {
///
/// ```rust
/// use helix_term::{ui::Text, compositor::Component};
- /// let boxed: Box<Component> = Box::new(Text::new("text".to_string()));
+ /// let boxed: Box<dyn Component> = Box::new(Text::new("text".to_string()));
/// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
/// ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index e344457c..35dbce2f 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -5,20 +5,20 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize;
use std::{
borrow::Cow,
- collections::HashMap,
+ collections::{BTreeSet, HashMap},
ops::{Deref, DerefMut},
};
#[macro_export]
macro_rules! key {
($key:ident) => {
- KeyEvent {
+ ::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
- KeyEvent {
+ ::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
@@ -78,19 +78,30 @@ macro_rules! keymap {
};
}
-#[derive(Debug, Clone, Deserialize)]
+#[derive(Debug, Clone)]
pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode"
- #[serde(skip)]
name: String,
- #[serde(flatten)]
map: HashMap<KeyEvent, KeyTrie>,
- #[serde(skip)]
order: Vec<KeyEvent>,
- #[serde(skip)]
pub is_sticky: bool,
}
+impl<'de> Deserialize<'de> for KeyTrieNode {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
+ let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
+ Ok(Self {
+ map,
+ order,
+ ..Default::default()
+ })
+ }
+}
+
impl KeyTrieNode {
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
Self {
@@ -118,7 +129,6 @@ impl KeyTrieNode {
}
self.map.insert(key, trie);
}
-
for &key in self.map.keys() {
if !self.order.contains(&key) {
self.order.push(key);
@@ -127,20 +137,29 @@ impl KeyTrieNode {
}
pub fn infobox(&self) -> Info {
- let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
+ let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len());
for (&key, trie) in self.iter() {
let desc = match trie {
- KeyTrie::Leaf(cmd) => cmd.doc(),
+ KeyTrie::Leaf(cmd) => {
+ if cmd.name() == "no_op" {
+ continue;
+ }
+ cmd.doc()
+ }
KeyTrie::Node(n) => n.name(),
};
match body.iter().position(|(d, _)| d == &desc) {
- // FIXME: multiple keys are ordered randomly (use BTreeSet)
- Some(pos) => body[pos].1.push(key),
- None => body.push((desc, vec![key])),
+ Some(pos) => {
+ body[pos].1.insert(key);
+ }
+ None => body.push((desc, BTreeSet::from([key]))),
}
}
body.sort_unstable_by_key(|(_, keys)| {
- self.order.iter().position(|&k| k == keys[0]).unwrap()
+ self.order
+ .iter()
+ .position(|&k| k == *keys.iter().next().unwrap())
+ .unwrap()
});
let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
@@ -151,6 +170,11 @@ impl KeyTrieNode {
}
Info::new(self.name(), body)
}
+
+ /// Get a reference to the key trie node's order.
+ pub fn order(&self) -> &[KeyEvent] {
+ self.order.as_slice()
+ }
}
impl Default for KeyTrieNode {
@@ -235,6 +259,7 @@ pub enum KeymapResultKind {
/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
/// reference to the sticky node if one is currently active.
+#[derive(Debug)]
pub struct KeymapResult<'a> {
pub kind: KeymapResultKind,
pub sticky: Option<&'a KeyTrieNode>,
@@ -395,6 +420,7 @@ impl Default for Keymaps {
"F" => find_prev_char,
"r" => replace,
"R" => replace_with_yanked,
+ "A-." => repeat_last_motion,
"~" => switch_case,
"`" => switch_to_lowercase,
@@ -427,6 +453,8 @@ impl Default for Keymaps {
"m" => goto_window_middle,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
+ "n" => goto_next_buffer,
+ "p" => goto_previous_buffer,
},
":" => command_mode,
@@ -476,10 +504,9 @@ impl Default for Keymaps {
},
"/" => search,
- // ? for search_reverse
+ "?" => rsearch,
"n" => search_next,
- "N" => extend_search_next,
- // N for search_prev
+ "N" => search_prev,
"*" => search_selection,
"u" => undo,
@@ -520,9 +547,13 @@ impl Default for Keymaps {
"C-w" => { "Window"
"C-w" | "w" => rotate_view,
- "C-h" | "h" => hsplit,
+ "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
+ "C-h" | "h" | "left" => jump_view_left,
+ "C-j" | "j" | "down" => jump_view_down,
+ "C-k" | "k" | "up" => jump_view_up,
+ "C-l" | "l" | "right" => jump_view_right,
},
// move under <space>c
@@ -621,6 +652,9 @@ impl Default for Keymaps {
"B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end,
+ "n" => extend_search_next,
+ "N" => extend_search_prev,
+
"t" => extend_till_char,
"f" => extend_next_char,
"T" => extend_till_prev_char,
@@ -669,63 +703,101 @@ pub fn merge_keys(mut config: Config) -> Config {
config
}
-#[test]
-fn merge_partial_keys() {
- let config = Config {
- keys: Keymaps(hashmap! {
- Mode::Normal => Keymap::new(
- keymap!({ "Normal mode"
- "i" => normal_mode,
- "无" => insert_mode,
- "z" => jump_backward,
- "g" => { "Merge into goto mode"
- "$" => goto_line_end,
- "g" => delete_char_forward,
- },
- })
- )
- }),
- ..Default::default()
- };
- let mut merged_config = merge_keys(config.clone());
- assert_ne!(config, merged_config);
-
- let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
- assert_eq!(
- keymap.get(key!('i')).kind,
- KeymapResultKind::Matched(Command::normal_mode),
- "Leaf should replace leaf"
- );
- assert_eq!(
- 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')).kind,
- KeymapResultKind::Matched(Command::jump_backward),
- "Leaf should replace node"
- );
- // Assumes that `g` is a node in default keymap
- assert_eq!(
- keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_line_end),
- "Leaf should be present in merged subnode"
- );
- // Assumes that `gg` is in default keymap
- assert_eq!(
- keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
- &KeyTrie::Leaf(Command::delete_char_forward),
- "Leaf should replace old leaf in merged subnode"
- );
- // Assumes that `ge` is in default keymap
- assert_eq!(
- keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_last_line),
- "Old leaves in subnode should be present in merged node"
- );
-
- assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
- assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
+#[cfg(test)]
+mod tests {
+ use super::*;
+ #[test]
+ fn merge_partial_keys() {
+ let config = Config {
+ keys: Keymaps(hashmap! {
+ Mode::Normal => Keymap::new(
+ keymap!({ "Normal mode"
+ "i" => normal_mode,
+ "无" => insert_mode,
+ "z" => jump_backward,
+ "g" => { "Merge into goto mode"
+ "$" => goto_line_end,
+ "g" => delete_char_forward,
+ },
+ })
+ )
+ }),
+ ..Default::default()
+ };
+ let mut merged_config = merge_keys(config.clone());
+ assert_ne!(config, merged_config);
+
+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
+ assert_eq!(
+ keymap.get(key!('i')).kind,
+ KeymapResultKind::Matched(Command::normal_mode),
+ "Leaf should replace leaf"
+ );
+ assert_eq!(
+ 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')).kind,
+ KeymapResultKind::Matched(Command::jump_backward),
+ "Leaf should replace node"
+ );
+ // Assumes that `g` is a node in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
+ &KeyTrie::Leaf(Command::goto_line_end),
+ "Leaf should be present in merged subnode"
+ );
+ // Assumes that `gg` is in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
+ &KeyTrie::Leaf(Command::delete_char_forward),
+ "Leaf should replace old leaf in merged subnode"
+ );
+ // Assumes that `ge` is in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
+ &KeyTrie::Leaf(Command::goto_last_line),
+ "Old leaves in subnode should be present in merged node"
+ );
+
+ assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
+ assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
+ }
+
+ #[test]
+ fn order_should_be_set() {
+ let config = Config {
+ keys: Keymaps(hashmap! {
+ Mode::Normal => Keymap::new(
+ keymap!({ "Normal mode"
+ "space" => { ""
+ "s" => { ""
+ "v" => vsplit,
+ "c" => hsplit,
+ },
+ },
+ })
+ )
+ }),
+ ..Default::default()
+ };
+ let mut merged_config = merge_keys(config.clone());
+ assert_ne!(config, merged_config);
+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
+ // Make sure mapping works
+ assert_eq!(
+ keymap
+ .root()
+ .search(&[key!(' '), key!('s'), key!('v')])
+ .unwrap(),
+ &KeyTrie::Leaf(Command::vsplit),
+ "Leaf should be present in merged subnode"
+ );
+ // Make sure an order was set during merge
+ let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
+ assert!(!node.node().unwrap().order().is_empty())
+ }
}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 180dacd1..f746895c 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
};
// Separate file config so we can include year, month and day in file logs
+ let file = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(logpath)?;
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
- .chain(fern::log_file(logpath)?);
+ .chain(file);
base_config.chain(file_config).apply()?;
@@ -55,6 +60,7 @@ ARGS:
FLAGS:
-h, --help Prints help information
+ --tutor Loads the tutorial
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index c75b24f1..dd782d29 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::Transaction;
-use helix_view::{graphics::Rect, Document, Editor, View};
+use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -30,31 +30,32 @@ impl menu::Item for CompletionItem {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
- Some(lsp::CompletionItemKind::Text) => "text",
- Some(lsp::CompletionItemKind::Method) => "method",
- Some(lsp::CompletionItemKind::Function) => "function",
- Some(lsp::CompletionItemKind::Constructor) => "constructor",
- Some(lsp::CompletionItemKind::Field) => "field",
- Some(lsp::CompletionItemKind::Variable) => "variable",
- Some(lsp::CompletionItemKind::Class) => "class",
- Some(lsp::CompletionItemKind::Interface) => "interface",
- Some(lsp::CompletionItemKind::Module) => "module",
- Some(lsp::CompletionItemKind::Property) => "property",
- Some(lsp::CompletionItemKind::Unit) => "unit",
- Some(lsp::CompletionItemKind::Value) => "value",
- Some(lsp::CompletionItemKind::Enum) => "enum",
- Some(lsp::CompletionItemKind::Keyword) => "keyword",
- Some(lsp::CompletionItemKind::Snippet) => "snippet",
- Some(lsp::CompletionItemKind::Color) => "color",
- Some(lsp::CompletionItemKind::File) => "file",
- Some(lsp::CompletionItemKind::Reference) => "reference",
- Some(lsp::CompletionItemKind::Folder) => "folder",
- Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
- Some(lsp::CompletionItemKind::Constant) => "constant",
- Some(lsp::CompletionItemKind::Struct) => "struct",
- Some(lsp::CompletionItemKind::Event) => "event",
- Some(lsp::CompletionItemKind::Operator) => "operator",
- Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
+ Some(lsp::CompletionItemKind::TEXT) => "text",
+ Some(lsp::CompletionItemKind::METHOD) => "method",
+ Some(lsp::CompletionItemKind::FUNCTION) => "function",
+ Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
+ Some(lsp::CompletionItemKind::FIELD) => "field",
+ Some(lsp::CompletionItemKind::VARIABLE) => "variable",
+ Some(lsp::CompletionItemKind::CLASS) => "class",
+ Some(lsp::CompletionItemKind::INTERFACE) => "interface",
+ Some(lsp::CompletionItemKind::MODULE) => "module",
+ Some(lsp::CompletionItemKind::PROPERTY) => "property",
+ Some(lsp::CompletionItemKind::UNIT) => "unit",
+ Some(lsp::CompletionItemKind::VALUE) => "value",
+ Some(lsp::CompletionItemKind::ENUM) => "enum",
+ Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
+ Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
+ Some(lsp::CompletionItemKind::COLOR) => "color",
+ Some(lsp::CompletionItemKind::FILE) => "file",
+ Some(lsp::CompletionItemKind::REFERENCE) => "reference",
+ Some(lsp::CompletionItemKind::FOLDER) => "folder",
+ Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
+ Some(lsp::CompletionItemKind::CONSTANT) => "constant",
+ Some(lsp::CompletionItemKind::STRUCT) => "struct",
+ Some(lsp::CompletionItemKind::EVENT) => "event",
+ Some(lsp::CompletionItemKind::OPERATOR) => "operator",
+ Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
+ Some(kind) => unimplemented!("{:?}", kind),
None => "",
}),
// self.detail.as_deref().unwrap_or("")
@@ -83,13 +84,13 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
- // let items: Vec<CompletionItem> = Vec::new();
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
- view: &View,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
+ start_offset: usize,
+ trigger_offset: usize,
) -> Transaction {
if let Some(edit) = &item.text_edit {
let edit = match edit {
@@ -105,63 +106,52 @@ impl Completion {
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
+ // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
+ // in these cases we need to check for a common prefix and remove it
+ let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
+ let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
- vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
+ vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
}
}
+ let (view, doc) = current!(editor);
+
+ // if more text was entered, remove it
+ doc.restore(view.id);
+
match event {
PromptEvent::Abort => {}
PromptEvent::Update => {
- let (view, doc) = current!(editor);
-
// always present here
let item = item.unwrap();
- // if more text was entered, remove it
- // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if trigger_offset < cursor {
- let remove = Transaction::change(
- doc.text(),
- vec![(trigger_offset, cursor, None)].into_iter(),
- );
- doc.apply(&remove, view.id);
- }
+ let transaction = item_to_transaction(
+ doc,
+ item,
+ offset_encoding,
+ start_offset,
+ trigger_offset,
+ );
+
+ // initialize a savepoint
+ doc.savepoint();
- let transaction = item_to_transaction(doc, view, item, offset_encoding);
doc.apply(&transaction, view.id);
}
PromptEvent::Validate => {
- let (view, doc) = current!(editor);
-
// always present here
let item = item.unwrap();
- // if more text was entered, remove it
- // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if trigger_offset < cursor {
- let remove = Transaction::change(
- doc.text(),
- vec![(trigger_offset, cursor, None)].into_iter(),
- );
- doc.apply(&remove, view.id);
- }
-
- let transaction = item_to_transaction(doc, view, item, offset_encoding);
+ let transaction = item_to_transaction(
+ doc,
+ item,
+ offset_encoding,
+ start_offset,
+ trigger_offset,
+ );
doc.apply(&transaction, view.id);
if let Some(additional_edits) = &item.additional_text_edits {
@@ -210,7 +200,7 @@ impl Completion {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
- if self.start_offset <= cursor {
+ if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.start_offset..cursor);
let text = Cow::from(fragment);
// TODO: logic is same as ui/picker
@@ -274,12 +264,10 @@ impl Component for Completion {
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
- let cursor_pos = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- - view.offset.row) as u16;
+ let text = doc.text().slice(..);
+ let cursor_pos = doc.selection(view.id).primary().cursor(text);
+ let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
+ let cursor_pos = (coords.row - view.offset.row) as u16;
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 037f04b8..26a0358d 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -689,6 +689,8 @@ impl EditorView {
theme: &Theme,
is_focused: bool,
) {
+ use tui::text::{Span, Spans};
+
//-------------------------------
// Left side of the status line.
//-------------------------------
@@ -707,17 +709,17 @@ impl EditorView {
})
.unwrap_or("");
- let style = if is_focused {
+ let base_style = if is_focused {
theme.get("ui.statusline")
} else {
theme.get("ui.statusline.inactive")
};
// statusline
- surface.set_style(viewport.with_height(1), style);
+ surface.set_style(viewport.with_height(1), base_style);
if is_focused {
- surface.set_string(viewport.x + 1, viewport.y, mode, style);
+ surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
}
- surface.set_string(viewport.x + 5, viewport.y, progress, style);
+ surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
if let Some(path) = doc.relative_path() {
let path = path.to_string_lossy();
@@ -728,7 +730,7 @@ impl EditorView {
viewport.y,
title,
viewport.width.saturating_sub(6) as usize,
- style,
+ base_style,
);
}
@@ -736,8 +738,50 @@ impl EditorView {
// Right side of the status line.
//-------------------------------
- // Compute the individual info strings.
- let diag_count = format!("{}", doc.diagnostics().len());
+ let mut right_side_text = Spans::default();
+
+ // Compute the individual info strings and add them to `right_side_text`.
+
+ // Diagnostics
+ let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
+ use helix_core::diagnostic::Severity;
+ match diag.severity {
+ Some(Severity::Warning) => counts.0 += 1,
+ Some(Severity::Error) | None => counts.1 += 1,
+ _ => {}
+ }
+ counts
+ });
+ let (warnings, errors) = diags;
+ let warning_style = theme.get("warning");
+ let error_style = theme.get("error");
+ for i in 0..2 {
+ let (count, style) = match i {
+ 0 => (warnings, warning_style),
+ 1 => (errors, error_style),
+ _ => unreachable!(),
+ };
+ if count == 0 {
+ continue;
+ }
+ let style = base_style.patch(style);
+ right_side_text.0.push(Span::styled("●", style));
+ right_side_text
+ .0
+ .push(Span::styled(format!(" {} ", count), base_style));
+ }
+
+ // Selections
+ let sels_count = doc.selection(view.id).len();
+ right_side_text.0.push(Span::styled(
+ format!(
+ " {} sel{} ",
+ sels_count,
+ if sels_count == 1 { "" } else { "s" }
+ ),
+ base_style,
+ ));
+
// let indent_info = match doc.indent_style {
// IndentStyle::Tabs => "tabs",
// IndentStyle::Spaces(1) => "spaces:1",
@@ -750,29 +794,28 @@ impl EditorView {
// IndentStyle::Spaces(8) => "spaces:8",
// _ => "indent:ERROR",
// };
- let position_info = {
- let pos = coords_at_pos(
- doc.text().slice(..),
- doc.selection(view.id)
- .primary()
- .cursor(doc.text().slice(..)),
- );
- format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
- };
- // Render them to the status line together.
- let right_side_text = format!(
- "{} {} ",
- &diag_count[..diag_count.len().min(4)],
- // indent_info,
- position_info
+ // Position
+ let pos = coords_at_pos(
+ doc.text().slice(..),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
);
- let text_len = right_side_text.len() as u16;
- surface.set_string(
- viewport.x + viewport.width.saturating_sub(text_len),
+ right_side_text.0.push(Span::styled(
+ format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
+ base_style,
+ ));
+
+ // Render to the statusline.
+ surface.set_spans(
+ viewport.x
+ + viewport
+ .width
+ .saturating_sub(right_side_text.width() as u16),
viewport.y,
- right_side_text,
- style,
+ &right_side_text,
+ right_side_text.width() as u16,
);
}
@@ -984,7 +1027,7 @@ impl EditorView {
pub fn set_completion(
&mut self,
- editor: &Editor,
+ editor: &mut Editor,
items: Vec<helix_lsp::lsp::CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
@@ -999,10 +1042,21 @@ impl EditorView {
return;
}
+ // Immediately initialize a savepoint
+ doc_mut!(editor).savepoint();
+
// TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height));
self.completion = Some(completion);
}
+
+ pub fn clear_completion(&mut self, editor: &mut Editor) {
+ self.completion = None;
+ // Clear any savepoints
+ let (_, doc) = current!(editor);
+ doc.savepoint = None;
+ editor.clear_idle_timer(); // don't retrigger
+ }
}
impl EditorView {
@@ -1022,12 +1076,12 @@ impl EditorView {
let editor = &mut cxt.editor;
let result = editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
if let Some((pos, view_id)) = result {
- let doc = &mut editor.documents[editor.tree.get(view_id).doc];
+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
if modifiers == crossterm::event::KeyModifiers::ALT {
let selection = doc.selection(view_id).clone();
@@ -1096,7 +1150,7 @@ impl EditorView {
};
let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
.map(|_| view.id)
});
@@ -1182,12 +1236,12 @@ impl EditorView {
}
let result = editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
if let Some((pos, view_id)) = result {
- let doc = &mut editor.documents[editor.tree.get(view_id).doc];
+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
commands::Command::paste_primary_clipboard_before.execute(cxt);
@@ -1254,8 +1308,7 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
- self.completion = None;
- cxt.editor.clear_idle_timer(); // don't retrigger
+ self.clear_completion(cxt.editor);
}
}
}
@@ -1268,8 +1321,7 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion {
completion.update(&mut cxt);
if completion.is_empty() {
- self.completion = None;
- cxt.editor.clear_idle_timer(); // don't retrigger
+ self.clear_completion(cxt.editor);
}
}
}
@@ -1397,8 +1449,10 @@ impl Component for EditorView {
info.render(area, surface, cx);
}
- if let Some(ref mut info) = self.autoinfo {
- info.render(area, surface, cx);
+ if cx.editor.config.auto_info {
+ if let Some(ref mut info) = self.autoinfo {
+ info.render(area, surface, cx);
+ }
}
let key_width = 15u16; // for showing pending keys
@@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
}
#[inline]
-fn abs_diff(a: usize, b: usize) -> usize {
+const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 055593fd..3c492d14 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
}
pub fn score(&mut self, pattern: &str) {
- // need to borrow via pattern match otherwise it complains about simultaneous borrow
- let Self {
- ref mut matcher,
- ref mut matches,
- ref options,
- ..
- } = *self;
-
// reuse the matches allocation
- matches.clear();
- matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
- let text = option.filter_text();
- // TODO: using fuzzy_indices could give us the char idx for match highlighting
- matcher
- .fuzzy_match(text, pattern)
- .map(|score| (index, score))
- }));
+ self.matches.clear();
+ self.matches.extend(
+ self.options
+ .iter()
+ .enumerate()
+ .filter_map(|(index, option)| {
+ let text = option.filter_text();
+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
+ self.matcher
+ .fuzzy_match(text, pattern)
+ .map(|score| (index, score))
+ }),
+ );
// matches.sort_unstable_by_key(|(_, score)| -score);
- matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
+ self.matches
+ .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
// reset cursor position
self.cursor = None;
@@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
pub fn move_up(&mut self) {
let len = self.matches.len();
- let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
+ let max_index = len.saturating_sub(1);
+ let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
self.cursor = Some(pos);
self.adjust_scroll();
}
@@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::CONTROL,
} => {
self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index e66673ca..00c70cea 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -29,6 +29,7 @@ pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
+ completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let (view, doc) = current!(cx.editor);
@@ -38,7 +39,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
history_register,
- |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
+ completion_fn,
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
PromptEvent::Abort => {
@@ -92,9 +93,25 @@ pub fn regex_prompt(
}
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
- use ignore::Walk;
+ use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
- let files = Walk::new(&root).filter_map(|entry| {
+
+ // We want to exclude files that the editor can't handle yet
+ let mut type_builder = TypesBuilder::new();
+ let mut walk_builder = WalkBuilder::new(&root);
+ let walk_builder = match type_builder.add(
+ "compressed",
+ "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
+ ) {
+ Err(_) => &walk_builder,
+ _ => {
+ type_builder.negate("all");
+ let excluded_types = type_builder.build().unwrap();
+ walk_builder.types(excluded_types)
+ }
+ };
+
+ let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 341235ee..3e805fac 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
-use std::{borrow::Cow, collections::HashMap, path::PathBuf};
+use std::{
+ borrow::Cow,
+ collections::HashMap,
+ io::Read,
+ path::{Path, PathBuf},
+};
use crate::ui::{Prompt, PromptEvent};
use helix_core::Position;
@@ -23,18 +28,58 @@ use helix_view::{
};
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
+/// Biggest file size to preview in bytes
+pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
-/// File path and line number (used to align and highlight a line)
+/// File path and range of lines (used to align and highlight lines)
type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> {
picker: Picker<T>,
/// Caches paths to documents
- preview_cache: HashMap<PathBuf, Document>,
+ preview_cache: HashMap<PathBuf, CachedPreview>,
+ read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
}
+pub enum CachedPreview {
+ Document(Document),
+ Binary,
+ LargeFile,
+ NotFound,
+}
+
+// We don't store this enum in the cache so as to avoid lifetime constraints
+// from borrowing a document already opened in the editor.
+pub enum Preview<'picker, 'editor> {
+ Cached(&'picker CachedPreview),
+ EditorDocument(&'editor Document),
+}
+
+impl Preview<'_, '_> {
+ fn document(&self) -> Option<&Document> {
+ match self {
+ Preview::EditorDocument(doc) => Some(doc),
+ Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
+ _ => None,
+ }
+ }
+
+ /// Alternate text to show for the preview.
+ fn placeholder(&self) -> &str {
+ match *self {
+ Self::EditorDocument(_) => "<File preview>",
+ Self::Cached(preview) => match preview {
+ CachedPreview::Document(_) => "<File preview>",
+ CachedPreview::Binary => "<Binary file>",
+ CachedPreview::LargeFile => "<File too large to preview>",
+ CachedPreview::NotFound => "<File not found>",
+ },
+ }
+ }
+}
+
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(),
+ read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn),
}
}
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
})
}
- fn calculate_preview(&mut self, editor: &Editor) {
- if let Some((path, _line)) = self.current_file(editor) {
- if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
- // TODO: enable syntax highlighting; blocked by async rendering
- let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
- self.preview_cache.insert(path, doc);
- }
+ /// Get (cached) preview for a given path. If a document corresponding
+ /// to the path is already open in the editor, it is used instead.
+ fn get_preview<'picker, 'editor>(
+ &'picker mut self,
+ path: &Path,
+ editor: &'editor Editor,
+ ) -> Preview<'picker, 'editor> {
+ if let Some(doc) = editor.document_by_path(path) {
+ return Preview::EditorDocument(doc);
+ }
+
+ if self.preview_cache.contains_key(path) {
+ return Preview::Cached(&self.preview_cache[path]);
}
+
+ let data = std::fs::File::open(path).and_then(|file| {
+ let metadata = file.metadata()?;
+ // Read up to 1kb to detect the content type
+ let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
+ let content_type = content_inspector::inspect(&self.read_buffer[..n]);
+ self.read_buffer.clear();
+ Ok((metadata, content_type))
+ });
+ let preview = data
+ .map(
+ |(metadata, content_type)| match (metadata.len(), content_type) {
+ (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
+ (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
+ _ => {
+ // TODO: enable syntax highlighting; blocked by async rendering
+ Document::open(path, None, Some(&editor.theme), None)
+ .map(CachedPreview::Document)
+ .unwrap_or(CachedPreview::NotFound)
+ }
+ },
+ )
+ .unwrap_or(CachedPreview::NotFound);
+ self.preview_cache.insert(path.to_owned(), preview);
+ Preview::Cached(&self.preview_cache[path])
}
}
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
// |picker | | |
// | | | |
// +---------+ +---------+
- self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
+ let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background);
let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1,
};
let inner = inner.inner(&margin);
-
block.render(preview_area, surface);
- if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
- cx.editor
- .document_by_path(&path)
- .or_else(|| self.preview_cache.get(&path))
- .zip(Some(range))
- }) {
+ if let Some((path, range)) = self.current_file(cx.editor) {
+ let preview = self.get_preview(&path, cx.editor);
+ let doc = match preview.document() {
+ Some(doc) => doc,
+ None => {
+ let alt_text = preview.placeholder();
+ let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
+ let y = inner.y + inner.height / 2;
+ surface.set_stringn(x, y, alt_text, inner.width as usize, text);
+ return;
+ }
+ };
+
// align to middle
- let first_line = line
+ let first_line = range
.map(|(start, end)| {
let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2);
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
);
// highlight the line
- if let Some((start, end)) = line {
+ if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16;
surface.set_style(
Rect::new(
@@ -234,37 +317,28 @@ impl<T> Picker<T> {
}
pub fn score(&mut self) {
- // need to borrow via pattern match otherwise it complains about simultaneous borrow
- let Self {
- ref mut matcher,
- ref mut matches,
- ref filters,
- ref format_fn,
- ..
- } = *self;
-
let pattern = &self.prompt.line;
// reuse the matches allocation
- matches.clear();
- matches.extend(
+ self.matches.clear();
+ self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
- if !filters.is_empty() {
- filters.binary_search(&index).ok()?;
+ if !self.filters.is_empty() {
+ self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
- let text = (format_fn)(option);
+ let text = (self.format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
- matcher
+ self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
- matches.sort_unstable_by_key(|(_, score)| -score);
+ self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = 0;
@@ -338,6 +412,10 @@ impl<T: 'static> Component for Picker<T> {
..
}
| KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -351,6 +429,10 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Tab, ..
}
| KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
- code: KeyCode::Char('h'),
+ code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
+ true,
);
}
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 56335fb3..593fd934 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -186,6 +186,11 @@ impl Prompt {
self.exit_selection();
}
+ pub fn insert_str(&mut self, s: &str) {
+ self.line.insert_str(self.cursor, s);
+ self.cursor += s.len();
+ }
+
pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement);
self.cursor = pos
@@ -475,6 +480,26 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
KeyEvent {
+ code: KeyCode::Char('s'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ use helix_core::textobject;
+ let range = textobject::textobject_word(
+ text,
+ doc.selection(view.id).primary(),
+ textobject::TextObject::Inside,
+ 1,
+ );
+ let line = text.slice(range.from()..range.to()).to_string();
+ if !line.is_empty() {
+ self.insert_str(line.as_str());
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
+ }
+ }
+ KeyEvent {
code: KeyCode::Enter,
..
} => {
@@ -502,6 +527,7 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
@@ -515,15 +541,22 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
code: KeyCode::Tab, ..
- } => self.change_completion_selection(CompletionDirection::Forward),
+ } => {
+ self.change_completion_selection(CompletionDirection::Forward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
+ }
KeyEvent {
code: KeyCode::BackTab,
..
- } => self.change_completion_selection(CompletionDirection::Backward),
+ } => {
+ self.change_completion_selection(CompletionDirection::Backward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
+ }
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
index 80a772a4..6df65d36 100644
--- a/helix-tui/Cargo.toml
+++ b/helix-tui/Cargo.toml
@@ -1,11 +1,11 @@
[package]
name = "helix-tui"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
@@ -19,7 +19,7 @@ default = ["crossterm"]
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.8"
-crossterm = { version = "0.21", optional = true }
+crossterm = { version = "0.22", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]}
-helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
-helix-core = { version = "0.4", path = "../helix-core" }
+helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
+helix-core = { version = "0.5", path = "../helix-core" }
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index 377e3e39..f480bc2f 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -266,12 +266,14 @@ impl Buffer {
where
S: AsRef<str>,
{
- self.set_string_truncated(x, y, string, width, style, false)
+ self.set_string_truncated(x, y, string, width, style, false, false)
}
/// Print at most the first `width` characters of a string if enough space is available
- /// until the end of the line. If `markend` is true appends a `…` at the end of
- /// truncated lines.
+ /// until the end of the line. If `ellipsis` is true appends a `…` at the end of
+ /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
+ /// instead of the end.
+ #[allow(clippy::too_many_arguments)]
pub fn set_string_truncated<S>(
&mut self,
x: u16,
@@ -280,6 +282,7 @@ impl Buffer {
width: usize,
style: Style,
ellipsis: bool,
+ truncate_start: bool,
) -> (u16, u16)
where
S: AsRef<str>,
@@ -289,28 +292,59 @@ impl Buffer {
let width = if ellipsis { width - 1 } else { width };
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
- for s in graphemes {
- let width = s.width();
- if width == 0 {
- continue;
+ if !truncate_start {
+ for s in graphemes {
+ let width = s.width();
+ if width == 0 {
+ continue;
+ }
+ // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
+ // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
+ if width > max_offset.saturating_sub(x_offset) {
+ break;
+ }
+
+ self.content[index].set_symbol(s);
+ self.content[index].set_style(style);
+ // Reset following cells if multi-width (they would be hidden by the grapheme),
+ for i in index + 1..index + width {
+ self.content[i].reset();
+ }
+ index += width;
+ x_offset += width;
}
- // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
- // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
- if width > max_offset.saturating_sub(x_offset) {
- break;
+ if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
+ self.content[index].set_symbol("…");
}
-
- self.content[index].set_symbol(s);
- self.content[index].set_style(style);
- // Reset following cells if multi-width (they would be hidden by the grapheme),
- for i in index + 1..index + width {
- self.content[i].reset();
+ } else {
+ let mut start_index = self.index_of(x, y);
+ let mut index = self.index_of(max_offset as u16, y);
+
+ let total_width = string.as_ref().width();
+ let truncated = total_width > width;
+ if ellipsis && truncated {
+ self.content[start_index].set_symbol("…");
+ start_index += 1;
+ }
+ if !truncated {
+ index -= width - total_width;
+ }
+ for s in graphemes.rev() {
+ let width = s.width();
+ if width == 0 {
+ continue;
+ }
+ let start = index - width;
+ if start < start_index {
+ break;
+ }
+ self.content[start].set_symbol(s);
+ self.content[start].set_style(style);
+ for i in start + 1..index {
+ self.content[i].reset();
+ }
+ index -= width;
}
- index += width;
- x_offset += width;
- }
- if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
- self.content[index].set_symbol("…");
}
(x_offset as u16, y)
}
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 1f55a36b..ffe6a111 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "helix-view"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
description = "UI abstractions for use in backends"
categories = ["editor"]
@@ -16,10 +16,10 @@ term = ["crossterm"]
[dependencies]
bitflags = "1.3"
anyhow = "1"
-helix-core = { version = "0.4", path = "../helix-core" }
-helix-lsp = { version = "0.4", path = "../helix-lsp"}
-helix-dap = { version = "0.4", path = "../helix-dap"}
-crossterm = { version = "0.21", optional = true }
+helix-core = { version = "0.5", path = "../helix-core" }
+helix-lsp = { version = "0.5", path = "../helix-lsp"}
+helix-dap = { version = "0.5", path = "../helix-dap"}
+crossterm = { version = "0.22", optional = true }
# Conversion traits
once_cell = "1.8"
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs
index a11224ac..a492652d 100644
--- a/helix-view/src/clipboard.rs
+++ b/helix-view/src/clipboard.rs
@@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
}
} else {
#[cfg(target_os = "windows")]
- return Box::new(provider::WindowsProvider::new());
+ return Box::new(provider::WindowsProvider::default());
#[cfg(not(target_os = "windows"))]
return Box::new(provider::NopProvider::new());
@@ -145,15 +145,15 @@ mod provider {
use anyhow::{bail, Context as _, Result};
use std::borrow::Cow;
+ #[cfg(not(target_os = "windows"))]
#[derive(Debug)]
pub struct NopProvider {
buf: String,
primary_buf: String,
}
+ #[cfg(not(target_os = "windows"))]
impl NopProvider {
- #[allow(dead_code)]
- // Only dead_code on Windows.
pub fn new() -> Self {
Self {
buf: String::new(),
@@ -162,6 +162,7 @@ mod provider {
}
}
+ #[cfg(not(target_os = "windows"))]
impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> {
Cow::Borrowed("none")
@@ -186,19 +187,8 @@ mod provider {
}
#[cfg(target_os = "windows")]
- #[derive(Debug)]
- pub struct WindowsProvider {
- selection_buf: String,
- }
-
- #[cfg(target_os = "windows")]
- impl WindowsProvider {
- pub fn new() -> Self {
- Self {
- selection_buf: String::new(),
- }
- }
- }
+ #[derive(Default, Debug)]
+ pub struct WindowsProvider;
#[cfg(target_os = "windows")]
impl ClipboardProvider for WindowsProvider {
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 1f1b1f5f..ce5df8ee 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -23,6 +23,8 @@ use crate::{DocumentId, Theme, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
+const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
Normal,
@@ -95,6 +97,9 @@ pub struct Document {
// it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome.
history: Cell<History>,
+
+ pub savepoint: Option<Transaction>,
+
last_saved_revision: usize,
version: i32, // should be usize?
@@ -306,8 +311,7 @@ where
T: Default,
F: FnOnce(T) -> T,
{
- let t = mem::take(mut_ref);
- let _ = mem::replace(mut_ref, f(t));
+ *mut_ref = f(mem::take(mut_ref));
}
use helix_lsp::lsp;
@@ -325,7 +329,8 @@ impl Document {
encoding,
text,
selections: HashMap::default(),
- indent_style: IndentStyle::Spaces(4),
+ indent_style: DEFAULT_INDENT,
+ line_ending: DEFAULT_LINE_ENDING,
mode: Mode::Normal,
restore_cursor: false,
syntax: None,
@@ -335,9 +340,9 @@ impl Document {
diagnostics: Vec::new(),
version: 0,
history: Cell::new(History::default()),
+ savepoint: None,
last_saved_revision: 0,
language_server: None,
- line_ending: DEFAULT_LINE_ENDING,
}
}
@@ -363,7 +368,7 @@ impl Document {
let mut doc = Self::from(rope, Some(encoding));
// set the path and try detecting the language
- doc.set_path(path)?;
+ doc.set_path(Some(path))?;
if let Some(loader) = config_loader {
doc.detect_language(theme, loader);
}
@@ -495,17 +500,15 @@ impl Document {
}
/// Detect the indentation used in the file, or otherwise defaults to the language indentation
- /// configured in `languages.toml`, with a fallback back to 2 space indentation if it isn't
+ /// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't
/// specified. Line ending is likewise auto-detected, and will fallback to the default OS
/// line ending.
pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
- IndentStyle::from_str(
- self.language
- .as_ref()
- .and_then(|config| config.indent.as_ref())
- .map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces.
- )
+ self.language
+ .as_ref()
+ .and_then(|config| config.indent.as_ref())
+ .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
});
self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
}
@@ -550,12 +553,14 @@ impl Document {
self.encoding
}
- pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
- let path = helix_core::path::get_canonicalized_path(path)?;
+ pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
+ let path = path
+ .map(helix_core::path::get_canonicalized_path)
+ .transpose()?;
// if parent doesn't exist we still want to open the document
// and error out when document is saved
- self.path = Some(path);
+ self.path = path;
Ok(())
}
@@ -635,6 +640,14 @@ impl Document {
if !transaction.changes().is_empty() {
self.version += 1;
+ // generate revert to savepoint
+ if self.savepoint.is_some() {
+ take_with(&mut self.savepoint, |prev_revert| {
+ let revert = transaction.invert(&old_doc);
+ Some(revert.compose(prev_revert.unwrap()))
+ });
+ }
+
// update tree-sitter syntax tree
if let Some(syntax) = &mut self.syntax {
// TODO: no unwrap
@@ -644,14 +657,13 @@ impl Document {
}
// map state.diagnostics over changes::map_pos too
- // NOTE: seems to do nothing since the language server resends diagnostics on each edit
- // for diagnostic in &mut self.diagnostics {
- // use helix_core::Assoc;
- // let changes = transaction.changes();
- // diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
- // diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
- // diagnostic.line = self.text.char_to_line(diagnostic.range.start);
- // }
+ for diagnostic in &mut self.diagnostics {
+ use helix_core::Assoc;
+ let changes = transaction.changes();
+ diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
+ diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
+ diagnostic.line = self.text.char_to_line(diagnostic.range.start);
+ }
// emit lsp notification
if let Some(language_server) = self.language_server() {
@@ -692,8 +704,8 @@ impl Document {
success
}
- /// Undo the last modification to the [`Document`].
- pub fn undo(&mut self, view_id: ViewId) {
+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
+ pub fn undo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take();
let success = if let Some(transaction) = history.undo() {
self.apply_impl(transaction, view_id)
@@ -706,10 +718,11 @@ impl Document {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
}
+ success
}
- /// Redo the last modification to the [`Document`].
- pub fn redo(&mut self, view_id: ViewId) {
+ /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
+ pub fn redo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take();
let success = if let Some(transaction) = history.redo() {
self.apply_impl(transaction, view_id)
@@ -722,6 +735,17 @@ impl Document {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
}
+ success
+ }
+
+ pub fn savepoint(&mut self) {
+ self.savepoint = Some(Transaction::new(self.text()));
+ }
+
+ pub fn restore(&mut self, view_id: ViewId) {
+ if let Some(revert) = self.savepoint.take() {
+ self.apply(&revert, view_id);
+ }
}
/// Undo modifications to the [`Document`] according to `uk`.
@@ -894,6 +918,9 @@ impl Document {
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
+ // sort by range
+ self.diagnostics
+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
}
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 60864e9e..591e0492 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -2,7 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect},
theme::{self, Theme},
- tree::Tree,
+ tree::{self, Tree},
Document, DocumentId, View, ViewId,
};
@@ -12,6 +12,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
collections::HashMap,
+ collections::BTreeMap,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@@ -19,8 +20,6 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use slotmap::SlotMap;
-
use anyhow::Error;
pub use helix_core::diagnostic::Severity;
@@ -63,6 +62,9 @@ pub struct Config {
/// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
#[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
pub idle_timeout: Duration,
+ pub completion_trigger_len: u8,
+ /// Whether to display infoboxes. Defaults to true.
+ pub auto_info: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -92,14 +94,29 @@ impl Default for Config {
auto_pairs: true,
auto_completion: true,
idle_timeout: Duration::from_millis(400),
+ completion_trigger_len: 2,
+ auto_info: true,
}
}
}
+pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
+impl Motion {
+ pub fn run(&self, e: &mut Editor) {
+ (self.0)(e)
+ }
+}
+impl std::fmt::Debug for Motion {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("motion")
+ }
+}
+
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
- pub documents: SlotMap<DocumentId, Document>,
+ pub next_document_id: usize,
+ pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
@@ -124,6 +141,7 @@ pub struct Editor {
pub config: Config,
pub idle_timer: Pin<Box<Sleep>>,
+ pub last_motion: Option<Motion>,
}
#[derive(Debug, Copy, Clone)]
@@ -148,7 +166,8 @@ impl Editor {
Self {
tree: Tree::new(area),
- documents: SlotMap::with_key(),
+ next_document_id: 0,
+ documents: BTreeMap::new(),
count: None,
selected_register: None,
theme: themes.default(),
@@ -166,6 +185,7 @@ impl Editor {
clipboard_provider: get_clipboard_provider(),
status_msg: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
+ last_motion: None,
config,
}
}
@@ -221,7 +241,7 @@ impl Editor {
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
- let doc = &self.documents[view.doc];
+ let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
}
@@ -230,22 +250,38 @@ impl Editor {
use crate::tree::Layout;
use helix_core::Selection;
- if !self.documents.contains_key(id) {
+ if !self.documents.contains_key(&id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action {
Action::Replace => {
- let view = view!(self);
- let jump = (
- view.doc,
- self.documents[view.doc].selection(view.id).clone(),
- );
-
+ let (view, doc) = current_ref!(self);
+ // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
+ // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
+ // of `self.tree`, which is mutably borrowed when `view_mut` is called.
+ let remove_empty_scratch = !doc.is_modified()
+ // If the buffer has no path and is not modified, it is an empty scratch buffer.
+ && doc.path().is_none()
+ // If the buffer we are changing to is not this buffer
+ && id != doc.id
+ // Ensure the buffer is not displayed in any other splits.
+ && !self
+ .tree
+ .traverse()
+ .any(|(_, v)| v.doc == doc.id && v.id != view.id);
let view = view_mut!(self);
- view.jumps.push(jump);
- view.last_accessed_doc = Some(view.doc);
+ if remove_empty_scratch {
+ // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
+ // borrow, invalidating direct access to `doc.id`.
+ let id = doc.id;
+ self.documents.remove(&id);
+ } else {
+ let jump = (view.doc, doc.selection(view.id).clone());
+ view.jumps.push(jump);
+ view.last_accessed_doc = Some(view.doc);
+ }
view.doc = id;
view.offset = Position::default();
@@ -272,14 +308,14 @@ impl Editor {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
- let doc = &mut self.documents[id];
+ let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
}
Action::VerticalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical);
// initialize selection for view
- let doc = &mut self.documents[id];
+ let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
}
}
@@ -288,9 +324,11 @@ impl Editor {
}
pub fn new_file(&mut self, action: Action) -> DocumentId {
- let doc = Document::default();
- let id = self.documents.insert(doc);
- self.documents[id].id = id;
+ let id = DocumentId(self.next_document_id);
+ self.next_document_id += 1;
+ let mut doc = Document::default();
+ doc.id = id;
+ self.documents.insert(id, doc);
self.switch(id, action);
id
}
@@ -313,7 +351,11 @@ impl Editor {
self.language_servers
.get(language)
.map_err(|e| {
- log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
+ log::error!(
+ "Failed to initialize the LSP for `{}` {{ {} }}",
+ language.scope(),
+ e
+ )
})
.ok()
});
@@ -336,8 +378,10 @@ impl Editor {
doc.set_language_server(Some(language_server));
}
- let id = self.documents.insert(doc);
- self.documents[id].id = id;
+ let id = DocumentId(self.next_document_id);
+ self.next_document_id += 1;
+ doc.id = id;
+ self.documents.insert(id, doc);
id
};
@@ -348,16 +392,20 @@ impl Editor {
pub fn close(&mut self, id: ViewId, close_buffer: bool) {
let view = self.tree.get(self.tree.focus);
// remove selection
- self.documents[view.doc].selections.remove(&id);
+ self.documents
+ .get_mut(&view.doc)
+ .unwrap()
+ .selections
+ .remove(&id);
if close_buffer {
// get around borrowck issues
- let doc = &self.documents[view.doc];
+ let doc = &self.documents[&view.doc];
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
- self.documents.remove(view.doc);
+ self.documents.remove(&view.doc);
}
self.tree.remove(id);
@@ -374,24 +422,40 @@ impl Editor {
self.tree.focus_next();
}
+ pub fn focus_right(&mut self) {
+ self.tree.focus_direction(tree::Direction::Right);
+ }
+
+ pub fn focus_left(&mut self) {
+ self.tree.focus_direction(tree::Direction::Left);
+ }
+
+ pub fn focus_up(&mut self) {
+ self.tree.focus_direction(tree::Direction::Up);
+ }
+
+ pub fn focus_down(&mut self) {
+ self.tree.focus_direction(tree::Direction::Down);
+ }
+
pub fn should_close(&self) -> bool {
self.tree.is_empty()
}
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id);
- let doc = &self.documents[view.doc];
+ let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
#[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> {
- self.documents.get(id)
+ self.documents.get(&id)
}
#[inline]
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
- self.documents.get_mut(id)
+ self.documents.get_mut(&id)
}
#[inline]
@@ -416,7 +480,7 @@ impl Editor {
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self);
- let doc = &self.documents[view.doc];
+ let doc = &self.documents[&view.doc];
let cursor = doc
.selection(view.id)
.primary()
diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs
index 629a3112..b5a002fa 100644
--- a/helix-view/src/info.rs
+++ b/helix-view/src/info.rs
@@ -1,6 +1,6 @@
use crate::input::KeyEvent;
use helix_core::unicode::width::UnicodeWidthStr;
-use std::fmt::Write;
+use std::{collections::BTreeSet, fmt::Write};
#[derive(Debug)]
/// Info box used in editor. Rendering logic will be in other crate.
@@ -16,7 +16,7 @@ pub struct Info {
}
impl Info {
- pub fn new(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
+ pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
let body = body
.into_iter()
.map(|(desc, events)| {
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 1e0ddfe2..580204cc 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers};
/// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 26a4d6d2..810aa063 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -54,7 +54,7 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
}
/// Represents a key.
-#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
+#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode {
/// Backspace key.
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index c37474d6..3e779356 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -12,8 +12,10 @@ pub mod theme;
pub mod tree;
pub mod view;
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
+pub struct DocumentId(usize);
+
slotmap::new_key_type! {
- pub struct DocumentId;
pub struct ViewId;
}
diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs
index 0bebd02f..63d76a42 100644
--- a/helix-view/src/macros.rs
+++ b/helix-view/src/macros.rs
@@ -13,7 +13,8 @@
macro_rules! current {
( $( $editor:ident ).+ ) => {{
let view = $crate::view_mut!( $( $editor ).+ );
- let doc = &mut $( $editor ).+ .documents[view.doc];
+ let id = view.doc;
+ let doc = $( $editor ).+ .documents.get_mut(&id).unwrap();
(view, doc)
}};
}
@@ -56,7 +57,7 @@ macro_rules! doc {
macro_rules! current_ref {
( $( $editor:ident ).+ ) => {{
let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
- let doc = &$( $editor ).+ .documents[view.doc];
+ let doc = &$( $editor ).+ .documents[&view.doc];
(view, doc)
}};
}
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 9c33685b..757316bd 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -1,6 +1,5 @@
use std::{
collections::HashMap,
- convert::TryFrom,
path::{Path, PathBuf},
};
diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs
index 576f64f0..064334b1 100644
--- a/helix-view/src/tree.rs
+++ b/helix-view/src/tree.rs
@@ -47,13 +47,21 @@ impl Node {
// TODO: screen coord to container + container coordinate helpers
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layout {
Horizontal,
Vertical,
// could explore stacked/tabbed
}
+#[derive(Debug, Clone, Copy)]
+pub enum Direction {
+ Up,
+ Down,
+ Left,
+ Right,
+}
+
#[derive(Debug)]
pub struct Container {
layout: Layout,
@@ -150,7 +158,6 @@ impl Tree {
} => container,
_ => unreachable!(),
};
-
if container.layout == layout {
// insert node after the current item if there is children already
let pos = if container.children.is_empty() {
@@ -393,6 +400,112 @@ impl Tree {
Traverse::new(self)
}
+ // Finds the split in the given direction if it exists
+ pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
+ let parent = self.nodes[id].parent;
+ // Base case, we found the root of the tree
+ if parent == id {
+ return None;
+ }
+ // Parent must always be a container
+ let parent_container = match &self.nodes[parent].content {
+ Content::Container(container) => container,
+ Content::View(_) => unreachable!(),
+ };
+
+ match (direction, parent_container.layout) {
+ (Direction::Up, Layout::Vertical)
+ | (Direction::Left, Layout::Horizontal)
+ | (Direction::Right, Layout::Horizontal)
+ | (Direction::Down, Layout::Vertical) => {
+ // The desired direction of movement is not possible within
+ // the parent container so the search must continue closer to
+ // the root of the split tree.
+ self.find_split_in_direction(parent, direction)
+ }
+ (Direction::Up, Layout::Horizontal)
+ | (Direction::Down, Layout::Horizontal)
+ | (Direction::Left, Layout::Vertical)
+ | (Direction::Right, Layout::Vertical) => {
+ // It's possible to move in the desired direction within
+ // the parent container so an attempt is made to find the
+ // correct child.
+ match self.find_child(id, &parent_container.children, direction) {
+ // Child is found, search is ended
+ Some(id) => Some(id),
+ // A child is not found. This could be because of either two scenarios
+ // 1. Its not possible to move in the desired direction, and search should end
+ // 2. A layout like the following with focus at X and desired direction Right
+ // | _ | x | |
+ // | _ _ _ | |
+ // | _ _ _ | |
+ // The container containing X ends at X so no rightward movement is possible
+ // however there still exists another view/container to the right that hasn't
+ // been explored. Thus another search is done here in the parent container
+ // before concluding it's not possible to move in the desired direction.
+ None => self.find_split_in_direction(parent, direction),
+ }
+ }
+ }
+ }
+
+ fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
+ let mut child_id = match direction {
+ // index wise in the child list the Up and Left represents a -1
+ // thus reversed iterator.
+ Direction::Up | Direction::Left => children
+ .iter()
+ .rev()
+ .skip_while(|i| **i != id)
+ .copied()
+ .nth(1)?,
+ // Down and Right => +1 index wise in the child list
+ Direction::Down | Direction::Right => {
+ children.iter().skip_while(|i| **i != id).copied().nth(1)?
+ }
+ };
+ let (current_x, current_y) = match &self.nodes[self.focus].content {
+ Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
+ Content::Container(_) => unreachable!(),
+ };
+
+ // If the child is a container the search finds the closest container child
+ // visually based on screen location.
+ while let Content::Container(container) = &self.nodes[child_id].content {
+ match (direction, container.layout) {
+ (_, Layout::Vertical) => {
+ // find closest split based on x because y is irrelevant
+ // in a vertical container (and already correct based on previous search)
+ child_id = *container.children.iter().min_by_key(|id| {
+ let x = match &self.nodes[**id].content {
+ Content::View(view) => view.inner_area().left(),
+ Content::Container(container) => container.area.left(),
+ };
+ (current_x as i16 - x as i16).abs()
+ })?;
+ }
+ (_, Layout::Horizontal) => {
+ // find closest split based on y because x is irrelevant
+ // in a horizontal container (and already correct based on previous search)
+ child_id = *container.children.iter().min_by_key(|id| {
+ let y = match &self.nodes[**id].content {
+ Content::View(view) => view.inner_area().top(),
+ Content::Container(container) => container.area.top(),
+ };
+ (current_y as i16 - y as i16).abs()
+ })?;
+ }
+ }
+ }
+ Some(child_id)
+ }
+
+ pub fn focus_direction(&mut self, direction: Direction) {
+ if let Some(id) = self.find_split_in_direction(self.focus, direction) {
+ self.focus = id;
+ }
+ }
+
pub fn focus_next(&mut self) {
// This function is very dumb, but that's because we don't store any parent links.
// (we'd be able to go parent.next_sibling() recursively until we find something)
@@ -420,13 +533,12 @@ impl Tree {
// if found = container -> found = first child
// }
- let iter = self.traverse();
-
- let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
- iter.next(); // take the focused value
-
- if let Some((key, _)) = iter.next() {
- self.focus = key;
+ let mut views = self
+ .traverse()
+ .skip_while(|&(id, _view)| id != self.focus)
+ .skip(1); // Skip focused value
+ if let Some((id, _)) = views.next() {
+ self.focus = id;
} else {
// extremely crude, take the first item again
let (key, _) = self.traverse().next().unwrap();
@@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
}
}
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::DocumentId;
+
+ #[test]
+ fn find_split_in_direction() {
+ let mut tree = Tree::new(Rect {
+ x: 0,
+ y: 0,
+ width: 180,
+ height: 80,
+ });
+ let mut view = View::new(DocumentId::default());
+ view.area = Rect::new(0, 0, 180, 80);
+ tree.insert(view);
+
+ let l0 = tree.focus;
+ let view = View::new(DocumentId::default());
+ tree.split(view, Layout::Vertical);
+ let r0 = tree.focus;
+
+ tree.focus = l0;
+ let view = View::new(DocumentId::default());
+ tree.split(view, Layout::Horizontal);
+ let l1 = tree.focus;
+
+ tree.focus = l0;
+ let view = View::new(DocumentId::default());
+ tree.split(view, Layout::Vertical);
+ let l2 = tree.focus;
+
+ // Tree in test
+ // | L0 | L2 | |
+ // | L1 | R0 |
+ tree.focus = l2;
+ assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
+ assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
+ assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));
+ assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up));
+
+ tree.focus = l1;
+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left));
+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down));
+ assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right));
+ assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up));
+
+ tree.focus = l0;
+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left));
+ assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down));
+ assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right));
+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up));
+
+ tree.focus = r0;
+ assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left));
+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down));
+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
+ }
+}
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 8a7d3374..ee236e94 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -2,10 +2,9 @@ use std::borrow::Cow;
use crate::{graphics::Rect, Document, DocumentId, ViewId};
use helix_core::{
- coords_at_pos,
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
- Position, RopeSlice, Selection,
+ visual_coords_at_pos, Position, RopeSlice, Selection,
};
type Jump = (DocumentId, Selection);
@@ -91,7 +90,10 @@ impl View {
.selection(self.id)
.primary()
.cursor(doc.text().slice(..));
- let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
+
+ let Position { col, row: line } =
+ visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
+
let inner_area = self.inner_area();
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
diff --git a/languages.toml b/languages.toml
index 050f1b4f..56d91a81 100644
--- a/languages.toml
+++ b/languages.toml
@@ -312,7 +312,7 @@ injection-regex = "php"
file-types = ["php"]
roots = []
-indent = { tab-width = 2, unit = " " }
+indent = { tab-width = 4, unit = " " }
[[language]]
name = "latex"
@@ -458,3 +458,12 @@ file-types = ["scm"]
roots = []
comment-token = ";"
indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "cmake"
+scope = "source.cmake"
+file-types = ["cmake", "CMakeLists.txt"]
+roots = []
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+language-server = { command = "cmake-language-server" }
diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm
index 754faeda..57898f27 100644
--- a/runtime/queries/bash/highlights.scm
+++ b/runtime/queries/bash/highlights.scm
@@ -7,7 +7,7 @@
(command_name) @function
-(variable_name) @property
+(variable_name) @variable.other.member
[
"case"
@@ -31,7 +31,7 @@
(function_definition name: (word) @function)
-(file_descriptor) @number
+(file_descriptor) @constant.numeric.integer
[
(command_substitution)
diff --git a/runtime/queries/c-sharp/highlights.scm b/runtime/queries/c-sharp/highlights.scm
index b76f4e60..6e84ad83 100644
--- a/runtime/queries/c-sharp/highlights.scm
+++ b/runtime/queries/c-sharp/highlights.scm
@@ -20,16 +20,16 @@
] @type.builtin
;; Enum
-(enum_member_declaration (identifier) @variable.property)
+(enum_member_declaration (identifier) @variable.other.member)
;; Literals
[
(real_literal)
(integer_literal)
-] @number
+] @constant.numeric.integer
+(character_literal) @constant.character
[
- (character_literal)
(string_literal)
(verbatim_string_literal)
(interpolated_string_text)
@@ -40,8 +40,8 @@
"$@\""
] @string
+(boolean_literal) @constant.builtin.boolean
[
- (boolean_literal)
(null_literal)
(void_keyword)
] @constant.builtin
@@ -98,7 +98,7 @@
;; Keywords
(modifier) @keyword
(this_expression) @keyword
-(escape_sequence) @keyword
+(escape_sequence) @constant.character.escape
[
"as"
diff --git a/runtime/queries/c/highlights.scm b/runtime/queries/c/highlights.scm
index 2c42710f..918f3f66 100644
--- a/runtime/queries/c/highlights.scm
+++ b/runtime/queries/c/highlights.scm
@@ -60,8 +60,8 @@
(system_lib_string) @string
(null) @constant
-(number_literal) @number
-(char_literal) @string
+(number_literal) @constant.numeric.integer
+(char_literal) @constant.character
(call_expression
function: (identifier) @function)
@@ -73,7 +73,7 @@
(preproc_function_def
name: (identifier) @function.special)
-(field_identifier) @property
+(field_identifier) @variable.other.member
(statement_identifier) @label
(type_identifier) @type
(primitive_type) @type
diff --git a/runtime/queries/cmake/highlights.scm b/runtime/queries/cmake/highlights.scm
new file mode 100644
index 00000000..71e9b5d9
--- /dev/null
+++ b/runtime/queries/cmake/highlights.scm
@@ -0,0 +1,97 @@
+[
+ (quoted_argument)
+ (bracket_argument)
+ ] @string
+
+(variable) @variable
+
+[
+ (bracket_comment)
+ (line_comment)
+ ] @comment
+
+(normal_command (identifier) @function)
+
+["ENV" "CACHE"] @string.special.symbol
+["$" "{" "}" "<" ">"] @punctuation
+["(" ")"] @punctuation.bracket
+
+[
+ (function)
+ (endfunction)
+ (macro)
+ (endmacro)
+ ] @keyword.function
+
+[
+ (if)
+ (elseif)
+ (else)
+ (endif)
+ ] @keyword.control.conditional
+
+[
+ (foreach)
+ (endforeach)
+ (while)
+ (endwhile)
+ ] @keyword.control.repeat
+
+(function_command
+ (function)
+ . (argument) @function
+ (argument)* @variable.parameter
+ )
+
+(macro_command
+ (macro)
+ . (argument) @function.macro
+ (argument)* @variable.parameter
+ )
+
+(normal_command
+ (identifier) @function.builtin
+ . (argument) @variable
+ (#match? @function.builtin "^(?i)(set)$"))
+
+(normal_command
+ (identifier) @function.builtin
+ . (argument)
+ (argument) @constant
+ (#match? @constant "^(?:PARENT_SCOPE|CACHE)$")
+ (#match? @function.builtin "^(?i)(unset)$"))
+
+(normal_command
+ (identifier) @function.builtin
+ . (argument)
+ . (argument)
+ (argument) @constant
+ (#match? @constant "^(?:PARENT_SCOPE|CACHE|FORCE)$")
+ (#match? @function.builtin "^(?i)(set)$")
+ )
+
+((argument) @constant.builtin.boolean
+ (#match? @constant.builtin.boolean "^(?i)(?:1|on|yes|true|y|0|off|no|false|n|ignore|notfound|.*-notfound)$")
+ )
+
+(if_command
+ (if)
+ (argument) @operator
+ (#match? @operator "^(?:NOT|AND|OR|COMMAND|POLICY|TARGET|TEST|DEFINED|IN_LIST|EXISTS|IS_NEWER_THAN|IS_DIRECTORY|IS_SYMLINK|IS_ABSOLUTE|MATCHES|LESS|GREATER|EQUAL|LESS_EQUAL|GREATER_EQUAL|STRLESS|STRGREATER|STREQUAL|STRLESS_EQUAL|STRGREATER_EQUAL|VERSION_LESS|VERSION_GREATER|VERSION_EQUAL|VERSION_LESS_EQUAL|VERSION_GREATER_EQUAL)$")
+)
+
+(normal_command
+ (identifier) @function.builtin
+ . (argument)
+ (argument) @constant
+ (#match? @constant "^(?:ALL|COMMAND|DEPENDS|BYPRODUCTS|WORKING_DIRECTORY|COMMENT|JOB_POOL|VERBATIM|USES_TERMINAL|COMMAND_EXPAND_LISTS|SOURCES)$")
+ (#match? @function.builtin "^(?i)(add_custom_target)$")
+ )
+
+(normal_command
+ (identifier) @function.builtin
+ (argument) @constant
+ (#match? @constant "^(?:OUTPUT|COMMAND|MAIN_DEPENDENCY|DEPENDS|BYPRODUCTS|IMPLICIT_DEPENDS|WORKING_DIRECTORY|COMMENT|DEPFILE|JOB_POOL|VERBATIM|APPEND|USES_TERMINAL|COMMAND_EXPAND_LISTS)$")
+ (#match? @function.builtin "^(?i)(add_custom_command)$")
+ )
+
diff --git a/runtime/queries/cpp/highlights.scm b/runtime/queries/cpp/highlights.scm
index 3315fde0..3348ef3c 100644
--- a/runtime/queries/cpp/highlights.scm
+++ b/runtime/queries/cpp/highlights.scm
@@ -3,7 +3,7 @@
; Functions
(call_expression
- function: (scoped_identifier
+ function: (qualified_identifier
name: (identifier) @function))
(template_function
@@ -13,15 +13,14 @@
name: (field_identifier) @function)
(template_function
- name: (scoped_identifier
- name: (identifier) @function))
+ name: (identifier) @function)
(function_declarator
- declarator: (scoped_identifier
+ declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
- declarator: (scoped_identifier
+ declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
diff --git a/runtime/queries/css/highlights.scm b/runtime/queries/css/highlights.scm
index 763661af..4dfc0c66 100644
--- a/runtime/queries/css/highlights.scm
+++ b/runtime/queries/css/highlights.scm
@@ -26,11 +26,11 @@
(pseudo_element_selector (tag_name) @attribute)
(pseudo_class_selector (class_name) @attribute)
-(class_name) @property
-(id_name) @property
-(namespace_name) @property
-(property_name) @property
-(feature_name) @property
+(class_name) @variable.other.member
+(id_name) @variable.other.member
+(namespace_name) @variable.other.member
+(property_name) @variable.other.member
+(feature_name) @variable.other.member
(attribute_name) @attribute
@@ -55,8 +55,8 @@
(string_value) @string
(color_value) @string.special
-(integer_value) @number
-(float_value) @number
+(integer_value) @constant.numeric.integer
+(float_value) @constant.numeric.float
(unit) @type
"#" @punctuation.delimiter
diff --git a/runtime/queries/elixir/highlights.scm b/runtime/queries/elixir/highlights.scm
index 6bf93a21..76fd2af9 100644
--- a/runtime/queries/elixir/highlights.scm
+++ b/runtime/queries/elixir/highlights.scm
@@ -1,125 +1,210 @@
-["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
-
-[(true) (false) (nil)] @constant.builtin
-
-(keyword
- [(keyword_literal)
- ":"] @tag)
-
-(keyword
- (keyword_string
- [(string_start)
- (string_content)
- (string_end)] @tag))
-
-[(atom_literal)
- (atom_start)
- (atom_content)
- (atom_end)] @tag
-
-[(comment)
- (unused_identifier)] @comment
-
-(escape_sequence) @escape
-
-(call function: (function_identifier) @keyword
- (#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
-
-(call function: (function_identifier) @keyword
- [(call
- function: (function_identifier) @function
- (arguments
- [(identifier) @variable.parameter
- (_ (identifier) @variable.parameter)
- (_ (_ (identifier) @variable.parameter))
- (_ (_ (_ (identifier) @variable.parameter)))
- (_ (_ (_ (_ (identifier) @variable.parameter))))
- (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
- (binary_op
- left:
- (call
- function: (function_identifier) @function
- (arguments
- [(identifier) @variable.parameter
- (_ (identifier) @variable.parameter)
- (_ (_ (identifier) @variable.parameter))
- (_ (_ (_ (identifier) @variable.parameter)))
- (_ (_ (_ (_ (identifier) @variable.parameter))))
- (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
+; The following code originates mostly from
+; https://github.com/elixir-lang/tree-sitter-elixir, with minor edits to
+; align the captures with helix. The following should be considered
+; Copyright 2021 The Elixir Team
+;
+; Licensed under the Apache License, Version 2.0 (the "License");
+; you may not use this file except in compliance with the License.
+; You may obtain a copy of the License at
+;
+; https://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing, software
+; distributed under the License is distributed on an "AS IS" BASIS,
+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+; See the License for the specific language governing permissions and
+; limitations under the License.
+
+; Reserved keywords
+
+["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
+
+; Operators
+
+; * doc string
+(unary_operator
+ operator: "@" @comment.block.documentation
+ operand: (call
+ target: (identifier) @comment.block.documentation.__attribute__
+ (arguments
+ [
+ (string) @comment.block.documentation
+ (charlist) @comment.block.documentation
+ (sigil
+ quoted_start: _ @comment.block.documentation
+ quoted_end: _ @comment.block.documentation) @comment.block.documentation
+ (boolean) @comment.block.documentation
+ ]))
+ (#match? @comment.block.documentation.__attribute__ "^(moduledoc|typedoc|doc)$"))
+
+; * module attribute
+(unary_operator
+ operator: "@" @variable.other.member
+ operand: [
+ (identifier) @variable.other.member
+ (call
+ target: (identifier) @variable.other.member)
+ (boolean) @variable.other.member
+ (nil) @variable.other.member
+ ])
+
+; * capture operator
+(unary_operator
+ operator: "&"
+ operand: [
+ (integer) @operator
+ (binary_operator
+ left: [
+ (call target: (dot left: (_) right: (identifier) @function))
+ (identifier) @function
+ ] operator: "/" right: (integer) @operator)
+ ])
+
+(operator_identifier) @operator
+
+(unary_operator
+ operator: _ @operator)
+
+(binary_operator
+ operator: _ @operator)
+
+(dot
+ operator: _ @operator)
+
+(stab_clause
+ operator: _ @operator)
+
+; Literals
+
+(nil) @constant.builtin
+
+(boolean) @constant.builtin.boolean
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
+
+(alias) @type
+
+(call
+ target: (dot
+ left: (atom) @type))
+
+(char) @constant.character
+
+; Quoted content
+
+(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
+
+(escape_sequence) @constant.character.escape
+
+[
+ (atom)
+ (quoted_atom)
+ (keyword)
+ (quoted_keyword)
+] @string.special.symbol
+
+[
+ (string)
+ (charlist)
+] @string
+
+; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters
+
+(sigil
+ (sigil_name) @__name__
+ quoted_start: _ @string
+ quoted_end: _ @string
+ (#match? @__name__ "^[sS]$")) @string
+
+(sigil
+ (sigil_name) @__name__
+ quoted_start: _ @string.regexp
+ quoted_end: _ @string.regexp
+ (#match? @__name__ "^[rR]$")) @string.regexp
+
+(sigil
+ (sigil_name) @__name__
+ quoted_start: _ @string.special
+ quoted_end: _ @string.special) @string.special
+
+; Calls
+
+; * definition keyword
+(call
+ target: (identifier) @keyword
+ (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
+
+; * kernel or special forms keyword
+(call
+ target: (identifier) @keyword
+ (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
+
+; * function call
+(call
+ target: [
+ ; local
+ (identifier) @function
+ ; remote
+ (dot
+ right: (identifier) @function)
+ ])
+
+; * just identifier in function definition
+(call
+ target: (identifier) @keyword
+ (arguments
+ [
+ (identifier) @function
+ (binary_operator
+ left: (identifier) @function
operator: "when")
- (binary_op
- left: (identifier) @variable.parameter
- operator: _ @function
- right: (identifier) @variable.parameter)]
- (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
-
-(call (function_identifier) @keyword
- [(call
- function: (function_identifier) @function)
- (identifier) @function
- (binary_op
- left:
- [(call
- function: (function_identifier) @function)
- (identifier) @function]
- operator: "when")]
- (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
-
-(anonymous_function
- (stab_expression
- left: (bare_arguments
- [(identifier) @variable.parameter
- (_ (identifier) @variable.parameter)
- (_ (_ (identifier) @variable.parameter))
- (_ (_ (_ (identifier) @variable.parameter)))
- (_ (_ (_ (_ (identifier) @variable.parameter))))
- (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
-
-(unary_op
- operator: "@"
- (call (identifier) @attribute
- (heredoc
- [(heredoc_start)
- (heredoc_content)
- (heredoc_end)] @doc))
- (#match? @attribute "^(doc|moduledoc)$"))
-
-(module) @type
-
-(unary_op
- operator: "@" @attribute
- [(call
- function: (function_identifier) @attribute)
- (identifier) @attribute])
-
-(unary_op
- operator: _ @operator)
-
-(binary_op
- operator: _ @operator)
-
-(heredoc
- [(heredoc_start)
- (heredoc_content)
- (heredoc_end)] @string)
-
-(string
- [(string_start)
- (string_content)
- (string_end)] @string)
-
-(sigil_start) @string.special
-(sigil_content) @string
-(sigil_end) @string.special
-
-(interpolation
- "#{" @punctuation.special
- "}" @punctuation.special)
+ ])
+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
+
+; * pipe into identifier (definition)
+(call
+ target: (identifier) @keyword
+ (arguments
+ (binary_operator
+ operator: "|>"
+ right: (identifier) @variable))
+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
+
+; * pipe into identifier (function call)
+(binary_operator
+ operator: "|>"
+ right: (identifier) @function)
+
+; Identifiers
+
+; * special
+(
+ (identifier) @constant.builtin
+ (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
+)
+
+; * unused
+(
+ (identifier) @comment
+ (#match? @comment "^_")
+)
+
+; * regular
+(identifier) @variable
+
+; Comment
+
+(comment) @comment
+
+; Punctuation
+
+[
+ "%"
+] @punctuation
[
","
- "->"
- "."
+ ";"
] @punctuation.delimiter
[
@@ -133,6 +218,4 @@
">>"
] @punctuation.bracket
-(special_identifier) @function.special
-
(ERROR) @warning
diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm
index 3129c4b2..56384d4d 100644
--- a/runtime/queries/go/highlights.scm
+++ b/runtime/queries/go/highlights.scm
@@ -25,7 +25,7 @@
(variadic_parameter_declaration (identifier) @variable.parameter)
(type_identifier) @type
-(field_identifier) @property
+(field_identifier) @variable.other.member
(identifier) @variable
(package_identifier) @variable
@@ -130,13 +130,13 @@
(rune_literal)
] @string
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
[
(int_literal)
(float_literal)
(imaginary_literal)
-] @number
+] @constant.numeric.integer
[
(true)
diff --git a/runtime/queries/go/textobjects.scm b/runtime/queries/go/textobjects.scm
new file mode 100644
index 00000000..9bcfc690
--- /dev/null
+++ b/runtime/queries/go/textobjects.scm
@@ -0,0 +1,21 @@
+(function_declaration
+ body: (block)? @function.inside) @function.around
+
+(func_literal
+ (_)? @function.inside) @function.around
+
+(method_declaration
+ body: (block)? @function.inside) @function.around
+
+;; struct and interface declaration as class textobject?
+(type_declaration
+ (type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around
+
+(type_declaration
+ (type_spec (type_identifier) (interface_type (method_spec_list (_)?) @class.inside))) @class.around
+
+(parameter_list
+ (_) @parameter.inside)
+
+(argument_list
+ (_) @parameter.inside)
diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm
index dada80b6..72187876 100644
--- a/runtime/queries/haskell/highlights.scm
+++ b/runtime/queries/haskell/highlights.scm
@@ -13,9 +13,9 @@
(constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class))
(instance (instance_head class: (class_name (type)) @class))
-(integer) @number
-(exp_literal (float)) @number
-(char) @literal
+(integer) @constant.numeric.integer
+(exp_literal (float)) @constant.numeric.float
+(char) @constant.character
(con_unit) @literal
(con_list) @literal
(tycon_arrow) @operator
diff --git a/runtime/queries/java/highlights.scm b/runtime/queries/java/highlights.scm
index e7d793df..77902fce 100644
--- a/runtime/queries/java/highlights.scm
+++ b/runtime/queries/java/highlights.scm
@@ -59,14 +59,15 @@
(hex_integer_literal)
(decimal_integer_literal)
(octal_integer_literal)
+] @constant.numeric.integer
+
+[
(decimal_floating_point_literal)
(hex_floating_point_literal)
-] @number
+] @constant.numeric.float
-[
- (character_literal)
- (string_literal)
-] @string
+(character_literal) @constant.character
+(string_literal) @string
[
(true)
diff --git a/runtime/queries/javascript/highlights.scm b/runtime/queries/javascript/highlights.scm
index e29829bf..6163b680 100644
--- a/runtime/queries/javascript/highlights.scm
+++ b/runtime/queries/javascript/highlights.scm
@@ -65,7 +65,7 @@
; Properties
;-----------
-(property_identifier) @property
+(property_identifier) @variable.other.member
; Literals
;---------
@@ -88,7 +88,7 @@
] @string
(regex) @string.regexp
-(number) @number
+(number) @constant.numeric.integer
; Tokens
;-------
diff --git a/runtime/queries/json/highlights.scm b/runtime/queries/json/highlights.scm
index b08ea439..6df6c9eb 100644
--- a/runtime/queries/json/highlights.scm
+++ b/runtime/queries/json/highlights.scm
@@ -1,9 +1,20 @@
+[
+ (true)
+ (false)
+] @constant.builtin.boolean
+(null) @constant.builtin
+(number) @constant.numeric
(pair
key: (_) @keyword)
(string) @string
+(escape_sequence) @constant.character.escape
+(ERROR) @error
-(object
- "{" @escape
- (_)
- "}" @escape)
+"," @punctuation.delimiter
+[
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm
index 7b7d426c..7c447985 100644
--- a/runtime/queries/julia/highlights.scm
+++ b/runtime/queries/julia/highlights.scm
@@ -15,7 +15,7 @@
(field_expression
(identifier)
- (identifier) @field .)
+ (identifier) @variable.other.member .)
(function_definition
name: (identifier) @function)
@@ -80,14 +80,14 @@
(struct_definition
name: (identifier) @type)
-(number) @number
+(number) @constant.numeric.integer
(range_expression
- (identifier) @number
- (eq? @number "end"))
+ (identifier) @constant.numeric.integer
+ (eq? @constant.numeric.integer "end"))
(range_expression
(_
- (identifier) @number
- (eq? @number "end")))
+ (identifier) @constant.numeric.integer
+ (eq? @constant.numeric.integer "end")))
(coefficient_expression
(number)
(identifier) @constant.builtin)
diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm
index 86c609c2..bdf5f2db 100644
--- a/runtime/queries/ledger/highlights.scm
+++ b/runtime/queries/ledger/highlights.scm
@@ -7,9 +7,9 @@
(date)
(interval)
(quantity)
-] @number
+] @constant.numeric.integer
-((account) @field)
+((account) @variable.other.member)
((commodity) @text.literal)
"include" @include
diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm
index 40c2be70..e73b32d6 100644
--- a/runtime/queries/lua/highlights.scm
+++ b/runtime/queries/lua/highlights.scm
@@ -150,14 +150,14 @@
(table ["{" "}"] @constructor)
(comment) @comment
(string) @string
-(number) @number
+(number) @constant.numeric.integer
(label_statement) @label
; A bit of a tricky one, this will only match field names
-(field . (identifier) @property (_))
+(field . (identifier) @variable.other.member (_))
(shebang) @comment
;; Property
-(property_identifier) @property
+(property_identifier) @variable.other.member
;; Variable
(identifier) @variable
diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm
index 741b73b5..66719e87 100644
--- a/runtime/queries/nix/highlights.scm
+++ b/runtime/queries/nix/highlights.scm
@@ -33,16 +33,14 @@
(uri) @string.special.uri
-[
- (integer)
- (float)
-] @number
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
(interpolation
"${" @punctuation.special
"}" @punctuation.special) @embedded
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
(function
universal: (identifier) @variable.parameter
@@ -66,8 +64,8 @@
(binary
operator: _ @operator)
-(attr_identifier) @property
-(inherit attrs: (attrs_inherited (identifier) @property) )
+(attr_identifier) @variable.other.member
+(inherit attrs: (attrs_inherited (identifier) @variable.other.member) )
[
";"
diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm
index 160f2cb4..15f46cc1 100644
--- a/runtime/queries/ocaml/highlights.scm
+++ b/runtime/queries/ocaml/highlights.scm
@@ -51,14 +51,14 @@
; Properties
;-----------
-[(label_name) (field_name) (instance_variable_name)] @property
+[(label_name) (field_name) (instance_variable_name)] @variable.other.member
; Constants
;----------
[(boolean) (unit)] @constant
-[(number) (signed_number)] @number
+[(number) (signed_number)] @constant.numeric.integer
(character) @constant.character
@@ -66,7 +66,7 @@
(quoted_string "{" @string "}" @string) @string
-(escape_sequence) @string.escape
+(escape_sequence) @constant.character.escape
[
(conversion_specification)
@@ -145,7 +145,7 @@
; Attributes
;-----------
-(attribute_id) @property
+(attribute_id) @variable.other.member
; Comments
;---------
diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm
index 02904555..46b5d26c 100644
--- a/runtime/queries/php/highlights.scm
+++ b/runtime/queries/php/highlights.scm
@@ -30,12 +30,12 @@
; Member
(property_element
- (variable_name) @property)
+ (variable_name) @variable.other.member)
(member_access_expression
- name: (variable_name (name)) @property)
+ name: (variable_name (name)) @variable.other.member)
(member_access_expression
- name: (name) @property)
+ name: (name) @variable.other.member)
; Variables
@@ -56,10 +56,10 @@
(string) @string
(heredoc) @string
-(boolean) @constant.builtin
+(boolean) @constant.builtin.boolean
(null) @constant.builtin
-(integer) @number
-(float) @number
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
(comment) @comment
"$" @operator
diff --git a/runtime/queries/php/indents.toml b/runtime/queries/php/indents.toml
new file mode 100644
index 00000000..85c104db
--- /dev/null
+++ b/runtime/queries/php/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+ "array_creation_expression",
+ "arguments",
+ "formal_parameters",
+ "compound_statement",
+ "declaration_list",
+ "binary_expression",
+ "return_statement",
+ "expression_statement",
+ "switch_block",
+ "anonymous_function_use_clause",
+]
+
+oudent = [
+ "}",
+ ")",
+]
diff --git a/runtime/queries/protobuf/highlights.scm b/runtime/queries/protobuf/highlights.scm
index cd021be1..c35c430e 100644
--- a/runtime/queries/protobuf/highlights.scm
+++ b/runtime/queries/protobuf/highlights.scm
@@ -34,16 +34,14 @@
[
(fieldName)
(optionName)
-] @property
+] @variable.other.member
(enumVariantName) @type.enum.variant
(fullIdent) @namespace
-[
- (intLit)
- (floatLit)
-] @number
-(boolLit) @constant.builtin
+(intLit) @constant.numeric.integer
+(floatLit) @constant.numeric.float
+(boolLit) @constant.builtin.boolean
(strLit) @string
(constant) @constant
diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm
index f64fecb2..9131acc5 100644
--- a/runtime/queries/python/highlights.scm
+++ b/runtime/queries/python/highlights.scm
@@ -29,7 +29,7 @@
name: (identifier) @function)
(identifier) @variable
-(attribute attribute: (identifier) @property)
+(attribute attribute: (identifier) @variable.other.member)
(type (identifier) @type)
; Literals
@@ -40,14 +40,11 @@
(false)
] @constant.builtin
-[
- (integer)
- (float)
-] @number
-
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
(comment) @comment
(string) @string
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
(interpolation
"{" @punctuation.special
diff --git a/runtime/queries/python/indents.toml b/runtime/queries/python/indents.toml
new file mode 100644
index 00000000..6bc68486
--- /dev/null
+++ b/runtime/queries/python/indents.toml
@@ -0,0 +1,39 @@
+indent = [
+ "list",
+ "tuple",
+ "dictionary",
+ "set",
+
+ "if_statement",
+ "for_statement",
+ "while_statement",
+ "with_statement",
+ "try_statement",
+ "import_from_statement",
+
+ "parenthesized_expression",
+ "generator_expression",
+ "list_comprehension",
+ "set_comprehension",
+ "dictionary_comprehension",
+
+ "tuple_pattern",
+ "list_pattern",
+ "argument_list",
+ "parameters",
+ "binary_operator",
+
+ "function_definition",
+ "class_definition",
+]
+
+outdent = [
+ ")",
+ "]",
+ "}",
+ "return_statement",
+ "pass_statement",
+ "raise_statement",
+]
+
+ignore = ["string"]
diff --git a/runtime/queries/python/textobjects.scm b/runtime/queries/python/textobjects.scm
new file mode 100644
index 00000000..a52538af
--- /dev/null
+++ b/runtime/queries/python/textobjects.scm
@@ -0,0 +1,14 @@
+(function_definition
+ body: (block)? @function.inside) @function.around
+
+(class_definition
+ body: (block)? @class.inside) @class.around
+
+(parameters
+ (_) @parameter.inside)
+
+(lambda_parameters
+ (_) @parameter.inside)
+
+(argument_list
+ (_) @parameter.inside)
diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm
index 8617d6f0..898f8f79 100644
--- a/runtime/queries/ruby/highlights.scm
+++ b/runtime/queries/ruby/highlights.scm
@@ -55,7 +55,7 @@
[
(class_variable)
(instance_variable)
-] @property
+] @variable.other.member
((identifier) @constant.builtin
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
@@ -101,12 +101,12 @@
] @string.special.symbol
(regex) @string.regexp
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
[
(integer)
(float)
-] @number
+] @constant.numeric.integer
[
(nil)
diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm
index 956a5dac..539d9550 100644
--- a/runtime/queries/rust/highlights.scm
+++ b/runtime/queries/rust/highlights.scm
@@ -15,15 +15,13 @@
; Primitives
; ---
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
(primitive_type) @type.builtin
(boolean_literal) @constant.builtin.boolean
+(integer_literal) @constant.numeric.integer
+(float_literal) @constant.numeric.float
+(char_literal) @constant.character
[
- (integer_literal)
- (float_literal)
-] @number
-[
- (char_literal)
(string_literal)
(raw_string_literal)
] @string
@@ -40,10 +38,10 @@
(enum_variant (identifier) @type.enum.variant)
(field_initializer
- (field_identifier) @property)
+ (field_identifier) @variable.other.member)
(shorthand_field_initializer
- (identifier) @variable.property)
-(shorthand_field_identifier) @variable.property
+ (identifier) @variable.other.member)
+(shorthand_field_identifier) @variable.other.member
(lifetime
"'" @label
@@ -81,9 +79,24 @@
] @punctuation.bracket)
; ---
-; Parameters
+; Variables
; ---
+(let_declaration
+ pattern: [
+ ((identifier) @variable)
+ ((tuple_pattern
+ (identifier) @variable))
+ ])
+
+; It needs to be anonymous to not conflict with `call_expression` further below.
+(_
+ value: (field_expression
+ value: (identifier)? @variable
+ field: (field_identifier) @variable.other.member))
+
+(arguments
+ (identifier) @variable.parameter)
(parameter
pattern: (identifier) @variable.parameter)
(closure_parameters
@@ -336,4 +349,4 @@
(type_identifier) @type
(identifier) @variable
-(field_identifier) @property
+(field_identifier) @variable.other.member
diff --git a/runtime/queries/rust/textobjects.scm b/runtime/queries/rust/textobjects.scm
new file mode 100644
index 00000000..e3132687
--- /dev/null
+++ b/runtime/queries/rust/textobjects.scm
@@ -0,0 +1,26 @@
+(function_item
+ body: (_) @function.inside) @function.around
+
+(struct_item
+ body: (_) @class.inside) @class.around
+
+(enum_item
+ body: (_) @class.inside) @class.around
+
+(union_item
+ body: (_) @class.inside) @class.around
+
+(trait_item
+ body: (_) @class.inside) @class.around
+
+(impl_item
+ body: (_) @class.inside) @class.around
+
+(parameters
+ (_) @parameter.inside)
+
+(closure_parameters
+ (_) @parameter.inside)
+
+(arguments
+ (_) @parameter.inside)
diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm
index 4c6f5f35..4fcdfd66 100644
--- a/runtime/queries/svelte/highlights.scm
+++ b/runtime/queries/svelte/highlights.scm
@@ -29,7 +29,7 @@
(#match? @_attr "^(href|src)$"))
(tag_name) @tag
-(attribute_name) @property
+(attribute_name) @variable.other.member
(erroneous_end_tag_name) @error
(comment) @comment
diff --git a/runtime/queries/toml/highlights.scm b/runtime/queries/toml/highlights.scm
index e4d6966f..2742b2be 100644
--- a/runtime/queries/toml/highlights.scm
+++ b/runtime/queries/toml/highlights.scm
@@ -1,17 +1,17 @@
; Properties
;-----------
-(bare_key) @property
+(bare_key) @variable.other.member
(quoted_key) @string
; Literals
;---------
-(boolean) @constant.builtin
+(boolean) @constant.builtin.boolean
(comment) @comment
(string) @string
-(integer) @number
-(float) @number
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
(offset_date_time) @string.special
(local_date_time) @string.special
(local_date) @string.special
diff --git a/runtime/queries/tsq/highlights.scm b/runtime/queries/tsq/highlights.scm
index 9ba5699a..549895c1 100644
--- a/runtime/queries/tsq/highlights.scm
+++ b/runtime/queries/tsq/highlights.scm
@@ -35,12 +35,12 @@
(comment) @comment
-(field_name) @property
+(field_name) @variable.other.member
(capture) @label
(predicate_name) @function
-(escape_sequence) @escape
+(escape_sequence) @constant.character.escape
(node_name) @variable
diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm
index 2955a4ce..a7efb5e7 100644
--- a/runtime/queries/yaml/highlights.scm
+++ b/runtime/queries/yaml/highlights.scm
@@ -1,12 +1,12 @@
-(block_mapping_pair key: (_) @property)
-(flow_mapping (_ key: (_) @property))
+(block_mapping_pair key: (_) @variable.other.member)
+(flow_mapping (_ key: (_) @variable.other.member))
(boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin
(double_quote_scalar) @string
(single_quote_scalar) @string
-(escape_sequence) @string.escape
-(integer_scalar) @number
-(float_scalar) @number
+(escape_sequence) @constant.character.escape
+(integer_scalar) @constant.numeric.integer
+(float_scalar) @constant.numeric.float
(comment) @comment
(anchor_name) @type
(alias_name) @type
diff --git a/runtime/queries/zig/highlights.scm b/runtime/queries/zig/highlights.scm
index 404a8682..34dbeacd 100644
--- a/runtime/queries/zig/highlights.scm
+++ b/runtime/queries/zig/highlights.scm
@@ -14,7 +14,7 @@ parameter: (IDENTIFIER) @variable.parameter
[
field_member: (IDENTIFIER)
field_access: (IDENTIFIER)
-] @variable.property
+] @variable.other.member
;; assume TitleCase is a type
(
@@ -75,9 +75,9 @@ field_constant: (IDENTIFIER) @constant
((BUILTINIDENTIFIER) @keyword.control.import
(#any-of? @keyword.control.import "@import" "@cImport"))
-(INTEGER) @number
+(INTEGER) @constant.numeric.integer
-(FLOAT) @number
+(FLOAT) @constant.numeric.float
[
(LINESTRING)
@@ -85,7 +85,7 @@ field_constant: (IDENTIFIER) @constant
] @string
(CHAR_LITERAL) @constant.character
-(EscapeSequence) @escape
+(EscapeSequence) @constant.character.escape
(FormatSequence) @string.special
[
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
new file mode 100644
index 00000000..d65995c0
--- /dev/null
+++ b/runtime/themes/base16_default_dark.toml
@@ -0,0 +1,59 @@
+# Author: RayGervais<raygervais@hotmail.ca>
+
+"ui.background" = { bg = "base00" }
+"ui.menu" = "base01"
+"ui.menu.selected" = { fg = "base04", bg = "base01" }
+"ui.linenr" = {fg = "base01" }
+"ui.popup" = { bg = "base01" }
+"ui.window" = { bg = "base01" }
+"ui.liner.selected" = "base02"
+"ui.selection" = "base02"
+"comment" = "base03"
+"ui.statusline" = {fg = "base04", bg = "base01" }
+"ui.help" = { fg = "base04", bg = "base01" }
+"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = { fg = "base05" }
+"operator" = "base05"
+"ui.text.focus" = { fg = "base05" }
+"variable" = "base08"
+"constant.numeric" = "base09"
+"constant" = "base09"
+"attributes" = "base09"
+"type" = "base0A"
+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
+"strings" = "base0B"
+"variable.other.member" = "base0B"
+"constant.character.escape" = "base0C"
+"function" = "base0D"
+"constructor" = "base0D"
+"special" = "base0D"
+"keyword" = "base0E"
+"label" = "base0E"
+"namespace" = "base0E"
+"ui.popup" = { bg = "base01" }
+"ui.window" = { bg = "base00" }
+"ui.help" = { bg = "base01", fg = "base06" }
+
+"info" = "base03"
+"hint" = "base03"
+"debug" = "base03"
+"diagnostic" = "base03"
+"error" = "base0E"
+
+[palette]
+base00 = "#181818" # Default Background
+base01 = "#282828" # Lighter Background (Used for status bars, line number and folding marks)
+base02 = "#383838" # Selection Background
+base03 = "#585858" # Comments, Invisibles, Line Highlighting
+base04 = "#b8b8b8" # Dark Foreground (Used for status bars)
+base05 = "#d8d8d8" # Default Foreground, Caret, Delimiters, Operators
+base06 = "#e8e8e8" # Light Foreground (Not often used)
+base07 = "#f8f8f8" # Light Background (Not often used)
+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml
index 37b9adbf..86a6c34b 100644
--- a/runtime/themes/bogster.toml
+++ b/runtime/themes/bogster.toml
@@ -8,7 +8,7 @@
"punctuation.delimiter" = "#dc7759"
"operator" = { fg = "#dc7759", modifiers = ["bold"] }
"special" = "#7fdc59"
-"property" = "#c6b8ad"
+"variable.other.member" = "#c6b8ad"
"variable" = "#c6b8ad"
"variable.parameter" = "#c6b8ad"
"type" = "#dc597f"
@@ -22,8 +22,8 @@
"constant" = "#59dcb7"
"constant.builtin" = "#59dcb7"
"string" = "#59dcb7"
-"number" = "#59c0dc"
-"escape" = { fg = "#7fdc59", modifiers = ["bold"] }
+"constant.numeric" = "#59c0dc"
+"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] }
"label" = "#59c0dc"
"module" = "#d32c5d"
diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml
index c48a7e28..0554f827 100644
--- a/runtime/themes/dark_plus.toml
+++ b/runtime/themes/dark_plus.toml
@@ -7,7 +7,7 @@
"type.builtin" = { fg = "type" }
"type.enum.variant" = { fg = "constant" }
"constructor" = { fg = "constant" }
-"property" = { fg = "variable" }
+"variable.other.member" = { fg = "variable" }
"keyword" = { fg = "keyword" }
"keyword.directive" = { fg = "keyword" }
@@ -31,47 +31,64 @@
"function.macro" = { fg = "keyword" }
"attribute" = { fg = "fn_declaration" }
-"comment" = { fg = "#6A9955" }
+"comment" = { fg = "dark_green" }
-"string" = { fg = "#ce9178" }
-"string.regexp" = { fg = "regex" }
-"number" = { fg = "#b5cea8" }
-"escape" = { fg = "#d7ba7d" }
+"string" = { fg = "orange" }
+"constant.character" = { fg = "orange" }
+"string.regexp" = { fg = "gold" }
+"constant.numeric" = { fg = "pale_green" }
+"constant.character.escape" = { fg = "gold" }
-"ui.background" = { fg = "#d4d4d4", bg = "#1e1e1e" }
+"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
"ui.window" = { bg = "widget" }
"ui.popup" = { bg = "widget" }
"ui.help" = { bg = "widget" }
"ui.menu.selected" = { bg = "widget" }
+# TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`.
"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
"ui.selection" = { bg = "#3a3d41" }
-"ui.selection.primary" = { bg = "#264f78" }
+"ui.selection.primary" = { bg = "dark_blue" }
-"ui.linenr" = { fg = "#858585" }
-"ui.linenr.selected" = { fg = "#c6c6c6" }
+"ui.linenr" = { fg = "dark_gray" }
+"ui.linenr.selected" = { fg = "light_gray2" }
-"ui.statusline" = { fg = "#ffffff", bg = "#007acc" }
-"ui.statusline.inactive" = { fg = "#ffffff", bg = "#007acc" }
+"ui.statusline" = { fg = "white", bg = "blue" }
+"ui.statusline.inactive" = { fg = "white", bg = "blue" }
"ui.text" = { fg = "text", bg = "background" }
-"ui.text.focus" = { fg = "#ffffff" }
+"ui.text.focus" = { fg = "white" }
-"warning" = { fg = "#cca700" }
-"error" = { fg = "#ff1212" }
-"info" = { fg = "#75beff" }
-"hint" = { fg = "#eeeeeeb3" }
+"warning" = { fg = "gold2" }
+"error" = { fg = "red" }
+"info" = { fg = "light_blue" }
+"hint" = { fg = "light_gray3" }
diagnostic = { modifiers = ["underlined"] }
[palette]
+white = "#ffffff"
+orange = "#ce9178"
+gold = "#d7ba7d"
+gold2 = "#cca700"
+pale_green = "#b5cea8"
+dark_green = "#6A9955"
+light_gray = "#d4d4d4"
+light_gray2 = "#c6c6c6"
+light_gray3 = "#eeeeee"
+dark_gray = "#858585"
+dark_gray2 = "#1e1e1e"
+blue = "#007acc"
+light_blue = "#75beff"
+dark_blue = "#264f78"
+red = "#ff1212"
+
type = "#4EC9B0"
keyword = "#569CD6"
-regex = "#CE9178"
special = "#C586C0"
variable = "#9CDCFE"
fn_declaration = "#DCDCAA"
diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml
index 462c8265..bbd005e6 100644
--- a/runtime/themes/everforest_dark.toml
+++ b/runtime/themes/everforest_dark.toml
@@ -8,16 +8,16 @@
# Email: sainnhe@gmail.com
# License: MIT License
-"escape" = "orange"
+"constant.character.escape" = "orange"
"type" = "yellow"
"constant" = "purple"
-"number" = "purple"
+"constant.numeric" = "purple"
"string" = "grey2"
"comment" = "grey0"
"variable" = "fg"
"variable.builtin" = "blue"
"variable.parameter" = "fg"
-"variable.property" = "fg"
+"variable.other.member" = "fg"
"label" = "aqua"
"punctuation" = "grey2"
"punctuation.delimiter" = "grey2"
@@ -32,7 +32,6 @@
"attribute" = "aqua"
"constructor" = "yellow"
"module" = "blue"
-"property" = "fg"
"special" = "orange"
"ui.background" = { bg = "bg0" }
diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml
index 0a6eec07..0ff039ea 100644
--- a/runtime/themes/gruvbox.toml
+++ b/runtime/themes/gruvbox.toml
@@ -9,8 +9,7 @@
"punctuation.delimiter" = "orange1"
"operator" = "purple1"
"special" = "purple0"
-"property" = "blue1"
-"variable.property" = "blue1"
+"variable.other.member" = "blue1"
"variable" = "fg1"
"variable.builtin" = "orange1"
"variable.parameter" = "fg2"
@@ -24,8 +23,8 @@
"constant" = { fg = "purple1" }
"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
"string" = "green1"
-"number" = "purple1"
-"escape" = { fg = "fg2", modifiers = ["bold"] }
+"constant.numeric" = "purple1"
+"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] }
"label" = "aqua1"
"module" = "aqua1"
diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml
index 6a177ec7..30829475 100644
--- a/runtime/themes/ingrid.toml
+++ b/runtime/themes/ingrid.toml
@@ -8,7 +8,7 @@
"punctuation.delimiter" = "#C97270"
"operator" = { fg = "#D74E50", modifiers = ["bold"] }
"special" = "#D68482"
-"property" = "#89BEB7"
+"variable.other.member" = "#89BEB7"
"variable" = "#A6B6CE"
"variable.parameter" = "#89BEB7"
"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
@@ -22,8 +22,8 @@
"constant" = "#D4A520"
"constant.builtin" = "#D4A520"
"string" = "#D74E50"
-"number" = "#D74E50"
-"escape" = { fg = "#D74E50", modifiers = ["bold"] }
+"constant.numeric" = "#D74E50"
+"constant.character.escape" = { fg = "#D74E50", modifiers = ["bold"] }
"label" = "#D68482"
"module" = "#839A53"
diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml
index a8f03ff3..38f9f170 100644
--- a/runtime/themes/monokai.toml
+++ b/runtime/themes/monokai.toml
@@ -7,7 +7,7 @@
"type.builtin" = { fg = "#66D9EF" }
"type.enum.variant" = { fg = "text" }
"constructor" = { fg = "text" }
-"property" = { fg = "variable" }
+"variable.other.member" = { fg = "variable" }
"keyword" = { fg = "keyword" }
"keyword.directive" = { fg = "keyword" }
@@ -34,9 +34,10 @@
"comment" = { fg = "#88846F" }
"string" = { fg = "#e6db74" }
+"constant.character" = { fg = "#e6db74" }
"string.regexp" = { fg = "regex" }
-"number" = { fg = "#ae81ff" }
-"escape" = { fg = "#ae81ff" }
+"constant.numeric" = { fg = "#ae81ff" }
+"constant.character.escape" = { fg = "#ae81ff" }
"ui.background" = { fg = "text", bg = "background" }
diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml
index ee7c8865..78736c3b 100644
--- a/runtime/themes/nord.toml
+++ b/runtime/themes/nord.toml
@@ -1,84 +1,102 @@
# Author : RayGervais<raygervais@hotmail.ca>
-# "ui.linenr.selected" = { fg = "#d8dee9" }
-"ui.text.focus" = { fg = "#88c0d0", modifiers= ["bold"] }
-# "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
+"ui.linenr.selected" = { fg = "nord4" }
+"ui.text.focus" = { fg = "nord8", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "nord8", bg = "nord2" }
-# "info" = "#b48ead"
-# "hint" = "#a3be8c"
+"info" = "nord8"
+"hint" = "nord8"
# Polar Night
# nord0 - background color
-"ui.background" = { bg = "#2e3440" }
-"ui.statusline.inactive" = { fg = "#d8dEE9", bg = "#2e3440" }
+"ui.background" = { bg = "nord0" }
+"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" }
# nord1 - status bars, panels, modals, autocompletion
-"ui.statusline" = { fg = "#88c0d0", bg = "#3b4252" }
+"ui.statusline" = { fg = "nord8", bg = "nord1" }
"ui.popup" = { bg = "#232d38" }
"ui.window" = { bg = "#232d38" }
-"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
+"ui.help" = { bg = "#232d38", fg = "nord4" }
# nord2 - active line, highlighting
-"ui.selection" = { bg = "#434c5e" }
-"ui.cursor.match" = { bg = "434c5e" }
+"ui.selection" = { bg = "nord2" }
+"ui.cursor.match" = { bg = "nord2" }
-# nord3 - comments
-"comment" = "#616E88"
-"ui.linenr" = { fg = "#616E88" }
+# nord3 - comments, nord3 based lighter color
+# relative: https://github.com/arcticicestudio/nord/issues/94
+"comment" = "gray"
+"ui.linenr" = { fg = "gray" }
# Snow Storm
# nord4 - cursor, variables, constants, attributes, fields
-"ui.cursor.primary" = { fg = "#d8dee9", modifiers = ["reversed"] }
-"attribute" = "#d8dee9"
-"variable" = "#d8dee9"
-"constant" = "#d8dee9"
-"variable.builtin" = "#d8dee9"
-"constant.builtin" = "#d8dee9"
-"namespace" = "#d8dee9"
+"ui.cursor.primary" = { fg = "nord4", modifiers = ["reversed"] }
+"attribute" = "nord4"
+"variable" = "nord4"
+"constant" = "nord4"
+"variable.builtin" = "nord4"
+"constant.builtin" = "nord4"
+"namespace" = "nord4"
# nord5 - suble UI text
# nord6 - base text, punctuation
-"ui.text" = { fg = "#eceff4" }
-"punctuation" = "#eceff4"
+"ui.text" = { fg = "nord6" }
+"punctuation" = "nord6"
# Frost
# nord7 - classes, types, primiatives
-"type" = "#8fbcbb"
-"type.builtin" = { fg = "#8fbcbb"}
-"label" = "#8fbcbb"
+"type" = "nord7"
+"type.builtin" = { fg = "nord7"}
+"label" = "nord7"
# nord8 - declaration, methods, routines
-"constructor" = "#88c0d0"
-"function" = "#88c0d0"
-"function.macro" = { fg = "#88c0d0" }
-"function.builtin" = { fg = "#88c0d0" }
+"constructor" = "nord8"
+"function" = "nord8"
+"function.macro" = { fg = "nord8" }
+"function.builtin" = { fg = "nord8" }
# nord9 - operator, tags, units, punctuations
-"punctuation.delimiter" = "#81a1c1"
-"operator" = { fg = "#81a1c1" }
-"property" = "#81a1c1"
+"punctuation.delimiter" = "nord9"
+"operator" = { fg = "nord9" }
+"variable.other.member" = "nord9"
# nord10 - keywords, special
-"keyword" = { fg = "#5e81ac" }
-"keyword.directive" = "#5e81ac"
-"variable.parameter" = "#5e81ac"
+"keyword" = { fg = "nord10" }
+"keyword.directive" = "nord10"
+"variable.parameter" = "nord10"
# Aurora
# nord11 - error
-"error" = "#bf616a"
+"error" = "nord11"
# nord12 - annotations, decorators
-"special" = "#d08770"
-"module" = "#d08770"
+"special" = "nord12"
+"module" = "nord12"
# nord13 - warnings, escape characters, regex
-"warning" = "#ebcb8b"
-"escape" = { fg = "#ebcb8b" }
+"warning" = "nord13"
+"constant.character.escape" = { fg = "nord13" }
# nord14 - strings
-"string" = "#a3be8c"
+"string" = "nord14"
# nord15 - integer, floating point
-"number" = "#b48ead"
+"constant.numeric" = "nord15"
+
+[palette]
+nord0 = "#2e3440"
+nord1 = "#3b4252"
+nord2 = "#434c5e"
+nord4 = "#d8dee9"
+nord6 = "#eceff4"
+nord7 = "#8fbcbb"
+nord8 = "#88c0d0"
+nord9 = "#81a1c1"
+nord10 = "#5e81ac"
+nord11 = "#bf616a"
+nord12 = "#d08770"
+nord13 = "#ebcb8b"
+nord14 = "#a3be8c"
+nord15 = "#b48ead"
+gray = "#616e88"
diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml
index 37057f58..40ed1abe 100644
--- a/runtime/themes/onedark.toml
+++ b/runtime/themes/onedark.toml
@@ -1,54 +1,71 @@
# Author : Gokul Soumya <gokulps15@gmail.com>
-"attribute" = { fg = "#E5C07B" }
-"comment" = { fg = "#5C6370", modifiers = ['italic'] }
-"constant" = { fg = "#56B6C2" }
-"constant.builtin" = { fg = "#61AFEF" }
-"constructor" = { fg = "#61AFEF" }
-"escape" = { fg = "#D19A66" }
-"function" = { fg = "#61AFEF" }
-"function.builtin" = { fg = "#61AFEF" }
-"function.macro" = { fg = "#C678DD" }
-"keyword" = { fg = "#E06C75" }
-"keyword.directive" = { fg = "#C678DD" }
-"label" = { fg = "#C678DD" }
-"namespace" = { fg = "#61AFEF" }
-"number" = { fg = "#D19A66" }
-"operator" = { fg = "#C678DD" }
-"property" = { fg = "#E06C75" }
-"special" = { fg = "#61AFEF" }
-"string" = { fg = "#98C379" }
-"type" = { fg = "#E5C07B" }
-"type.builtin" = { fg = "#E5C07B" }
-"variable" = { fg = "#61AFEF" }
-"variable.builtin" = { fg = "#61AFEF" }
-"variable.parameter" = { fg = "#E06C75" }
+"attribute" = { fg = "yellow" }
+"comment" = { fg = "light-gray", modifiers = ["italic"] }
+"constant" = { fg = "cyan" }
+"constant.builtin" = { fg = "blue" }
+"constructor" = { fg = "blue" }
+"escape" = { fg = "gold" }
+"function" = { fg = "blue" }
+"function.builtin" = { fg = "blue" }
+"function.macro" = { fg = "purple" }
+"keyword" = { fg = "red" }
+"keyword.control" = { fg = "purple" }
+"keyword.directive" = { fg = "purple" }
+"label" = { fg = "purple" }
+"namespace" = { fg = "blue" }
+"number" = { fg = "gold" }
+"operator" = { fg = "purple" }
+"property" = { fg = "red" }
+"special" = { fg = "blue" }
+"string" = { fg = "green" }
+"type" = { fg = "yellow" }
+"type.builtin" = { fg = "yellow" }
+# "variable" = { fg = "blue" }
+"variable.builtin" = { fg = "blue" }
+"variable.parameter" = { fg = "red" }
diagnostic = { modifiers = ["underlined"] }
-"info" = { fg = "#61afef", modifiers = ['bold'] }
-"hint" = { fg = "#98c379", modifiers = ['bold'] }
-"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
-"error" = { fg = "#e06c75", modifiers = ['bold'] }
+"info" = { fg = "blue", modifiers = ["bold"] }
+"hint" = { fg = "green", modifiers = ["bold"] }
+"warning" = { fg = "yellow", modifiers = ["bold"] }
+"error" = { fg = "red", modifiers = ["bold"] }
-"ui.background" = { bg = "#282C34" }
+"ui.background" = { bg = "black" }
-"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] }
-"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] }
-"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']}
+"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }
+"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]}
-"ui.selection" = { bg = "#5C6370" }
-"ui.selection.primary" = { bg = "#3E4452" }
+"ui.selection" = { bg = "light-gray" }
+"ui.selection.primary" = { bg = "gray" }
-"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
-"ui.linenr.selected" = { fg = "#ABB2BF" }
+"ui.linenr" = { fg = "linenr", modifiers = ["dim"] }
+"ui.linenr.selected" = { fg = "white" }
-"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
-"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" }
+"ui.statusline" = { fg = "white", bg = "light-black" }
+"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" }
-"ui.text" = { fg = "#ABB2BF" }
-"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
+"ui.text" = { fg = "white" }
+"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] }
-"ui.help" = { bg = "#3E4452" }
-"ui.popup" = { bg = "#3E4452" }
-"ui.window" = { bg = "#3E4452" }
-"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
+"ui.help" = { bg = "gray" }
+"ui.popup" = { bg = "gray" }
+"ui.window" = { bg = "gray" }
+"ui.menu.selected" = { fg = "black", bg = "blue" }
+
+[palette]
+
+yellow = "#E5C07B"
+blue = "#61AFEF"
+red = "#E06C75"
+purple = "#C678DD"
+green = "#98C379"
+gold = "#D19A66"
+cyan = "#56B6C2"
+white = "#ABB2BF"
+black = "#282C34"
+light-black = "#2C323C"
+gray = "#3E4452"
+light-gray = "#5C6370"
+linenr = "#4B5263"
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
new file mode 100644
index 00000000..53777008
--- /dev/null
+++ b/runtime/themes/rose_pine.toml
@@ -0,0 +1,61 @@
+# Author: RayGervais<raygervais@hotmail.ca>
+
+"ui.background" = { bg = "base" }
+"ui.menu" = "surface"
+"ui.menu.selected" = { fg = "iris", bg = "surface" }
+"ui.linenr" = {fg = "subtle" }
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "overlay" }
+"ui.liner.selected" = "highlightOverlay"
+"ui.selection" = "highlight"
+"comment" = "subtle"
+"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.help" = { fg = "foam", bg = "surface" }
+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
+"ui.text" = { fg = "text" }
+"operator" = "rose"
+"ui.text.focus" = { fg = "base05" }
+"variable" = "text"
+"constant.numeric" = "iris"
+"constant" = "gold"
+"attributes" = "gold"
+"type" = "foam"
+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
+"string" = "gold"
+"property" = "foam"
+"constant.character.escape" = "subtle"
+"function" = "rose"
+"function.builtin" = "rose"
+"function.method" = "foam"
+"constructor" = "gold"
+"special" = "gold"
+"keyword" = "pine"
+"label" = "iris"
+"namespace" = "pine"
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "base" }
+"ui.help" = { bg = "overlay", fg = "foam" }
+"text" = "text"
+
+"info" = "gold"
+"hint" = "gold"
+"debug" = "rose"
+"diagnostic" = "rose"
+"error" = "love"
+
+[palette]
+base = "#191724"
+surface = "#1f1d2e"
+overlay = "#26233a"
+inactive = "#555169"
+subtle = "#6e6a86"
+text = "#e0def4"
+love = "#eb6f92"
+gold = "#f6c177"
+rose = "#ebbcba"
+pine = "#31748f"
+foam = "#9ccfd8"
+iris = "#c4a7e7"
+highlight = "#2a2837"
+highlightInactive = "#211f2d"
+highlightOverlay = "#3a384a"
diff --git a/theme.toml b/theme.toml
index 82b71a7d..3956e25e 100644
--- a/theme.toml
+++ b/theme.toml
@@ -6,7 +6,7 @@ punctuation = "lavender"
"punctuation.delimiter" = "lavender"
operator = "lilac"
special = "honey"
-property = "white"
+variable.other.member = "white"
variable = "lavender"
# variable = "almond" # TODO: metavariables only
# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }