on master help sign in
Merge branch 'master' into debug
commitf2b709a3c3a9cc036bfea46734efd7e4100eb34b authorBlaž Hrastnik <blaz@mxxn.io> dateSun 7 Nov 2021, 00:28 (5 years ago) parentcde57daf979bdc
112 files changed+2911−1205
.github/workflows/build.yml+9−9 .gitmodules+5−1 CHANGELOG.md+87 Cargo.lock+51−40 README.md+3−3 TODO.md−6 book/src/SUMMARY.md+2 book/src/configuration.md+2 book/src/guides/README.md+4 book/src/guides/textobject.md+30 book/src/keymap.md+88−57 book/src/remapping.md+1−1 book/src/themes.md+7−4 book/src/usage.md+12−3 flake.lock+52−16 flake.nix+3−1 helix-core/Cargo.toml+4−3 helix-core/src/auto_pairs.rs+3 helix-core/src/chars.rs+2 helix-core/src/comment.rs+4−1 helix-core/src/diagnostic.rs+6−1 helix-core/src/graphemes.rs+3−1 helix-core/src/history.rs+65−40 helix-core/src/indent.rs+1 helix-core/src/lib.rs+2−1 helix-core/src/line_ending.rs+3−3 helix-core/src/movement.rs+4 helix-core/src/object.rs+7−2 helix-core/src/position.rs+73−8 helix-core/src/register.rs+2−2 helix-core/src/selection.rs+11 helix-core/src/syntax.rs+49−41 helix-core/src/textobject.rs+51 helix-core/src/transaction.rs+10 helix-dap/Cargo.toml+1−1 helix-lsp/Cargo.toml+6−6 helix-lsp/src/client.rs+4−3 helix-syntax/Cargo.toml+2−2 helix-syntax/languages/tree-sitter-cmake+1 helix-syntax/languages/tree-sitter-cpp+1−1 helix-syntax/languages/tree-sitter-elixir+1−1 helix-term/Cargo.toml+9−9 helix-term/src/application.rs+17−9 helix-term/src/args.rs+2 helix-term/src/commands.rs+433−197 helix-term/src/compositor.rs+1−1 helix-term/src/keymap.rs+150−78 helix-term/src/main.rs+7−1 helix-term/src/ui/completion.rs+61−73 helix-term/src/ui/editor.rs+94−40 helix-term/src/ui/menu.rs+25−18 helix-term/src/ui/mod.rs+20−3 helix-term/src/ui/picker.rs+120−37 helix-term/src/ui/prompt.rs+35−2 helix-tui/Cargo.toml+5−5 helix-tui/src/buffer.rs+56−22 helix-view/Cargo.toml+6−6 helix-view/src/clipboard.rs+6−16 helix-view/src/document.rs+54−27 helix-view/src/editor.rs+94−30 helix-view/src/info.rs+2−2 helix-view/src/input.rs+1−1 helix-view/src/keyboard.rs+1−1 helix-view/src/lib.rs+3−1 helix-view/src/macros.rs+3−2 helix-view/src/theme.rs−1 helix-view/src/tree.rs+182−9 helix-view/src/view.rs+5−3 languages.toml+10−1 runtime/queries/bash/highlights.scm+2−2 runtime/queries/c-sharp/highlights.scm+5−5 runtime/queries/c/highlights.scm+3−3 runtime/queries/cmake/highlights.scm+97 runtime/queries/cpp/highlights.scm+4−5 runtime/queries/css/highlights.scm+7−7 runtime/queries/elixir/highlights.scm+203−120 runtime/queries/go/highlights.scm+3−3 runtime/queries/go/textobjects.scm+21 runtime/queries/haskell/highlights.scm+3−3 runtime/queries/java/highlights.scm+6−5 runtime/queries/javascript/highlights.scm+2−2 runtime/queries/json/highlights.scm+15−4 runtime/queries/julia/highlights.scm+6−6 runtime/queries/ledger/highlights.scm+2−2 runtime/queries/lua/highlights.scm+3−3 runtime/queries/nix/highlights.scm+5−7 runtime/queries/ocaml/highlights.scm+4−4 runtime/queries/php/highlights.scm+6−6 runtime/queries/php/indents.toml+17 runtime/queries/protobuf/highlights.scm+4−6 runtime/queries/python/highlights.scm+4−7 runtime/queries/python/indents.toml+39 runtime/queries/python/textobjects.scm+14 runtime/queries/ruby/highlights.scm+3−3 runtime/queries/rust/highlights.scm+24−11 runtime/queries/rust/textobjects.scm+26 runtime/queries/svelte/highlights.scm+1−1 runtime/queries/toml/highlights.scm+4−4 runtime/queries/tsq/highlights.scm+2−2 runtime/queries/yaml/highlights.scm+5−5 runtime/queries/zig/highlights.scm+4−4 runtime/themes/base16_default_dark.toml+59 runtime/themes/bogster.toml+3−3 runtime/themes/dark_plus.toml+35−18 runtime/themes/everforest_dark.toml+3−4 runtime/themes/gruvbox.toml+3−4 runtime/themes/ingrid.toml+3−3 runtime/themes/monokai.toml+4−3 runtime/themes/nord.toml+61−43 runtime/themes/onedark.toml+60−43 runtime/themes/rose_pine.toml+61 theme.toml+1−1
.github/workflows/build.yml+9−9
@@ -28,19 +28,19 @@ jobs:
2828 uses: actions/cache@v2.1.6
2929 with:
3030 path: ~/.cargo/registry
31− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
31+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
3232
3333 - name: Cache cargo index
3434 uses: actions/cache@v2.1.6
3535 with:
3636 path: ~/.cargo/git
37− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
37+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
3838
3939 - name: Cache cargo target dir
4040 uses: actions/cache@v2.1.6
4141 with:
4242 path: target
43− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
43+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
4444
4545 - name: Run cargo check
4646 uses: actions-rs/cargo@v1
@@ -67,19 +67,19 @@ jobs:
6767 uses: actions/cache@v2.1.6
6868 with:
6969 path: ~/.cargo/registry
70− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
70+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
7171
7272 - name: Cache cargo index
7373 uses: actions/cache@v2.1.6
7474 with:
7575 path: ~/.cargo/git
76− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
76+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
7777
7878 - name: Cache cargo target dir
7979 uses: actions/cache@v2.1.6
8080 with:
8181 path: target
82− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
82+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
8383
8484 - name: Run cargo test
8585 uses: actions-rs/cargo@v1
@@ -112,19 +112,19 @@ jobs:
112112 uses: actions/cache@v2.1.6
113113 with:
114114 path: ~/.cargo/registry
115− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
115+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
116116
117117 - name: Cache cargo index
118118 uses: actions/cache@v2.1.6
119119 with:
120120 path: ~/.cargo/git
121− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
121+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
122122
123123 - name: Cache cargo target dir
124124 uses: actions/cache@v2.1.6
125125 with:
126126 path: target
127− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
127+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
128128
129129 - name: Run cargo fmt
130130 uses: actions-rs/cargo@v1
.gitmodules+5−1
@@ -84,7 +84,7 @@
8484 shallow = true
8585[submodule "helix-syntax/languages/tree-sitter-elixir"]
8686 path = helix-syntax/languages/tree-sitter-elixir
87− url = https://github.com/IceDragon200/tree-sitter-elixir
87+ url = https://github.com/elixir-lang/tree-sitter-elixir
8888 shallow = true
8989[submodule "helix-syntax/languages/tree-sitter-nix"]
9090 path = helix-syntax/languages/tree-sitter-nix
@@ -130,3 +130,7 @@
130130 path = helix-syntax/languages/tree-sitter-tsq
131131 url = https://github.com/tree-sitter/tree-sitter-tsq
132132 shallow = true
133+[submodule "helix-syntax/languages/tree-sitter-cmake"]
134+ path = helix-syntax/languages/tree-sitter-cmake
135+ url = https://github.com/uyha/tree-sitter-cmake
136+ shallow = true
CHANGELOG.md+87
@@ -1,4 +1,85 @@
11
2+# 0.5.0 (2021-10-28)
3+
4+A big shout out to all the contributors! We had 46 contributors in this release.
5+
6+Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)!
7+
8+The following is a quick rundown of the larger changes, there were many more
9+(check the git history for more details).
10+
11+Breaking changes:
12+
13+- A couple of keymaps moved to resolve a few conflicting keybinds.
14+ - Documentation popups were moved from `K` to `space+k`
15+ - `K` is now `keep_selections` which filters selections to only keeps ones matching the regex
16+ - `keep_primary_selection` moved from `space+space` to `,`
17+ - `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one
18+ - Opening files in a split moved from `C-h` to `C-s`
19+- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html)
20+
21+Features:
22+
23+- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript)
24+- Autocompletion! Completion now triggers automatically after a set idle timeout
25+- Completion documentation is now displayed next to the popup ([#691](https://github.com/helix-editor/helix/pull/691))
26+- Treesitter textobjects (select a function via `mf`, class via `mc`) ([#728](https://github.com/helix-editor/helix/pull/728))
27+- Global search across entire workspace `space+/` ([#651](https://github.com/helix-editor/helix/pull/651))
28+- Relative line number support ([#485](https://github.com/helix-editor/helix/pull/485))
29+- Prompts now store a history (72cf86e)
30+- `:vsplit` and `:hsplit` commands ([#639](https://github.com/helix-editor/helix/pull/639))
31+- `C-w h/j/k/l` can now be used to navigate between splits ([#860](https://github.com/helix-editor/helix/pull/860))
32+- `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))
33+- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) ([#547](https://github.com/helix-editor/helix/pull/547))
34+- Searching now defaults to smart case search (case insensitive unless uppercase is used) ([#761](https://github.com/helix-editor/helix/pull/761))
35+- The preview pane was improved to highlight and center line ranges
36+- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc)
37+- Show hidden files in completions ([#648](https://github.com/helix-editor/helix/pull/648))
38+- Grammar injections are now properly handled (dd0b15e)
39+- `v` in select mode now switches back to normal mode ([#660](https://github.com/helix-editor/helix/pull/660))
40+- View mode can now be triggered as a "sticky" mode ([#719](https://github.com/helix-editor/helix/pull/719))
41+- `f`/`t` and object selection motions can now be repeated via `Alt-.` ([#891](https://github.com/helix-editor/helix/pull/891))
42+- Statusline now displays total selection count and diagnostics counts for both errors and warnings ([#916](https://github.com/helix-editor/helix/pull/916))
43+
44+New grammars:
45+
46+- Ledger ([#572](https://github.com/helix-editor/helix/pull/572))
47+- Protobuf ([#614](https://github.com/helix-editor/helix/pull/614))
48+- Zig ([#631](https://github.com/helix-editor/helix/pull/631))
49+- YAML ([#667](https://github.com/helix-editor/helix/pull/667))
50+- Lua ([#665](https://github.com/helix-editor/helix/pull/665))
51+- OCaml ([#666](https://github.com/helix-editor/helix/pull/666))
52+- Svelte ([#733](https://github.com/helix-editor/helix/pull/733))
53+- Vue ([#787](https://github.com/helix-editor/helix/pull/787))
54+- Tree-sitter queries ([#845](https://github.com/helix-editor/helix/pull/845))
55+- CMake ([#888](https://github.com/helix-editor/helix/pull/888))
56+- Elixir (we switched over to the official grammar) (6c0786e)
57+- Language server definitions for Nix and Elixir ([#725](https://github.com/helix-editor/helix/pull/725))
58+- Python now uses `pylsp` instead of `pyls`
59+- Python now supports indentation
60+
61+New themes:
62+
63+- Monokai ([#628](https://github.com/helix-editor/helix/pull/628))
64+- Everforest Dark ([#760](https://github.com/helix-editor/helix/pull/760))
65+- Nord ([#799](https://github.com/helix-editor/helix/pull/799))
66+- Base16 Default Dark ([#833](https://github.com/helix-editor/helix/pull/833))
67+- Rose Pine ([#897](https://github.com/helix-editor/helix/pull/897))
68+
69+Fixes:
70+
71+- Fix crash on empty rust file ([#592](https://github.com/helix-editor/helix/pull/592))
72+- Exit select mode after toggle comment ([#598](https://github.com/helix-editor/helix/pull/598))
73+- Pin popups with no positioning to the initial position (12ea3888)
74+- xsel copy should not freeze the editor (6dd7dc4)
75+- `*` now only sets the search register and doesn't jump to the next occurrence (3426285)
76+- Goto line start/end commands extend when in select mode ([#739](https://github.com/helix-editor/helix/pull/739))
77+- Fix documentation popups sometimes not getting fully highlighted (066367c)
78+- Refactor apply_workspace_edit to remove assert (b02d872)
79+- Wrap around the top of the picker menu when scrolling (c7d6e44)
80+- Don't allow closing the last split if there's unsaved changes (3ff5b00)
81+- Indentation used different default on hx vs hx new_file.txt (c913bad)
82+
283# 0.4.1 (2021-08-14)
384
485A minor release that includes:
@@ -7,6 +88,8 @@ A minor release that includes:
788
889# 0.4.0 (2021-08-13)
990
91+A big shout out to all the contributors! We had 28 contributors in this release.
92+
1093Two months have passed, so this is another big release. A big thank you to all
1194the contributors and package maintainers!
1295
@@ -44,6 +127,8 @@ selections in the future as well as resolves many bugs and edge cases.
44127
45128# 0.3.0 (2021-06-27)
46129
130+A big shout out to all the contributors! We had 24 contributors in this release.
131+
47132Another big release.
48133
49134Highlights:
@@ -90,6 +175,8 @@ Includes a fix where wq/wqa could exit before file saving completed.
90175
91176# 0.2.0
92177
178+A big shout out to all the contributors! We had 18 contributors in this release.
179+
93180Enough has changed to bump the version. We're skipping 0.1.x because
94181previously the CLI would always report version as 0.1.0, and we'd like
95182to distinguish it in bug reports..
Cargo.lock+51−40
@@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
7878
7979[[package]]
8080name = "chardetng"
81−version = "0.1.14"
81+version = "0.1.15"
8282source = "registry+https://github.com/rust-lang/crates.io-index"
83−checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c"
83+checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
8484dependencies = [
8585 "cfg-if",
8686 "encoding_rs",
@@ -101,15 +101,24 @@ dependencies = [
101101
102102[[package]]
103103name = "clipboard-win"
104−version = "4.2.1"
104+version = "4.2.2"
105105source = "registry+https://github.com/rust-lang/crates.io-index"
106−checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
106+checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
107107dependencies = [
108108 "error-code",
109109 "str-buf",
110110 "winapi",
111111]
112112
113+[[package]]
114+name = "content_inspector"
115+version = "0.2.4"
116+source = "registry+https://github.com/rust-lang/crates.io-index"
117+checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
118+dependencies = [
119+ "memchr",
120+]
121+
113122[[package]]
114123name = "crossbeam-utils"
115124version = "0.8.5"
@@ -122,9 +131,9 @@ dependencies = [
122131
123132[[package]]
124133name = "crossterm"
125−version = "0.21.0"
134+version = "0.22.1"
126135source = "registry+https://github.com/rust-lang/crates.io-index"
127−checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
136+checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
128137dependencies = [
129138 "bitflags",
130139 "crossterm_winapi",
@@ -139,9 +148,9 @@ dependencies = [
139148
140149[[package]]
141150name = "crossterm_winapi"
142−version = "0.8.0"
151+version = "0.9.0"
143152source = "registry+https://github.com/rust-lang/crates.io-index"
144−checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
153+checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
145154dependencies = [
146155 "winapi",
147156]
@@ -175,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
175184
176185[[package]]
177186name = "encoding_rs"
178−version = "0.8.28"
187+version = "0.8.29"
179188source = "registry+https://github.com/rust-lang/crates.io-index"
180−checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
189+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
181190dependencies = [
182191 "cfg-if",
183192]
@@ -358,11 +367,12 @@ dependencies = [
358367
359368[[package]]
360369name = "helix-core"
361−version = "0.4.1"
370+version = "0.5.0"
362371dependencies = [
363372 "arc-swap",
364373 "etcetera",
365374 "helix-syntax",
375+ "log",
366376 "once_cell",
367377 "quickcheck",
368378 "regex",
@@ -395,7 +405,7 @@ dependencies = [
395405
396406[[package]]
397407name = "helix-lsp"
398−version = "0.4.1"
408+version = "0.5.0"
399409dependencies = [
400410 "anyhow",
401411 "futures-executor",
@@ -413,7 +423,7 @@ dependencies = [
413423
414424[[package]]
415425name = "helix-syntax"
416−version = "0.4.1"
426+version = "0.5.0"
417427dependencies = [
418428 "anyhow",
419429 "cc",
@@ -424,10 +434,11 @@ dependencies = [
424434
425435[[package]]
426436name = "helix-term"
427−version = "0.4.1"
437+version = "0.5.0"
428438dependencies = [
429439 "anyhow",
430440 "chrono",
441+ "content_inspector",
431442 "crossterm",
432443 "fern",
433444 "futures-util",
@@ -455,7 +466,7 @@ dependencies = [
455466
456467[[package]]
457468name = "helix-tui"
458−version = "0.4.1"
469+version = "0.5.0"
459470dependencies = [
460471 "bitflags",
461472 "cassowary",
@@ -468,7 +479,7 @@ dependencies = [
468479
469480[[package]]
470481name = "helix-view"
471−version = "0.4.1"
482+version = "0.5.0"
472483dependencies = [
473484 "anyhow",
474485 "bitflags",
@@ -532,9 +543,9 @@ dependencies = [
532543
533544[[package]]
534545name = "instant"
535−version = "0.1.11"
546+version = "0.1.12"
536547source = "registry+https://github.com/rust-lang/crates.io-index"
537−checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
548+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
538549dependencies = [
539550 "cfg-if",
540551]
@@ -566,9 +577,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
566577
567578[[package]]
568579name = "libc"
569−version = "0.2.103"
580+version = "0.2.104"
570581source = "registry+https://github.com/rust-lang/crates.io-index"
571−checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
582+checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
572583
573584[[package]]
574585name = "libloading"
@@ -600,9 +611,9 @@ dependencies = [
600611
601612[[package]]
602613name = "lsp-types"
603−version = "0.90.1"
614+version = "0.91.1"
604615source = "registry+https://github.com/rust-lang/crates.io-index"
605−checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3"
616+checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
606617dependencies = [
607618 "bitflags",
608619 "serde",
@@ -640,9 +651,9 @@ dependencies = [
640651
641652[[package]]
642653name = "mio"
643−version = "0.7.13"
654+version = "0.7.14"
644655source = "registry+https://github.com/rust-lang/crates.io-index"
645−checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
656+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
646657dependencies = [
647658 "libc",
648659 "log",
@@ -755,9 +766,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
755766
756767[[package]]
757768name = "proc-macro2"
758−version = "1.0.29"
769+version = "1.0.30"
759770source = "registry+https://github.com/rust-lang/crates.io-index"
760−checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
771+checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
761772dependencies = [
762773 "unicode-xid",
763774]
@@ -784,9 +795,9 @@ dependencies = [
784795
785796[[package]]
786797name = "quote"
787−version = "1.0.9"
798+version = "1.0.10"
788799source = "registry+https://github.com/rust-lang/crates.io-index"
789−checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
800+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
790801dependencies = [
791802 "proc-macro2",
792803]
@@ -973,9 +984,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
973984
974985[[package]]
975986name = "slab"
976−version = "0.4.4"
987+version = "0.4.5"
977988source = "registry+https://github.com/rust-lang/crates.io-index"
978−checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
989+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
979990
980991[[package]]
981992name = "slotmap"
@@ -1000,9 +1011,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
10001011
10011012[[package]]
10021013name = "syn"
1003−version = "1.0.78"
1014+version = "1.0.80"
10041015source = "registry+https://github.com/rust-lang/crates.io-index"
1005−checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
1016+checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
10061017dependencies = [
10071018 "proc-macro2",
10081019 "quote",
@@ -1075,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
10751086
10761087[[package]]
10771088name = "tokio"
1078−version = "1.12.0"
1089+version = "1.13.0"
10791090source = "registry+https://github.com/rust-lang/crates.io-index"
1080−checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
1091+checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
10811092dependencies = [
10821093 "autocfg",
10831094 "bytes",
@@ -1095,9 +1106,9 @@ dependencies = [
10951106
10961107[[package]]
10971108name = "tokio-macros"
1098−version = "1.4.1"
1109+version = "1.5.0"
10991110source = "registry+https://github.com/rust-lang/crates.io-index"
1100−checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb"
1111+checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
11011112dependencies = [
11021113 "proc-macro2",
11031114 "quote",
@@ -1106,9 +1117,9 @@ dependencies = [
11061117
11071118[[package]]
11081119name = "tokio-stream"
1109−version = "0.1.7"
1120+version = "0.1.8"
11101121source = "registry+https://github.com/rust-lang/crates.io-index"
1111−checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
1122+checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
11121123dependencies = [
11131124 "futures-core",
11141125 "pin-project-lite",
@@ -1145,9 +1156,9 @@ dependencies = [
11451156
11461157[[package]]
11471158name = "unicode-bidi"
1148−version = "0.3.6"
1159+version = "0.3.7"
11491160source = "registry+https://github.com/rust-lang/crates.io-index"
1150−checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
1161+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
11511162
11521163[[package]]
11531164name = "unicode-general-category"
README.md+3−3
@@ -27,11 +27,11 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
2727It's a terminal-based editor first, but I'd like to explore a custom renderer
2828(similar to emacs) in wgpu or skulpin.
2929
30−# Installation
31−
3230Note: Only certain languages have indentation definitions at the moment. Check
3331`runtime/queries/<lang>/` for `indents.toml`.
3432
33+# Installation
34+
3535We provide packaging for various distributions, but here's a quick method to
3636build from source.
3737
@@ -71,7 +71,7 @@ Some suggestions to get started:
7171
7272- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
7373- Help with packaging on various distributions needed!
74−- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
74+- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
7575 * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
7676 * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
7777- If your preferred language is missing, integrating a tree-sitter grammar for
TODO.md−6
@@ -6,10 +6,6 @@
66 - clojure
77 - erlang
88
9−as you type completion!
10−- [ ] use signature_help_provider and completion_provider trigger characters in
11− a hook to trigger signature help text / autocompletion
12−- [ ] document.on_type provider triggers
139- [ ] completion isIncomplete support
1410
15111
@@ -18,8 +14,6 @@ as you type completion!
1814
1915- [ ] = for auto indent line/selection
2016- [ ] :x for closing buffers
21−- [ ] repeat selection
22−
2317- [ ] lsp: signature help
2418
25192
book/src/SUMMARY.md+2
@@ -8,3 +8,5 @@
88 - [Keymap](./keymap.md)
99 - [Key Remapping](./remapping.md)
1010 - [Hooks](./hooks.md)
11+- [Guides](./guides/README.md)
12+ - [Adding Textobject Queries](./guides/textobject.md)
book/src/configuration.md+2
@@ -21,6 +21,8 @@ To override global configuration parameters, create a `config.toml` file located
2121| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
2222| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
2323| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
24+| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
25+| `auto-info` | Whether to display infoboxes | `true` |
2426
2527## LSP
2628
book/src/guides/README.mdnew file+4
@@ -0,0 +1,4 @@
1+# Guides
2+
3+This section contains guides for adding new language server configurations,
4+tree-sitter grammers, textobject queries, etc.
book/src/guides/textobject.mdnew file+30
@@ -0,0 +1,30 @@
1+# Adding Textobject Queries
2+
3+Textobjects that are language specific ([like functions, classes, etc][textobjects])
4+require an accompanying tree-sitter grammar and a `textobjects.scm` query file
5+to work properly. Tree-sitter allows us to query the source code syntax tree
6+and capture specific parts of it. The queries are written in a lisp dialect.
7+More information on how to write queries can be found in the [official tree-sitter
8+documentation](tree-sitter-queries).
9+
10+Query files should be placed in `runtime/queries/{language}/textobjects.scm`
11+when contributing. Note that to test the query files locally you should put
12+them under your local runtime directory (`~/.config/helix/runtime` on Linux
13+for example).
14+
15+The following [captures][tree-sitter-captures] are recognized:
16+
17+| Capture Name |
18+| --- |
19+| `function.inside` |
20+| `function.around` |
21+| `class.inside` |
22+| `class.around` |
23+| `parameter.inside` |
24+
25+[Example query files][textobject-examples] can be found in the helix GitHub repository.
26+
27+[textobjects]: ../usage.md#textobjects
28+[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
29+[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
30+[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=
book/src/keymap.md+88−57
@@ -6,38 +6,39 @@
66
77> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
88
9−| Key | Description | Command |
10−| ----- | ----------- | ------- |
11−| `h`, `Left` | Move left | `move_char_left` |
12−| `j`, `Down` | Move down | `move_char_right` |
13−| `k`, `Up` | Move up | `move_line_up` |
14−| `l`, `Right` | Move right | `move_line_down` |
15−| `w` | Move next word start | `move_next_word_start` |
16−| `b` | Move previous word start | `move_prev_word_start` |
17−| `e` | Move next word end | `move_next_word_end` |
18−| `W` | Move next WORD start | `move_next_long_word_start` |
19−| `B` | Move previous WORD start | `move_prev_long_word_start` |
20−| `E` | Move next WORD end | `move_next_long_word_end` |
21−| `t` | Find 'till next char | `find_till_char` |
22−| `f` | Find next char | `find_next_char` |
23−| `T` | Find 'till previous char | `till_prev_char` |
24−| `F` | Find previous char | `find_prev_char` |
25−| `Home` | Move to the start of the line | `goto_line_start` |
26−| `End` | Move to the end of the line | `goto_line_end` |
27−| `PageUp` | Move page up | `page_up` |
28−| `PageDown` | Move page down | `page_down` |
29−| `Ctrl-u` | Move half page up | `half_page_up` |
30−| `Ctrl-d` | Move half page down | `half_page_down` |
31−| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
32−| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33−| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34−| `g` | Enter [goto mode](#goto-mode) | N/A |
35−| `m` | Enter [match mode](#match-mode) | N/A |
36−| `:` | Enter command mode | `command_mode` |
37−| `z` | Enter [view mode](#view-mode) | N/A |
38−| `Z` | Enter sticky [view mode](#view-mode) | N/A |
39−| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
40−| `Space` | Enter [space mode](#space-mode) | N/A |
9+| Key | Description | Command |
10+| ----- | ----------- | ------- |
11+| `h`/`Left` | Move left | `move_char_left` |
12+| `j`/`Down` | Move down | `move_line_down` |
13+| `k`/`Up` | Move up | `move_line_up` |
14+| `l`/`Right` | Move right | `move_char_right` |
15+| `w` | Move next word start | `move_next_word_start` |
16+| `b` | Move previous word start | `move_prev_word_start` |
17+| `e` | Move next word end | `move_next_word_end` |
18+| `W` | Move next WORD start | `move_next_long_word_start` |
19+| `B` | Move previous WORD start | `move_prev_long_word_start` |
20+| `E` | Move next WORD end | `move_next_long_word_end` |
21+| `t` | Find 'till next char | `find_till_char` |
22+| `f` | Find next char | `find_next_char` |
23+| `T` | Find 'till previous char | `till_prev_char` |
24+| `F` | Find previous char | `find_prev_char` |
25+| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
26+| `Home` | Move to the start of the line | `goto_line_start` |
27+| `End` | Move to the end of the line | `goto_line_end` |
28+| `PageUp` | Move page up | `page_up` |
29+| `PageDown` | Move page down | `page_down` |
30+| `Ctrl-u` | Move half page up | `half_page_up` |
31+| `Ctrl-d` | Move half page down | `half_page_down` |
32+| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
33+| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
34+| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
35+| `g` | Enter [goto mode](#goto-mode) | N/A |
36+| `m` | Enter [match mode](#match-mode) | N/A |
37+| `:` | Enter command mode | `command_mode` |
38+| `z` | Enter [view mode](#view-mode) | N/A |
39+| `Z` | Enter sticky [view mode](#view-mode) | N/A |
40+| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
41+| `Space` | Enter [space mode](#space-mode) | N/A |
4142
4243### Changes
4344
@@ -54,6 +55,7 @@
5455| `A` | Insert at the end of the line | `append_to_line` |
5556| `o` | Open new line below selection | `open_below` |
5657| `O` | Open new line above selection | `open_above` |
58+| `.` | Repeat last change | N/A |
5759| `u` | Undo change | `undo` |
5860| `U` | Redo change | `redo` |
5961| `y` | Yank selection | `yank` |
@@ -86,8 +88,9 @@
8688| `;` | Collapse selection onto a single cursor | `collapse_selection` |
8789| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
8890| `,` | Keep only the primary selection | `keep_primary_selection` |
89−| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
90−| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
91+| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
92+| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
93+| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
9194| `(` | Rotate main selection backward | `rotate_selections_backward` |
9295| `)` | Rotate main selection forward | `rotate_selections_forward` |
9396| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
@@ -103,13 +106,13 @@
103106
104107### Search
105108
106−> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
107109
108110| Key | Description | Command |
109111| ----- | ----------- | ------- |
110112| `/` | Search for regex pattern | `search` |
113+| `?` | Search for previous pattern | `rsearch` |
111114| `n` | Select next search match | `search_next` |
112−| `N` | Add next search match to selection | `extend_search_next` |
115+| `N` | Select previous search match | `search_prev` |
113116| `*` | Use current selection as the search pattern | `search_selection` |
114117
115118### Minor modes
@@ -158,6 +161,8 @@ Jumps to various locations.
158161| `r` | Go to references | `goto_reference` |
159162| `i` | Go to implementation | `goto_implementation` |
160163| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
164+| `n` | Go to next buffer | `goto_next_buffer` |
165+| `p` | Go to previous buffer | `goto_previous_buffer` |
161166
162167#### Match mode
163168
@@ -180,12 +185,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
180185
181186This layer is similar to vim keybindings as kakoune does not support window.
182187
183−| Key | Description | Command |
184−| ----- | ------------- | ------- |
185−| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
186−| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
187−| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
188−| `q`, `Ctrl-q` | Close current window | `wclose` |
188+| Key | Description | Command |
189+| ----- | ------------- | ------- |
190+| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
191+| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
192+| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
193+| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
194+| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
195+| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
196+| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
197+| `q`, `Ctrl-q` | Close current window | `wclose` |
189198
190199#### Space mode
191200
@@ -213,12 +222,12 @@ This layer is a kludge of mappings, mostly pickers.
213222
214223Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
215224
216−| Key | Description | Command |
217−| ----- | ----------- | ------- |
218−| `[d` | Go to previous diagnostic | `goto_prev_diag` |
219−| `]d` | Go to next diagnostic | `goto_next_diag` |
220−| `[D` | Go to first diagnostic in document | `goto_first_diag` |
221−| `]D` | Go to last diagnostic in document | `goto_last_diag` |
225+| Key | Description | Command |
226+| ----- | ----------- | ------- |
227+| `[d` | Go to previous diagnostic | `goto_prev_diag` |
228+| `]d` | Go to next diagnostic | `goto_next_diag` |
229+| `[D` | Go to first diagnostic in document | `goto_first_diag` |
230+| `]D` | Go to last diagnostic in document | `goto_last_diag` |
222231| `[space` | Add newline above | `add_newline_above` |
223232| `]space` | Add newline below | `add_newline_below` |
224233
@@ -242,12 +251,34 @@ commands (including goto) to extend the existing selection instead of replacing
242251
243252Keys to use within picker. Remapping currently not supported.
244253
245−| Key | Description |
246−| ----- | ------------- |
247−| `Up`, `Ctrl-p` | Previous entry |
248−| `Down`, `Ctrl-n` | Next entry |
249−| `Ctrl-space` | Filter options |
250−| `Enter` | Open selected |
251−| `Ctrl-h` | Open horizontally |
252−| `Ctrl-v` | Open vertically |
253−| `Escape`, `Ctrl-c` | Close picker |
254+| Key | Description |
255+| ----- | ------------- |
256+| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
257+| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
258+| `Ctrl-space` | Filter options |
259+| `Enter` | Open selected |
260+| `Ctrl-s` | Open horizontally |
261+| `Ctrl-v` | Open vertically |
262+| `Escape`, `Ctrl-c` | Close picker |
263+
264+# Prompt
265+Keys to use within prompt, Remapping currently not supported.
266+| Key | Description |
267+| ----- | ------------- |
268+| `Escape`, `Ctrl-c` | Close prompt |
269+| `Alt-b`, `Alt-Left` | Backward a word |
270+| `Ctrl-b`, `Left` | Backward a char |
271+| `Alt-f`, `Alt-Right` | Forward a word |
272+| `Ctrl-f`, `Right` | Forward a char |
273+| `Ctrl-e`, `End` | move prompt end |
274+| `Ctrl-a`, `Home` | move prompt start |
275+| `Ctrl-w` | delete previous word |
276+| `Ctrl-k` | delete to end of line |
277+| `backspace` | delete previous char |
278+| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
279+| `Ctrl-p`, `Up` | select previous history |
280+| `Ctrl-n`, `Down` | select next history |
281+| `Tab` | slect next completion item |
282+| `BackTab` | slect previous completion item |
283+| `Enter` | Open selected |
284+
book/src/remapping.md+1−1
@@ -2,7 +2,7 @@
22
33One-way key remapping is temporarily supported via a simple TOML configuration
44file. (More powerful solutions such as rebinding via commands will be
5−available in the feature).
5+available in the future).
66
77To remap keys, write a `config.toml` file in your `helix` configuration
88directory (default `~/.config/helix` in Linux systems) with a structure like
book/src/themes.md+7−4
@@ -103,8 +103,6 @@ We use a similar set of scopes as
103103[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
104104[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
105105
106−- `escape` (TODO: rename to (constant).character.escape)
107−
108106- `type` - Types
109107 - `builtin` - Primitive types provided by the language (`int`, `usize`)
110108
@@ -112,13 +110,17 @@ We use a similar set of scopes as
112110 - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
113111 - `boolean`
114112 - `character`
113+ - `escape`
114+ - `numeric` (numbers)
115+ - `integer`
116+ - `float`
115117
116−- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
117118- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
118119 - `regexp` - Regular expressions
119120 - `special`
120121 - `path`
121122 - `url`
123+ - `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
122124
123125- `comment` - Code comments
124126 - `line` - Single line comments (`//`)
@@ -128,7 +130,8 @@ We use a similar set of scopes as
128130- `variable` - Variables
129131 - `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
130132 - `parameter` - Function parameters
131− - `property`
133+ - `other`
134+ - `member` - Fields of composite data types (e.g. structs, unions)
132135 - `function` (TODO: ?)
133136
134137- `label`
book/src/usage.md+12−3
@@ -2,6 +2,8 @@
22
33(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
44
5+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.
6+
57## Registers
68
79Vim-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.
4951
5052## Textobjects
5153
52−Currently supported: `word`, `surround`.
54+Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
5355
5456![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
57+![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
5558
5659- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
5760- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
@@ -60,5 +63,11 @@ Currently supported: `word`, `surround`.
6063| --- | --- |
6164| `w` | Word |
6265| `(`, `[`, `'`, etc | Specified surround pairs |
63−
64−Textobjects based on treesitter, like `function`, `class`, etc are planned.
66+| `f` | Function |
67+| `c` | Class |
68+| `p` | Parameter |
69+
70+Note: `f`, `c`, etc need a tree-sitter grammar active for the current
71+document and a special tree-sitter query file to work properly. [Only
72+some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=)
73+currently have the query file implemented. Contributions are welcome !
flake.lock+52−16
@@ -2,11 +2,11 @@
22 "nodes": {
33 "devshell": {
44 "locked": {
5− "lastModified": 1630239564,
6− "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
5+ "lastModified": 1632436039,
6+ "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
77 "owner": "numtide",
88 "repo": "devshell",
9− "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
9+ "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
1010 "type": "github"
1111 },
1212 "original": {
@@ -15,6 +15,21 @@
1515 "type": "github"
1616 }
1717 },
18+ "flake-utils": {
19+ "locked": {
20+ "lastModified": 1623875721,
21+ "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
22+ "owner": "numtide",
23+ "repo": "flake-utils",
24+ "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
25+ "type": "github"
26+ },
27+ "original": {
28+ "owner": "numtide",
29+ "repo": "flake-utils",
30+ "type": "github"
31+ }
32+ },
1833 "flakeCompat": {
1934 "flake": false,
2035 "locked": {
@@ -37,14 +52,16 @@
3752 "nixpkgs": [
3853 "nixpkgs"
3954 ],
40− "rustOverlay": "rustOverlay"
55+ "rustOverlay": [
56+ "rust-overlay"
57+ ]
4158 },
4259 "locked": {
43− "lastModified": 1631254163,
44− "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
60+ "lastModified": 1634796585,
61+ "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
4562 "owner": "yusdacra",
4663 "repo": "nix-cargo-integration",
47− "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
64+ "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
4865 "type": "github"
4966 },
5067 "original": {
@@ -55,11 +72,11 @@
5572 },
5673 "nixpkgs": {
5774 "locked": {
58− "lastModified": 1631206977,
59− "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
75+ "lastModified": 1634782485,
76+ "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
6077 "owner": "nixos",
6178 "repo": "nixpkgs",
62− "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
79+ "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
6380 "type": "github"
6481 },
6582 "original": {
@@ -69,21 +86,40 @@
6986 "type": "github"
7087 }
7188 },
89+ "nixpkgs_2": {
90+ "locked": {
91+ "lastModified": 1628186154,
92+ "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
93+ "owner": "NixOS",
94+ "repo": "nixpkgs",
95+ "rev": "06552b72346632b6943c8032e57e702ea12413bf",
96+ "type": "github"
97+ },
98+ "original": {
99+ "owner": "NixOS",
100+ "repo": "nixpkgs",
101+ "type": "github"
102+ }
103+ },
72104 "root": {
73105 "inputs": {
74106 "flakeCompat": "flakeCompat",
75107 "nixCargoIntegration": "nixCargoIntegration",
76− "nixpkgs": "nixpkgs"
108+ "nixpkgs": "nixpkgs",
109+ "rust-overlay": "rust-overlay"
77110 }
78111 },
79− "rustOverlay": {
80− "flake": false,
112+ "rust-overlay": {
113+ "inputs": {
114+ "flake-utils": "flake-utils",
115+ "nixpkgs": "nixpkgs_2"
116+ },
81117 "locked": {
82− "lastModified": 1631240108,
83− "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
118+ "lastModified": 1634869268,
119+ "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
84120 "owner": "oxalica",
85121 "repo": "rust-overlay",
86− "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
122+ "rev": "c02c2d86354327317546501af001886fbb53d374",
87123 "type": "github"
88124 },
89125 "original": {
flake.nix+3−1
@@ -3,9 +3,11 @@
33
44 inputs = {
55 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6+ rust-overlay.url = "github:oxalica/rust-overlay";
67 nixCargoIntegration = {
78 url = "github:yusdacra/nix-cargo-integration";
89 inputs.nixpkgs.follows = "nixpkgs";
10+ inputs.rustOverlay.follows = "rust-overlay";
911 };
1012 flakeCompat = {
1113 url = "github:edolstra/flake-compat";
@@ -61,7 +63,7 @@
6163 '';
6264 };
6365 shell = common: prev: {
64− packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
66+ packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
6567 env = prev.env ++ [
6668 { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
6769 { name = "RUST_BACKTRACE"; value = "1"; }
helix-core/Cargo.toml+4−3
@@ -1,8 +1,8 @@
11[package]
22name = "helix-core"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
66license = "MPL-2.0"
77description = "Helix editor core editing primitives"
88categories = ["editor"]
@@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
1313[features]
1414
1515[dependencies]
16−helix-syntax = { version = "0.4", path = "../helix-syntax" }
16+helix-syntax = { version = "0.5", path = "../helix-syntax" }
1717
1818ropey = "1.3"
1919smallvec = "1.7"
@@ -27,6 +27,7 @@ once_cell = "1.8"
2727arc-swap = "1"
2828regex = "1"
2929
30+log = "0.4"
3031serde = { version = "1.0", features = ["derive"] }
3132serde_json = "1.0"
3233toml = "0.5"
helix-core/src/auto_pairs.rs+3
@@ -1,3 +1,6 @@
1+//! When typing the opening character of one of the possible pairs defined below,
2+//! this module provides the functionality to insert the paired closing character.
3+
14use crate::{Range, Rope, Selection, Tendril, Transaction};
25use smallvec::SmallVec;
36
helix-core/src/chars.rs+2
@@ -1,3 +1,5 @@
1+//! Utility functions to categorize a `char`.
2+
13use crate::LineEnding;
24
35#[derive(Debug, Eq, PartialEq)]
helix-core/src/comment.rs+4−1
@@ -1,3 +1,6 @@
1+//! This module contains the functionality toggle comments on lines over the selection
2+//! using the comment character defined in the user's `languages.toml`
3+
14use crate::{
25 find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
36};
@@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
6063 let token = token.unwrap_or("//");
6164 let comment = Tendril::from(format!("{} ", token));
6265
63− let mut lines: Vec<usize> = Vec::new();
66+ let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
6467
6568 let mut min_next_line = 0;
6669 for selection in selection {
helix-core/src/diagnostic.rs+6−1
@@ -1,3 +1,6 @@
1+//! LSP diagnostic utility types.
2+
3+/// Describes the severity level of a [`Diagnostic`].
14#[derive(Debug, Eq, PartialEq)]
25pub enum Severity {
36 Error,
@@ -6,12 +9,14 @@ pub enum Severity {
69 Hint,
710}
811
9−#[derive(Debug)]
12+/// A range of `char`s within the text.
13+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
1014pub struct Range {
1115 pub start: usize,
1216 pub end: usize,
1317}
1418
19+/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
1520#[derive(Debug)]
1621pub struct Diagnostic {
1722 pub range: Range,
helix-core/src/graphemes.rs+3−1
@@ -1,4 +1,6 @@
1−// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
1+//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
2+//!
3+//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
24use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
35use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
46use unicode_width::UnicodeWidthStr;
helix-core/src/history.rs+65−40
@@ -4,48 +4,50 @@ use regex::Regex;
44use std::num::NonZeroUsize;
55use std::time::{Duration, Instant};
66
7−// Stores the history of changes to a buffer.
8−//
9−// Currently the history is represented as a vector of revisions. The vector
10−// always has at least one element: the empty root revision. Each revision
11−// with the exception of the root has a parent revision, a [Transaction]
12−// that can be applied to its parent to transition from the parent to itself,
13−// and an inversion of that transaction to transition from the parent to its
14−// latest child.
15−//
16−// When using `u` to undo a change, an inverse of the stored transaction will
17−// be applied which will transition the buffer to the parent state.
18−//
19−// Each revision with the exception of the last in the vector also has a
20−// last child revision. When using `U` to redo a change, the last child transaction
21−// will be applied to the current state of the buffer.
22−//
23−// The current revision is the one currently displayed in the buffer.
24−//
25−// Commiting a new revision to the history will update the last child of the
26−// current revision, and push a new revision to the end of the vector.
27−//
28−// Revisions are commited with a timestamp. :earlier and :later can be used
29−// to jump to the closest revision to a moment in time relative to the timestamp
30−// of the current revision plus (:later) or minus (:earlier) the duration
31−// given to the command. If a single integer is given, the editor will instead
32−// jump the given number of revisions in the vector.
33−//
34−// Limitations:
35−// * Changes in selections currently don't commit history changes. The selection
36−// will only be updated to the state after a commited buffer change.
37−// * The vector of history revisions is currently unbounded. This might
38−// cause the memory consumption to grow significantly large during long
39−// editing sessions.
40−// * Because delete transactions currently don't store the text that they
41−// delete, we also store an inversion of the transaction.
7+/// Stores the history of changes to a buffer.
8+///
9+/// Currently the history is represented as a vector of revisions. The vector
10+/// always has at least one element: the empty root revision. Each revision
11+/// with the exception of the root has a parent revision, a [Transaction]
12+/// that can be applied to its parent to transition from the parent to itself,
13+/// and an inversion of that transaction to transition from the parent to its
14+/// latest child.
15+///
16+/// When using `u` to undo a change, an inverse of the stored transaction will
17+/// be applied which will transition the buffer to the parent state.
18+///
19+/// Each revision with the exception of the last in the vector also has a
20+/// last child revision. When using `U` to redo a change, the last child transaction
21+/// will be applied to the current state of the buffer.
22+///
23+/// The current revision is the one currently displayed in the buffer.
24+///
25+/// Commiting a new revision to the history will update the last child of the
26+/// current revision, and push a new revision to the end of the vector.
27+///
28+/// Revisions are commited with a timestamp. :earlier and :later can be used
29+/// to jump to the closest revision to a moment in time relative to the timestamp
30+/// of the current revision plus (:later) or minus (:earlier) the duration
31+/// given to the command. If a single integer is given, the editor will instead
32+/// jump the given number of revisions in the vector.
33+///
34+/// Limitations:
35+/// * Changes in selections currently don't commit history changes. The selection
36+/// will only be updated to the state after a commited buffer change.
37+/// * The vector of history revisions is currently unbounded. This might
38+/// cause the memory consumption to grow significantly large during long
39+/// editing sessions.
40+/// * Because delete transactions currently don't store the text that they
41+/// delete, we also store an inversion of the transaction.
42+///
43+/// Using time to navigate the history: <https://github.com/helix-editor/helix/pull/194>
4244#[derive(Debug)]
4345pub struct History {
4446 revisions: Vec<Revision>,
4547 current: usize,
4648}
4749
48−// A single point in history. See [History] for more information.
50+/// A single point in history. See [History] for more information.
4951#[derive(Debug)]
5052struct Revision {
5153 parent: usize,
@@ -111,6 +113,7 @@ impl History {
111113 self.current == 0
112114 }
113115
116+ /// Undo the last edit.
114117 pub fn undo(&mut self) -> Option<&Transaction> {
115118 if self.at_root() {
116119 return None;
@@ -121,6 +124,7 @@ impl History {
121124 Some(&current_revision.inversion)
122125 }
123126
127+ /// Redo the last edit.
124128 pub fn redo(&mut self) -> Option<&Transaction> {
125129 let current_revision = &self.revisions[self.current];
126130 let last_child = current_revision.last_child?;
@@ -147,8 +151,8 @@ impl History {
147151 }
148152 }
149153
150− // List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
151− // Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
154+ /// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
155+ /// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
152156 fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
153157 let mut path = Vec::new();
154158 while n != a {
@@ -158,6 +162,7 @@ impl History {
158162 path
159163 }
160164
165+ /// Create a [`Transaction`] that will jump to a specific revision in the history.
161166 fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
162167 let lca = self.lowest_common_ancestor(self.current, to);
163168 let up = self.path_up(self.current, lca);
@@ -171,10 +176,12 @@ impl History {
171176 up_txns.chain(down_txns).collect()
172177 }
173178
179+ /// Creates a [`Transaction`] that will undo `delta` revisions.
174180 fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
175181 self.jump_to(self.current.saturating_sub(delta))
176182 }
177183
184+ /// Creates a [`Transaction`] that will redo `delta` revisions.
178185 fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
179186 self.jump_to(
180187 self.current
@@ -183,7 +190,7 @@ impl History {
183190 )
184191 }
185192
186− // Helper for a binary search case below.
193+ /// Helper for a binary search case below.
187194 fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
188195 let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
189196 let dur_i = self.revisions[i].timestamp.duration_since(instant);
@@ -194,6 +201,8 @@ impl History {
194201 }
195202 }
196203
204+ /// Creates a [`Transaction`] that will match a revision created at around
205+ /// `instant`.
197206 fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
198207 let search_result = self
199208 .revisions
@@ -209,6 +218,8 @@ impl History {
209218 self.jump_to(revision)
210219 }
211220
221+ /// Creates a [`Transaction`] that will match a revision created `duration` ago
222+ /// from the timestamp of current revision.
212223 fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
213224 match self.revisions[self.current].timestamp.checked_sub(duration) {
214225 Some(instant) => self.jump_instant(instant),
@@ -216,6 +227,8 @@ impl History {
216227 }
217228 }
218229
230+ /// Creates a [`Transaction`] that will match a revision created `duration` in
231+ /// the future from the timestamp of the current revision.
219232 fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
220233 match self.revisions[self.current].timestamp.checked_add(duration) {
221234 Some(instant) => self.jump_instant(instant),
@@ -223,6 +236,7 @@ impl History {
223236 }
224237 }
225238
239+ /// Creates an undo [`Transaction`].
226240 pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
227241 use UndoKind::*;
228242 match uk {
@@ -231,6 +245,7 @@ impl History {
231245 }
232246 }
233247
248+ /// Creates a redo [`Transaction`].
234249 pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
235250 use UndoKind::*;
236251 match uk {
@@ -240,13 +255,14 @@ impl History {
240255 }
241256}
242257
258+/// Whether to undo by a number of edits or a duration of time.
243259#[derive(Debug, PartialEq)]
244260pub enum UndoKind {
245261 Steps(usize),
246262 TimePeriod(std::time::Duration),
247263}
248264
249−// A subset of sytemd.time time span syntax units.
265+/// A subset of sytemd.time time span syntax units.
250266const TIME_UNITS: &[(&[&str], &str, u64)] = &[
251267 (&["seconds", "second", "sec", "s"], "seconds", 1),
252268 (&["minutes", "minute", "min", "m"], "minutes", 60),
@@ -254,11 +270,20 @@ const TIME_UNITS: &[(&[&str], &str, u64)] = &[
254270 (&["days", "day", "d"], "days", 24 * 60 * 60),
255271];
256272
273+/// Checks if the duration input can be turned into a valid duration. It must be a
274+/// positive integer and denote the [unit of time.](`TIME_UNITS`)
275+/// Examples of valid durations:
276+/// * `5 sec`
277+/// * `5 min`
278+/// * `5 hr`
279+/// * `5 days`
257280static DURATION_VALIDATION_REGEX: Lazy<Regex> =
258281 Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
259282
283+/// Captures both the number and unit as separate capture groups.
260284static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
261285
286+/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`].
262287fn parse_human_duration(s: &str) -> Result<Duration, String> {
263288 if !DURATION_VALIDATION_REGEX.is_match(s) {
264289 return Err("duration should be composed \
helix-core/src/indent.rs+1
@@ -464,6 +464,7 @@ where
464464 unit: String::from(" "),
465465 }),
466466 indent_query: OnceCell::new(),
467+ textobject_query: OnceCell::new(),
467468 debugger: None,
468469 }],
469470 });
helix-core/src/lib.rs+2−1
@@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
3535 line.chars().position(|ch| !ch.is_whitespace())
3636}
3737
38+/// Find `.git` root.
3839pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
3940 let current_dir = std::env::current_dir().expect("unable to determine current directory");
4041
@@ -193,7 +194,7 @@ pub use tendril::StrTendril as Tendril;
193194pub use {regex, tree_sitter};
194195
195196pub use graphemes::RopeGraphemes;
196−pub use position::{coords_at_pos, pos_at_coords, Position};
197+pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position};
197198pub use selection::{Range, Selection};
198199pub use smallvec::{smallvec, SmallVec};
199200pub use syntax::Syntax;
helix-core/src/line_ending.rs+3−3
@@ -20,7 +20,7 @@ pub enum LineEnding {
2020
2121impl LineEnding {
2222 #[inline]
23− pub fn len_chars(&self) -> usize {
23+ pub const fn len_chars(&self) -> usize {
2424 match self {
2525 Self::Crlf => 2,
2626 _ => 1,
@@ -28,7 +28,7 @@ impl LineEnding {
2828 }
2929
3030 #[inline]
31− pub fn as_str(&self) -> &'static str {
31+ pub const fn as_str(&self) -> &'static str {
3232 match self {
3333 Self::Crlf => "\u{000D}\u{000A}",
3434 Self::LF => "\u{000A}",
@@ -42,7 +42,7 @@ impl LineEnding {
4242 }
4343
4444 #[inline]
45− pub fn from_char(ch: char) -> Option<LineEnding> {
45+ pub const fn from_char(ch: char) -> Option<LineEnding> {
4646 match ch {
4747 '\u{000A}' => Some(LineEnding::LF),
4848 '\u{000B}' => Some(LineEnding::VT),
helix-core/src/movement.rs+4
@@ -53,6 +53,10 @@ pub fn move_vertically(
5353 let pos = range.cursor(slice);
5454
5555 // Compute the current position's 2d coordinates.
56+ // TODO: switch this to use `visual_coords_at_pos` rather than
57+ // `coords_at_pos` as this will cause a jerky movement when the visual
58+ // position does not match, like moving from a line with tabs/CJK to
59+ // a line without
5660 let Position { row, col } = coords_at_pos(slice, pos);
5761 let horiz = range.horiz.unwrap_or(col as u32);
5862
helix-core/src/object.rs+7−2
@@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
1313 let parent = match tree
1414 .root_node()
1515 .descendant_for_byte_range(from, to)
16− .and_then(|node| node.parent())
17− {
16+ .and_then(|node| {
17+ if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
18+ node.parent()
19+ } else {
20+ Some(node)
21+ }
22+ }) {
1823 Some(parent) => parent,
1924 None => return range,
2025 };
helix-core/src/position.rs+73−8
@@ -2,6 +2,7 @@ use crate::{
22 chars::char_is_line_ending,
33 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
44 line_ending::line_end_char_index,
5+ unicode::width::UnicodeWidthChar,
56 RopeSlice,
67};
78
@@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
5455}
5556/// Convert a character index to (line, column) coordinates.
5657///
57−/// TODO: this should be split into two methods: one for visual
58−/// row/column, and one for "objective" row/column (possibly with
59−/// the column specified in `char`s). The former would be used
60−/// for cursor movement, and the latter would be used for e.g. the
61−/// row:column display in the status line.
58+/// column in `char` count which can be used for row:column display in
59+/// status line. See [`visual_coords_at_pos`] for a visual one.
6260pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
6361 let line = text.char_to_line(pos);
6462
@@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
6967 Position::new(line, col)
7068}
7169
70+/// Convert a character index to (line, column) coordinates visually.
71+///
72+/// Takes \t, double-width characters (CJK) into account as well as text
73+/// not in the document in the future.
74+/// See [`coords_at_pos`] for an "objective" one.
75+pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
76+ let line = text.char_to_line(pos);
77+
78+ let line_start = text.line_to_char(line);
79+ let pos = ensure_grapheme_boundary_prev(text, pos);
80+ let col = text
81+ .slice(line_start..pos)
82+ .chars()
83+ .flat_map(|c| match c {
84+ '\t' => Some(tab_width),
85+ c => UnicodeWidthChar::width(c),
86+ })
87+ .sum();
88+
89+ Position::new(line, col)
90+}
91+
7292/// Convert (line, column) coordinates to a character index.
7393///
7494/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -130,7 +150,6 @@ mod test {
130150 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
131151
132152 // Test with wide characters.
133− // TODO: account for character width.
134153 let text = Rope::from("今日はいい\n");
135154 let slice = text.slice(..);
136155 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -151,7 +170,6 @@ mod test {
151170 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
152171
153172 // Test with wide-character grapheme clusters.
154− // TODO: account for character width.
155173 let text = Rope::from("किमपि\n");
156174 let slice = text.slice(..);
157175 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -161,7 +179,6 @@ mod test {
161179 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
162180
163181 // Test with tabs.
164− // Todo: account for tab stops.
165182 let text = Rope::from("\tHello\n");
166183 let slice = text.slice(..);
167184 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -169,6 +186,54 @@ mod test {
169186 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
170187 }
171188
189+ #[test]
190+ fn test_visual_coords_at_pos() {
191+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
192+ let slice = text.slice(..);
193+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
194+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n
195+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w
196+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o
197+ assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d
198+
199+ // Test with wide characters.
200+ let text = Rope::from("今日はいい\n");
201+ let slice = text.slice(..);
202+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
203+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into());
204+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into());
205+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into());
206+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into());
207+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into());
208+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
209+
210+ // Test with grapheme clusters.
211+ let text = Rope::from("a̐éö̲\r\n");
212+ let slice = text.slice(..);
213+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
214+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into());
215+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into());
216+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into());
217+ assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into());
218+
219+ // Test with wide-character grapheme clusters.
220+ // TODO: account for cluster.
221+ let text = Rope::from("किमपि\n");
222+ let slice = text.slice(..);
223+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
224+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into());
225+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into());
226+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into());
227+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
228+
229+ // Test with tabs.
230+ let text = Rope::from("\tHello\n");
231+ let slice = text.slice(..);
232+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
233+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into());
234+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
235+ }
236+
172237 #[test]
173238 fn test_pos_at_coords() {
174239 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
helix-core/src/register.rs+2−2
@@ -7,7 +7,7 @@ pub struct Register {
77}
88
99impl Register {
10− pub fn new(name: char) -> Self {
10+ pub const fn new(name: char) -> Self {
1111 Self {
1212 name,
1313 values: Vec::new(),
@@ -18,7 +18,7 @@ impl Register {
1818 Self { name, values }
1919 }
2020
21− pub fn name(&self) -> char {
21+ pub const fn name(&self) -> char {
2222 self.name
2323 }
2424
helix-core/src/selection.rs+11
@@ -362,6 +362,11 @@ impl Selection {
362362
363363 /// Adds a new range to the selection and makes it the primary range.
364364 pub fn remove(mut self, index: usize) -> Self {
365+ assert!(
366+ self.ranges.len() > 1,
367+ "can't remove the last range from a selection!"
368+ );
369+
365370 self.ranges.remove(index);
366371 if index < self.primary_index || self.primary_index == self.ranges.len() {
367372 self.primary_index -= 1;
@@ -369,6 +374,12 @@ impl Selection {
369374 self
370375 }
371376
377+ /// Replace a range in the selection with a new range.
378+ pub fn replace(mut self, index: usize, range: Range) -> Self {
379+ self.ranges[index] = range;
380+ self.normalize()
381+ }
382+
372383 /// Map selections over a set of changes. Useful for adjusting the selection position after
373384 /// applying changes to a document.
374385 pub fn map(self, changes: &ChangeSet) -> Self {
helix-core/src/syntax.rs+49−41
@@ -49,7 +49,7 @@ pub struct Configuration {
4949#[serde(rename_all = "kebab-case")]
5050pub struct LanguageConfiguration {
5151 #[serde(rename = "name")]
52− pub(crate) language_id: String,
52+ pub language_id: String,
5353 pub scope: String, // source.rust
5454 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
5555 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
7676
7777 #[serde(skip)]
7878 pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
79+ #[serde(skip)]
80+ pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
7981 #[serde(skip_serializing_if = "Option::is_none")]
8082 pub debugger: Option<DebugAdapterConfig>,
8183}
@@ -160,6 +162,32 @@ pub struct IndentQuery {
160162 pub outdent: HashSet<String>,
161163}
162164
165+#[derive(Debug)]
166+pub struct TextObjectQuery {
167+ pub query: Query,
168+}
169+
170+impl TextObjectQuery {
171+ /// Run the query on the given node and return sub nodes which match given
172+ /// capture ("function.inside", "class.around", etc).
173+ pub fn capture_nodes<'a>(
174+ &'a self,
175+ capture_name: &str,
176+ node: Node<'a>,
177+ slice: RopeSlice<'a>,
178+ cursor: &'a mut QueryCursor,
179+ ) -> Option<impl Iterator<Item = Node<'a>>> {
180+ let capture_idx = self.query.capture_index_for_name(capture_name)?;
181+ let captures = cursor.captures(&self.query, node, RopeProvider(slice));
182+
183+ captures
184+ .filter_map(move |(mat, idx)| {
185+ (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
186+ })
187+ .into()
188+ }
189+}
190+
163191fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
164192 let path = crate::RUNTIME_DIR
165193 .join("queries")
@@ -208,13 +236,14 @@ impl LanguageConfiguration {
208236 // highlights_query += "\n(ERROR) @error";
209237
210238 let injections_query = read_query(&language, "injections.scm");
211−
212239 let locals_query = read_query(&language, "locals.scm");
213240
214241 if highlights_query.is_empty() {
215242 None
216243 } else {
217− let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
244+ let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
245+ .map_err(|e| log::info!("{}", e))
246+ .ok()?;
218247 let config = HighlightConfiguration::new(
219248 language,
220249 &highlights_query,
@@ -258,6 +287,18 @@ impl LanguageConfiguration {
258287 .as_ref()
259288 }
260289
290+ pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
291+ self.textobject_query
292+ .get_or_init(|| -> Option<TextObjectQuery> {
293+ let lang_name = self.language_id.to_ascii_lowercase();
294+ let query_text = read_query(&lang_name, "textobjects.scm");
295+ let lang = self.highlight_config.get()?.as_ref()?.language;
296+ let query = Query::new(lang, &query_text).ok()?;
297+ Some(TextObjectQuery { query })
298+ })
299+ .as_ref()
300+ }
301+
261302 pub fn scope(&self) -> &str {
262303 &self.scope
263304 }
@@ -451,7 +492,7 @@ impl Syntax {
451492
452493 /// Iterate over the highlighted regions for a given slice of source code.
453494 pub fn highlight_iter<'a>(
454− &self,
495+ &'a self,
455496 source: RopeSlice<'a>,
456497 range: Option<std::ops::Range<usize>>,
457498 cancellation_flag: Option<&'a AtomicUsize>,
@@ -466,11 +507,10 @@ impl Syntax {
466507 let highlighter = &mut ts_parser.borrow_mut();
467508 highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
468509 });
469− let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
510+ let tree_ref = self.tree();
470511 let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
471− let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
472− let config_ref =
473− unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
512+ let query_ref = &self.config.query;
513+ let config_ref = self.config.as_ref();
474514
475515 // if reusing cursors & no range this resets to whole range
476516 cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@@ -582,39 +622,7 @@ impl LanguageLayer {
582622 self.tree.as_ref(),
583623 )
584624 .ok_or(Error::Cancelled)?;
585− // unsafe { syntax.parser.set_cancellation_flag(None) };
586− // let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new);
587−
588− // Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526)
589− // if let Some(combined_injections_query) = &config.combined_injections_query {
590− // let mut injections_by_pattern_index =
591− // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
592− // let matches =
593− // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
594− // for mat in matches {
595− // let entry = &mut injections_by_pattern_index[mat.pattern_index];
596− // let (language_name, content_node, include_children) =
597− // injection_for_match(config, combined_injections_query, &mat, source);
598− // if language_name.is_some() {
599− // entry.0 = language_name;
600− // }
601− // if let Some(content_node) = content_node {
602− // entry.1.push(content_node);
603− // }
604− // entry.2 = include_children;
605− // }
606− // for (lang_name, content_nodes, includes_children) in injections_by_pattern_index {
607− // if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
608− // if let Some(next_config) = (injection_callback)(lang_name) {
609− // let ranges =
610− // Self::intersect_ranges(&ranges, &content_nodes, includes_children);
611− // if !ranges.is_empty() {
612− // queue.push((next_config, depth + 1, ranges));
613− // }
614− // }
615− // }
616− // }
617− // }
625+
618626 self.tree = Some(tree)
619627 }
620628 Ok(())
helix-core/src/textobject.rs+51
@@ -1,9 +1,13 @@
1+use std::fmt::Display;
2+
13use ropey::RopeSlice;
4+use tree_sitter::{Node, QueryCursor};
25
36use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
47use crate::graphemes::next_grapheme_boundary;
58use crate::movement::Direction;
69use crate::surround;
10+use crate::syntax::LanguageConfiguration;
711use crate::Range;
812
913fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
@@ -51,6 +55,15 @@ pub enum TextObject {
5155 Inside,
5256}
5357
58+impl Display for TextObject {
59+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60+ f.write_str(match self {
61+ Self::Around => "around",
62+ Self::Inside => "inside",
63+ })
64+ }
65+}
66+
5467// count doesn't do anything yet
5568pub fn textobject_word(
5669 slice: RopeSlice,
@@ -108,6 +121,44 @@ pub fn textobject_surround(
108121 .unwrap_or(range)
109122}
110123
124+/// Transform the given range to select text objects based on tree-sitter.
125+/// `object_name` is a query capture base name like "function", "class", etc.
126+/// `slice_tree` is the tree-sitter node corresponding to given text slice.
127+pub fn textobject_treesitter(
128+ slice: RopeSlice,
129+ range: Range,
130+ textobject: TextObject,
131+ object_name: &str,
132+ slice_tree: Node,
133+ lang_config: &LanguageConfiguration,
134+ _count: usize,
135+) -> Range {
136+ let get_range = move || -> Option<Range> {
137+ let byte_pos = slice.char_to_byte(range.cursor(slice));
138+
139+ let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
140+ let mut cursor = QueryCursor::new();
141+ let node = lang_config
142+ .textobject_query()?
143+ .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
144+ .filter(|node| node.byte_range().contains(&byte_pos))
145+ .min_by_key(|node| node.byte_range().len())?;
146+
147+ let len = slice.len_bytes();
148+ let start_byte = node.start_byte();
149+ let end_byte = node.end_byte();
150+ if start_byte >= len || end_byte >= len {
151+ return None;
152+ }
153+
154+ let start_char = slice.byte_to_char(start_byte);
155+ let end_char = slice.byte_to_char(end_byte);
156+
157+ Some(Range::new(start_char, end_char))
158+ };
159+ get_range().unwrap_or(range)
160+}
161+
111162#[cfg(test)]
112163mod test {
113164 use super::TextObject::*;
helix-core/src/transaction.rs+10
@@ -132,6 +132,9 @@ impl ChangeSet {
132132 if self.changes.is_empty() {
133133 return other;
134134 }
135+ if other.changes.is_empty() {
136+ return self;
137+ }
135138
136139 let len = self.changes.len();
137140
@@ -465,6 +468,13 @@ impl Transaction {
465468 }
466469 }
467470
471+ pub fn compose(mut self, other: Self) -> Self {
472+ self.changes = self.changes.compose(other.changes);
473+ // Other selection takes precedence
474+ self.selection = other.selection;
475+ self
476+ }
477+
468478 pub fn with_selection(mut self, selection: Selection) -> Self {
469479 self.selection = Some(selection);
470480 self
helix-dap/Cargo.toml+1−1
@@ -1,6 +1,6 @@
11[package]
22name = "helix-dap"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
55edition = "2018"
66license = "MPL-2.0"
helix-lsp/Cargo.toml+6−6
@@ -1,8 +1,8 @@
11[package]
22name = "helix-lsp"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
66license = "MPL-2.0"
77description = "LSP client implementation for Helix project"
88categories = ["editor"]
@@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
1212# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313
1414[dependencies]
15−helix-core = { version = "0.4", path = "../helix-core" }
15+helix-core = { version = "0.5", path = "../helix-core" }
1616
1717anyhow = "1.0"
1818futures-executor = "0.3"
1919futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
2020jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
2121log = "0.4"
22−lsp-types = { version = "0.90", features = ["proposed"] }
22+lsp-types = { version = "0.91", features = ["proposed"] }
2323serde = { version = "1.0", features = ["derive"] }
2424serde_json = "1.0"
2525thiserror = "1.0"
26−tokio = { version = "1.12", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27−tokio-stream = "0.1.7"
26+tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27+tokio-stream = "0.1.8"
helix-lsp/src/client.rs+4−3
@@ -461,7 +461,7 @@ impl Client {
461461 };
462462
463463 let changes = match sync_capabilities {
464− lsp::TextDocumentSyncKind::Full => {
464+ lsp::TextDocumentSyncKind::FULL => {
465465 vec![lsp::TextDocumentContentChangeEvent {
466466 // range = None -> whole document
467467 range: None, //Some(Range)
@@ -469,10 +469,11 @@ impl Client {
469469 text: new_text.to_string(),
470470 }]
471471 }
472− lsp::TextDocumentSyncKind::Incremental => {
472+ lsp::TextDocumentSyncKind::INCREMENTAL => {
473473 Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding)
474474 }
475− lsp::TextDocumentSyncKind::None => return None,
475+ lsp::TextDocumentSyncKind::NONE => return None,
476+ kind => unimplemented!("{:?}", kind),
476477 };
477478
478479 Some(self.notify::<lsp::notification::DidChangeTextDocument>(
helix-syntax/Cargo.toml+2−2
@@ -1,8 +1,8 @@
11[package]
22name = "helix-syntax"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
66license = "MPL-2.0"
77description = "Tree-sitter grammars support"
88categories = ["editor"]
helix-syntax/languages/tree-sitter-cmakenew file+1

Submodule changed.

helix-syntax/languages/tree-sitter-cpp+1−1

Submodule changed.

helix-syntax/languages/tree-sitter-elixir+1−1

Submodule changed.

helix-term/Cargo.toml+9−9
@@ -1,9 +1,9 @@
11[package]
22name = "helix-term"
3−version = "0.4.1"
3+version = "0.5.0"
44description = "A post-modern text editor."
55authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
6−edition = "2018"
6+edition = "2021"
77license = "MPL-2.0"
88categories = ["editor", "command-line-utilities"]
99repository = "https://github.com/helix-editor/helix"
@@ -21,10 +21,10 @@ name = "hx"
2121path = "src/main.rs"
2222
2323[dependencies]
24−helix-core = { version = "0.4", path = "../helix-core" }
25−helix-view = { version = "0.4", path = "../helix-view" }
26−helix-lsp = { version = "0.4", path = "../helix-lsp" }
27−helix-dap = { version = "0.4", path = "../helix-dap" }
24+helix-core = { version = "0.5", path = "../helix-core" }
25+helix-view = { version = "0.5", path = "../helix-view" }
26+helix-lsp = { version = "0.5", path = "../helix-lsp" }
27+helix-dap = { version = "0.5", path = "../helix-dap" }
2828
2929anyhow = "1"
3030once_cell = "1.8"
@@ -32,7 +32,7 @@ once_cell = "1.8"
3232tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
3333num_cpus = "1"
3434tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
35−crossterm = { version = "0.21", features = ["event-stream"] }
35+crossterm = { version = "0.22", features = ["event-stream"] }
3636signal-hook = "0.3"
3737tokio-stream = "0.1"
3838futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -45,10 +45,10 @@ log = "0.4"
4545# File picker
4646fuzzy-matcher = "0.3"
4747ignore = "0.4"
48−# shellexpand = "2.1"
49−# dirs-next = "2.0"
5048# markdown doc rendering
5149pulldown-cmark = { version = "0.8", default-features = false }
50+# file type detection
51+content_inspector = "0.2.4"
5252
5353# config
5454toml = "0.5"
helix-term/src/application.rs+17−9
@@ -99,12 +99,17 @@ impl Application {
9999 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
100100 compositor.push(editor_view);
101101
102− if !args.files.is_empty() {
102+ if args.load_tutor {
103+ let path = helix_core::runtime_dir().join("tutor.txt");
104+ editor.open(path, Action::VerticalSplit)?;
105+ // Unset path to prevent accidentally saving to the original tutor file.
106+ doc_mut!(editor).set_path(None)?;
107+ } else if !args.files.is_empty() {
103108 let first = &args.files[0]; // we know it's not empty
104109 if first.is_dir() {
105110 std::env::set_current_dir(&first)?;
106111 editor.new_file(Action::VerticalSplit);
107− compositor.push(Box::new(ui::file_picker(first.clone())));
112+ compositor.push(Box::new(ui::file_picker(".".into())));
108113 } else {
109114 let nr_of_files = args.files.len();
110115 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -240,7 +245,7 @@ impl Application {
240245 }
241246
242247 pub fn handle_idle_timeout(&mut self) {
243− use crate::commands::{completion, Context};
248+ use crate::commands::{insert::idle_completion, Context};
244249 use helix_view::document::Mode;
245250
246251 if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
@@ -267,7 +272,7 @@ impl Application {
267272 callback: None,
268273 on_next_key_callback: None,
269274 };
270− completion(&mut cx);
275+ idle_completion(&mut cx);
271276 self.render();
272277 }
273278
@@ -548,10 +553,11 @@ impl Application {
548553 message: diagnostic.message,
549554 severity: diagnostic.severity.map(
550555 |severity| match severity {
551− DiagnosticSeverity::Error => Error,
552− DiagnosticSeverity::Warning => Warning,
553− DiagnosticSeverity::Information => Info,
554− DiagnosticSeverity::Hint => Hint,
556+ DiagnosticSeverity::ERROR => Error,
557+ DiagnosticSeverity::WARNING => Warning,
558+ DiagnosticSeverity::INFORMATION => Info,
559+ DiagnosticSeverity::HINT => Hint,
560+ severity => unimplemented!("{:?}", severity),
555561 },
556562 ),
557563 // code
@@ -727,7 +733,9 @@ impl Application {
727733 let mut stdout = stdout();
728734 // reset cursor shape
729735 write!(stdout, "\x1B[2 q")?;
730− execute!(stdout, DisableMouseCapture)?;
736+ // Ignore errors on disabling, this might trigger on windows if we call
737+ // disable without calling enable previously
738+ let _ = execute!(stdout, DisableMouseCapture);
731739 execute!(stdout, terminal::LeaveAlternateScreen)?;
732740 terminal::disable_raw_mode()?;
733741 Ok(())
helix-term/src/args.rs+2
@@ -5,6 +5,7 @@ use std::path::PathBuf;
55pub struct Args {
66 pub display_help: bool,
77 pub display_version: bool,
8+ pub load_tutor: bool,
89 pub verbosity: u64,
910 pub files: Vec<PathBuf>,
1011}
@@ -22,6 +23,7 @@ impl Args {
2223 "--" => break, // stop parsing at this point treat the remaining as files
2324 "--version" => args.display_version = true,
2425 "--help" => args.display_help = true,
26+ "--tutor" => args.load_tutor = true,
2527 arg if arg.starts_with("--") => {
2628 return Err(Error::msg(format!(
2729 "unexpected double dash argument: {}",
helix-term/src/commands.rs+433−197
@@ -16,8 +16,13 @@ use helix_core::{
1616};
1717
1818use helix_view::{
19− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
20− view::View, Document, DocumentId, Editor, ViewId,
19+ clipboard::ClipboardType,
20+ document::Mode,
21+ editor::{Action, Motion},
22+ input::KeyEvent,
23+ keyboard::KeyCode,
24+ view::View,
25+ Document, DocumentId, Editor, ViewId,
2126};
2227
2328use anyhow::{anyhow, bail, Context as _};
@@ -202,6 +207,7 @@ impl Command {
202207 find_prev_char, "Move to previous occurance of char",
203208 extend_till_prev_char, "Extend till previous occurance of char",
204209 extend_prev_char, "Extend to previous occurance of char",
210+ repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)",
205211 replace, "Replace with new char",
206212 switch_case, "Switch (toggle) case",
207213 switch_to_uppercase, "Switch to uppercase",
@@ -215,8 +221,11 @@ impl Command {
215221 split_selection, "Split selection into subselections on regex matches",
216222 split_selection_on_newline, "Split selection on newlines",
217223 search, "Search for regex pattern",
224+ rsearch, "Reverse search for regex pattern",
218225 search_next, "Select next search match",
226+ search_prev, "Select previous search match",
219227 extend_search_next, "Add next search match to selection",
228+ extend_search_prev, "Add previous search match to selection",
220229 search_selection, "Use current selection as search pattern",
221230 global_search, "Global Search in workspace folder",
222231 extend_line, "Select current line, if already selected, extend to next line",
@@ -260,6 +269,8 @@ impl Command {
260269 goto_prev_diag, "Goto previous diagnostic",
261270 goto_line_start, "Goto line start",
262271 goto_line_end, "Goto line end",
272+ goto_next_buffer, "Goto next buffer",
273+ goto_previous_buffer, "Goto previous buffer",
263274 // TODO: different description ?
264275 goto_line_end_newline, "Goto line end",
265276 goto_first_nonwhitespace, "Goto first non-blank in line",
@@ -305,6 +316,10 @@ impl Command {
305316 expand_selection, "Expand selection to parent syntax node",
306317 jump_forward, "Jump forward on jumplist",
307318 jump_backward, "Jump backward on jumplist",
319+ jump_view_right, "Jump to the split to the right",
320+ jump_view_left, "Jump to the split to the left",
321+ jump_view_up, "Jump to the split above",
322+ jump_view_down, "Jump to the split below",
308323 rotate_view, "Goto next window",
309324 hsplit, "Horizontal bottom split",
310325 vsplit, "Vertical right split",
@@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) {
528543 )
529544}
530545
546+fn goto_next_buffer(cx: &mut Context) {
547+ goto_buffer(cx, Direction::Forward);
548+}
549+
550+fn goto_previous_buffer(cx: &mut Context) {
551+ goto_buffer(cx, Direction::Backward);
552+}
553+
554+fn goto_buffer(cx: &mut Context, direction: Direction) {
555+ let current = view!(cx.editor).doc;
556+
557+ let id = match direction {
558+ Direction::Forward => {
559+ let iter = cx.editor.documents.keys();
560+ let mut iter = iter.skip_while(|id| *id != &current);
561+ iter.next(); // skip current item
562+ iter.next().or_else(|| cx.editor.documents.keys().next())
563+ }
564+ Direction::Backward => {
565+ let iter = cx.editor.documents.keys();
566+ let mut iter = iter.rev().skip_while(|id| *id != &current);
567+ iter.next(); // skip current item
568+ iter.next()
569+ .or_else(|| cx.editor.documents.keys().rev().next())
570+ }
571+ }
572+ .unwrap();
573+
574+ let id = *id;
575+
576+ cx.editor.switch(id, Action::Replace);
577+}
578+
531579fn extend_to_line_start(cx: &mut Context) {
532580 let (view, doc) = current!(cx.editor);
533581 goto_line_start_impl(view, doc, Movement::Extend)
@@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) {
631679 } else {
632680 push_jump(cx.editor);
633681 let (view, doc) = current!(cx.editor);
634− doc.set_selection(view.id, Selection::point(0));
682+ let text = doc.text().slice(..);
683+ let selection = doc
684+ .selection(view.id)
685+ .clone()
686+ .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select));
687+ doc.set_selection(view.id, selection);
635688 }
636689}
637690
638691fn goto_file_end(cx: &mut Context) {
639692 push_jump(cx.editor);
640693 let (view, doc) = current!(cx.editor);
641− doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
694+ let text = doc.text().slice(..);
695+ let pos = doc.text().len_chars();
696+ let selection = doc
697+ .selection(view.id)
698+ .clone()
699+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
700+ doc.set_selection(view.id, selection);
642701}
643702
644703fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
@@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
681740 extend_word_impl(cx, movement::move_next_long_word_end)
682741}
683742
684−#[inline]
685−fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
743+fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
686744where
687745 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
688746{
@@ -704,13 +762,7 @@ where
704762 // usually mix line endings. But we should fix it eventually
705763 // anyway.
706764 {
707− current!(cx.editor)
708− .1
709− .line_ending
710− .as_str()
711− .chars()
712− .next()
713− .unwrap()
765+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
714766 }
715767
716768 KeyEvent {
@@ -720,29 +772,48 @@ where
720772 _ => return,
721773 };
722774
723− let (view, doc) = current!(cx.editor);
724− let text = doc.text().slice(..);
775+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
776+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
777+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
778+ })));
779+ })
780+}
725781
726− let selection = doc.selection(view.id).clone().transform(|range| {
727− // TODO: use `Range::cursor()` here instead. However, that works in terms of
728− // graphemes, whereas this function doesn't yet. So we're doing the same logic
729− // here, but just in terms of chars instead.
730− let search_start_pos = if range.anchor < range.head {
731− range.head - 1
732− } else {
733− range.head
734− };
782+//
735783
736− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
737− if extend {
738− range.put_cursor(text, pos, true)
739− } else {
740− Range::point(range.cursor(text)).put_cursor(text, pos, true)
741− }
742− })
743− });
744− doc.set_selection(view.id, selection);
745− })
784+#[inline]
785+fn find_char_impl<F>(
786+ editor: &mut Editor,
787+ search_fn: &F,
788+ inclusive: bool,
789+ extend: bool,
790+ ch: char,
791+ count: usize,
792+) where
793+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
794+{
795+ let (view, doc) = current!(editor);
796+ let text = doc.text().slice(..);
797+
798+ let selection = doc.selection(view.id).clone().transform(|range| {
799+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
800+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
801+ // here, but just in terms of chars instead.
802+ let search_start_pos = if range.anchor < range.head {
803+ range.head - 1
804+ } else {
805+ range.head
806+ };
807+
808+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
809+ if extend {
810+ range.put_cursor(text, pos, true)
811+ } else {
812+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
813+ }
814+ })
815+ });
816+ doc.set_selection(view.id, selection);
746817}
747818
748819fn find_next_char_impl(
@@ -756,6 +827,10 @@ fn find_next_char_impl(
756827 if inclusive {
757828 search::find_nth_next(text, ch, pos, n)
758829 } else {
830+ let n = match text.get_char(pos) {
831+ Some(next_ch) if next_ch == ch => n + 1,
832+ _ => n,
833+ };
759834 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
760835 }
761836}
@@ -770,80 +845,52 @@ fn find_prev_char_impl(
770845 if inclusive {
771846 search::find_nth_prev(text, ch, pos, n)
772847 } else {
848+ let n = match text.get_char(pos.saturating_sub(1)) {
849+ Some(next_ch) if next_ch == ch => n + 1,
850+ _ => n,
851+ };
773852 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
774853 }
775854}
776855
777856fn find_till_char(cx: &mut Context) {
778− find_char_impl(
779− cx,
780− find_next_char_impl,
781− false, /* inclusive */
782− false, /* extend */
783− )
857+ will_find_char(cx, find_next_char_impl, false, false)
784858}
785859
786860fn find_next_char(cx: &mut Context) {
787− find_char_impl(
788− cx,
789− find_next_char_impl,
790− true, /* inclusive */
791− false, /* extend */
792− )
861+ will_find_char(cx, find_next_char_impl, true, false)
793862}
794863
795864fn extend_till_char(cx: &mut Context) {
796− find_char_impl(
797− cx,
798− find_next_char_impl,
799− false, /* inclusive */
800− true, /* extend */
801− )
865+ will_find_char(cx, find_next_char_impl, false, true)
802866}
803867
804868fn extend_next_char(cx: &mut Context) {
805− find_char_impl(
806− cx,
807− find_next_char_impl,
808− true, /* inclusive */
809− true, /* extend */
810− )
869+ will_find_char(cx, find_next_char_impl, true, true)
811870}
812871
813872fn till_prev_char(cx: &mut Context) {
814− find_char_impl(
815− cx,
816− find_prev_char_impl,
817− false, /* inclusive */
818− false, /* extend */
819− )
873+ will_find_char(cx, find_prev_char_impl, false, false)
820874}
821875
822876fn find_prev_char(cx: &mut Context) {
823− find_char_impl(
824− cx,
825− find_prev_char_impl,
826− true, /* inclusive */
827− false, /* extend */
828− )
877+ will_find_char(cx, find_prev_char_impl, true, false)
829878}
830879
831880fn extend_till_prev_char(cx: &mut Context) {
832− find_char_impl(
833− cx,
834− find_prev_char_impl,
835− false, /* inclusive */
836− true, /* extend */
837− )
881+ will_find_char(cx, find_prev_char_impl, false, true)
838882}
839883
840884fn extend_prev_char(cx: &mut Context) {
841− find_char_impl(
842− cx,
843− find_prev_char_impl,
844− true, /* inclusive */
845− true, /* extend */
846− )
885+ will_find_char(cx, find_prev_char_impl, true, true)
886+}
887+
888+fn repeat_last_motion(cx: &mut Context) {
889+ let last_motion = cx.editor.last_motion.take();
890+ if let Some(m) = &last_motion {
891+ m.run(cx.editor);
892+ cx.editor.last_motion = last_motion;
893+ }
847894}
848895
849896fn replace(cx: &mut Context) {
@@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) {
10911138 cx,
10921139 "select:".into(),
10931140 Some(reg),
1141+ |_input: &str| Vec::new(),
10941142 move |view, doc, regex, event| {
10951143 if event != PromptEvent::Update {
10961144 return;
@@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) {
11131161 cx,
11141162 "split:".into(),
11151163 Some(reg),
1164+ |_input: &str| Vec::new(),
11161165 move |view, doc, regex, event| {
11171166 if event != PromptEvent::Update {
11181167 return;
@@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) {
11371186 doc.set_selection(view.id, selection);
11381187}
11391188
1140−fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
1189+fn search_impl(
1190+ doc: &mut Document,
1191+ view: &mut View,
1192+ contents: &str,
1193+ regex: &Regex,
1194+ movement: Movement,
1195+ direction: Direction,
1196+) {
11411197 let text = doc.text().slice(..);
11421198 let selection = doc.selection(view.id);
11431199
1144− // Get the right side of the primary block cursor.
1145− let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1146− text,
1147− selection.primary().cursor(text),
1148− ));
1200+ // Get the right side of the primary block cursor for forward search, or the
1201+ //grapheme before the start of the selection for reverse search.
1202+ let start = match direction {
1203+ Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary(
1204+ text,
1205+ selection.primary().to(),
1206+ )),
1207+ Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary(
1208+ text,
1209+ selection.primary().from(),
1210+ )),
1211+ };
1212+
1213+ //A regex::Match returns byte-positions in the str. In the case where we
1214+ //do a reverse search and wraparound to the end, we don't need to search
1215+ //the text before the current cursor position for matches, but by slicing
1216+ //it out, we need to add it back to the position of the selection.
1217+ let mut offset = 0;
11491218
11501219 // use find_at to find the next match after the cursor, loop around the end
11511220 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1152− let mat = regex
1153− .find_at(contents, start)
1154− .or_else(|| regex.find(contents));
1221+ let mat = match direction {
1222+ Direction::Forward => regex
1223+ .find_at(contents, start)
1224+ .or_else(|| regex.find(contents)),
1225+ Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
1226+ offset = start;
1227+ regex.find_iter(&contents[start..]).last()
1228+ }),
1229+ };
11551230 // TODO: message on wraparound
11561231 if let Some(mat) = mat {
1157− let start = text.byte_to_char(mat.start());
1158− let end = text.byte_to_char(mat.end());
1232+ let start = text.byte_to_char(mat.start() + offset);
1233+ let end = text.byte_to_char(mat.end() + offset);
11591234
11601235 if end == 0 {
11611236 // skip empty matches that don't make sense
11621237 return;
11631238 }
11641239
1165− let selection = if extend {
1166− selection.clone().push(Range::new(start, end))
1240+ // Determine range direction based on the primary range
1241+ let primary = selection.primary();
1242+ let range = if primary.head < primary.anchor {
1243+ Range::new(end, start)
11671244 } else {
1168− Selection::single(start, end)
1245+ Range::new(start, end)
1246+ };
1247+
1248+ let selection = match movement {
1249+ Movement::Extend => selection.clone().push(range),
1250+ Movement::Move => selection.clone().replace(selection.primary_index(), range),
11691251 };
11701252
11711253 doc.set_selection(view.id, selection);
@@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
11731255 };
11741256}
11751257
1258+fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1259+ let mut items = reg
1260+ .and_then(|reg| cx.editor.registers.get(reg))
1261+ .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1262+ items.sort_unstable();
1263+ items.dedup();
1264+ items.into_iter().cloned().collect()
1265+}
1266+
11761267// TODO: use one function for search vs extend
11771268fn search(cx: &mut Context) {
1269+ searcher(cx, Direction::Forward)
1270+}
1271+
1272+fn rsearch(cx: &mut Context) {
1273+ searcher(cx, Direction::Backward)
1274+}
1275+// TODO: use one function for search vs extend
1276+fn searcher(cx: &mut Context, direction: Direction) {
11781277 let reg = cx.register.unwrap_or('/');
11791278 let (_, doc) = current!(cx.editor);
11801279
@@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) {
11831282 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
11841283 // feed chunks into the regex yet
11851284 let contents = doc.text().slice(..).to_string();
1285+ let completions = search_completions(cx, Some(reg));
11861286
11871287 let prompt = ui::regex_prompt(
11881288 cx,
11891289 "search:".into(),
11901290 Some(reg),
1291+ move |input: &str| {
1292+ completions
1293+ .iter()
1294+ .filter(|comp| comp.starts_with(input))
1295+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1296+ .collect()
1297+ },
11911298 move |view, doc, regex, event| {
11921299 if event != PromptEvent::Update {
11931300 return;
11941301 }
1195− search_impl(doc, view, &contents, &regex, false);
1302+ search_impl(doc, view, &contents, &regex, Movement::Move, direction);
11961303 },
11971304 );
11981305
11991306 cx.push_layer(Box::new(prompt));
12001307}
12011308
1202−fn search_next_impl(cx: &mut Context, extend: bool) {
1309+fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
12031310 let (view, doc) = current!(cx.editor);
12041311 let registers = &cx.editor.registers;
12051312 if let Some(query) = registers.read('/') {
@@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
12141321 .case_insensitive(case_insensitive)
12151322 .build()
12161323 {
1217− search_impl(doc, view, &contents, &regex, extend);
1324+ search_impl(doc, view, &contents, &regex, movement, direction);
12181325 } else {
12191326 // get around warning `mutable_borrow_reservation_conflict`
12201327 // which will be a hard error in the future
@@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
12261333}
12271334
12281335fn search_next(cx: &mut Context) {
1229− search_next_impl(cx, false);
1336+ search_next_or_prev_impl(cx, Movement::Move, Direction::Forward);
12301337}
12311338
1339+fn search_prev(cx: &mut Context) {
1340+ search_next_or_prev_impl(cx, Movement::Move, Direction::Backward);
1341+}
12321342fn extend_search_next(cx: &mut Context) {
1233− search_next_impl(cx, true);
1343+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward);
1344+}
1345+
1346+fn extend_search_prev(cx: &mut Context) {
1347+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward);
12341348}
12351349
12361350fn search_selection(cx: &mut Context) {
@@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) {
12471361 let (all_matches_sx, all_matches_rx) =
12481362 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
12491363 let smart_case = cx.editor.config.smart_case;
1364+
1365+ let completions = search_completions(cx, None);
12501366 let prompt = ui::regex_prompt(
12511367 cx,
12521368 "global search:".into(),
12531369 None,
1370+ move |input: &str| {
1371+ completions
1372+ .iter()
1373+ .filter(|comp| comp.starts_with(input))
1374+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1375+ .collect()
1376+ },
12541377 move |_view, _doc, regex, event| {
12551378 if event != PromptEvent::Validate {
12561379 return;
@@ -1572,7 +1695,8 @@ mod cmd {
15721695 let (_, doc) = current!(cx.editor);
15731696
15741697 if let Some(path) = path {
1575− doc.set_path(path.as_ref()).context("invalid filepath")?;
1698+ doc.set_path(Some(path.as_ref()))
1699+ .context("invalid filepath")?;
15761700 }
15771701 if doc.path().is_none() {
15781702 bail!("cannot write a buffer without a filename");
@@ -1635,7 +1759,7 @@ mod cmd {
16351759
16361760 // If no argument, report current indent style.
16371761 if args.is_empty() {
1638− let style = current!(cx.editor).1.indent_style;
1762+ let style = doc!(cx.editor).indent_style;
16391763 cx.editor.set_status(match style {
16401764 Tabs => "tabs".into(),
16411765 Spaces(1) => "1 space".into(),
@@ -1674,7 +1798,7 @@ mod cmd {
16741798
16751799 // If no argument, report current line ending setting.
16761800 if args.is_empty() {
1677− let line_ending = current!(cx.editor).1.line_ending;
1801+ let line_ending = doc!(cx.editor).line_ending;
16781802 cx.editor.set_status(match line_ending {
16791803 Crlf => "crlf".into(),
16801804 LF => "line feed".into(),
@@ -1790,7 +1914,7 @@ mod cmd {
17901914 let mut errors = String::new();
17911915
17921916 // save all documents
1793− for (_, doc) in &mut cx.editor.documents {
1917+ for doc in &mut cx.editor.documents.values_mut() {
17941918 if doc.path().is_none() {
17951919 errors.push_str("cannot write a buffer without a filename\n");
17961920 continue;
@@ -2085,8 +2209,7 @@ mod cmd {
20852209 args: &[&str],
20862210 _event: PromptEvent,
20872211 ) -> anyhow::Result<()> {
2088− let (_, doc) = current!(cx.editor);
2089− let id = doc.id();
2212+ let id = view!(cx.editor).doc;
20902213
20912214 if let Some(path) = args.get(0) {
20922215 cx.editor.open(path.into(), Action::VerticalSplit)?;
@@ -2102,8 +2225,7 @@ mod cmd {
21022225 args: &[&str],
21032226 _event: PromptEvent,
21042227 ) -> anyhow::Result<()> {
2105− let (_, doc) = current!(cx.editor);
2106− let id = doc.id();
2228+ let id = view!(cx.editor).doc;
21072229
21082230 if let Some(path) = args.get(0) {
21092231 cx.editor.open(path.into(), Action::HorizontalSplit)?;
@@ -2188,6 +2310,18 @@ mod cmd {
21882310 Ok(())
21892311 }
21902312
2313+ fn tutor(
2314+ cx: &mut compositor::Context,
2315+ _args: &[&str],
2316+ _event: PromptEvent,
2317+ ) -> anyhow::Result<()> {
2318+ let path = helix_core::runtime_dir().join("tutor.txt");
2319+ cx.editor.open(path, Action::Replace)?;
2320+ // Unset path to prevent accidentally saving to the original tutor file.
2321+ doc_mut!(cx.editor).set_path(None)?;
2322+ Ok(())
2323+ }
2324+
21912325 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
21922326 TypableCommand {
21932327 name: "quit",
@@ -2199,7 +2333,7 @@ mod cmd {
21992333 TypableCommand {
22002334 name: "quit!",
22012335 aliases: &["q!"],
2202− doc: "Close the current view.",
2336+ doc: "Close the current view forcefully (ignoring unsaved changes).",
22032337 fun: force_quit,
22042338 completer: None,
22052339 },
@@ -2262,35 +2396,35 @@ mod cmd {
22622396 TypableCommand {
22632397 name: "write-quit",
22642398 aliases: &["wq", "x"],
2265− doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)",
2399+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
22662400 fun: write_quit,
22672401 completer: Some(completers::filename),
22682402 },
22692403 TypableCommand {
22702404 name: "write-quit!",
22712405 aliases: &["wq!", "x!"],
2272− doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2406+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
22732407 fun: force_write_quit,
22742408 completer: Some(completers::filename),
22752409 },
22762410 TypableCommand {
22772411 name: "write-all",
22782412 aliases: &["wa"],
2279− doc: "Writes changes from all views to disk.",
2413+ doc: "Write changes from all views to disk.",
22802414 fun: write_all,
22812415 completer: None,
22822416 },
22832417 TypableCommand {
22842418 name: "write-quit-all",
22852419 aliases: &["wqa", "xa"],
2286− doc: "Writes changes from all views to disk and close all views.",
2420+ doc: "Write changes from all views to disk and close all views.",
22872421 fun: write_all_quit,
22882422 completer: None,
22892423 },
22902424 TypableCommand {
22912425 name: "write-quit-all!",
22922426 aliases: &["wqa!", "xa!"],
2293− doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
2427+ doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
22942428 fun: force_write_all_quit,
22952429 completer: None,
22962430 },
@@ -2461,7 +2595,14 @@ mod cmd {
24612595 doc: "Open the file in a horizontal split.",
24622596 fun: hsplit,
24632597 completer: Some(completers::filename),
2464− }
2598+ },
2599+ TypableCommand {
2600+ name: "tutor",
2601+ aliases: &[],
2602+ doc: "Open the tutorial.",
2603+ fun: tutor,
2604+ completer: None,
2605+ },
24652606 ];
24662607
24672608 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) {
25612702 cx.editor
25622703 .documents
25632704 .iter()
2564− .map(|(id, doc)| (id, doc.path().cloned()))
2705+ .map(|(id, doc)| (*id, doc.path().cloned()))
25652706 .collect(),
25662707 move |(id, path): &(DocumentId, Option<PathBuf>)| {
25672708 let path = path.as_deref().map(helix_core::path::get_relative_path);
@@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) {
25802721 editor.switch(*id, Action::Replace);
25812722 },
25822723 |editor, (id, path)| {
2583− let doc = &editor.documents.get(*id)?;
2724+ let doc = &editor.documents.get(id)?;
25842725 let &view_id = doc.selections().keys().next()?;
25852726 let line = doc
25862727 .selection(view_id)
@@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) {
29963137 doc.text().len_lines() - 1
29973138 };
29983139 let line_idx = std::cmp::min(count.get() - 1, max_line);
3140+ let text = doc.text().slice(..);
29993141 let pos = doc.text().line_to_char(line_idx);
3000− doc.set_selection(view.id, Selection::point(pos));
3142+ let selection = doc
3143+ .selection(view.id)
3144+ .clone()
3145+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3146+ doc.set_selection(view.id, selection);
30013147 }
30023148}
30033149
@@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) {
30113157 } else {
30123158 doc.text().len_lines() - 1
30133159 };
3160+ let text = doc.text().slice(..);
30143161 let pos = doc.text().line_to_char(line_idx);
3015− doc.set_selection(view.id, Selection::point(pos));
3162+ let selection = doc
3163+ .selection(view.id)
3164+ .clone()
3165+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3166+ doc.set_selection(view.id, selection);
30163167}
30173168
30183169fn goto_last_accessed_file(cx: &mut Context) {
@@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) {
33063457 let editor = &mut cx.editor;
33073458 let (_, doc) = current!(editor);
33083459
3309− let diag = if let Some(diag) = doc.diagnostics().first() {
3310− diag.range.start
3311− } else {
3312− return;
3460+ let pos = match doc.diagnostics().first() {
3461+ Some(diag) => diag.range.start,
3462+ None => return,
33133463 };
33143464
3315− goto_pos(editor, diag);
3465+ goto_pos(editor, pos);
33163466}
33173467
33183468fn goto_last_diag(cx: &mut Context) {
33193469 let editor = &mut cx.editor;
33203470 let (_, doc) = current!(editor);
33213471
3322− let diag = if let Some(diag) = doc.diagnostics().last() {
3323− diag.range.start
3324− } else {
3325− return;
3472+ let pos = match doc.diagnostics().last() {
3473+ Some(diag) => diag.range.start,
3474+ None => return,
33263475 };
33273476
3328− goto_pos(editor, diag);
3477+ goto_pos(editor, pos);
33293478}
33303479
33313480fn goto_next_diag(cx: &mut Context) {
@@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) {
33363485 .selection(view.id)
33373486 .primary()
33383487 .cursor(doc.text().slice(..));
3339− let diag = if let Some(diag) = doc
3488+
3489+ let diag = doc
33403490 .diagnostics()
33413491 .iter()
3342− .map(|diag| diag.range.start)
3343− .find(|&pos| pos > cursor_pos)
3344− {
3345− diag
3346− } else if let Some(diag) = doc.diagnostics().first() {
3347− diag.range.start
3348− } else {
3349− return;
3492+ .find(|diag| diag.range.start > cursor_pos)
3493+ .or_else(|| doc.diagnostics().first());
3494+
3495+ let pos = match diag {
3496+ Some(diag) => diag.range.start,
3497+ None => return,
33503498 };
33513499
3352− goto_pos(editor, diag);
3500+ goto_pos(editor, pos);
33533501}
33543502
33553503fn goto_prev_diag(cx: &mut Context) {
@@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) {
33603508 .selection(view.id)
33613509 .primary()
33623510 .cursor(doc.text().slice(..));
3363− let diag = if let Some(diag) = doc
3511+
3512+ let diag = doc
33643513 .diagnostics()
33653514 .iter()
33663515 .rev()
3367− .map(|diag| diag.range.start)
3368− .find(|&pos| pos < cursor_pos)
3369− {
3370− diag
3371− } else if let Some(diag) = doc.diagnostics().last() {
3372− diag.range.start
3373− } else {
3374− return;
3516+ .find(|diag| diag.range.start < cursor_pos)
3517+ .or_else(|| doc.diagnostics().last());
3518+
3519+ let pos = match diag {
3520+ Some(diag) => diag.range.start,
3521+ None => return,
33753522 };
33763523
3377− goto_pos(editor, diag);
3524+ goto_pos(editor, pos);
33783525}
33793526
33803527fn signature_help(cx: &mut Context) {
@@ -3423,7 +3570,26 @@ pub mod insert {
34233570 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
34243571 pub type PostHook = fn(&mut Context, char);
34253572
3426− fn completion(cx: &mut Context, ch: char) {
3573+ // It trigger completion when idle timer reaches deadline
3574+ // Only trigger completion if the word under cursor is longer than n characters
3575+ pub fn idle_completion(cx: &mut Context) {
3576+ let (view, doc) = current!(cx.editor);
3577+ let text = doc.text().slice(..);
3578+ let cursor = doc.selection(view.id).primary().cursor(text);
3579+
3580+ use helix_core::chars::char_is_word;
3581+ let mut iter = text.chars_at(cursor);
3582+ iter.reverse();
3583+ for _ in 0..cx.editor.config.completion_trigger_len {
3584+ match iter.next() {
3585+ Some(c) if char_is_word(c) => {}
3586+ _ => return,
3587+ }
3588+ }
3589+ super::completion(cx);
3590+ }
3591+
3592+ fn language_server_completion(cx: &mut Context, ch: char) {
34273593 // if ch matches completion char, trigger completion
34283594 let doc = doc_mut!(cx.editor);
34293595 let language_server = match doc.language_server() {
@@ -3433,19 +3599,14 @@ pub mod insert {
34333599
34343600 let capabilities = language_server.capabilities();
34353601
3436− if let lsp::ServerCapabilities {
3437− completion_provider:
3438− Some(lsp::CompletionOptions {
3439− trigger_characters: Some(triggers),
3440− ..
3441− }),
3602+ if let Some(lsp::CompletionOptions {
3603+ trigger_characters: Some(triggers),
34423604 ..
3443− } = capabilities
3605+ }) = &capabilities.completion_provider
34443606 {
34453607 // TODO: what if trigger is multiple chars long
3446− let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3447−
3448− if is_trigger {
3608+ if triggers.iter().any(|trigger| trigger.contains(ch)) {
3609+ cx.editor.clear_idle_timer();
34493610 super::completion(cx);
34503611 }
34513612 }
@@ -3527,7 +3688,8 @@ pub mod insert {
35273688 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
35283689 // this could also generically look at Transaction, but it's a bit annoying to look at
35293690 // Operation instead of Change.
3530− for hook in &[completion, signature_help] {
3691+ for hook in &[language_server_completion, signature_help] {
3692+ // for hook in &[signature_help] {
35313693 hook(cx, c);
35323694 }
35333695 }
@@ -3668,13 +3830,19 @@ pub mod insert {
36683830fn undo(cx: &mut Context) {
36693831 let (view, doc) = current!(cx.editor);
36703832 let view_id = view.id;
3671− doc.undo(view_id);
3833+ let success = doc.undo(view_id);
3834+ if !success {
3835+ cx.editor.set_status("Already at oldest change".to_owned());
3836+ }
36723837}
36733838
36743839fn redo(cx: &mut Context) {
36753840 let (view, doc) = current!(cx.editor);
36763841 let view_id = view.id;
3677− doc.redo(view_id);
3842+ let success = doc.redo(view_id);
3843+ if !success {
3844+ cx.editor.set_status("Already at newest change".to_owned());
3845+ }
36783846}
36793847
36803848// Yank / Paste
@@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl(
37353903}
37363904
37373905fn yank_joined_to_clipboard(cx: &mut Context) {
3738− let line_ending = current!(cx.editor).1.line_ending;
3906+ let line_ending = doc!(cx.editor).line_ending;
37393907 let _ = yank_joined_to_clipboard_impl(
37403908 &mut cx.editor,
37413909 line_ending.as_str(),
@@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) {
37693937}
37703938
37713939fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3772− let line_ending = current!(cx.editor).1.line_ending;
3940+ let line_ending = doc!(cx.editor).line_ending;
37733941 let _ = yank_joined_to_clipboard_impl(
37743942 &mut cx.editor,
37753943 line_ending.as_str(),
@@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) {
38824050 let registers = &mut cx.editor.registers;
38834051
38844052 if let Some(values) = registers.read(reg_name) {
3885− if let Some(yank) = values.first() {
4053+ if !values.is_empty() {
4054+ let repeat = std::iter::repeat(
4055+ values
4056+ .last()
4057+ .map(|value| Tendril::from_slice(value))
4058+ .unwrap(),
4059+ );
4060+ let mut values = values
4061+ .iter()
4062+ .map(|value| Tendril::from_slice(value))
4063+ .chain(repeat);
38864064 let selection = doc.selection(view.id);
38874065 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
38884066 if !range.is_empty() {
3889− (range.from(), range.to(), Some(yank.as_str().into()))
4067+ (range.from(), range.to(), Some(values.next().unwrap()))
38904068 } else {
38914069 (range.from(), range.to(), None)
38924070 }
@@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) {
41284306 cx,
41294307 "keep:".into(),
41304308 Some(reg),
4309+ |_input: &str| Vec::new(),
41314310 move |view, doc, regex, event| {
41324311 if event != PromptEvent::Update {
41334312 return;
@@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) {
42284407 iter.reverse();
42294408 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
42304409 let start_offset = cursor.saturating_sub(offset);
4410+ let prefix = text.slice(start_offset..cursor).to_string();
42314411
42324412 cx.callback(
42334413 future,
@@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) {
42404420 return;
42414421 }
42424422
4243− let items = match response {
4423+ let mut items = match response {
42444424 Some(lsp::CompletionResponse::Array(items)) => items,
42454425 // TODO: do something with is_incomplete
42464426 Some(lsp::CompletionResponse::List(lsp::CompletionList {
@@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) {
42504430 None => Vec::new(),
42514431 };
42524432
4433+ if !prefix.is_empty() {
4434+ items = items
4435+ .into_iter()
4436+ .filter(|item| {
4437+ item.filter_text
4438+ .as_ref()
4439+ .unwrap_or(&item.label)
4440+ .starts_with(&prefix)
4441+ })
4442+ .collect();
4443+ }
4444+
42534445 if items.is_empty() {
42544446 // editor.set_error("No completion available".to_string());
42554447 return;
@@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
44014593// tree sitter node selection
44024594
44034595fn expand_selection(cx: &mut Context) {
4404− let (view, doc) = current!(cx.editor);
4596+ let motion = |editor: &mut Editor| {
4597+ let (view, doc) = current!(editor);
44054598
4406− if let Some(syntax) = doc.syntax() {
4407− let text = doc.text().slice(..);
4408− let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4409− doc.set_selection(view.id, selection);
4410− }
4599+ if let Some(syntax) = doc.syntax() {
4600+ let text = doc.text().slice(..);
4601+ let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4602+ doc.set_selection(view.id, selection);
4603+ }
4604+ };
4605+ motion(&mut cx.editor);
4606+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
44114607}
44124608
44134609fn match_brackets(cx: &mut Context) {
44144610 let (view, doc) = current!(cx.editor);
44154611
44164612 if let Some(syntax) = doc.syntax() {
4417− let pos = doc
4418− .selection(view.id)
4419− .primary()
4420− .cursor(doc.text().slice(..));
4421− if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4422− let selection = Selection::point(pos);
4423− doc.set_selection(view.id, selection);
4424− };
4613+ let text = doc.text().slice(..);
4614+ let selection = doc.selection(view.id).clone().transform(|range| {
4615+ if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4616+ range.put_cursor(text, pos, doc.mode == Mode::Select)
4617+ } else {
4618+ range
4619+ }
4620+ });
4621+ doc.set_selection(view.id, selection);
44254622 }
44264623}
44274624
@@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) {
44294626
44304627fn jump_forward(cx: &mut Context) {
44314628 let count = cx.count();
4432− let (view, _doc) = current!(cx.editor);
4629+ let view = view_mut!(cx.editor);
44334630
44344631 if let Some((id, selection)) = view.jumps.forward(count) {
44354632 view.doc = *id;
@@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) {
44634660 cx.editor.focus_next()
44644661}
44654662
4663+fn jump_view_right(cx: &mut Context) {
4664+ cx.editor.focus_right()
4665+}
4666+
4667+fn jump_view_left(cx: &mut Context) {
4668+ cx.editor.focus_left()
4669+}
4670+
4671+fn jump_view_up(cx: &mut Context) {
4672+ cx.editor.focus_up()
4673+}
4674+
4675+fn jump_view_down(cx: &mut Context) {
4676+ cx.editor.focus_down()
4677+}
4678+
44664679// split helper, clear it later
44674680fn split(cx: &mut Context, action: Action) {
44684681 let (view, doc) = current!(cx.editor);
@@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
45524765 let count = cx.count();
45534766 cx.on_next_key(move |cx, event| {
45544767 if let Some(ch) = event.char() {
4555− let (view, doc) = current!(cx.editor);
4556− let text = doc.text().slice(..);
4768+ let textobject = move |editor: &mut Editor| {
4769+ let (view, doc) = current!(editor);
4770+ let text = doc.text().slice(..);
4771+
4772+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4773+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4774+ Some(t) => t,
4775+ None => return range,
4776+ };
4777+ textobject::textobject_treesitter(
4778+ text,
4779+ range,
4780+ objtype,
4781+ obj_name,
4782+ syntax.tree().root_node(),
4783+ lang_config,
4784+ count,
4785+ )
4786+ };
45574787
4558− let selection = doc.selection(view.id).clone().transform(|range| {
4559− match ch {
4560− 'w' => textobject::textobject_word(text, range, objtype, count),
4561− // TODO: cancel new ranges if inconsistent surround matches across lines
4562− ch if !ch.is_ascii_alphanumeric() => {
4563− textobject::textobject_surround(text, range, objtype, ch, count)
4788+ let selection = doc.selection(view.id).clone().transform(|range| {
4789+ match ch {
4790+ 'w' => textobject::textobject_word(text, range, objtype, count),
4791+ 'c' => textobject_treesitter("class", range),
4792+ 'f' => textobject_treesitter("function", range),
4793+ 'p' => textobject_treesitter("parameter", range),
4794+ // TODO: cancel new ranges if inconsistent surround matches across lines
4795+ ch if !ch.is_ascii_alphanumeric() => {
4796+ textobject::textobject_surround(text, range, objtype, ch, count)
4797+ }
4798+ _ => range,
45644799 }
4565− _ => range,
4566− }
4567− });
4568− doc.set_selection(view.id, selection);
4800+ });
4801+ doc.set_selection(view.id, selection);
4802+ };
4803+ textobject(&mut cx.editor);
4804+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
45694805 }
45704806 })
45714807}
@@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) {
45774813 let selection = doc.selection(view.id);
45784814 let (open, close) = surround::get_pair(ch);
45794815
4580− let mut changes = Vec::new();
4816+ let mut changes = Vec::with_capacity(selection.len() * 2);
45814817 for range in selection.iter() {
45824818 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
45834819 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
helix-term/src/compositor.rs+1−1
@@ -207,7 +207,7 @@ pub trait AnyComponent {
207207 ///
208208 /// ```rust
209209 /// use helix_term::{ui::Text, compositor::Component};
210− /// let boxed: Box<Component> = Box::new(Text::new("text".to_string()));
210+ /// let boxed: Box<dyn Component> = Box::new(Text::new("text".to_string()));
211211 /// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
212212 /// ```
213213 fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
helix-term/src/keymap.rs+150−78
@@ -5,20 +5,20 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent};
55use serde::Deserialize;
66use std::{
77 borrow::Cow,
8− collections::HashMap,
8+ collections::{BTreeSet, HashMap},
99 ops::{Deref, DerefMut},
1010};
1111
1212#[macro_export]
1313macro_rules! key {
1414 ($key:ident) => {
15− KeyEvent {
15+ ::helix_view::input::KeyEvent {
1616 code: ::helix_view::keyboard::KeyCode::$key,
1717 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
1818 }
1919 };
2020 ($($ch:tt)*) => {
21− KeyEvent {
21+ ::helix_view::input::KeyEvent {
2222 code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
2323 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
2424 }
@@ -78,19 +78,30 @@ macro_rules! keymap {
7878 };
7979}
8080
81−#[derive(Debug, Clone, Deserialize)]
81+#[derive(Debug, Clone)]
8282pub struct KeyTrieNode {
8383 /// A label for keys coming under this node, like "Goto mode"
84− #[serde(skip)]
8584 name: String,
86− #[serde(flatten)]
8785 map: HashMap<KeyEvent, KeyTrie>,
88− #[serde(skip)]
8986 order: Vec<KeyEvent>,
90− #[serde(skip)]
9187 pub is_sticky: bool,
9288}
9389
90+impl<'de> Deserialize<'de> for KeyTrieNode {
91+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92+ where
93+ D: serde::Deserializer<'de>,
94+ {
95+ let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
96+ let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
97+ Ok(Self {
98+ map,
99+ order,
100+ ..Default::default()
101+ })
102+ }
103+}
104+
94105impl KeyTrieNode {
95106 pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
96107 Self {
@@ -118,7 +129,6 @@ impl KeyTrieNode {
118129 }
119130 self.map.insert(key, trie);
120131 }
121−
122132 for &key in self.map.keys() {
123133 if !self.order.contains(&key) {
124134 self.order.push(key);
@@ -127,20 +137,29 @@ impl KeyTrieNode {
127137 }
128138
129139 pub fn infobox(&self) -> Info {
130− let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
140+ let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len());
131141 for (&key, trie) in self.iter() {
132142 let desc = match trie {
133− KeyTrie::Leaf(cmd) => cmd.doc(),
143+ KeyTrie::Leaf(cmd) => {
144+ if cmd.name() == "no_op" {
145+ continue;
146+ }
147+ cmd.doc()
148+ }
134149 KeyTrie::Node(n) => n.name(),
135150 };
136151 match body.iter().position(|(d, _)| d == &desc) {
137− // FIXME: multiple keys are ordered randomly (use BTreeSet)
138− Some(pos) => body[pos].1.push(key),
139− None => body.push((desc, vec![key])),
152+ Some(pos) => {
153+ body[pos].1.insert(key);
154+ }
155+ None => body.push((desc, BTreeSet::from([key]))),
140156 }
141157 }
142158 body.sort_unstable_by_key(|(_, keys)| {
143− self.order.iter().position(|&k| k == keys[0]).unwrap()
159+ self.order
160+ .iter()
161+ .position(|&k| k == *keys.iter().next().unwrap())
162+ .unwrap()
144163 });
145164 let prefix = format!("{} ", self.name());
146165 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
@@ -151,6 +170,11 @@ impl KeyTrieNode {
151170 }
152171 Info::new(self.name(), body)
153172 }
173+
174+ /// Get a reference to the key trie node's order.
175+ pub fn order(&self) -> &[KeyEvent] {
176+ self.order.as_slice()
177+ }
154178}
155179
156180impl Default for KeyTrieNode {
@@ -235,6 +259,7 @@ pub enum KeymapResultKind {
235259
236260/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
237261/// reference to the sticky node if one is currently active.
262+#[derive(Debug)]
238263pub struct KeymapResult<'a> {
239264 pub kind: KeymapResultKind,
240265 pub sticky: Option<&'a KeyTrieNode>,
@@ -395,6 +420,7 @@ impl Default for Keymaps {
395420 "F" => find_prev_char,
396421 "r" => replace,
397422 "R" => replace_with_yanked,
423+ "A-." => repeat_last_motion,
398424
399425 "~" => switch_case,
400426 "`" => switch_to_lowercase,
@@ -427,6 +453,8 @@ impl Default for Keymaps {
427453 "m" => goto_window_middle,
428454 "b" => goto_window_bottom,
429455 "a" => goto_last_accessed_file,
456+ "n" => goto_next_buffer,
457+ "p" => goto_previous_buffer,
430458 },
431459 ":" => command_mode,
432460
@@ -476,10 +504,9 @@ impl Default for Keymaps {
476504 },
477505
478506 "/" => search,
479− // ? for search_reverse
507+ "?" => rsearch,
480508 "n" => search_next,
481− "N" => extend_search_next,
482− // N for search_prev
509+ "N" => search_prev,
483510 "*" => search_selection,
484511
485512 "u" => undo,
@@ -520,9 +547,13 @@ impl Default for Keymaps {
520547
521548 "C-w" => { "Window"
522549 "C-w" | "w" => rotate_view,
523− "C-h" | "h" => hsplit,
550+ "C-s" | "s" => hsplit,
524551 "C-v" | "v" => vsplit,
525552 "C-q" | "q" => wclose,
553+ "C-h" | "h" | "left" => jump_view_left,
554+ "C-j" | "j" | "down" => jump_view_down,
555+ "C-k" | "k" | "up" => jump_view_up,
556+ "C-l" | "l" | "right" => jump_view_right,
526557 },
527558
528559 // move under <space>c
@@ -621,6 +652,9 @@ impl Default for Keymaps {
621652 "B" => extend_prev_long_word_start,
622653 "E" => extend_next_long_word_end,
623654
655+ "n" => extend_search_next,
656+ "N" => extend_search_prev,
657+
624658 "t" => extend_till_char,
625659 "f" => extend_next_char,
626660 "T" => extend_till_prev_char,
@@ -669,63 +703,101 @@ pub fn merge_keys(mut config: Config) -> Config {
669703 config
670704}
671705
672−#[test]
673−fn merge_partial_keys() {
674− let config = Config {
675− keys: Keymaps(hashmap! {
676− Mode::Normal => Keymap::new(
677− keymap!({ "Normal mode"
678− "i" => normal_mode,
679− "无" => insert_mode,
680− "z" => jump_backward,
681− "g" => { "Merge into goto mode"
682− "$" => goto_line_end,
683− "g" => delete_char_forward,
684− },
685− })
686− )
687− }),
688− ..Default::default()
689− };
690− let mut merged_config = merge_keys(config.clone());
691− assert_ne!(config, merged_config);
692−
693− let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
694− assert_eq!(
695− keymap.get(key!('i')).kind,
696− KeymapResultKind::Matched(Command::normal_mode),
697− "Leaf should replace leaf"
698− );
699− assert_eq!(
700− keymap.get(key!('无')).kind,
701− KeymapResultKind::Matched(Command::insert_mode),
702− "New leaf should be present in merged keymap"
703− );
704− // Assumes that z is a node in the default keymap
705− assert_eq!(
706− keymap.get(key!('z')).kind,
707− KeymapResultKind::Matched(Command::jump_backward),
708− "Leaf should replace node"
709− );
710− // Assumes that `g` is a node in default keymap
711− assert_eq!(
712− keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
713− &KeyTrie::Leaf(Command::goto_line_end),
714− "Leaf should be present in merged subnode"
715− );
716− // Assumes that `gg` is in default keymap
717− assert_eq!(
718− keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
719− &KeyTrie::Leaf(Command::delete_char_forward),
720− "Leaf should replace old leaf in merged subnode"
721− );
722− // Assumes that `ge` is in default keymap
723− assert_eq!(
724− keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
725− &KeyTrie::Leaf(Command::goto_last_line),
726− "Old leaves in subnode should be present in merged node"
727− );
728−
729− assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
730− assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
706+#[cfg(test)]
707+mod tests {
708+ use super::*;
709+ #[test]
710+ fn merge_partial_keys() {
711+ let config = Config {
712+ keys: Keymaps(hashmap! {
713+ Mode::Normal => Keymap::new(
714+ keymap!({ "Normal mode"
715+ "i" => normal_mode,
716+ "无" => insert_mode,
717+ "z" => jump_backward,
718+ "g" => { "Merge into goto mode"
719+ "$" => goto_line_end,
720+ "g" => delete_char_forward,
721+ },
722+ })
723+ )
724+ }),
725+ ..Default::default()
726+ };
727+ let mut merged_config = merge_keys(config.clone());
728+ assert_ne!(config, merged_config);
729+
730+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
731+ assert_eq!(
732+ keymap.get(key!('i')).kind,
733+ KeymapResultKind::Matched(Command::normal_mode),
734+ "Leaf should replace leaf"
735+ );
736+ assert_eq!(
737+ keymap.get(key!('无')).kind,
738+ KeymapResultKind::Matched(Command::insert_mode),
739+ "New leaf should be present in merged keymap"
740+ );
741+ // Assumes that z is a node in the default keymap
742+ assert_eq!(
743+ keymap.get(key!('z')).kind,
744+ KeymapResultKind::Matched(Command::jump_backward),
745+ "Leaf should replace node"
746+ );
747+ // Assumes that `g` is a node in default keymap
748+ assert_eq!(
749+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
750+ &KeyTrie::Leaf(Command::goto_line_end),
751+ "Leaf should be present in merged subnode"
752+ );
753+ // Assumes that `gg` is in default keymap
754+ assert_eq!(
755+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
756+ &KeyTrie::Leaf(Command::delete_char_forward),
757+ "Leaf should replace old leaf in merged subnode"
758+ );
759+ // Assumes that `ge` is in default keymap
760+ assert_eq!(
761+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
762+ &KeyTrie::Leaf(Command::goto_last_line),
763+ "Old leaves in subnode should be present in merged node"
764+ );
765+
766+ assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
767+ assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
768+ }
769+
770+ #[test]
771+ fn order_should_be_set() {
772+ let config = Config {
773+ keys: Keymaps(hashmap! {
774+ Mode::Normal => Keymap::new(
775+ keymap!({ "Normal mode"
776+ "space" => { ""
777+ "s" => { ""
778+ "v" => vsplit,
779+ "c" => hsplit,
780+ },
781+ },
782+ })
783+ )
784+ }),
785+ ..Default::default()
786+ };
787+ let mut merged_config = merge_keys(config.clone());
788+ assert_ne!(config, merged_config);
789+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
790+ // Make sure mapping works
791+ assert_eq!(
792+ keymap
793+ .root()
794+ .search(&[key!(' '), key!('s'), key!('v')])
795+ .unwrap(),
796+ &KeyTrie::Leaf(Command::vsplit),
797+ "Leaf should be present in merged subnode"
798+ );
799+ // Make sure an order was set during merge
800+ let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
801+ assert!(!node.node().unwrap().order().is_empty())
802+ }
731803}
helix-term/src/main.rs+7−1
@@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
1616 };
1717
1818 // Separate file config so we can include year, month and day in file logs
19+ let file = std::fs::OpenOptions::new()
20+ .write(true)
21+ .create(true)
22+ .truncate(true)
23+ .open(logpath)?;
1924 let file_config = fern::Dispatch::new()
2025 .format(|out, message, record| {
2126 out.finish(format_args!(
@@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
2631 message
2732 ))
2833 })
29− .chain(fern::log_file(logpath)?);
34+ .chain(file);
3035
3136 base_config.chain(file_config).apply()?;
3237
@@ -55,6 +60,7 @@ ARGS:
5560
5661FLAGS:
5762 -h, --help Prints help information
63+ --tutor Loads the tutorial
5864 -v Increases logging verbosity each use for up to 3 times
5965 (default file: {})
6066 -V, --version Prints version information
helix-term/src/ui/completion.rs+61−73
@@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
55use std::borrow::Cow;
66
77use helix_core::Transaction;
8−use helix_view::{graphics::Rect, Document, Editor, View};
8+use helix_view::{graphics::Rect, Document, Editor};
99
1010use crate::commands;
1111use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -30,31 +30,32 @@ impl menu::Item for CompletionItem {
3030 menu::Row::new(vec![
3131 menu::Cell::from(self.label.as_str()),
3232 menu::Cell::from(match self.kind {
33− Some(lsp::CompletionItemKind::Text) => "text",
34− Some(lsp::CompletionItemKind::Method) => "method",
35− Some(lsp::CompletionItemKind::Function) => "function",
36− Some(lsp::CompletionItemKind::Constructor) => "constructor",
37− Some(lsp::CompletionItemKind::Field) => "field",
38− Some(lsp::CompletionItemKind::Variable) => "variable",
39− Some(lsp::CompletionItemKind::Class) => "class",
40− Some(lsp::CompletionItemKind::Interface) => "interface",
41− Some(lsp::CompletionItemKind::Module) => "module",
42− Some(lsp::CompletionItemKind::Property) => "property",
43− Some(lsp::CompletionItemKind::Unit) => "unit",
44− Some(lsp::CompletionItemKind::Value) => "value",
45− Some(lsp::CompletionItemKind::Enum) => "enum",
46− Some(lsp::CompletionItemKind::Keyword) => "keyword",
47− Some(lsp::CompletionItemKind::Snippet) => "snippet",
48− Some(lsp::CompletionItemKind::Color) => "color",
49− Some(lsp::CompletionItemKind::File) => "file",
50− Some(lsp::CompletionItemKind::Reference) => "reference",
51− Some(lsp::CompletionItemKind::Folder) => "folder",
52− Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
53− Some(lsp::CompletionItemKind::Constant) => "constant",
54− Some(lsp::CompletionItemKind::Struct) => "struct",
55− Some(lsp::CompletionItemKind::Event) => "event",
56− Some(lsp::CompletionItemKind::Operator) => "operator",
57− Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
33+ Some(lsp::CompletionItemKind::TEXT) => "text",
34+ Some(lsp::CompletionItemKind::METHOD) => "method",
35+ Some(lsp::CompletionItemKind::FUNCTION) => "function",
36+ Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
37+ Some(lsp::CompletionItemKind::FIELD) => "field",
38+ Some(lsp::CompletionItemKind::VARIABLE) => "variable",
39+ Some(lsp::CompletionItemKind::CLASS) => "class",
40+ Some(lsp::CompletionItemKind::INTERFACE) => "interface",
41+ Some(lsp::CompletionItemKind::MODULE) => "module",
42+ Some(lsp::CompletionItemKind::PROPERTY) => "property",
43+ Some(lsp::CompletionItemKind::UNIT) => "unit",
44+ Some(lsp::CompletionItemKind::VALUE) => "value",
45+ Some(lsp::CompletionItemKind::ENUM) => "enum",
46+ Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
47+ Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
48+ Some(lsp::CompletionItemKind::COLOR) => "color",
49+ Some(lsp::CompletionItemKind::FILE) => "file",
50+ Some(lsp::CompletionItemKind::REFERENCE) => "reference",
51+ Some(lsp::CompletionItemKind::FOLDER) => "folder",
52+ Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
53+ Some(lsp::CompletionItemKind::CONSTANT) => "constant",
54+ Some(lsp::CompletionItemKind::STRUCT) => "struct",
55+ Some(lsp::CompletionItemKind::EVENT) => "event",
56+ Some(lsp::CompletionItemKind::OPERATOR) => "operator",
57+ Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
58+ Some(kind) => unimplemented!("{:?}", kind),
5859 None => "",
5960 }),
6061 // self.detail.as_deref().unwrap_or("")
@@ -83,13 +84,13 @@ impl Completion {
8384 start_offset: usize,
8485 trigger_offset: usize,
8586 ) -> Self {
86− // let items: Vec<CompletionItem> = Vec::new();
8787 let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
8888 fn item_to_transaction(
8989 doc: &Document,
90− view: &View,
9190 item: &CompletionItem,
9291 offset_encoding: helix_lsp::OffsetEncoding,
92+ start_offset: usize,
93+ trigger_offset: usize,
9394 ) -> Transaction {
9495 if let Some(edit) = &item.text_edit {
9596 let edit = match edit {
@@ -105,63 +106,52 @@ impl Completion {
105106 )
106107 } else {
107108 let text = item.insert_text.as_ref().unwrap_or(&item.label);
108− let cursor = doc
109− .selection(view.id)
110− .primary()
111− .cursor(doc.text().slice(..));
109+ // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
110+ // in these cases we need to check for a common prefix and remove it
111+ let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
112+ let text = text.trim_start_matches::<&str>(&prefix);
112113 Transaction::change(
113114 doc.text(),
114− vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
115+ vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
115116 )
116117 }
117118 }
118119
120+ let (view, doc) = current!(editor);
121+
122+ // if more text was entered, remove it
123+ doc.restore(view.id);
124+
119125 match event {
120126 PromptEvent::Abort => {}
121127 PromptEvent::Update => {
122− let (view, doc) = current!(editor);
123−
124128 // always present here
125129 let item = item.unwrap();
126130
127− // if more text was entered, remove it
128− // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
129− let cursor = doc
130− .selection(view.id)
131− .primary()
132− .cursor(doc.text().slice(..));
133− if trigger_offset < cursor {
134− let remove = Transaction::change(
135− doc.text(),
136− vec![(trigger_offset, cursor, None)].into_iter(),
137− );
138− doc.apply(&remove, view.id);
139− }
131+ let transaction = item_to_transaction(
132+ doc,
133+ item,
134+ offset_encoding,
135+ start_offset,
136+ trigger_offset,
137+ );
138+
139+ // initialize a savepoint
140+ doc.savepoint();
140141
141− let transaction = item_to_transaction(doc, view, item, offset_encoding);
142142 doc.apply(&transaction, view.id);
143143 }
144144 PromptEvent::Validate => {
145− let (view, doc) = current!(editor);
146−
147145 // always present here
148146 let item = item.unwrap();
149147
150− // if more text was entered, remove it
151− // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
152− let cursor = doc
153− .selection(view.id)
154− .primary()
155− .cursor(doc.text().slice(..));
156− if trigger_offset < cursor {
157− let remove = Transaction::change(
158− doc.text(),
159− vec![(trigger_offset, cursor, None)].into_iter(),
160− );
161− doc.apply(&remove, view.id);
162− }
163−
164− let transaction = item_to_transaction(doc, view, item, offset_encoding);
148+ let transaction = item_to_transaction(
149+ doc,
150+ item,
151+ offset_encoding,
152+ start_offset,
153+ trigger_offset,
154+ );
165155 doc.apply(&transaction, view.id);
166156
167157 if let Some(additional_edits) = &item.additional_text_edits {
@@ -210,7 +200,7 @@ impl Completion {
210200 .selection(view.id)
211201 .primary()
212202 .cursor(doc.text().slice(..));
213− if self.start_offset <= cursor {
203+ if self.trigger_offset <= cursor {
214204 let fragment = doc.text().slice(self.start_offset..cursor);
215205 let text = Cow::from(fragment);
216206 // TODO: logic is same as ui/picker
@@ -274,12 +264,10 @@ impl Component for Completion {
274264 .language()
275265 .and_then(|scope| scope.strip_prefix("source."))
276266 .unwrap_or("");
277− let cursor_pos = doc
278− .selection(view.id)
279− .primary()
280− .cursor(doc.text().slice(..));
281− let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
282− - view.offset.row) as u16;
267+ let text = doc.text().slice(..);
268+ let cursor_pos = doc.selection(view.id).primary().cursor(text);
269+ let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
270+ let cursor_pos = (coords.row - view.offset.row) as u16;
283271 let mut markdown_doc = match &option.documentation {
284272 Some(lsp::Documentation::String(contents))
285273 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
helix-term/src/ui/editor.rs+94−40
@@ -689,6 +689,8 @@ impl EditorView {
689689 theme: &Theme,
690690 is_focused: bool,
691691 ) {
692+ use tui::text::{Span, Spans};
693+
692694 //-------------------------------
693695 // Left side of the status line.
694696 //-------------------------------
@@ -707,17 +709,17 @@ impl EditorView {
707709 })
708710 .unwrap_or("");
709711
710− let style = if is_focused {
712+ let base_style = if is_focused {
711713 theme.get("ui.statusline")
712714 } else {
713715 theme.get("ui.statusline.inactive")
714716 };
715717 // statusline
716− surface.set_style(viewport.with_height(1), style);
718+ surface.set_style(viewport.with_height(1), base_style);
717719 if is_focused {
718− surface.set_string(viewport.x + 1, viewport.y, mode, style);
720+ surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
719721 }
720− surface.set_string(viewport.x + 5, viewport.y, progress, style);
722+ surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
721723
722724 if let Some(path) = doc.relative_path() {
723725 let path = path.to_string_lossy();
@@ -728,7 +730,7 @@ impl EditorView {
728730 viewport.y,
729731 title,
730732 viewport.width.saturating_sub(6) as usize,
731− style,
733+ base_style,
732734 );
733735 }
734736
@@ -736,8 +738,50 @@ impl EditorView {
736738 // Right side of the status line.
737739 //-------------------------------
738740
739− // Compute the individual info strings.
740− let diag_count = format!("{}", doc.diagnostics().len());
741+ let mut right_side_text = Spans::default();
742+
743+ // Compute the individual info strings and add them to `right_side_text`.
744+
745+ // Diagnostics
746+ let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
747+ use helix_core::diagnostic::Severity;
748+ match diag.severity {
749+ Some(Severity::Warning) => counts.0 += 1,
750+ Some(Severity::Error) | None => counts.1 += 1,
751+ _ => {}
752+ }
753+ counts
754+ });
755+ let (warnings, errors) = diags;
756+ let warning_style = theme.get("warning");
757+ let error_style = theme.get("error");
758+ for i in 0..2 {
759+ let (count, style) = match i {
760+ 0 => (warnings, warning_style),
761+ 1 => (errors, error_style),
762+ _ => unreachable!(),
763+ };
764+ if count == 0 {
765+ continue;
766+ }
767+ let style = base_style.patch(style);
768+ right_side_text.0.push(Span::styled("●", style));
769+ right_side_text
770+ .0
771+ .push(Span::styled(format!(" {} ", count), base_style));
772+ }
773+
774+ // Selections
775+ let sels_count = doc.selection(view.id).len();
776+ right_side_text.0.push(Span::styled(
777+ format!(
778+ " {} sel{} ",
779+ sels_count,
780+ if sels_count == 1 { "" } else { "s" }
781+ ),
782+ base_style,
783+ ));
784+
741785 // let indent_info = match doc.indent_style {
742786 // IndentStyle::Tabs => "tabs",
743787 // IndentStyle::Spaces(1) => "spaces:1",
@@ -750,29 +794,28 @@ impl EditorView {
750794 // IndentStyle::Spaces(8) => "spaces:8",
751795 // _ => "indent:ERROR",
752796 // };
753− let position_info = {
754− let pos = coords_at_pos(
755− doc.text().slice(..),
756− doc.selection(view.id)
757− .primary()
758− .cursor(doc.text().slice(..)),
759− );
760− format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
761− };
762797
763− // Render them to the status line together.
764− let right_side_text = format!(
765− "{} {} ",
766− &diag_count[..diag_count.len().min(4)],
767− // indent_info,
768− position_info
798+ // Position
799+ let pos = coords_at_pos(
800+ doc.text().slice(..),
801+ doc.selection(view.id)
802+ .primary()
803+ .cursor(doc.text().slice(..)),
769804 );
770− let text_len = right_side_text.len() as u16;
771− surface.set_string(
772− viewport.x + viewport.width.saturating_sub(text_len),
805+ right_side_text.0.push(Span::styled(
806+ format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
807+ base_style,
808+ ));
809+
810+ // Render to the statusline.
811+ surface.set_spans(
812+ viewport.x
813+ + viewport
814+ .width
815+ .saturating_sub(right_side_text.width() as u16),
773816 viewport.y,
774− right_side_text,
775− style,
817+ &right_side_text,
818+ right_side_text.width() as u16,
776819 );
777820 }
778821
@@ -984,7 +1027,7 @@ impl EditorView {
9841027
9851028 pub fn set_completion(
9861029 &mut self,
987− editor: &Editor,
1030+ editor: &mut Editor,
9881031 items: Vec<helix_lsp::lsp::CompletionItem>,
9891032 offset_encoding: helix_lsp::OffsetEncoding,
9901033 start_offset: usize,
@@ -999,10 +1042,21 @@ impl EditorView {
9991042 return;
10001043 }
10011044
1045+ // Immediately initialize a savepoint
1046+ doc_mut!(editor).savepoint();
1047+
10021048 // TODO : propagate required size on resize to completion too
10031049 completion.required_size((size.width, size.height));
10041050 self.completion = Some(completion);
10051051 }
1052+
1053+ pub fn clear_completion(&mut self, editor: &mut Editor) {
1054+ self.completion = None;
1055+ // Clear any savepoints
1056+ let (_, doc) = current!(editor);
1057+ doc.savepoint = None;
1058+ editor.clear_idle_timer(); // don't retrigger
1059+ }
10061060}
10071061
10081062impl EditorView {
@@ -1022,12 +1076,12 @@ impl EditorView {
10221076 let editor = &mut cxt.editor;
10231077
10241078 let result = editor.tree.views().find_map(|(view, _focus)| {
1025− view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
1079+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
10261080 .map(|pos| (pos, view.id))
10271081 });
10281082
10291083 if let Some((pos, view_id)) = result {
1030− let doc = &mut editor.documents[editor.tree.get(view_id).doc];
1084+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
10311085
10321086 if modifiers == crossterm::event::KeyModifiers::ALT {
10331087 let selection = doc.selection(view_id).clone();
@@ -1096,7 +1150,7 @@ impl EditorView {
10961150 };
10971151
10981152 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
1099− view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
1153+ view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
11001154 .map(|_| view.id)
11011155 });
11021156
@@ -1182,12 +1236,12 @@ impl EditorView {
11821236 }
11831237
11841238 let result = editor.tree.views().find_map(|(view, _focus)| {
1185− view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
1239+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
11861240 .map(|pos| (pos, view.id))
11871241 });
11881242
11891243 if let Some((pos, view_id)) = result {
1190− let doc = &mut editor.documents[editor.tree.get(view_id).doc];
1244+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
11911245 doc.set_selection(view_id, Selection::point(pos));
11921246 editor.tree.focus = view_id;
11931247 commands::Command::paste_primary_clipboard_before.execute(cxt);
@@ -1254,8 +1308,7 @@ impl Component for EditorView {
12541308
12551309 if callback.is_some() {
12561310 // assume close_fn
1257− self.completion = None;
1258− cxt.editor.clear_idle_timer(); // don't retrigger
1311+ self.clear_completion(cxt.editor);
12591312 }
12601313 }
12611314 }
@@ -1268,8 +1321,7 @@ impl Component for EditorView {
12681321 if let Some(completion) = &mut self.completion {
12691322 completion.update(&mut cxt);
12701323 if completion.is_empty() {
1271− self.completion = None;
1272− cxt.editor.clear_idle_timer(); // don't retrigger
1324+ self.clear_completion(cxt.editor);
12731325 }
12741326 }
12751327 }
@@ -1397,8 +1449,10 @@ impl Component for EditorView {
13971449 info.render(area, surface, cx);
13981450 }
13991451
1400− if let Some(ref mut info) = self.autoinfo {
1401− info.render(area, surface, cx);
1452+ if cx.editor.config.auto_info {
1453+ if let Some(ref mut info) = self.autoinfo {
1454+ info.render(area, surface, cx);
1455+ }
14021456 }
14031457
14041458 let key_width = 15u16; // for showing pending keys
@@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
14691523}
14701524
14711525#[inline]
1472−fn abs_diff(a: usize, b: usize) -> usize {
1526+const fn abs_diff(a: usize, b: usize) -> usize {
14731527 if a > b {
14741528 a - b
14751529 } else {
helix-term/src/ui/menu.rs+25−18
@@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
6464 }
6565
6666 pub fn score(&mut self, pattern: &str) {
67− // need to borrow via pattern match otherwise it complains about simultaneous borrow
68− let Self {
69− ref mut matcher,
70− ref mut matches,
71− ref options,
72− ..
73− } = *self;
74−
7567 // reuse the matches allocation
76− matches.clear();
77− matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
78− let text = option.filter_text();
79− // TODO: using fuzzy_indices could give us the char idx for match highlighting
80− matcher
81− .fuzzy_match(text, pattern)
82− .map(|score| (index, score))
83− }));
68+ self.matches.clear();
69+ self.matches.extend(
70+ self.options
71+ .iter()
72+ .enumerate()
73+ .filter_map(|(index, option)| {
74+ let text = option.filter_text();
75+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
76+ self.matcher
77+ .fuzzy_match(text, pattern)
78+ .map(|score| (index, score))
79+ }),
80+ );
8481 // matches.sort_unstable_by_key(|(_, score)| -score);
85− matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
82+ self.matches
83+ .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
8684
8785 // reset cursor position
8886 self.cursor = None;
@@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
10098
10199 pub fn move_up(&mut self) {
102100 let len = self.matches.len();
103− let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
101+ let max_index = len.saturating_sub(1);
102+ let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
104103 self.cursor = Some(pos);
105104 self.adjust_scroll();
106105 }
@@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
216215 | KeyEvent {
217216 code: KeyCode::Char('p'),
218217 modifiers: KeyModifiers::CONTROL,
218+ }
219+ | KeyEvent {
220+ code: KeyCode::Char('k'),
221+ modifiers: KeyModifiers::CONTROL,
219222 } => {
220223 self.move_up();
221224 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
233236 | KeyEvent {
234237 code: KeyCode::Char('n'),
235238 modifiers: KeyModifiers::CONTROL,
239+ }
240+ | KeyEvent {
241+ code: KeyCode::Char('j'),
242+ modifiers: KeyModifiers::CONTROL,
236243 } => {
237244 self.move_down();
238245 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
helix-term/src/ui/mod.rs+20−3
@@ -29,6 +29,7 @@ pub fn regex_prompt(
2929 cx: &mut crate::commands::Context,
3030 prompt: std::borrow::Cow<'static, str>,
3131 history_register: Option<char>,
32+ completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
3233 fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
3334) -> Prompt {
3435 let (view, doc) = current!(cx.editor);
@@ -38,7 +39,7 @@ pub fn regex_prompt(
3839 Prompt::new(
3940 prompt,
4041 history_register,
41− |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
42+ completion_fn,
4243 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
4344 match event {
4445 PromptEvent::Abort => {
@@ -92,9 +93,25 @@ pub fn regex_prompt(
9293}
9394
9495pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
95− use ignore::Walk;
96+ use ignore::{types::TypesBuilder, WalkBuilder};
9697 use std::time;
97− let files = Walk::new(&root).filter_map(|entry| {
98+
99+ // We want to exclude files that the editor can't handle yet
100+ let mut type_builder = TypesBuilder::new();
101+ let mut walk_builder = WalkBuilder::new(&root);
102+ let walk_builder = match type_builder.add(
103+ "compressed",
104+ "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
105+ ) {
106+ Err(_) => &walk_builder,
107+ _ => {
108+ type_builder.negate("all");
109+ let excluded_types = type_builder.build().unwrap();
110+ walk_builder.types(excluded_types)
111+ }
112+ };
113+
114+ let files = walk_builder.build().filter_map(|entry| {
98115 let entry = entry.ok()?;
99116 // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
100117 if entry.path().is_dir() {
helix-term/src/ui/picker.rs+120−37
@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
1212use fuzzy_matcher::FuzzyMatcher;
1313use tui::widgets::Widget;
1414
15−use std::{borrow::Cow, collections::HashMap, path::PathBuf};
15+use std::{
16+ borrow::Cow,
17+ collections::HashMap,
18+ io::Read,
19+ path::{Path, PathBuf},
20+};
1621
1722use crate::ui::{Prompt, PromptEvent};
1823use helix_core::Position;
@@ -23,18 +28,58 @@ use helix_view::{
2328};
2429
2530pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
31+/// Biggest file size to preview in bytes
32+pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
2633
27−/// File path and line number (used to align and highlight a line)
34+/// File path and range of lines (used to align and highlight lines)
2835type FileLocation = (PathBuf, Option<(usize, usize)>);
2936
3037pub struct FilePicker<T> {
3138 picker: Picker<T>,
3239 /// Caches paths to documents
33− preview_cache: HashMap<PathBuf, Document>,
40+ preview_cache: HashMap<PathBuf, CachedPreview>,
41+ read_buffer: Vec<u8>,
3442 /// Given an item in the picker, return the file path and line number to display.
3543 file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
3644}
3745
46+pub enum CachedPreview {
47+ Document(Document),
48+ Binary,
49+ LargeFile,
50+ NotFound,
51+}
52+
53+// We don't store this enum in the cache so as to avoid lifetime constraints
54+// from borrowing a document already opened in the editor.
55+pub enum Preview<'picker, 'editor> {
56+ Cached(&'picker CachedPreview),
57+ EditorDocument(&'editor Document),
58+}
59+
60+impl Preview<'_, '_> {
61+ fn document(&self) -> Option<&Document> {
62+ match self {
63+ Preview::EditorDocument(doc) => Some(doc),
64+ Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
65+ _ => None,
66+ }
67+ }
68+
69+ /// Alternate text to show for the preview.
70+ fn placeholder(&self) -> &str {
71+ match *self {
72+ Self::EditorDocument(_) => "<File preview>",
73+ Self::Cached(preview) => match preview {
74+ CachedPreview::Document(_) => "<File preview>",
75+ CachedPreview::Binary => "<Binary file>",
76+ CachedPreview::LargeFile => "<File too large to preview>",
77+ CachedPreview::NotFound => "<File not found>",
78+ },
79+ }
80+ }
81+}
82+
3883impl<T> FilePicker<T> {
3984 pub fn new(
4085 options: Vec<T>,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
4590 Self {
4691 picker: Picker::new(false, options, format_fn, callback_fn),
4792 preview_cache: HashMap::new(),
93+ read_buffer: Vec::with_capacity(1024),
4894 file_fn: Box::new(preview_fn),
4995 }
5096 }
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
60106 })
61107 }
62108
63− fn calculate_preview(&mut self, editor: &Editor) {
64− if let Some((path, _line)) = self.current_file(editor) {
65− if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
66− // TODO: enable syntax highlighting; blocked by async rendering
67− let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
68− self.preview_cache.insert(path, doc);
69− }
109+ /// Get (cached) preview for a given path. If a document corresponding
110+ /// to the path is already open in the editor, it is used instead.
111+ fn get_preview<'picker, 'editor>(
112+ &'picker mut self,
113+ path: &Path,
114+ editor: &'editor Editor,
115+ ) -> Preview<'picker, 'editor> {
116+ if let Some(doc) = editor.document_by_path(path) {
117+ return Preview::EditorDocument(doc);
118+ }
119+
120+ if self.preview_cache.contains_key(path) {
121+ return Preview::Cached(&self.preview_cache[path]);
70122 }
123+
124+ let data = std::fs::File::open(path).and_then(|file| {
125+ let metadata = file.metadata()?;
126+ // Read up to 1kb to detect the content type
127+ let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
128+ let content_type = content_inspector::inspect(&self.read_buffer[..n]);
129+ self.read_buffer.clear();
130+ Ok((metadata, content_type))
131+ });
132+ let preview = data
133+ .map(
134+ |(metadata, content_type)| match (metadata.len(), content_type) {
135+ (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
136+ (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
137+ _ => {
138+ // TODO: enable syntax highlighting; blocked by async rendering
139+ Document::open(path, None, Some(&editor.theme), None)
140+ .map(CachedPreview::Document)
141+ .unwrap_or(CachedPreview::NotFound)
142+ }
143+ },
144+ )
145+ .unwrap_or(CachedPreview::NotFound);
146+ self.preview_cache.insert(path.to_owned(), preview);
147+ Preview::Cached(&self.preview_cache[path])
71148 }
72149}
73150
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
79156 // |picker | | |
80157 // | | | |
81158 // +---------+ +---------+
82− self.calculate_preview(cx.editor);
83159 let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
84160 let area = inner_rect(area);
85161 // -- Render the frame:
86162 // clear area
87163 let background = cx.editor.theme.get("ui.background");
164+ let text = cx.editor.theme.get("ui.text");
88165 surface.clear_with(area, background);
89166
90167 let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
113190 horizontal: 1,
114191 };
115192 let inner = inner.inner(&margin);
116−
117193 block.render(preview_area, surface);
118194
119− if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
120− cx.editor
121− .document_by_path(&path)
122− .or_else(|| self.preview_cache.get(&path))
123− .zip(Some(range))
124− }) {
195+ if let Some((path, range)) = self.current_file(cx.editor) {
196+ let preview = self.get_preview(&path, cx.editor);
197+ let doc = match preview.document() {
198+ Some(doc) => doc,
199+ None => {
200+ let alt_text = preview.placeholder();
201+ let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
202+ let y = inner.y + inner.height / 2;
203+ surface.set_stringn(x, y, alt_text, inner.width as usize, text);
204+ return;
205+ }
206+ };
207+
125208 // align to middle
126− let first_line = line
209+ let first_line = range
127210 .map(|(start, end)| {
128211 let height = end.saturating_sub(start) + 1;
129212 let middle = start + (height.saturating_sub(1) / 2);
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
150233 );
151234
152235 // highlight the line
153− if let Some((start, end)) = line {
236+ if let Some((start, end)) = range {
154237 let offset = start.saturating_sub(first_line) as u16;
155238 surface.set_style(
156239 Rect::new(
@@ -234,37 +317,28 @@ impl<T> Picker<T> {
234317 }
235318
236319 pub fn score(&mut self) {
237− // need to borrow via pattern match otherwise it complains about simultaneous borrow
238− let Self {
239− ref mut matcher,
240− ref mut matches,
241− ref filters,
242− ref format_fn,
243− ..
244− } = *self;
245−
246320 let pattern = &self.prompt.line;
247321
248322 // reuse the matches allocation
249− matches.clear();
250− matches.extend(
323+ self.matches.clear();
324+ self.matches.extend(
251325 self.options
252326 .iter()
253327 .enumerate()
254328 .filter_map(|(index, option)| {
255329 // filter options first before matching
256− if !filters.is_empty() {
257− filters.binary_search(&index).ok()?;
330+ if !self.filters.is_empty() {
331+ self.filters.binary_search(&index).ok()?;
258332 }
259333 // TODO: maybe using format_fn isn't the best idea here
260− let text = (format_fn)(option);
334+ let text = (self.format_fn)(option);
261335 // TODO: using fuzzy_indices could give us the char idx for match highlighting
262− matcher
336+ self.matcher
263337 .fuzzy_match(&text, pattern)
264338 .map(|score| (index, score))
265339 }),
266340 );
267− matches.sort_unstable_by_key(|(_, score)| -score);
341+ self.matches.sort_unstable_by_key(|(_, score)| -score);
268342
269343 // reset cursor position
270344 self.cursor = 0;
@@ -337,6 +411,10 @@ impl<T: 'static> Component for Picker<T> {
337411 code: KeyCode::BackTab,
338412 ..
339413 }
414+ | KeyEvent {
415+ code: KeyCode::Char('k'),
416+ modifiers: KeyModifiers::CONTROL,
417+ }
340418 | KeyEvent {
341419 code: KeyCode::Char('p'),
342420 modifiers: KeyModifiers::CONTROL,
@@ -350,6 +428,10 @@ impl<T: 'static> Component for Picker<T> {
350428 | KeyEvent {
351429 code: KeyCode::Tab, ..
352430 }
431+ | KeyEvent {
432+ code: KeyCode::Char('j'),
433+ modifiers: KeyModifiers::CONTROL,
434+ }
353435 | KeyEvent {
354436 code: KeyCode::Char('n'),
355437 modifiers: KeyModifiers::CONTROL,
@@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
375457 return close_fn;
376458 }
377459 KeyEvent {
378− code: KeyCode::Char('h'),
460+ code: KeyCode::Char('s'),
379461 modifiers: KeyModifiers::CONTROL,
380462 } => {
381463 if let Some(option) = self.selection() {
@@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
485567 text_style
486568 },
487569 true,
570+ true,
488571 );
489572 }
490573 }
helix-term/src/ui/prompt.rs+35−2
@@ -186,6 +186,11 @@ impl Prompt {
186186 self.exit_selection();
187187 }
188188
189+ pub fn insert_str(&mut self, s: &str) {
190+ self.line.insert_str(self.cursor, s);
191+ self.cursor += s.len();
192+ }
193+
189194 pub fn move_cursor(&mut self, movement: Movement) {
190195 let pos = self.eval_movement(movement);
191196 self.cursor = pos
@@ -474,6 +479,26 @@ impl Component for Prompt {
474479 self.delete_char_backwards();
475480 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
476481 }
482+ KeyEvent {
483+ code: KeyCode::Char('s'),
484+ modifiers: KeyModifiers::CONTROL,
485+ } => {
486+ let (view, doc) = current!(cx.editor);
487+ let text = doc.text().slice(..);
488+
489+ use helix_core::textobject;
490+ let range = textobject::textobject_word(
491+ text,
492+ doc.selection(view.id).primary(),
493+ textobject::TextObject::Inside,
494+ 1,
495+ );
496+ let line = text.slice(range.from()..range.to()).to_string();
497+ if !line.is_empty() {
498+ self.insert_str(line.as_str());
499+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
500+ }
501+ }
477502 KeyEvent {
478503 code: KeyCode::Enter,
479504 ..
@@ -502,6 +527,7 @@ impl Component for Prompt {
502527 if let Some(register) = self.history_register {
503528 let register = cx.editor.registers.get_mut(register);
504529 self.change_history(register.read(), CompletionDirection::Backward);
530+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
505531 }
506532 }
507533 KeyEvent {
@@ -515,15 +541,22 @@ impl Component for Prompt {
515541 if let Some(register) = self.history_register {
516542 let register = cx.editor.registers.get_mut(register);
517543 self.change_history(register.read(), CompletionDirection::Forward);
544+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
518545 }
519546 }
520547 KeyEvent {
521548 code: KeyCode::Tab, ..
522− } => self.change_completion_selection(CompletionDirection::Forward),
549+ } => {
550+ self.change_completion_selection(CompletionDirection::Forward);
551+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
552+ }
523553 KeyEvent {
524554 code: KeyCode::BackTab,
525555 ..
526− } => self.change_completion_selection(CompletionDirection::Backward),
556+ } => {
557+ self.change_completion_selection(CompletionDirection::Backward);
558+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
559+ }
527560 KeyEvent {
528561 code: KeyCode::Char('q'),
529562 modifiers: KeyModifiers::CONTROL,
helix-tui/Cargo.toml+5−5
@@ -1,11 +1,11 @@
11[package]
22name = "helix-tui"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
55description = """
66A library to build rich terminal user interfaces or dashboards
77"""
8−edition = "2018"
8+edition = "2021"
99license = "MPL-2.0"
1010categories = ["editor"]
1111repository = "https://github.com/helix-editor/helix"
@@ -19,7 +19,7 @@ default = ["crossterm"]
1919bitflags = "1.3"
2020cassowary = "0.3"
2121unicode-segmentation = "1.8"
22−crossterm = { version = "0.21", optional = true }
22+crossterm = { version = "0.22", optional = true }
2323serde = { version = "1", "optional" = true, features = ["derive"]}
24−helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
25−helix-core = { version = "0.4", path = "../helix-core" }
24+helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
25+helix-core = { version = "0.5", path = "../helix-core" }
helix-tui/src/buffer.rs+56−22
@@ -266,12 +266,14 @@ impl Buffer {
266266 where
267267 S: AsRef<str>,
268268 {
269− self.set_string_truncated(x, y, string, width, style, false)
269+ self.set_string_truncated(x, y, string, width, style, false, false)
270270 }
271271
272272 /// Print at most the first `width` characters of a string if enough space is available
273− /// until the end of the line. If `markend` is true appends a `…` at the end of
274− /// truncated lines.
273+ /// until the end of the line. If `ellipsis` is true appends a `…` at the end of
274+ /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
275+ /// instead of the end.
276+ #[allow(clippy::too_many_arguments)]
275277 pub fn set_string_truncated<S>(
276278 &mut self,
277279 x: u16,
@@ -280,6 +282,7 @@ impl Buffer {
280282 width: usize,
281283 style: Style,
282284 ellipsis: bool,
285+ truncate_start: bool,
283286 ) -> (u16, u16)
284287 where
285288 S: AsRef<str>,
@@ -289,28 +292,59 @@ impl Buffer {
289292 let width = if ellipsis { width - 1 } else { width };
290293 let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
291294 let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
292− for s in graphemes {
293− let width = s.width();
294− if width == 0 {
295− continue;
295+ if !truncate_start {
296+ for s in graphemes {
297+ let width = s.width();
298+ if width == 0 {
299+ continue;
300+ }
301+ // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
302+ // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
303+ if width > max_offset.saturating_sub(x_offset) {
304+ break;
305+ }
306+
307+ self.content[index].set_symbol(s);
308+ self.content[index].set_style(style);
309+ // Reset following cells if multi-width (they would be hidden by the grapheme),
310+ for i in index + 1..index + width {
311+ self.content[i].reset();
312+ }
313+ index += width;
314+ x_offset += width;
296315 }
297− // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
298− // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
299− if width > max_offset.saturating_sub(x_offset) {
300− break;
316+ if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
317+ self.content[index].set_symbol("…");
301318 }
302−
303− self.content[index].set_symbol(s);
304− self.content[index].set_style(style);
305− // Reset following cells if multi-width (they would be hidden by the grapheme),
306− for i in index + 1..index + width {
307− self.content[i].reset();
319+ } else {
320+ let mut start_index = self.index_of(x, y);
321+ let mut index = self.index_of(max_offset as u16, y);
322+
323+ let total_width = string.as_ref().width();
324+ let truncated = total_width > width;
325+ if ellipsis && truncated {
326+ self.content[start_index].set_symbol("…");
327+ start_index += 1;
328+ }
329+ if !truncated {
330+ index -= width - total_width;
331+ }
332+ for s in graphemes.rev() {
333+ let width = s.width();
334+ if width == 0 {
335+ continue;
336+ }
337+ let start = index - width;
338+ if start < start_index {
339+ break;
340+ }
341+ self.content[start].set_symbol(s);
342+ self.content[start].set_style(style);
343+ for i in start + 1..index {
344+ self.content[i].reset();
345+ }
346+ index -= width;
308347 }
309− index += width;
310− x_offset += width;
311− }
312− if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
313− self.content[index].set_symbol("…");
314348 }
315349 (x_offset as u16, y)
316350 }
helix-view/Cargo.toml+6−6
@@ -1,8 +1,8 @@
11[package]
22name = "helix-view"
3−version = "0.4.1"
3+version = "0.5.0"
44authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
66license = "MPL-2.0"
77description = "UI abstractions for use in backends"
88categories = ["editor"]
@@ -16,10 +16,10 @@ term = ["crossterm"]
1616[dependencies]
1717bitflags = "1.3"
1818anyhow = "1"
19−helix-core = { version = "0.4", path = "../helix-core" }
20−helix-lsp = { version = "0.4", path = "../helix-lsp"}
21−helix-dap = { version = "0.4", path = "../helix-dap"}
22−crossterm = { version = "0.21", optional = true }
19+helix-core = { version = "0.5", path = "../helix-core" }
20+helix-lsp = { version = "0.5", path = "../helix-lsp"}
21+helix-dap = { version = "0.5", path = "../helix-dap"}
22+crossterm = { version = "0.22", optional = true }
2323
2424# Conversion traits
2525once_cell = "1.8"
helix-view/src/clipboard.rs+6−16
@@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
116116 }
117117 } else {
118118 #[cfg(target_os = "windows")]
119− return Box::new(provider::WindowsProvider::new());
119+ return Box::new(provider::WindowsProvider::default());
120120
121121 #[cfg(not(target_os = "windows"))]
122122 return Box::new(provider::NopProvider::new());
@@ -145,15 +145,15 @@ mod provider {
145145 use anyhow::{bail, Context as _, Result};
146146 use std::borrow::Cow;
147147
148+ #[cfg(not(target_os = "windows"))]
148149 #[derive(Debug)]
149150 pub struct NopProvider {
150151 buf: String,
151152 primary_buf: String,
152153 }
153154
155+ #[cfg(not(target_os = "windows"))]
154156 impl NopProvider {
155− #[allow(dead_code)]
156− // Only dead_code on Windows.
157157 pub fn new() -> Self {
158158 Self {
159159 buf: String::new(),
@@ -162,6 +162,7 @@ mod provider {
162162 }
163163 }
164164
165+ #[cfg(not(target_os = "windows"))]
165166 impl ClipboardProvider for NopProvider {
166167 fn name(&self) -> Cow<str> {
167168 Cow::Borrowed("none")
@@ -186,19 +187,8 @@ mod provider {
186187 }
187188
188189 #[cfg(target_os = "windows")]
189− #[derive(Debug)]
190− pub struct WindowsProvider {
191− selection_buf: String,
192− }
193−
194− #[cfg(target_os = "windows")]
195− impl WindowsProvider {
196− pub fn new() -> Self {
197− Self {
198− selection_buf: String::new(),
199− }
200− }
201− }
190+ #[derive(Default, Debug)]
191+ pub struct WindowsProvider;
202192
203193 #[cfg(target_os = "windows")]
204194 impl ClipboardProvider for WindowsProvider {
helix-view/src/document.rs+54−27
@@ -23,6 +23,8 @@ use crate::{DocumentId, Theme, ViewId};
2323/// 8kB of buffer space for encoding and decoding `Rope`s.
2424const BUF_SIZE: usize = 8192;
2525
26+const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
27+
2628#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
2729pub enum Mode {
2830 Normal,
@@ -95,6 +97,9 @@ pub struct Document {
9597 // it back as it separated from the edits. We could split out the parts manually but that will
9698 // be more troublesome.
9799 history: Cell<History>,
100+
101+ pub savepoint: Option<Transaction>,
102+
98103 last_saved_revision: usize,
99104 version: i32, // should be usize?
100105
@@ -306,8 +311,7 @@ where
306311 T: Default,
307312 F: FnOnce(T) -> T,
308313{
309− let t = mem::take(mut_ref);
310− let _ = mem::replace(mut_ref, f(t));
314+ *mut_ref = f(mem::take(mut_ref));
311315}
312316
313317use helix_lsp::lsp;
@@ -325,7 +329,8 @@ impl Document {
325329 encoding,
326330 text,
327331 selections: HashMap::default(),
328− indent_style: IndentStyle::Spaces(4),
332+ indent_style: DEFAULT_INDENT,
333+ line_ending: DEFAULT_LINE_ENDING,
329334 mode: Mode::Normal,
330335 restore_cursor: false,
331336 syntax: None,
@@ -335,9 +340,9 @@ impl Document {
335340 diagnostics: Vec::new(),
336341 version: 0,
337342 history: Cell::new(History::default()),
343+ savepoint: None,
338344 last_saved_revision: 0,
339345 language_server: None,
340− line_ending: DEFAULT_LINE_ENDING,
341346 }
342347 }
343348
@@ -363,7 +368,7 @@ impl Document {
363368 let mut doc = Self::from(rope, Some(encoding));
364369
365370 // set the path and try detecting the language
366− doc.set_path(path)?;
371+ doc.set_path(Some(path))?;
367372 if let Some(loader) = config_loader {
368373 doc.detect_language(theme, loader);
369374 }
@@ -495,17 +500,15 @@ impl Document {
495500 }
496501
497502 /// Detect the indentation used in the file, or otherwise defaults to the language indentation
498− /// configured in `languages.toml`, with a fallback back to 2 space indentation if it isn't
503+ /// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't
499504 /// specified. Line ending is likewise auto-detected, and will fallback to the default OS
500505 /// line ending.
501506 pub fn detect_indent_and_line_ending(&mut self) {
502507 self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
503− IndentStyle::from_str(
504− self.language
505− .as_ref()
506− .and_then(|config| config.indent.as_ref())
507− .map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces.
508− )
508+ self.language
509+ .as_ref()
510+ .and_then(|config| config.indent.as_ref())
511+ .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
509512 });
510513 self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
511514 }
@@ -550,12 +553,14 @@ impl Document {
550553 self.encoding
551554 }
552555
553− pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
554− let path = helix_core::path::get_canonicalized_path(path)?;
556+ pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
557+ let path = path
558+ .map(helix_core::path::get_canonicalized_path)
559+ .transpose()?;
555560
556561 // if parent doesn't exist we still want to open the document
557562 // and error out when document is saved
558− self.path = Some(path);
563+ self.path = path;
559564
560565 Ok(())
561566 }
@@ -635,6 +640,14 @@ impl Document {
635640 if !transaction.changes().is_empty() {
636641 self.version += 1;
637642
643+ // generate revert to savepoint
644+ if self.savepoint.is_some() {
645+ take_with(&mut self.savepoint, |prev_revert| {
646+ let revert = transaction.invert(&old_doc);
647+ Some(revert.compose(prev_revert.unwrap()))
648+ });
649+ }
650+
638651 // update tree-sitter syntax tree
639652 if let Some(syntax) = &mut self.syntax {
640653 // TODO: no unwrap
@@ -644,14 +657,13 @@ impl Document {
644657 }
645658
646659 // map state.diagnostics over changes::map_pos too
647− // NOTE: seems to do nothing since the language server resends diagnostics on each edit
648− // for diagnostic in &mut self.diagnostics {
649− // use helix_core::Assoc;
650− // let changes = transaction.changes();
651− // diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
652− // diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
653− // diagnostic.line = self.text.char_to_line(diagnostic.range.start);
654− // }
660+ for diagnostic in &mut self.diagnostics {
661+ use helix_core::Assoc;
662+ let changes = transaction.changes();
663+ diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
664+ diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
665+ diagnostic.line = self.text.char_to_line(diagnostic.range.start);
666+ }
655667
656668 // emit lsp notification
657669 if let Some(language_server) = self.language_server() {
@@ -692,8 +704,8 @@ impl Document {
692704 success
693705 }
694706
695− /// Undo the last modification to the [`Document`].
696− pub fn undo(&mut self, view_id: ViewId) {
707+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
708+ pub fn undo(&mut self, view_id: ViewId) -> bool {
697709 let mut history = self.history.take();
698710 let success = if let Some(transaction) = history.undo() {
699711 self.apply_impl(transaction, view_id)
@@ -706,10 +718,11 @@ impl Document {
706718 // reset changeset to fix len
707719 self.changes = ChangeSet::new(self.text());
708720 }
721+ success
709722 }
710723
711− /// Redo the last modification to the [`Document`].
712− pub fn redo(&mut self, view_id: ViewId) {
724+ /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
725+ pub fn redo(&mut self, view_id: ViewId) -> bool {
713726 let mut history = self.history.take();
714727 let success = if let Some(transaction) = history.redo() {
715728 self.apply_impl(transaction, view_id)
@@ -722,6 +735,17 @@ impl Document {
722735 // reset changeset to fix len
723736 self.changes = ChangeSet::new(self.text());
724737 }
738+ success
739+ }
740+
741+ pub fn savepoint(&mut self) {
742+ self.savepoint = Some(Transaction::new(self.text()));
743+ }
744+
745+ pub fn restore(&mut self, view_id: ViewId) {
746+ if let Some(revert) = self.savepoint.take() {
747+ self.apply(&revert, view_id);
748+ }
725749 }
726750
727751 /// Undo modifications to the [`Document`] according to `uk`.
@@ -894,6 +918,9 @@ impl Document {
894918
895919 pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
896920 self.diagnostics = diagnostics;
921+ // sort by range
922+ self.diagnostics
923+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
897924 }
898925}
899926
helix-view/src/editor.rs+94−30
@@ -2,7 +2,7 @@ use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
33 graphics::{CursorKind, Rect},
44 theme::{self, Theme},
5− tree::Tree,
5+ tree::{self, Tree},
66 Document, DocumentId, View, ViewId,
77};
88
@@ -12,6 +12,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
1212
1313use std::{
1414 collections::HashMap,
15+ collections::BTreeMap,
1516 path::{Path, PathBuf},
1617 pin::Pin,
1718 sync::Arc,
@@ -19,8 +20,6 @@ use std::{
1920
2021use tokio::time::{sleep, Duration, Instant, Sleep};
2122
22−use slotmap::SlotMap;
23−
2423use anyhow::Error;
2524
2625pub use helix_core::diagnostic::Severity;
@@ -63,6 +62,9 @@ pub struct Config {
6362 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
6463 #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
6564 pub idle_timeout: Duration,
65+ pub completion_trigger_len: u8,
66+ /// Whether to display infoboxes. Defaults to true.
67+ pub auto_info: bool,
6668}
6769
6870#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -92,14 +94,29 @@ impl Default for Config {
9294 auto_pairs: true,
9395 auto_completion: true,
9496 idle_timeout: Duration::from_millis(400),
97+ completion_trigger_len: 2,
98+ auto_info: true,
9599 }
96100 }
97101}
98102
103+pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
104+impl Motion {
105+ pub fn run(&self, e: &mut Editor) {
106+ (self.0)(e)
107+ }
108+}
109+impl std::fmt::Debug for Motion {
110+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111+ f.write_str("motion")
112+ }
113+}
114+
99115#[derive(Debug)]
100116pub struct Editor {
101117 pub tree: Tree,
102− pub documents: SlotMap<DocumentId, Document>,
118+ pub next_document_id: usize,
119+ pub documents: BTreeMap<DocumentId, Document>,
103120 pub count: Option<std::num::NonZeroUsize>,
104121 pub selected_register: Option<char>,
105122 pub registers: Registers,
@@ -124,6 +141,7 @@ pub struct Editor {
124141 pub config: Config,
125142
126143 pub idle_timer: Pin<Box<Sleep>>,
144+ pub last_motion: Option<Motion>,
127145}
128146
129147#[derive(Debug, Copy, Clone)]
@@ -148,7 +166,8 @@ impl Editor {
148166
149167 Self {
150168 tree: Tree::new(area),
151− documents: SlotMap::with_key(),
169+ next_document_id: 0,
170+ documents: BTreeMap::new(),
152171 count: None,
153172 selected_register: None,
154173 theme: themes.default(),
@@ -166,6 +185,7 @@ impl Editor {
166185 clipboard_provider: get_clipboard_provider(),
167186 status_msg: None,
168187 idle_timer: Box::pin(sleep(config.idle_timeout)),
188+ last_motion: None,
169189 config,
170190 }
171191 }
@@ -221,7 +241,7 @@ impl Editor {
221241
222242 fn _refresh(&mut self) {
223243 for (view, _) in self.tree.views_mut() {
224− let doc = &self.documents[view.doc];
244+ let doc = &self.documents[&view.doc];
225245 view.ensure_cursor_in_view(doc, self.config.scrolloff)
226246 }
227247 }
@@ -230,22 +250,38 @@ impl Editor {
230250 use crate::tree::Layout;
231251 use helix_core::Selection;
232252
233− if !self.documents.contains_key(id) {
253+ if !self.documents.contains_key(&id) {
234254 log::error!("cannot switch to document that does not exist (anymore)");
235255 return;
236256 }
237257
238258 match action {
239259 Action::Replace => {
240− let view = view!(self);
241− let jump = (
242− view.doc,
243− self.documents[view.doc].selection(view.id).clone(),
244− );
245−
260+ let (view, doc) = current_ref!(self);
261+ // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
262+ // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
263+ // of `self.tree`, which is mutably borrowed when `view_mut` is called.
264+ let remove_empty_scratch = !doc.is_modified()
265+ // If the buffer has no path and is not modified, it is an empty scratch buffer.
266+ && doc.path().is_none()
267+ // If the buffer we are changing to is not this buffer
268+ && id != doc.id
269+ // Ensure the buffer is not displayed in any other splits.
270+ && !self
271+ .tree
272+ .traverse()
273+ .any(|(_, v)| v.doc == doc.id && v.id != view.id);
246274 let view = view_mut!(self);
247− view.jumps.push(jump);
248− view.last_accessed_doc = Some(view.doc);
275+ if remove_empty_scratch {
276+ // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
277+ // borrow, invalidating direct access to `doc.id`.
278+ let id = doc.id;
279+ self.documents.remove(&id);
280+ } else {
281+ let jump = (view.doc, doc.selection(view.id).clone());
282+ view.jumps.push(jump);
283+ view.last_accessed_doc = Some(view.doc);
284+ }
249285 view.doc = id;
250286 view.offset = Position::default();
251287
@@ -272,14 +308,14 @@ impl Editor {
272308 let view = View::new(id);
273309 let view_id = self.tree.split(view, Layout::Horizontal);
274310 // initialize selection for view
275− let doc = &mut self.documents[id];
311+ let doc = self.documents.get_mut(&id).unwrap();
276312 doc.selections.insert(view_id, Selection::point(0));
277313 }
278314 Action::VerticalSplit => {
279315 let view = View::new(id);
280316 let view_id = self.tree.split(view, Layout::Vertical);
281317 // initialize selection for view
282− let doc = &mut self.documents[id];
318+ let doc = self.documents.get_mut(&id).unwrap();
283319 doc.selections.insert(view_id, Selection::point(0));
284320 }
285321 }
@@ -288,9 +324,11 @@ impl Editor {
288324 }
289325
290326 pub fn new_file(&mut self, action: Action) -> DocumentId {
291− let doc = Document::default();
292− let id = self.documents.insert(doc);
293− self.documents[id].id = id;
327+ let id = DocumentId(self.next_document_id);
328+ self.next_document_id += 1;
329+ let mut doc = Document::default();
330+ doc.id = id;
331+ self.documents.insert(id, doc);
294332 self.switch(id, action);
295333 id
296334 }
@@ -313,7 +351,11 @@ impl Editor {
313351 self.language_servers
314352 .get(language)
315353 .map_err(|e| {
316− log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
354+ log::error!(
355+ "Failed to initialize the LSP for `{}` {{ {} }}",
356+ language.scope(),
357+ e
358+ )
317359 })
318360 .ok()
319361 });
@@ -336,8 +378,10 @@ impl Editor {
336378 doc.set_language_server(Some(language_server));
337379 }
338380
339− let id = self.documents.insert(doc);
340− self.documents[id].id = id;
381+ let id = DocumentId(self.next_document_id);
382+ self.next_document_id += 1;
383+ doc.id = id;
384+ self.documents.insert(id, doc);
341385 id
342386 };
343387
@@ -348,16 +392,20 @@ impl Editor {
348392 pub fn close(&mut self, id: ViewId, close_buffer: bool) {
349393 let view = self.tree.get(self.tree.focus);
350394 // remove selection
351− self.documents[view.doc].selections.remove(&id);
395+ self.documents
396+ .get_mut(&view.doc)
397+ .unwrap()
398+ .selections
399+ .remove(&id);
352400
353401 if close_buffer {
354402 // get around borrowck issues
355− let doc = &self.documents[view.doc];
403+ let doc = &self.documents[&view.doc];
356404
357405 if let Some(language_server) = doc.language_server() {
358406 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
359407 }
360− self.documents.remove(view.doc);
408+ self.documents.remove(&view.doc);
361409 }
362410
363411 self.tree.remove(id);
@@ -374,24 +422,40 @@ impl Editor {
374422 self.tree.focus_next();
375423 }
376424
425+ pub fn focus_right(&mut self) {
426+ self.tree.focus_direction(tree::Direction::Right);
427+ }
428+
429+ pub fn focus_left(&mut self) {
430+ self.tree.focus_direction(tree::Direction::Left);
431+ }
432+
433+ pub fn focus_up(&mut self) {
434+ self.tree.focus_direction(tree::Direction::Up);
435+ }
436+
437+ pub fn focus_down(&mut self) {
438+ self.tree.focus_direction(tree::Direction::Down);
439+ }
440+
377441 pub fn should_close(&self) -> bool {
378442 self.tree.is_empty()
379443 }
380444
381445 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
382446 let view = self.tree.get_mut(id);
383− let doc = &self.documents[view.doc];
447+ let doc = &self.documents[&view.doc];
384448 view.ensure_cursor_in_view(doc, self.config.scrolloff)
385449 }
386450
387451 #[inline]
388452 pub fn document(&self, id: DocumentId) -> Option<&Document> {
389− self.documents.get(id)
453+ self.documents.get(&id)
390454 }
391455
392456 #[inline]
393457 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
394− self.documents.get_mut(id)
458+ self.documents.get_mut(&id)
395459 }
396460
397461 #[inline]
@@ -416,7 +480,7 @@ impl Editor {
416480
417481 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
418482 let view = view!(self);
419− let doc = &self.documents[view.doc];
483+ let doc = &self.documents[&view.doc];
420484 let cursor = doc
421485 .selection(view.id)
422486 .primary()
helix-view/src/info.rs+2−2
@@ -1,6 +1,6 @@
11use crate::input::KeyEvent;
22use helix_core::unicode::width::UnicodeWidthStr;
3−use std::fmt::Write;
3+use std::{collections::BTreeSet, fmt::Write};
44
55#[derive(Debug)]
66/// Info box used in editor. Rendering logic will be in other crate.
@@ -16,7 +16,7 @@ pub struct Info {
1616}
1717
1818impl Info {
19− pub fn new(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
19+ pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
2020 let body = body
2121 .into_iter()
2222 .map(|(desc, events)| {
helix-view/src/input.rs+1−1
@@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers};
88
99/// Represents a key event.
1010// We use a newtype here because we want to customize Deserialize and Display.
11−#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
11+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
1212pub struct KeyEvent {
1313 pub code: KeyCode,
1414 pub modifiers: KeyModifiers,
helix-view/src/keyboard.rs+1−1
@@ -54,7 +54,7 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
5454}
5555
5656/// Represents a key.
57−#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
57+#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
5858#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5959pub enum KeyCode {
6060 /// Backspace key.
helix-view/src/lib.rs+3−1
@@ -12,8 +12,10 @@ pub mod theme;
1212pub mod tree;
1313pub mod view;
1414
15+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
16+pub struct DocumentId(usize);
17+
1518slotmap::new_key_type! {
16− pub struct DocumentId;
1719 pub struct ViewId;
1820}
1921
helix-view/src/macros.rs+3−2
@@ -13,7 +13,8 @@
1313macro_rules! current {
1414 ( $( $editor:ident ).+ ) => {{
1515 let view = $crate::view_mut!( $( $editor ).+ );
16− let doc = &mut $( $editor ).+ .documents[view.doc];
16+ let id = view.doc;
17+ let doc = $( $editor ).+ .documents.get_mut(&id).unwrap();
1718 (view, doc)
1819 }};
1920}
@@ -56,7 +57,7 @@ macro_rules! doc {
5657macro_rules! current_ref {
5758 ( $( $editor:ident ).+ ) => {{
5859 let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
59− let doc = &$( $editor ).+ .documents[view.doc];
60+ let doc = &$( $editor ).+ .documents[&view.doc];
6061 (view, doc)
6162 }};
6263}
helix-view/src/theme.rs−1
@@ -1,6 +1,5 @@
11use std::{
22 collections::HashMap,
3− convert::TryFrom,
43 path::{Path, PathBuf},
54};
65
helix-view/src/tree.rs+182−9
@@ -47,13 +47,21 @@ impl Node {
4747
4848// TODO: screen coord to container + container coordinate helpers
4949
50−#[derive(Debug, PartialEq, Eq)]
50+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5151pub enum Layout {
5252 Horizontal,
5353 Vertical,
5454 // could explore stacked/tabbed
5555}
5656
57+#[derive(Debug, Clone, Copy)]
58+pub enum Direction {
59+ Up,
60+ Down,
61+ Left,
62+ Right,
63+}
64+
5765#[derive(Debug)]
5866pub struct Container {
5967 layout: Layout,
@@ -150,7 +158,6 @@ impl Tree {
150158 } => container,
151159 _ => unreachable!(),
152160 };
153−
154161 if container.layout == layout {
155162 // insert node after the current item if there is children already
156163 let pos = if container.children.is_empty() {
@@ -393,6 +400,112 @@ impl Tree {
393400 Traverse::new(self)
394401 }
395402
403+ // Finds the split in the given direction if it exists
404+ pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
405+ let parent = self.nodes[id].parent;
406+ // Base case, we found the root of the tree
407+ if parent == id {
408+ return None;
409+ }
410+ // Parent must always be a container
411+ let parent_container = match &self.nodes[parent].content {
412+ Content::Container(container) => container,
413+ Content::View(_) => unreachable!(),
414+ };
415+
416+ match (direction, parent_container.layout) {
417+ (Direction::Up, Layout::Vertical)
418+ | (Direction::Left, Layout::Horizontal)
419+ | (Direction::Right, Layout::Horizontal)
420+ | (Direction::Down, Layout::Vertical) => {
421+ // The desired direction of movement is not possible within
422+ // the parent container so the search must continue closer to
423+ // the root of the split tree.
424+ self.find_split_in_direction(parent, direction)
425+ }
426+ (Direction::Up, Layout::Horizontal)
427+ | (Direction::Down, Layout::Horizontal)
428+ | (Direction::Left, Layout::Vertical)
429+ | (Direction::Right, Layout::Vertical) => {
430+ // It's possible to move in the desired direction within
431+ // the parent container so an attempt is made to find the
432+ // correct child.
433+ match self.find_child(id, &parent_container.children, direction) {
434+ // Child is found, search is ended
435+ Some(id) => Some(id),
436+ // A child is not found. This could be because of either two scenarios
437+ // 1. Its not possible to move in the desired direction, and search should end
438+ // 2. A layout like the following with focus at X and desired direction Right
439+ // | _ | x | |
440+ // | _ _ _ | |
441+ // | _ _ _ | |
442+ // The container containing X ends at X so no rightward movement is possible
443+ // however there still exists another view/container to the right that hasn't
444+ // been explored. Thus another search is done here in the parent container
445+ // before concluding it's not possible to move in the desired direction.
446+ None => self.find_split_in_direction(parent, direction),
447+ }
448+ }
449+ }
450+ }
451+
452+ fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
453+ let mut child_id = match direction {
454+ // index wise in the child list the Up and Left represents a -1
455+ // thus reversed iterator.
456+ Direction::Up | Direction::Left => children
457+ .iter()
458+ .rev()
459+ .skip_while(|i| **i != id)
460+ .copied()
461+ .nth(1)?,
462+ // Down and Right => +1 index wise in the child list
463+ Direction::Down | Direction::Right => {
464+ children.iter().skip_while(|i| **i != id).copied().nth(1)?
465+ }
466+ };
467+ let (current_x, current_y) = match &self.nodes[self.focus].content {
468+ Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
469+ Content::Container(_) => unreachable!(),
470+ };
471+
472+ // If the child is a container the search finds the closest container child
473+ // visually based on screen location.
474+ while let Content::Container(container) = &self.nodes[child_id].content {
475+ match (direction, container.layout) {
476+ (_, Layout::Vertical) => {
477+ // find closest split based on x because y is irrelevant
478+ // in a vertical container (and already correct based on previous search)
479+ child_id = *container.children.iter().min_by_key(|id| {
480+ let x = match &self.nodes[**id].content {
481+ Content::View(view) => view.inner_area().left(),
482+ Content::Container(container) => container.area.left(),
483+ };
484+ (current_x as i16 - x as i16).abs()
485+ })?;
486+ }
487+ (_, Layout::Horizontal) => {
488+ // find closest split based on y because x is irrelevant
489+ // in a horizontal container (and already correct based on previous search)
490+ child_id = *container.children.iter().min_by_key(|id| {
491+ let y = match &self.nodes[**id].content {
492+ Content::View(view) => view.inner_area().top(),
493+ Content::Container(container) => container.area.top(),
494+ };
495+ (current_y as i16 - y as i16).abs()
496+ })?;
497+ }
498+ }
499+ }
500+ Some(child_id)
501+ }
502+
503+ pub fn focus_direction(&mut self, direction: Direction) {
504+ if let Some(id) = self.find_split_in_direction(self.focus, direction) {
505+ self.focus = id;
506+ }
507+ }
508+
396509 pub fn focus_next(&mut self) {
397510 // This function is very dumb, but that's because we don't store any parent links.
398511 // (we'd be able to go parent.next_sibling() recursively until we find something)
@@ -420,13 +533,12 @@ impl Tree {
420533 // if found = container -> found = first child
421534 // }
422535
423− let iter = self.traverse();
424−
425− let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
426− iter.next(); // take the focused value
427−
428− if let Some((key, _)) = iter.next() {
429− self.focus = key;
536+ let mut views = self
537+ .traverse()
538+ .skip_while(|&(id, _view)| id != self.focus)
539+ .skip(1); // Skip focused value
540+ if let Some((id, _)) = views.next() {
541+ self.focus = id;
430542 } else {
431543 // extremely crude, take the first item again
432544 let (key, _) = self.traverse().next().unwrap();
@@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
472584 }
473585 }
474586}
587+
588+#[cfg(test)]
589+mod test {
590+ use super::*;
591+ use crate::DocumentId;
592+
593+ #[test]
594+ fn find_split_in_direction() {
595+ let mut tree = Tree::new(Rect {
596+ x: 0,
597+ y: 0,
598+ width: 180,
599+ height: 80,
600+ });
601+ let mut view = View::new(DocumentId::default());
602+ view.area = Rect::new(0, 0, 180, 80);
603+ tree.insert(view);
604+
605+ let l0 = tree.focus;
606+ let view = View::new(DocumentId::default());
607+ tree.split(view, Layout::Vertical);
608+ let r0 = tree.focus;
609+
610+ tree.focus = l0;
611+ let view = View::new(DocumentId::default());
612+ tree.split(view, Layout::Horizontal);
613+ let l1 = tree.focus;
614+
615+ tree.focus = l0;
616+ let view = View::new(DocumentId::default());
617+ tree.split(view, Layout::Vertical);
618+ let l2 = tree.focus;
619+
620+ // Tree in test
621+ // | L0 | L2 | |
622+ // | L1 | R0 |
623+ tree.focus = l2;
624+ assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
625+ assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
626+ assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));
627+ assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up));
628+
629+ tree.focus = l1;
630+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left));
631+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down));
632+ assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right));
633+ assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up));
634+
635+ tree.focus = l0;
636+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left));
637+ assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down));
638+ assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right));
639+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up));
640+
641+ tree.focus = r0;
642+ assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left));
643+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down));
644+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
645+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
646+ }
647+}
helix-view/src/view.rs+5−3
@@ -2,10 +2,9 @@ use std::borrow::Cow;
22
33use crate::{graphics::Rect, Document, DocumentId, ViewId};
44use helix_core::{
5− coords_at_pos,
65 graphemes::{grapheme_width, RopeGraphemes},
76 line_ending::line_end_char_index,
8− Position, RopeSlice, Selection,
7+ visual_coords_at_pos, Position, RopeSlice, Selection,
98};
109
1110type Jump = (DocumentId, Selection);
@@ -91,7 +90,10 @@ impl View {
9190 .selection(self.id)
9291 .primary()
9392 .cursor(doc.text().slice(..));
94− let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
93+
94+ let Position { col, row: line } =
95+ visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
96+
9597 let inner_area = self.inner_area();
9698 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
9799
languages.toml+10−1
@@ -312,7 +312,7 @@ injection-regex = "php"
312312file-types = ["php"]
313313roots = []
314314
315−indent = { tab-width = 2, unit = " " }
315+indent = { tab-width = 4, unit = " " }
316316
317317[[language]]
318318name = "latex"
@@ -458,3 +458,12 @@ file-types = ["scm"]
458458roots = []
459459comment-token = ";"
460460indent = { tab-width = 2, unit = " " }
461+
462+[[language]]
463+name = "cmake"
464+scope = "source.cmake"
465+file-types = ["cmake", "CMakeLists.txt"]
466+roots = []
467+comment-token = "#"
468+indent = { tab-width = 2, unit = " " }
469+language-server = { command = "cmake-language-server" }
runtime/queries/bash/highlights.scm+2−2
@@ -7,7 +7,7 @@
77
88(command_name) @function
99
10−(variable_name) @property
10+(variable_name) @variable.other.member
1111
1212[
1313 "case"
@@ -31,7 +31,7 @@
3131
3232(function_definition name: (word) @function)
3333
34−(file_descriptor) @number
34+(file_descriptor) @constant.numeric.integer
3535
3636[
3737 (command_substitution)
runtime/queries/c-sharp/highlights.scm+5−5
@@ -20,16 +20,16 @@
2020] @type.builtin
2121
2222;; Enum
23−(enum_member_declaration (identifier) @variable.property)
23+(enum_member_declaration (identifier) @variable.other.member)
2424
2525;; Literals
2626[
2727 (real_literal)
2828 (integer_literal)
29−] @number
29+] @constant.numeric.integer
3030
31+(character_literal) @constant.character
3132[
32− (character_literal)
3333 (string_literal)
3434 (verbatim_string_literal)
3535 (interpolated_string_text)
@@ -40,8 +40,8 @@
4040 "$@\""
4141 ] @string
4242
43+(boolean_literal) @constant.builtin.boolean
4344[
44− (boolean_literal)
4545 (null_literal)
4646 (void_keyword)
4747] @constant.builtin
@@ -98,7 +98,7 @@
9898;; Keywords
9999(modifier) @keyword
100100(this_expression) @keyword
101−(escape_sequence) @keyword
101+(escape_sequence) @constant.character.escape
102102
103103[
104104 "as"
runtime/queries/c/highlights.scm+3−3
@@ -60,8 +60,8 @@
6060(system_lib_string) @string
6161
6262(null) @constant
63−(number_literal) @number
64−(char_literal) @string
63+(number_literal) @constant.numeric.integer
64+(char_literal) @constant.character
6565
6666(call_expression
6767 function: (identifier) @function)
@@ -73,7 +73,7 @@
7373(preproc_function_def
7474 name: (identifier) @function.special)
7575
76−(field_identifier) @property
76+(field_identifier) @variable.other.member
7777(statement_identifier) @label
7878(type_identifier) @type
7979(primitive_type) @type
runtime/queries/cmake/highlights.scmnew file+97
@@ -0,0 +1,97 @@
1+[
2+ (quoted_argument)
3+ (bracket_argument)
4+ ] @string
5+
6+(variable) @variable
7+
8+[
9+ (bracket_comment)
10+ (line_comment)
11+ ] @comment
12+
13+(normal_command (identifier) @function)
14+
15+["ENV" "CACHE"] @string.special.symbol
16+["$" "{" "}" "<" ">"] @punctuation
17+["(" ")"] @punctuation.bracket
18+
19+[
20+ (function)
21+ (endfunction)
22+ (macro)
23+ (endmacro)
24+ ] @keyword.function
25+
26+[
27+ (if)
28+ (elseif)
29+ (else)
30+ (endif)
31+ ] @keyword.control.conditional
32+
33+[
34+ (foreach)
35+ (endforeach)
36+ (while)
37+ (endwhile)
38+ ] @keyword.control.repeat
39+
40+(function_command
41+ (function)
42+ . (argument) @function
43+ (argument)* @variable.parameter
44+ )
45+
46+(macro_command
47+ (macro)
48+ . (argument) @function.macro
49+ (argument)* @variable.parameter
50+ )
51+
52+(normal_command
53+ (identifier) @function.builtin
54+ . (argument) @variable
55+ (#match? @function.builtin "^(?i)(set)$"))
56+
57+(normal_command
58+ (identifier) @function.builtin
59+ . (argument)
60+ (argument) @constant
61+ (#match? @constant "^(?:PARENT_SCOPE|CACHE)$")
62+ (#match? @function.builtin "^(?i)(unset)$"))
63+
64+(normal_command
65+ (identifier) @function.builtin
66+ . (argument)
67+ . (argument)
68+ (argument) @constant
69+ (#match? @constant "^(?:PARENT_SCOPE|CACHE|FORCE)$")
70+ (#match? @function.builtin "^(?i)(set)$")
71+ )
72+
73+((argument) @constant.builtin.boolean
74+ (#match? @constant.builtin.boolean "^(?i)(?:1|on|yes|true|y|0|off|no|false|n|ignore|notfound|.*-notfound)$")
75+ )
76+
77+(if_command
78+ (if)
79+ (argument) @operator
80+ (#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)$")
81+)
82+
83+(normal_command
84+ (identifier) @function.builtin
85+ . (argument)
86+ (argument) @constant
87+ (#match? @constant "^(?:ALL|COMMAND|DEPENDS|BYPRODUCTS|WORKING_DIRECTORY|COMMENT|JOB_POOL|VERBATIM|USES_TERMINAL|COMMAND_EXPAND_LISTS|SOURCES)$")
88+ (#match? @function.builtin "^(?i)(add_custom_target)$")
89+ )
90+
91+(normal_command
92+ (identifier) @function.builtin
93+ (argument) @constant
94+ (#match? @constant "^(?:OUTPUT|COMMAND|MAIN_DEPENDENCY|DEPENDS|BYPRODUCTS|IMPLICIT_DEPENDS|WORKING_DIRECTORY|COMMENT|DEPFILE|JOB_POOL|VERBATIM|APPEND|USES_TERMINAL|COMMAND_EXPAND_LISTS)$")
95+ (#match? @function.builtin "^(?i)(add_custom_command)$")
96+ )
97+
runtime/queries/cpp/highlights.scm+4−5
@@ -3,7 +3,7 @@
33; Functions
44
55(call_expression
6− function: (scoped_identifier
6+ function: (qualified_identifier
77 name: (identifier) @function))
88
99(template_function
@@ -13,15 +13,14 @@
1313 name: (field_identifier) @function)
1414
1515(template_function
16− name: (scoped_identifier
17− name: (identifier) @function))
16+ name: (identifier) @function)
1817
1918(function_declarator
20− declarator: (scoped_identifier
19+ declarator: (qualified_identifier
2120 name: (identifier) @function))
2221
2322(function_declarator
24− declarator: (scoped_identifier
23+ declarator: (qualified_identifier
2524 name: (identifier) @function))
2625
2726(function_declarator
runtime/queries/css/highlights.scm+7−7
@@ -26,11 +26,11 @@
2626(pseudo_element_selector (tag_name) @attribute)
2727(pseudo_class_selector (class_name) @attribute)
2828
29−(class_name) @property
30−(id_name) @property
31−(namespace_name) @property
32−(property_name) @property
33−(feature_name) @property
29+(class_name) @variable.other.member
30+(id_name) @variable.other.member
31+(namespace_name) @variable.other.member
32+(property_name) @variable.other.member
33+(feature_name) @variable.other.member
3434
3535(attribute_name) @attribute
3636
@@ -55,8 +55,8 @@
5555(string_value) @string
5656(color_value) @string.special
5757
58−(integer_value) @number
59−(float_value) @number
58+(integer_value) @constant.numeric.integer
59+(float_value) @constant.numeric.float
6060(unit) @type
6161
6262"#" @punctuation.delimiter
runtime/queries/elixir/highlights.scm+203−120
@@ -1,125 +1,210 @@
1−["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
2−
3−[(true) (false) (nil)] @constant.builtin
4−
5−(keyword
6− [(keyword_literal)
7− ":"] @tag)
8−
9−(keyword
10− (keyword_string
11− [(string_start)
12− (string_content)
13− (string_end)] @tag))
14−
15−[(atom_literal)
16− (atom_start)
17− (atom_content)
18− (atom_end)] @tag
19−
20−[(comment)
21− (unused_identifier)] @comment
22−
23−(escape_sequence) @escape
24−
25−(call function: (function_identifier) @keyword
26− (#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)$"))
27−
28−(call function: (function_identifier) @keyword
29− [(call
30− function: (function_identifier) @function
31− (arguments
32− [(identifier) @variable.parameter
33− (_ (identifier) @variable.parameter)
34− (_ (_ (identifier) @variable.parameter))
35− (_ (_ (_ (identifier) @variable.parameter)))
36− (_ (_ (_ (_ (identifier) @variable.parameter))))
37− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
38− (binary_op
39− left:
40− (call
41− function: (function_identifier) @function
42− (arguments
43− [(identifier) @variable.parameter
44− (_ (identifier) @variable.parameter)
45− (_ (_ (identifier) @variable.parameter))
46− (_ (_ (_ (identifier) @variable.parameter)))
47− (_ (_ (_ (_ (identifier) @variable.parameter))))
48− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
1+; The following code originates mostly from
2+; https://github.com/elixir-lang/tree-sitter-elixir, with minor edits to
3+; align the captures with helix. The following should be considered
4+; Copyright 2021 The Elixir Team
5+;
6+; Licensed under the Apache License, Version 2.0 (the "License");
7+; you may not use this file except in compliance with the License.
8+; You may obtain a copy of the License at
9+;
10+; https://www.apache.org/licenses/LICENSE-2.0
11+;
12+; Unless required by applicable law or agreed to in writing, software
13+; distributed under the License is distributed on an "AS IS" BASIS,
14+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+; See the License for the specific language governing permissions and
16+; limitations under the License.
17+
18+; Reserved keywords
19+
20+["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
21+
22+; Operators
23+
24+; * doc string
25+(unary_operator
26+ operator: "@" @comment.block.documentation
27+ operand: (call
28+ target: (identifier) @comment.block.documentation.__attribute__
29+ (arguments
30+ [
31+ (string) @comment.block.documentation
32+ (charlist) @comment.block.documentation
33+ (sigil
34+ quoted_start: _ @comment.block.documentation
35+ quoted_end: _ @comment.block.documentation) @comment.block.documentation
36+ (boolean) @comment.block.documentation
37+ ]))
38+ (#match? @comment.block.documentation.__attribute__ "^(moduledoc|typedoc|doc)$"))
39+
40+; * module attribute
41+(unary_operator
42+ operator: "@" @variable.other.member
43+ operand: [
44+ (identifier) @variable.other.member
45+ (call
46+ target: (identifier) @variable.other.member)
47+ (boolean) @variable.other.member
48+ (nil) @variable.other.member
49+ ])
50+
51+; * capture operator
52+(unary_operator
53+ operator: "&"
54+ operand: [
55+ (integer) @operator
56+ (binary_operator
57+ left: [
58+ (call target: (dot left: (_) right: (identifier) @function))
59+ (identifier) @function
60+ ] operator: "/" right: (integer) @operator)
61+ ])
62+
63+(operator_identifier) @operator
64+
65+(unary_operator
66+ operator: _ @operator)
67+
68+(binary_operator
69+ operator: _ @operator)
70+
71+(dot
72+ operator: _ @operator)
73+
74+(stab_clause
75+ operator: _ @operator)
76+
77+; Literals
78+
79+(nil) @constant.builtin
80+
81+(boolean) @constant.builtin.boolean
82+(integer) @constant.numeric.integer
83+(float) @constant.numeric.float
84+
85+(alias) @type
86+
87+(call
88+ target: (dot
89+ left: (atom) @type))
90+
91+(char) @constant.character
92+
93+; Quoted content
94+
95+(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
96+
97+(escape_sequence) @constant.character.escape
98+
99+[
100+ (atom)
101+ (quoted_atom)
102+ (keyword)
103+ (quoted_keyword)
104+] @string.special.symbol
105+
106+[
107+ (string)
108+ (charlist)
109+] @string
110+
111+; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters
112+
113+(sigil
114+ (sigil_name) @__name__
115+ quoted_start: _ @string
116+ quoted_end: _ @string
117+ (#match? @__name__ "^[sS]$")) @string
118+
119+(sigil
120+ (sigil_name) @__name__
121+ quoted_start: _ @string.regexp
122+ quoted_end: _ @string.regexp
123+ (#match? @__name__ "^[rR]$")) @string.regexp
124+
125+(sigil
126+ (sigil_name) @__name__
127+ quoted_start: _ @string.special
128+ quoted_end: _ @string.special) @string.special
129+
130+; Calls
131+
132+; * definition keyword
133+(call
134+ target: (identifier) @keyword
135+ (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
136+
137+; * kernel or special forms keyword
138+(call
139+ target: (identifier) @keyword
140+ (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
141+
142+; * function call
143+(call
144+ target: [
145+ ; local
146+ (identifier) @function
147+ ; remote
148+ (dot
149+ right: (identifier) @function)
150+ ])
151+
152+; * just identifier in function definition
153+(call
154+ target: (identifier) @keyword
155+ (arguments
156+ [
157+ (identifier) @function
158+ (binary_operator
159+ left: (identifier) @function
49160 operator: "when")
50− (binary_op
51− left: (identifier) @variable.parameter
52− operator: _ @function
53− right: (identifier) @variable.parameter)]
54− (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
55−
56−(call (function_identifier) @keyword
57− [(call
58− function: (function_identifier) @function)
59− (identifier) @function
60− (binary_op
61− left:
62− [(call
63− function: (function_identifier) @function)
64− (identifier) @function]
65− operator: "when")]
66− (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
67−
68−(anonymous_function
69− (stab_expression
70− left: (bare_arguments
71− [(identifier) @variable.parameter
72− (_ (identifier) @variable.parameter)
73− (_ (_ (identifier) @variable.parameter))
74− (_ (_ (_ (identifier) @variable.parameter)))
75− (_ (_ (_ (_ (identifier) @variable.parameter))))
76− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
77−
78−(unary_op
79− operator: "@"
80− (call (identifier) @attribute
81− (heredoc
82− [(heredoc_start)
83− (heredoc_content)
84− (heredoc_end)] @doc))
85− (#match? @attribute "^(doc|moduledoc)$"))
86−
87−(module) @type
88−
89−(unary_op
90− operator: "@" @attribute
91− [(call
92− function: (function_identifier) @attribute)
93− (identifier) @attribute])
94−
95−(unary_op
96− operator: _ @operator)
97−
98−(binary_op
99− operator: _ @operator)
100−
101−(heredoc
102− [(heredoc_start)
103− (heredoc_content)
104− (heredoc_end)] @string)
105−
106−(string
107− [(string_start)
108− (string_content)
109− (string_end)] @string)
110−
111−(sigil_start) @string.special
112−(sigil_content) @string
113−(sigil_end) @string.special
114−
115−(interpolation
116− "#{" @punctuation.special
117− "}" @punctuation.special)
161+ ])
162+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
163+
164+; * pipe into identifier (definition)
165+(call
166+ target: (identifier) @keyword
167+ (arguments
168+ (binary_operator
169+ operator: "|>"
170+ right: (identifier) @variable))
171+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
172+
173+; * pipe into identifier (function call)
174+(binary_operator
175+ operator: "|>"
176+ right: (identifier) @function)
177+
178+; Identifiers
179+
180+; * special
181+(
182+ (identifier) @constant.builtin
183+ (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
184+)
185+
186+; * unused
187+(
188+ (identifier) @comment
189+ (#match? @comment "^_")
190+)
191+
192+; * regular
193+(identifier) @variable
194+
195+; Comment
196+
197+(comment) @comment
198+
199+; Punctuation
200+
201+[
202+ "%"
203+] @punctuation
118204
119205[
120206 ","
121− "->"
122− "."
207+ ";"
123208] @punctuation.delimiter
124209
125210[
@@ -133,6 +218,4 @@
133218 ">>"
134219] @punctuation.bracket
135220
136−(special_identifier) @function.special
137−
138221(ERROR) @warning
runtime/queries/go/highlights.scm+3−3
@@ -25,7 +25,7 @@
2525(variadic_parameter_declaration (identifier) @variable.parameter)
2626
2727(type_identifier) @type
28−(field_identifier) @property
28+(field_identifier) @variable.other.member
2929(identifier) @variable
3030(package_identifier) @variable
3131
@@ -130,13 +130,13 @@
130130 (rune_literal)
131131] @string
132132
133−(escape_sequence) @escape
133+(escape_sequence) @constant.character.escape
134134
135135[
136136 (int_literal)
137137 (float_literal)
138138 (imaginary_literal)
139−] @number
139+] @constant.numeric.integer
140140
141141[
142142 (true)
runtime/queries/go/textobjects.scmnew file+21
@@ -0,0 +1,21 @@
1+(function_declaration
2+ body: (block)? @function.inside) @function.around
3+
4+(func_literal
5+ (_)? @function.inside) @function.around
6+
7+(method_declaration
8+ body: (block)? @function.inside) @function.around
9+
10+;; struct and interface declaration as class textobject?
11+(type_declaration
12+ (type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around
13+
14+(type_declaration
15+ (type_spec (type_identifier) (interface_type (method_spec_list (_)?) @class.inside))) @class.around
16+
17+(parameter_list
18+ (_) @parameter.inside)
19+
20+(argument_list
21+ (_) @parameter.inside)
runtime/queries/haskell/highlights.scm+3−3
@@ -13,9 +13,9 @@
1313(constraint class: (class_name (type)) @class)
1414(class (class_head class: (class_name (type)) @class))
1515(instance (instance_head class: (class_name (type)) @class))
16−(integer) @number
17−(exp_literal (float)) @number
18−(char) @literal
16+(integer) @constant.numeric.integer
17+(exp_literal (float)) @constant.numeric.float
18+(char) @constant.character
1919(con_unit) @literal
2020(con_list) @literal
2121(tycon_arrow) @operator
runtime/queries/java/highlights.scm+6−5
@@ -59,14 +59,15 @@
5959 (hex_integer_literal)
6060 (decimal_integer_literal)
6161 (octal_integer_literal)
62+] @constant.numeric.integer
63+
64+[
6265 (decimal_floating_point_literal)
6366 (hex_floating_point_literal)
64−] @number
67+] @constant.numeric.float
6568
66−[
67− (character_literal)
68− (string_literal)
69−] @string
69+(character_literal) @constant.character
70+(string_literal) @string
7071
7172[
7273 (true)
runtime/queries/javascript/highlights.scm+2−2
@@ -65,7 +65,7 @@
6565; Properties
6666;-----------
6767
68−(property_identifier) @property
68+(property_identifier) @variable.other.member
6969
7070; Literals
7171;---------
@@ -88,7 +88,7 @@
8888] @string
8989
9090(regex) @string.regexp
91−(number) @number
91+(number) @constant.numeric.integer
9292
9393; Tokens
9494;-------
runtime/queries/json/highlights.scm+15−4
@@ -1,9 +1,20 @@
1+[
2+ (true)
3+ (false)
4+] @constant.builtin.boolean
5+(null) @constant.builtin
6+(number) @constant.numeric
17(pair
28 key: (_) @keyword)
39
410(string) @string
11+(escape_sequence) @constant.character.escape
12+(ERROR) @error
513
6−(object
7− "{" @escape
8− (_)
9− "}" @escape)
14+"," @punctuation.delimiter
15+[
16+ "["
17+ "]"
18+ "{"
19+ "}"
20+] @punctuation.bracket
runtime/queries/julia/highlights.scm+6−6
@@ -15,7 +15,7 @@
1515
1616(field_expression
1717 (identifier)
18− (identifier) @field .)
18+ (identifier) @variable.other.member .)
1919
2020(function_definition
2121 name: (identifier) @function)
@@ -80,14 +80,14 @@
8080(struct_definition
8181 name: (identifier) @type)
8282
83−(number) @number
83+(number) @constant.numeric.integer
8484(range_expression
85− (identifier) @number
86− (eq? @number "end"))
85+ (identifier) @constant.numeric.integer
86+ (eq? @constant.numeric.integer "end"))
8787(range_expression
8888 (_
89− (identifier) @number
90− (eq? @number "end")))
89+ (identifier) @constant.numeric.integer
90+ (eq? @constant.numeric.integer "end")))
9191(coefficient_expression
9292 (number)
9393 (identifier) @constant.builtin)
runtime/queries/ledger/highlights.scm+2−2
@@ -7,9 +7,9 @@
77 (date)
88 (interval)
99 (quantity)
10−] @number
10+] @constant.numeric.integer
1111
12−((account) @field)
12+((account) @variable.other.member)
1313((commodity) @text.literal)
1414
1515"include" @include
runtime/queries/lua/highlights.scm+3−3
@@ -150,14 +150,14 @@
150150(table ["{" "}"] @constructor)
151151(comment) @comment
152152(string) @string
153−(number) @number
153+(number) @constant.numeric.integer
154154(label_statement) @label
155155; A bit of a tricky one, this will only match field names
156−(field . (identifier) @property (_))
156+(field . (identifier) @variable.other.member (_))
157157(shebang) @comment
158158
159159;; Property
160−(property_identifier) @property
160+(property_identifier) @variable.other.member
161161
162162;; Variable
163163(identifier) @variable
runtime/queries/nix/highlights.scm+5−7
@@ -33,16 +33,14 @@
3333
3434(uri) @string.special.uri
3535
36−[
37− (integer)
38− (float)
39−] @number
36+(integer) @constant.numeric.integer
37+(float) @constant.numeric.float
4038
4139(interpolation
4240 "${" @punctuation.special
4341 "}" @punctuation.special) @embedded
4442
45−(escape_sequence) @escape
43+(escape_sequence) @constant.character.escape
4644
4745(function
4846 universal: (identifier) @variable.parameter
@@ -66,8 +64,8 @@
6664(binary
6765 operator: _ @operator)
6866
69−(attr_identifier) @property
70−(inherit attrs: (attrs_inherited (identifier) @property) )
67+(attr_identifier) @variable.other.member
68+(inherit attrs: (attrs_inherited (identifier) @variable.other.member) )
7169
7270[
7371 ";"
runtime/queries/ocaml/highlights.scm+4−4
@@ -51,14 +51,14 @@
5151; Properties
5252;-----------
5353
54−[(label_name) (field_name) (instance_variable_name)] @property
54+[(label_name) (field_name) (instance_variable_name)] @variable.other.member
5555
5656; Constants
5757;----------
5858
5959[(boolean) (unit)] @constant
6060
61−[(number) (signed_number)] @number
61+[(number) (signed_number)] @constant.numeric.integer
6262
6363(character) @constant.character
6464
@@ -66,7 +66,7 @@
6666
6767(quoted_string "{" @string "}" @string) @string
6868
69−(escape_sequence) @string.escape
69+(escape_sequence) @constant.character.escape
7070
7171[
7272 (conversion_specification)
@@ -145,7 +145,7 @@
145145; Attributes
146146;-----------
147147
148−(attribute_id) @property
148+(attribute_id) @variable.other.member
149149
150150; Comments
151151;---------
runtime/queries/php/highlights.scm+6−6
@@ -30,12 +30,12 @@
3030; Member
3131
3232(property_element
33− (variable_name) @property)
33+ (variable_name) @variable.other.member)
3434
3535(member_access_expression
36− name: (variable_name (name)) @property)
36+ name: (variable_name (name)) @variable.other.member)
3737(member_access_expression
38− name: (name) @property)
38+ name: (name) @variable.other.member)
3939
4040; Variables
4141
@@ -56,10 +56,10 @@
5656
5757(string) @string
5858(heredoc) @string
59−(boolean) @constant.builtin
59+(boolean) @constant.builtin.boolean
6060(null) @constant.builtin
61−(integer) @number
62−(float) @number
61+(integer) @constant.numeric.integer
62+(float) @constant.numeric.float
6363(comment) @comment
6464
6565"$" @operator
runtime/queries/php/indents.tomlnew file+17
@@ -0,0 +1,17 @@
1+indent = [
2+ "array_creation_expression",
3+ "arguments",
4+ "formal_parameters",
5+ "compound_statement",
6+ "declaration_list",
7+ "binary_expression",
8+ "return_statement",
9+ "expression_statement",
10+ "switch_block",
11+ "anonymous_function_use_clause",
12+]
13+
14+oudent = [
15+ "}",
16+ ")",
17+]
runtime/queries/protobuf/highlights.scm+4−6
@@ -34,16 +34,14 @@
3434[
3535 (fieldName)
3636 (optionName)
37−] @property
37+] @variable.other.member
3838(enumVariantName) @type.enum.variant
3939
4040(fullIdent) @namespace
4141
42−[
43− (intLit)
44− (floatLit)
45−] @number
46−(boolLit) @constant.builtin
42+(intLit) @constant.numeric.integer
43+(floatLit) @constant.numeric.float
44+(boolLit) @constant.builtin.boolean
4745(strLit) @string
4846
4947(constant) @constant
runtime/queries/python/highlights.scm+4−7
@@ -29,7 +29,7 @@
2929 name: (identifier) @function)
3030
3131(identifier) @variable
32−(attribute attribute: (identifier) @property)
32+(attribute attribute: (identifier) @variable.other.member)
3333(type (identifier) @type)
3434
3535; Literals
@@ -40,14 +40,11 @@
4040 (false)
4141] @constant.builtin
4242
43−[
44− (integer)
45− (float)
46−] @number
47−
43+(integer) @constant.numeric.integer
44+(float) @constant.numeric.float
4845(comment) @comment
4946(string) @string
50−(escape_sequence) @escape
47+(escape_sequence) @constant.character.escape
5148
5249(interpolation
5350 "{" @punctuation.special
runtime/queries/python/indents.tomlnew file+39
@@ -0,0 +1,39 @@
1+indent = [
2+ "list",
3+ "tuple",
4+ "dictionary",
5+ "set",
6+
7+ "if_statement",
8+ "for_statement",
9+ "while_statement",
10+ "with_statement",
11+ "try_statement",
12+ "import_from_statement",
13+
14+ "parenthesized_expression",
15+ "generator_expression",
16+ "list_comprehension",
17+ "set_comprehension",
18+ "dictionary_comprehension",
19+
20+ "tuple_pattern",
21+ "list_pattern",
22+ "argument_list",
23+ "parameters",
24+ "binary_operator",
25+
26+ "function_definition",
27+ "class_definition",
28+]
29+
30+outdent = [
31+ ")",
32+ "]",
33+ "}",
34+ "return_statement",
35+ "pass_statement",
36+ "raise_statement",
37+]
38+
39+ignore = ["string"]
runtime/queries/python/textobjects.scmnew file+14
@@ -0,0 +1,14 @@
1+(function_definition
2+ body: (block)? @function.inside) @function.around
3+
4+(class_definition
5+ body: (block)? @class.inside) @class.around
6+
7+(parameters
8+ (_) @parameter.inside)
9+
10+(lambda_parameters
11+ (_) @parameter.inside)
12+
13+(argument_list
14+ (_) @parameter.inside)
runtime/queries/ruby/highlights.scm+3−3
@@ -55,7 +55,7 @@
5555[
5656 (class_variable)
5757 (instance_variable)
58−] @property
58+] @variable.other.member
5959
6060((identifier) @constant.builtin
6161 (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
@@ -101,12 +101,12 @@
101101] @string.special.symbol
102102
103103(regex) @string.regexp
104−(escape_sequence) @escape
104+(escape_sequence) @constant.character.escape
105105
106106[
107107 (integer)
108108 (float)
109−] @number
109+] @constant.numeric.integer
110110
111111[
112112 (nil)
runtime/queries/rust/highlights.scm+24−11
@@ -15,15 +15,13 @@
1515; Primitives
1616; ---
1717
18−(escape_sequence) @escape
18+(escape_sequence) @constant.character.escape
1919(primitive_type) @type.builtin
2020(boolean_literal) @constant.builtin.boolean
21+(integer_literal) @constant.numeric.integer
22+(float_literal) @constant.numeric.float
23+(char_literal) @constant.character
2124[
22− (integer_literal)
23− (float_literal)
24−] @number
25−[
26− (char_literal)
2725 (string_literal)
2826 (raw_string_literal)
2927] @string
@@ -40,10 +38,10 @@
4038(enum_variant (identifier) @type.enum.variant)
4139
4240(field_initializer
43− (field_identifier) @property)
41+ (field_identifier) @variable.other.member)
4442(shorthand_field_initializer
45− (identifier) @variable.property)
46−(shorthand_field_identifier) @variable.property
43+ (identifier) @variable.other.member)
44+(shorthand_field_identifier) @variable.other.member
4745
4846(lifetime
4947 "'" @label
@@ -81,9 +79,24 @@
8179 ] @punctuation.bracket)
8280
8381; ---
84−; Parameters
82+; Variables
8583; ---
8684
85+(let_declaration
86+ pattern: [
87+ ((identifier) @variable)
88+ ((tuple_pattern
89+ (identifier) @variable))
90+ ])
91+
92+; It needs to be anonymous to not conflict with `call_expression` further below.
93+(_
94+ value: (field_expression
95+ value: (identifier)? @variable
96+ field: (field_identifier) @variable.other.member))
97+
98+(arguments
99+ (identifier) @variable.parameter)
87100(parameter
88101 pattern: (identifier) @variable.parameter)
89102(closure_parameters
@@ -336,4 +349,4 @@
336349
337350(type_identifier) @type
338351(identifier) @variable
339−(field_identifier) @property
352+(field_identifier) @variable.other.member
runtime/queries/rust/textobjects.scmnew file+26
@@ -0,0 +1,26 @@
1+(function_item
2+ body: (_) @function.inside) @function.around
3+
4+(struct_item
5+ body: (_) @class.inside) @class.around
6+
7+(enum_item
8+ body: (_) @class.inside) @class.around
9+
10+(union_item
11+ body: (_) @class.inside) @class.around
12+
13+(trait_item
14+ body: (_) @class.inside) @class.around
15+
16+(impl_item
17+ body: (_) @class.inside) @class.around
18+
19+(parameters
20+ (_) @parameter.inside)
21+
22+(closure_parameters
23+ (_) @parameter.inside)
24+
25+(arguments
26+ (_) @parameter.inside)
runtime/queries/svelte/highlights.scm+1−1
@@ -29,7 +29,7 @@
2929 (#match? @_attr "^(href|src)$"))
3030
3131(tag_name) @tag
32−(attribute_name) @property
32+(attribute_name) @variable.other.member
3333(erroneous_end_tag_name) @error
3434(comment) @comment
3535
runtime/queries/toml/highlights.scm+4−4
@@ -1,17 +1,17 @@
11; Properties
22;-----------
33
4−(bare_key) @property
4+(bare_key) @variable.other.member
55(quoted_key) @string
66
77; Literals
88;---------
99
10−(boolean) @constant.builtin
10+(boolean) @constant.builtin.boolean
1111(comment) @comment
1212(string) @string
13−(integer) @number
14−(float) @number
13+(integer) @constant.numeric.integer
14+(float) @constant.numeric.float
1515(offset_date_time) @string.special
1616(local_date_time) @string.special
1717(local_date) @string.special
runtime/queries/tsq/highlights.scm+2−2
@@ -35,12 +35,12 @@
3535
3636(comment) @comment
3737
38−(field_name) @property
38+(field_name) @variable.other.member
3939
4040(capture) @label
4141
4242(predicate_name) @function
4343
44−(escape_sequence) @escape
44+(escape_sequence) @constant.character.escape
4545
4646(node_name) @variable
runtime/queries/yaml/highlights.scm+5−5
@@ -1,12 +1,12 @@
1−(block_mapping_pair key: (_) @property)
2−(flow_mapping (_ key: (_) @property))
1+(block_mapping_pair key: (_) @variable.other.member)
2+(flow_mapping (_ key: (_) @variable.other.member))
33(boolean_scalar) @constant.builtin.boolean
44(null_scalar) @constant.builtin
55(double_quote_scalar) @string
66(single_quote_scalar) @string
7−(escape_sequence) @string.escape
8−(integer_scalar) @number
9−(float_scalar) @number
7+(escape_sequence) @constant.character.escape
8+(integer_scalar) @constant.numeric.integer
9+(float_scalar) @constant.numeric.float
1010(comment) @comment
1111(anchor_name) @type
1212(alias_name) @type
runtime/queries/zig/highlights.scm+4−4
@@ -14,7 +14,7 @@ parameter: (IDENTIFIER) @variable.parameter
1414[
1515 field_member: (IDENTIFIER)
1616 field_access: (IDENTIFIER)
17−] @variable.property
17+] @variable.other.member
1818
1919;; assume TitleCase is a type
2020(
@@ -75,9 +75,9 @@ field_constant: (IDENTIFIER) @constant
7575((BUILTINIDENTIFIER) @keyword.control.import
7676 (#any-of? @keyword.control.import "@import" "@cImport"))
7777
78−(INTEGER) @number
78+(INTEGER) @constant.numeric.integer
7979
80−(FLOAT) @number
80+(FLOAT) @constant.numeric.float
8181
8282[
8383 (LINESTRING)
@@ -85,7 +85,7 @@ field_constant: (IDENTIFIER) @constant
8585] @string
8686
8787(CHAR_LITERAL) @constant.character
88−(EscapeSequence) @escape
88+(EscapeSequence) @constant.character.escape
8989(FormatSequence) @string.special
9090
9191[
runtime/themes/base16_default_dark.tomlnew file+59
@@ -0,0 +1,59 @@
1+# Author: RayGervais<raygervais@hotmail.ca>
2+
3+"ui.background" = { bg = "base00" }
4+"ui.menu" = "base01"
5+"ui.menu.selected" = { fg = "base04", bg = "base01" }
6+"ui.linenr" = {fg = "base01" }
7+"ui.popup" = { bg = "base01" }
8+"ui.window" = { bg = "base01" }
9+"ui.liner.selected" = "base02"
10+"ui.selection" = "base02"
11+"comment" = "base03"
12+"ui.statusline" = {fg = "base04", bg = "base01" }
13+"ui.help" = { fg = "base04", bg = "base01" }
14+"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
15+"ui.text" = { fg = "base05" }
16+"operator" = "base05"
17+"ui.text.focus" = { fg = "base05" }
18+"variable" = "base08"
19+"constant.numeric" = "base09"
20+"constant" = "base09"
21+"attributes" = "base09"
22+"type" = "base0A"
23+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
24+"strings" = "base0B"
25+"variable.other.member" = "base0B"
26+"constant.character.escape" = "base0C"
27+"function" = "base0D"
28+"constructor" = "base0D"
29+"special" = "base0D"
30+"keyword" = "base0E"
31+"label" = "base0E"
32+"namespace" = "base0E"
33+"ui.popup" = { bg = "base01" }
34+"ui.window" = { bg = "base00" }
35+"ui.help" = { bg = "base01", fg = "base06" }
36+
37+"info" = "base03"
38+"hint" = "base03"
39+"debug" = "base03"
40+"diagnostic" = "base03"
41+"error" = "base0E"
42+
43+[palette]
44+base00 = "#181818" # Default Background
45+base01 = "#282828" # Lighter Background (Used for status bars, line number and folding marks)
46+base02 = "#383838" # Selection Background
47+base03 = "#585858" # Comments, Invisibles, Line Highlighting
48+base04 = "#b8b8b8" # Dark Foreground (Used for status bars)
49+base05 = "#d8d8d8" # Default Foreground, Caret, Delimiters, Operators
50+base06 = "#e8e8e8" # Light Foreground (Not often used)
51+base07 = "#f8f8f8" # Light Background (Not often used)
52+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
53+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
54+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
55+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
56+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
57+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
58+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
59+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
runtime/themes/bogster.toml+3−3
@@ -8,7 +8,7 @@
88"punctuation.delimiter" = "#dc7759"
99"operator" = { fg = "#dc7759", modifiers = ["bold"] }
1010"special" = "#7fdc59"
11−"property" = "#c6b8ad"
11+"variable.other.member" = "#c6b8ad"
1212"variable" = "#c6b8ad"
1313"variable.parameter" = "#c6b8ad"
1414"type" = "#dc597f"
@@ -22,8 +22,8 @@
2222"constant" = "#59dcb7"
2323"constant.builtin" = "#59dcb7"
2424"string" = "#59dcb7"
25−"number" = "#59c0dc"
26−"escape" = { fg = "#7fdc59", modifiers = ["bold"] }
25+"constant.numeric" = "#59c0dc"
26+"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] }
2727"label" = "#59c0dc"
2828
2929"module" = "#d32c5d"
runtime/themes/dark_plus.toml+35−18
@@ -7,7 +7,7 @@
77"type.builtin" = { fg = "type" }
88"type.enum.variant" = { fg = "constant" }
99"constructor" = { fg = "constant" }
10−"property" = { fg = "variable" }
10+"variable.other.member" = { fg = "variable" }
1111
1212"keyword" = { fg = "keyword" }
1313"keyword.directive" = { fg = "keyword" }
@@ -31,47 +31,64 @@
3131"function.macro" = { fg = "keyword" }
3232"attribute" = { fg = "fn_declaration" }
3333
34−"comment" = { fg = "#6A9955" }
34+"comment" = { fg = "dark_green" }
3535
36−"string" = { fg = "#ce9178" }
37−"string.regexp" = { fg = "regex" }
38−"number" = { fg = "#b5cea8" }
39−"escape" = { fg = "#d7ba7d" }
36+"string" = { fg = "orange" }
37+"constant.character" = { fg = "orange" }
38+"string.regexp" = { fg = "gold" }
39+"constant.numeric" = { fg = "pale_green" }
40+"constant.character.escape" = { fg = "gold" }
4041
41−"ui.background" = { fg = "#d4d4d4", bg = "#1e1e1e" }
42+"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
4243
4344"ui.window" = { bg = "widget" }
4445"ui.popup" = { bg = "widget" }
4546"ui.help" = { bg = "widget" }
4647"ui.menu.selected" = { bg = "widget" }
4748
49+# TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`.
4850"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
4951"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
5052"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
5153
5254"ui.selection" = { bg = "#3a3d41" }
53−"ui.selection.primary" = { bg = "#264f78" }
55+"ui.selection.primary" = { bg = "dark_blue" }
5456
55−"ui.linenr" = { fg = "#858585" }
56−"ui.linenr.selected" = { fg = "#c6c6c6" }
57+"ui.linenr" = { fg = "dark_gray" }
58+"ui.linenr.selected" = { fg = "light_gray2" }
5759
58−"ui.statusline" = { fg = "#ffffff", bg = "#007acc" }
59−"ui.statusline.inactive" = { fg = "#ffffff", bg = "#007acc" }
60+"ui.statusline" = { fg = "white", bg = "blue" }
61+"ui.statusline.inactive" = { fg = "white", bg = "blue" }
6062
6163"ui.text" = { fg = "text", bg = "background" }
62−"ui.text.focus" = { fg = "#ffffff" }
64+"ui.text.focus" = { fg = "white" }
6365
64−"warning" = { fg = "#cca700" }
65−"error" = { fg = "#ff1212" }
66−"info" = { fg = "#75beff" }
67−"hint" = { fg = "#eeeeeeb3" }
66+"warning" = { fg = "gold2" }
67+"error" = { fg = "red" }
68+"info" = { fg = "light_blue" }
69+"hint" = { fg = "light_gray3" }
6870
6971diagnostic = { modifiers = ["underlined"] }
7072
7173[palette]
74+white = "#ffffff"
75+orange = "#ce9178"
76+gold = "#d7ba7d"
77+gold2 = "#cca700"
78+pale_green = "#b5cea8"
79+dark_green = "#6A9955"
80+light_gray = "#d4d4d4"
81+light_gray2 = "#c6c6c6"
82+light_gray3 = "#eeeeee"
83+dark_gray = "#858585"
84+dark_gray2 = "#1e1e1e"
85+blue = "#007acc"
86+light_blue = "#75beff"
87+dark_blue = "#264f78"
88+red = "#ff1212"
89+
7290type = "#4EC9B0"
7391keyword = "#569CD6"
74−regex = "#CE9178"
7592special = "#C586C0"
7693variable = "#9CDCFE"
7794fn_declaration = "#DCDCAA"
runtime/themes/everforest_dark.toml+3−4
@@ -8,16 +8,16 @@
88# Email: sainnhe@gmail.com
99# License: MIT License
1010
11−"escape" = "orange"
11+"constant.character.escape" = "orange"
1212"type" = "yellow"
1313"constant" = "purple"
14−"number" = "purple"
14+"constant.numeric" = "purple"
1515"string" = "grey2"
1616"comment" = "grey0"
1717"variable" = "fg"
1818"variable.builtin" = "blue"
1919"variable.parameter" = "fg"
20−"variable.property" = "fg"
20+"variable.other.member" = "fg"
2121"label" = "aqua"
2222"punctuation" = "grey2"
2323"punctuation.delimiter" = "grey2"
@@ -32,7 +32,6 @@
3232"attribute" = "aqua"
3333"constructor" = "yellow"
3434"module" = "blue"
35−"property" = "fg"
3635"special" = "orange"
3736
3837"ui.background" = { bg = "bg0" }
runtime/themes/gruvbox.toml+3−4
@@ -9,8 +9,7 @@
99"punctuation.delimiter" = "orange1"
1010"operator" = "purple1"
1111"special" = "purple0"
12−"property" = "blue1"
13−"variable.property" = "blue1"
12+"variable.other.member" = "blue1"
1413"variable" = "fg1"
1514"variable.builtin" = "orange1"
1615"variable.parameter" = "fg2"
@@ -24,8 +23,8 @@
2423"constant" = { fg = "purple1" }
2524"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
2625"string" = "green1"
27−"number" = "purple1"
28−"escape" = { fg = "fg2", modifiers = ["bold"] }
26+"constant.numeric" = "purple1"
27+"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] }
2928"label" = "aqua1"
3029"module" = "aqua1"
3130
runtime/themes/ingrid.toml+3−3
@@ -8,7 +8,7 @@
88"punctuation.delimiter" = "#C97270"
99"operator" = { fg = "#D74E50", modifiers = ["bold"] }
1010"special" = "#D68482"
11−"property" = "#89BEB7"
11+"variable.other.member" = "#89BEB7"
1212"variable" = "#A6B6CE"
1313"variable.parameter" = "#89BEB7"
1414"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
@@ -22,8 +22,8 @@
2222"constant" = "#D4A520"
2323"constant.builtin" = "#D4A520"
2424"string" = "#D74E50"
25−"number" = "#D74E50"
26−"escape" = { fg = "#D74E50", modifiers = ["bold"] }
25+"constant.numeric" = "#D74E50"
26+"constant.character.escape" = { fg = "#D74E50", modifiers = ["bold"] }
2727"label" = "#D68482"
2828
2929"module" = "#839A53"
runtime/themes/monokai.toml+4−3
@@ -7,7 +7,7 @@
77"type.builtin" = { fg = "#66D9EF" }
88"type.enum.variant" = { fg = "text" }
99"constructor" = { fg = "text" }
10−"property" = { fg = "variable" }
10+"variable.other.member" = { fg = "variable" }
1111
1212"keyword" = { fg = "keyword" }
1313"keyword.directive" = { fg = "keyword" }
@@ -34,9 +34,10 @@
3434"comment" = { fg = "#88846F" }
3535
3636"string" = { fg = "#e6db74" }
37+"constant.character" = { fg = "#e6db74" }
3738"string.regexp" = { fg = "regex" }
38−"number" = { fg = "#ae81ff" }
39−"escape" = { fg = "#ae81ff" }
39+"constant.numeric" = { fg = "#ae81ff" }
40+"constant.character.escape" = { fg = "#ae81ff" }
4041
4142"ui.background" = { fg = "text", bg = "background" }
4243
runtime/themes/nord.toml+61−43
@@ -1,84 +1,102 @@
11# Author : RayGervais<raygervais@hotmail.ca>
22
3−# "ui.linenr.selected" = { fg = "#d8dee9" }
4−"ui.text.focus" = { fg = "#88c0d0", modifiers= ["bold"] }
5−# "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
3+"ui.linenr.selected" = { fg = "nord4" }
4+"ui.text.focus" = { fg = "nord8", modifiers= ["bold"] }
5+"ui.menu.selected" = { fg = "nord8", bg = "nord2" }
66
7−# "info" = "#b48ead"
8−# "hint" = "#a3be8c"
7+"info" = "nord8"
8+"hint" = "nord8"
99
1010# Polar Night
1111# nord0 - background color
12−"ui.background" = { bg = "#2e3440" }
13−"ui.statusline.inactive" = { fg = "#d8dEE9", bg = "#2e3440" }
12+"ui.background" = { bg = "nord0" }
13+"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" }
1414
1515
1616# nord1 - status bars, panels, modals, autocompletion
17−"ui.statusline" = { fg = "#88c0d0", bg = "#3b4252" }
17+"ui.statusline" = { fg = "nord8", bg = "nord1" }
1818"ui.popup" = { bg = "#232d38" }
1919"ui.window" = { bg = "#232d38" }
20−"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
20+"ui.help" = { bg = "#232d38", fg = "nord4" }
2121
2222# nord2 - active line, highlighting
23−"ui.selection" = { bg = "#434c5e" }
24−"ui.cursor.match" = { bg = "434c5e" }
23+"ui.selection" = { bg = "nord2" }
24+"ui.cursor.match" = { bg = "nord2" }
2525
26−# nord3 - comments
27−"comment" = "#616E88"
28−"ui.linenr" = { fg = "#616E88" }
26+# nord3 - comments, nord3 based lighter color
27+# relative: https://github.com/arcticicestudio/nord/issues/94
28+"comment" = "gray"
29+"ui.linenr" = { fg = "gray" }
2930
3031# Snow Storm
3132# nord4 - cursor, variables, constants, attributes, fields
32−"ui.cursor.primary" = { fg = "#d8dee9", modifiers = ["reversed"] }
33−"attribute" = "#d8dee9"
34−"variable" = "#d8dee9"
35−"constant" = "#d8dee9"
36−"variable.builtin" = "#d8dee9"
37−"constant.builtin" = "#d8dee9"
38−"namespace" = "#d8dee9"
33+"ui.cursor.primary" = { fg = "nord4", modifiers = ["reversed"] }
34+"attribute" = "nord4"
35+"variable" = "nord4"
36+"constant" = "nord4"
37+"variable.builtin" = "nord4"
38+"constant.builtin" = "nord4"
39+"namespace" = "nord4"
3940
4041# nord5 - suble UI text
4142
4243# nord6 - base text, punctuation
43−"ui.text" = { fg = "#eceff4" }
44−"punctuation" = "#eceff4"
44+"ui.text" = { fg = "nord6" }
45+"punctuation" = "nord6"
4546
4647# Frost
4748# nord7 - classes, types, primiatives
48−"type" = "#8fbcbb"
49−"type.builtin" = { fg = "#8fbcbb"}
50−"label" = "#8fbcbb"
49+"type" = "nord7"
50+"type.builtin" = { fg = "nord7"}
51+"label" = "nord7"
5152
5253# nord8 - declaration, methods, routines
53−"constructor" = "#88c0d0"
54−"function" = "#88c0d0"
55−"function.macro" = { fg = "#88c0d0" }
56−"function.builtin" = { fg = "#88c0d0" }
54+"constructor" = "nord8"
55+"function" = "nord8"
56+"function.macro" = { fg = "nord8" }
57+"function.builtin" = { fg = "nord8" }
5758
5859# nord9 - operator, tags, units, punctuations
59−"punctuation.delimiter" = "#81a1c1"
60−"operator" = { fg = "#81a1c1" }
61−"property" = "#81a1c1"
60+"punctuation.delimiter" = "nord9"
61+"operator" = { fg = "nord9" }
62+"variable.other.member" = "nord9"
6263
6364# nord10 - keywords, special
64−"keyword" = { fg = "#5e81ac" }
65−"keyword.directive" = "#5e81ac"
66−"variable.parameter" = "#5e81ac"
65+"keyword" = { fg = "nord10" }
66+"keyword.directive" = "nord10"
67+"variable.parameter" = "nord10"
6768
6869# Aurora
6970# nord11 - error
70−"error" = "#bf616a"
71+"error" = "nord11"
7172
7273# nord12 - annotations, decorators
73−"special" = "#d08770"
74−"module" = "#d08770"
74+"special" = "nord12"
75+"module" = "nord12"
7576
7677# nord13 - warnings, escape characters, regex
77−"warning" = "#ebcb8b"
78−"escape" = { fg = "#ebcb8b" }
78+"warning" = "nord13"
79+"constant.character.escape" = { fg = "nord13" }
7980
8081# nord14 - strings
81−"string" = "#a3be8c"
82+"string" = "nord14"
8283
8384# nord15 - integer, floating point
84−"number" = "#b48ead"
85+"constant.numeric" = "nord15"
86+
87+[palette]
88+nord0 = "#2e3440"
89+nord1 = "#3b4252"
90+nord2 = "#434c5e"
91+nord4 = "#d8dee9"
92+nord6 = "#eceff4"
93+nord7 = "#8fbcbb"
94+nord8 = "#88c0d0"
95+nord9 = "#81a1c1"
96+nord10 = "#5e81ac"
97+nord11 = "#bf616a"
98+nord12 = "#d08770"
99+nord13 = "#ebcb8b"
100+nord14 = "#a3be8c"
101+nord15 = "#b48ead"
102+gray = "#616e88"
runtime/themes/onedark.toml+60−43
@@ -1,54 +1,71 @@
11# Author : Gokul Soumya <gokulps15@gmail.com>
22
3−"attribute" = { fg = "#E5C07B" }
4−"comment" = { fg = "#5C6370", modifiers = ['italic'] }
5−"constant" = { fg = "#56B6C2" }
6−"constant.builtin" = { fg = "#61AFEF" }
7−"constructor" = { fg = "#61AFEF" }
8−"escape" = { fg = "#D19A66" }
9−"function" = { fg = "#61AFEF" }
10−"function.builtin" = { fg = "#61AFEF" }
11−"function.macro" = { fg = "#C678DD" }
12−"keyword" = { fg = "#E06C75" }
13−"keyword.directive" = { fg = "#C678DD" }
14−"label" = { fg = "#C678DD" }
15−"namespace" = { fg = "#61AFEF" }
16−"number" = { fg = "#D19A66" }
17−"operator" = { fg = "#C678DD" }
18−"property" = { fg = "#E06C75" }
19−"special" = { fg = "#61AFEF" }
20−"string" = { fg = "#98C379" }
21−"type" = { fg = "#E5C07B" }
22−"type.builtin" = { fg = "#E5C07B" }
23−"variable" = { fg = "#61AFEF" }
24−"variable.builtin" = { fg = "#61AFEF" }
25−"variable.parameter" = { fg = "#E06C75" }
3+"attribute" = { fg = "yellow" }
4+"comment" = { fg = "light-gray", modifiers = ["italic"] }
5+"constant" = { fg = "cyan" }
6+"constant.builtin" = { fg = "blue" }
7+"constructor" = { fg = "blue" }
8+"escape" = { fg = "gold" }
9+"function" = { fg = "blue" }
10+"function.builtin" = { fg = "blue" }
11+"function.macro" = { fg = "purple" }
12+"keyword" = { fg = "red" }
13+"keyword.control" = { fg = "purple" }
14+"keyword.directive" = { fg = "purple" }
15+"label" = { fg = "purple" }
16+"namespace" = { fg = "blue" }
17+"number" = { fg = "gold" }
18+"operator" = { fg = "purple" }
19+"property" = { fg = "red" }
20+"special" = { fg = "blue" }
21+"string" = { fg = "green" }
22+"type" = { fg = "yellow" }
23+"type.builtin" = { fg = "yellow" }
24+# "variable" = { fg = "blue" }
25+"variable.builtin" = { fg = "blue" }
26+"variable.parameter" = { fg = "red" }
2627
2728diagnostic = { modifiers = ["underlined"] }
28−"info" = { fg = "#61afef", modifiers = ['bold'] }
29−"hint" = { fg = "#98c379", modifiers = ['bold'] }
30−"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
31−"error" = { fg = "#e06c75", modifiers = ['bold'] }
29+"info" = { fg = "blue", modifiers = ["bold"] }
30+"hint" = { fg = "green", modifiers = ["bold"] }
31+"warning" = { fg = "yellow", modifiers = ["bold"] }
32+"error" = { fg = "red", modifiers = ["bold"] }
3233
33−"ui.background" = { bg = "#282C34" }
34+"ui.background" = { bg = "black" }
3435
35−"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] }
36−"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] }
37−"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']}
36+"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
37+"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }
38+"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]}
3839
39−"ui.selection" = { bg = "#5C6370" }
40−"ui.selection.primary" = { bg = "#3E4452" }
40+"ui.selection" = { bg = "light-gray" }
41+"ui.selection.primary" = { bg = "gray" }
4142
42−"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
43−"ui.linenr.selected" = { fg = "#ABB2BF" }
43+"ui.linenr" = { fg = "linenr", modifiers = ["dim"] }
44+"ui.linenr.selected" = { fg = "white" }
4445
45−"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
46−"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" }
46+"ui.statusline" = { fg = "white", bg = "light-black" }
47+"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" }
4748
48−"ui.text" = { fg = "#ABB2BF" }
49−"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
49+"ui.text" = { fg = "white" }
50+"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] }
5051
51−"ui.help" = { bg = "#3E4452" }
52−"ui.popup" = { bg = "#3E4452" }
53−"ui.window" = { bg = "#3E4452" }
54−"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
52+"ui.help" = { bg = "gray" }
53+"ui.popup" = { bg = "gray" }
54+"ui.window" = { bg = "gray" }
55+"ui.menu.selected" = { fg = "black", bg = "blue" }
56+
57+[palette]
58+
59+yellow = "#E5C07B"
60+blue = "#61AFEF"
61+red = "#E06C75"
62+purple = "#C678DD"
63+green = "#98C379"
64+gold = "#D19A66"
65+cyan = "#56B6C2"
66+white = "#ABB2BF"
67+black = "#282C34"
68+light-black = "#2C323C"
69+gray = "#3E4452"
70+light-gray = "#5C6370"
71+linenr = "#4B5263"
runtime/themes/rose_pine.tomlnew file+61
@@ -0,0 +1,61 @@
1+# Author: RayGervais<raygervais@hotmail.ca>
2+
3+"ui.background" = { bg = "base" }
4+"ui.menu" = "surface"
5+"ui.menu.selected" = { fg = "iris", bg = "surface" }
6+"ui.linenr" = {fg = "subtle" }
7+"ui.popup" = { bg = "overlay" }
8+"ui.window" = { bg = "overlay" }
9+"ui.liner.selected" = "highlightOverlay"
10+"ui.selection" = "highlight"
11+"comment" = "subtle"
12+"ui.statusline" = {fg = "foam", bg = "surface" }
13+"ui.help" = { fg = "foam", bg = "surface" }
14+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
15+"ui.text" = { fg = "text" }
16+"operator" = "rose"
17+"ui.text.focus" = { fg = "base05" }
18+"variable" = "text"
19+"constant.numeric" = "iris"
20+"constant" = "gold"
21+"attributes" = "gold"
22+"type" = "foam"
23+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
24+"string" = "gold"
25+"property" = "foam"
26+"constant.character.escape" = "subtle"
27+"function" = "rose"
28+"function.builtin" = "rose"
29+"function.method" = "foam"
30+"constructor" = "gold"
31+"special" = "gold"
32+"keyword" = "pine"
33+"label" = "iris"
34+"namespace" = "pine"
35+"ui.popup" = { bg = "overlay" }
36+"ui.window" = { bg = "base" }
37+"ui.help" = { bg = "overlay", fg = "foam" }
38+"text" = "text"
39+
40+"info" = "gold"
41+"hint" = "gold"
42+"debug" = "rose"
43+"diagnostic" = "rose"
44+"error" = "love"
45+
46+[palette]
47+base = "#191724"
48+surface = "#1f1d2e"
49+overlay = "#26233a"
50+inactive = "#555169"
51+subtle = "#6e6a86"
52+text = "#e0def4"
53+love = "#eb6f92"
54+gold = "#f6c177"
55+rose = "#ebbcba"
56+pine = "#31748f"
57+foam = "#9ccfd8"
58+iris = "#c4a7e7"
59+highlight = "#2a2837"
60+highlightInactive = "#211f2d"
61+highlightOverlay = "#3a384a"
theme.toml+1−1
@@ -6,7 +6,7 @@ punctuation = "lavender"
66"punctuation.delimiter" = "lavender"
77operator = "lilac"
88special = "honey"
9−property = "white"
9+variable.other.member = "white"
1010variable = "lavender"
1111# variable = "almond" # TODO: metavariables only
1212# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }