on master help sign in
Merge branch 'master' into debug
f2b709aBlaž Hrastnik
.github/workflows/build.yml+9−9
@@ -28,19 +28,19 @@ jobs:
28 uses: actions/cache@v2.1.6
28 uses: actions/cache@v2.1.6
29 with:
29 with:
30 path: ~/.cargo/registry
30 path: ~/.cargo/registry
31− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
31+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
32
32
33 - name: Cache cargo index
33 - name: Cache cargo index
34 uses: actions/cache@v2.1.6
34 uses: actions/cache@v2.1.6
35 with:
35 with:
36 path: ~/.cargo/git
36 path: ~/.cargo/git
37− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
37+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
38
38
39 - name: Cache cargo target dir
39 - name: Cache cargo target dir
40 uses: actions/cache@v2.1.6
40 uses: actions/cache@v2.1.6
41 with:
41 with:
42 path: target
42 path: target
43− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
43+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
44
44
45 - name: Run cargo check
45 - name: Run cargo check
46 uses: actions-rs/cargo@v1
46 uses: actions-rs/cargo@v1
@@ -67,19 +67,19 @@ jobs:
67 uses: actions/cache@v2.1.6
67 uses: actions/cache@v2.1.6
68 with:
68 with:
69 path: ~/.cargo/registry
69 path: ~/.cargo/registry
70− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
70+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
71
71
72 - name: Cache cargo index
72 - name: Cache cargo index
73 uses: actions/cache@v2.1.6
73 uses: actions/cache@v2.1.6
74 with:
74 with:
75 path: ~/.cargo/git
75 path: ~/.cargo/git
76− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
76+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
77
77
78 - name: Cache cargo target dir
78 - name: Cache cargo target dir
79 uses: actions/cache@v2.1.6
79 uses: actions/cache@v2.1.6
80 with:
80 with:
81 path: target
81 path: target
82− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
82+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
83
83
84 - name: Run cargo test
84 - name: Run cargo test
85 uses: actions-rs/cargo@v1
85 uses: actions-rs/cargo@v1
@@ -112,19 +112,19 @@ jobs:
112 uses: actions/cache@v2.1.6
112 uses: actions/cache@v2.1.6
113 with:
113 with:
114 path: ~/.cargo/registry
114 path: ~/.cargo/registry
115− key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
115+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
116
116
117 - name: Cache cargo index
117 - name: Cache cargo index
118 uses: actions/cache@v2.1.6
118 uses: actions/cache@v2.1.6
119 with:
119 with:
120 path: ~/.cargo/git
120 path: ~/.cargo/git
121− key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
121+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
122
122
123 - name: Cache cargo target dir
123 - name: Cache cargo target dir
124 uses: actions/cache@v2.1.6
124 uses: actions/cache@v2.1.6
125 with:
125 with:
126 path: target
126 path: target
127− key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
127+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
128
128
129 - name: Run cargo fmt
129 - name: Run cargo fmt
130 uses: actions-rs/cargo@v1
130 uses: actions-rs/cargo@v1
.gitmodules+5−1
@@ -84,7 +84,7 @@
84 shallow = true
84 shallow = true
85[submodule "helix-syntax/languages/tree-sitter-elixir"]
85[submodule "helix-syntax/languages/tree-sitter-elixir"]
86 path = helix-syntax/languages/tree-sitter-elixir
86 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
88 shallow = true
88 shallow = true
89[submodule "helix-syntax/languages/tree-sitter-nix"]
89[submodule "helix-syntax/languages/tree-sitter-nix"]
90 path = helix-syntax/languages/tree-sitter-nix
90 path = helix-syntax/languages/tree-sitter-nix
@@ -130,3 +130,7 @@
130 path = helix-syntax/languages/tree-sitter-tsq
130 path = helix-syntax/languages/tree-sitter-tsq
131 url = https://github.com/tree-sitter/tree-sitter-tsq
131 url = https://github.com/tree-sitter/tree-sitter-tsq
132 shallow = true
132 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 @@
1
1
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+
2# 0.4.1 (2021-08-14)
83# 0.4.1 (2021-08-14)
3
84
4A minor release that includes:
85A minor release that includes:
@@ -7,6 +88,8 @@ A minor release that includes:
7
88
8# 0.4.0 (2021-08-13)
89# 0.4.0 (2021-08-13)
9
90
91+A big shout out to all the contributors! We had 28 contributors in this release.
92+
10Two months have passed, so this is another big release. A big thank you to all
93Two months have passed, so this is another big release. A big thank you to all
11the contributors and package maintainers!
94the contributors and package maintainers!
12
95
@@ -44,6 +127,8 @@ selections in the future as well as resolves many bugs and edge cases.
44
127
45# 0.3.0 (2021-06-27)
128# 0.3.0 (2021-06-27)
46
129
130+A big shout out to all the contributors! We had 24 contributors in this release.
131+
47Another big release.
132Another big release.
48
133
49Highlights:
134Highlights:
@@ -90,6 +175,8 @@ Includes a fix where wq/wqa could exit before file saving completed.
90
175
91# 0.2.0
176# 0.2.0
92
177
178+A big shout out to all the contributors! We had 18 contributors in this release.
179+
93Enough has changed to bump the version. We're skipping 0.1.x because
180Enough has changed to bump the version. We're skipping 0.1.x because
94previously the CLI would always report version as 0.1.0, and we'd like
181previously the CLI would always report version as 0.1.0, and we'd like
95to distinguish it in bug reports..
182to distinguish it in bug reports..
Cargo.lock+51−40
@@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
78
78
79[[package]]
79[[package]]
80name = "chardetng"
80name = "chardetng"
81−version = "0.1.14"
81+version = "0.1.15"
82source = "registry+https://github.com/rust-lang/crates.io-index"
82source = "registry+https://github.com/rust-lang/crates.io-index"
83−checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c"
83+checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
84dependencies = [
84dependencies = [
85 "cfg-if",
85 "cfg-if",
86 "encoding_rs",
86 "encoding_rs",
@@ -101,15 +101,24 @@ dependencies = [
101
101
102[[package]]
102[[package]]
103name = "clipboard-win"
103name = "clipboard-win"
104−version = "4.2.1"
104+version = "4.2.2"
105source = "registry+https://github.com/rust-lang/crates.io-index"
105source = "registry+https://github.com/rust-lang/crates.io-index"
106−checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
106+checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
107dependencies = [
107dependencies = [
108 "error-code",
108 "error-code",
109 "str-buf",
109 "str-buf",
110 "winapi",
110 "winapi",
111]
111]
112
112
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+
113[[package]]
122[[package]]
114name = "crossbeam-utils"
123name = "crossbeam-utils"
115version = "0.8.5"
124version = "0.8.5"
@@ -122,9 +131,9 @@ dependencies = [
122
131
123[[package]]
132[[package]]
124name = "crossterm"
133name = "crossterm"
125−version = "0.21.0"
134+version = "0.22.1"
126source = "registry+https://github.com/rust-lang/crates.io-index"
135source = "registry+https://github.com/rust-lang/crates.io-index"
127−checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
136+checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
128dependencies = [
137dependencies = [
129 "bitflags",
138 "bitflags",
130 "crossterm_winapi",
139 "crossterm_winapi",
@@ -139,9 +148,9 @@ dependencies = [
139
148
140[[package]]
149[[package]]
141name = "crossterm_winapi"
150name = "crossterm_winapi"
142−version = "0.8.0"
151+version = "0.9.0"
143source = "registry+https://github.com/rust-lang/crates.io-index"
152source = "registry+https://github.com/rust-lang/crates.io-index"
144−checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
153+checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
145dependencies = [
154dependencies = [
146 "winapi",
155 "winapi",
147]
156]
@@ -175,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
175
184
176[[package]]
185[[package]]
177name = "encoding_rs"
186name = "encoding_rs"
178−version = "0.8.28"
187+version = "0.8.29"
179source = "registry+https://github.com/rust-lang/crates.io-index"
188source = "registry+https://github.com/rust-lang/crates.io-index"
180−checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
189+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
181dependencies = [
190dependencies = [
182 "cfg-if",
191 "cfg-if",
183]
192]
@@ -358,11 +367,12 @@ dependencies = [
358
367
359[[package]]
368[[package]]
360name = "helix-core"
369name = "helix-core"
361−version = "0.4.1"
370+version = "0.5.0"
362dependencies = [
371dependencies = [
363 "arc-swap",
372 "arc-swap",
364 "etcetera",
373 "etcetera",
365 "helix-syntax",
374 "helix-syntax",
375+ "log",
366 "once_cell",
376 "once_cell",
367 "quickcheck",
377 "quickcheck",
368 "regex",
378 "regex",
@@ -395,7 +405,7 @@ dependencies = [
395
405
396[[package]]
406[[package]]
397name = "helix-lsp"
407name = "helix-lsp"
398−version = "0.4.1"
408+version = "0.5.0"
399dependencies = [
409dependencies = [
400 "anyhow",
410 "anyhow",
401 "futures-executor",
411 "futures-executor",
@@ -413,7 +423,7 @@ dependencies = [
413
423
414[[package]]
424[[package]]
415name = "helix-syntax"
425name = "helix-syntax"
416−version = "0.4.1"
426+version = "0.5.0"
417dependencies = [
427dependencies = [
418 "anyhow",
428 "anyhow",
419 "cc",
429 "cc",
@@ -424,10 +434,11 @@ dependencies = [
424
434
425[[package]]
435[[package]]
426name = "helix-term"
436name = "helix-term"
427−version = "0.4.1"
437+version = "0.5.0"
428dependencies = [
438dependencies = [
429 "anyhow",
439 "anyhow",
430 "chrono",
440 "chrono",
441+ "content_inspector",
431 "crossterm",
442 "crossterm",
432 "fern",
443 "fern",
433 "futures-util",
444 "futures-util",
@@ -455,7 +466,7 @@ dependencies = [
455
466
456[[package]]
467[[package]]
457name = "helix-tui"
468name = "helix-tui"
458−version = "0.4.1"
469+version = "0.5.0"
459dependencies = [
470dependencies = [
460 "bitflags",
471 "bitflags",
461 "cassowary",
472 "cassowary",
@@ -468,7 +479,7 @@ dependencies = [
468
479
469[[package]]
480[[package]]
470name = "helix-view"
481name = "helix-view"
471−version = "0.4.1"
482+version = "0.5.0"
472dependencies = [
483dependencies = [
473 "anyhow",
484 "anyhow",
474 "bitflags",
485 "bitflags",
@@ -532,9 +543,9 @@ dependencies = [
532
543
533[[package]]
544[[package]]
534name = "instant"
545name = "instant"
535−version = "0.1.11"
546+version = "0.1.12"
536source = "registry+https://github.com/rust-lang/crates.io-index"
547source = "registry+https://github.com/rust-lang/crates.io-index"
537−checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
548+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
538dependencies = [
549dependencies = [
539 "cfg-if",
550 "cfg-if",
540]
551]
@@ -566,9 +577,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
566
577
567[[package]]
578[[package]]
568name = "libc"
579name = "libc"
569−version = "0.2.103"
580+version = "0.2.104"
570source = "registry+https://github.com/rust-lang/crates.io-index"
581source = "registry+https://github.com/rust-lang/crates.io-index"
571−checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
582+checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
572
583
573[[package]]
584[[package]]
574name = "libloading"
585name = "libloading"
@@ -600,9 +611,9 @@ dependencies = [
600
611
601[[package]]
612[[package]]
602name = "lsp-types"
613name = "lsp-types"
603−version = "0.90.1"
614+version = "0.91.1"
604source = "registry+https://github.com/rust-lang/crates.io-index"
615source = "registry+https://github.com/rust-lang/crates.io-index"
605−checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3"
616+checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
606dependencies = [
617dependencies = [
607 "bitflags",
618 "bitflags",
608 "serde",
619 "serde",
@@ -640,9 +651,9 @@ dependencies = [
640
651
641[[package]]
652[[package]]
642name = "mio"
653name = "mio"
643−version = "0.7.13"
654+version = "0.7.14"
644source = "registry+https://github.com/rust-lang/crates.io-index"
655source = "registry+https://github.com/rust-lang/crates.io-index"
645−checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
656+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
646dependencies = [
657dependencies = [
647 "libc",
658 "libc",
648 "log",
659 "log",
@@ -755,9 +766,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
755
766
756[[package]]
767[[package]]
757name = "proc-macro2"
768name = "proc-macro2"
758−version = "1.0.29"
769+version = "1.0.30"
759source = "registry+https://github.com/rust-lang/crates.io-index"
770source = "registry+https://github.com/rust-lang/crates.io-index"
760−checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
771+checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
761dependencies = [
772dependencies = [
762 "unicode-xid",
773 "unicode-xid",
763]
774]
@@ -784,9 +795,9 @@ dependencies = [
784
795
785[[package]]
796[[package]]
786name = "quote"
797name = "quote"
787−version = "1.0.9"
798+version = "1.0.10"
788source = "registry+https://github.com/rust-lang/crates.io-index"
799source = "registry+https://github.com/rust-lang/crates.io-index"
789−checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
800+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
790dependencies = [
801dependencies = [
791 "proc-macro2",
802 "proc-macro2",
792]
803]
@@ -973,9 +984,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
973
984
974[[package]]
985[[package]]
975name = "slab"
986name = "slab"
976−version = "0.4.4"
987+version = "0.4.5"
977source = "registry+https://github.com/rust-lang/crates.io-index"
988source = "registry+https://github.com/rust-lang/crates.io-index"
978−checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
989+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
979
990
980[[package]]
991[[package]]
981name = "slotmap"
992name = "slotmap"
@@ -1000,9 +1011,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
1000
1011
1001[[package]]
1012[[package]]
1002name = "syn"
1013name = "syn"
1003−version = "1.0.78"
1014+version = "1.0.80"
1004source = "registry+https://github.com/rust-lang/crates.io-index"
1015source = "registry+https://github.com/rust-lang/crates.io-index"
1005−checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
1016+checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
1006dependencies = [
1017dependencies = [
1007 "proc-macro2",
1018 "proc-macro2",
1008 "quote",
1019 "quote",
@@ -1075,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1075
1086
1076[[package]]
1087[[package]]
1077name = "tokio"
1088name = "tokio"
1078−version = "1.12.0"
1089+version = "1.13.0"
1079source = "registry+https://github.com/rust-lang/crates.io-index"
1090source = "registry+https://github.com/rust-lang/crates.io-index"
1080−checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
1091+checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
1081dependencies = [
1092dependencies = [
1082 "autocfg",
1093 "autocfg",
1083 "bytes",
1094 "bytes",
@@ -1095,9 +1106,9 @@ dependencies = [
1095
1106
1096[[package]]
1107[[package]]
1097name = "tokio-macros"
1108name = "tokio-macros"
1098−version = "1.4.1"
1109+version = "1.5.0"
1099source = "registry+https://github.com/rust-lang/crates.io-index"
1110source = "registry+https://github.com/rust-lang/crates.io-index"
1100−checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb"
1111+checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
1101dependencies = [
1112dependencies = [
1102 "proc-macro2",
1113 "proc-macro2",
1103 "quote",
1114 "quote",
@@ -1106,9 +1117,9 @@ dependencies = [
1106
1117
1107[[package]]
1118[[package]]
1108name = "tokio-stream"
1119name = "tokio-stream"
1109−version = "0.1.7"
1120+version = "0.1.8"
1110source = "registry+https://github.com/rust-lang/crates.io-index"
1121source = "registry+https://github.com/rust-lang/crates.io-index"
1111−checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
1122+checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
1112dependencies = [
1123dependencies = [
1113 "futures-core",
1124 "futures-core",
1114 "pin-project-lite",
1125 "pin-project-lite",
@@ -1145,9 +1156,9 @@ dependencies = [
1145
1156
1146[[package]]
1157[[package]]
1147name = "unicode-bidi"
1158name = "unicode-bidi"
1148−version = "0.3.6"
1159+version = "0.3.7"
1149source = "registry+https://github.com/rust-lang/crates.io-index"
1160source = "registry+https://github.com/rust-lang/crates.io-index"
1150−checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
1161+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
1151
1162
1152[[package]]
1163[[package]]
1153name = "unicode-general-category"
1164name = "unicode-general-category"
README.md+3−3
@@ -27,11 +27,11 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
27It's a terminal-based editor first, but I'd like to explore a custom renderer
27It's a terminal-based editor first, but I'd like to explore a custom renderer
28(similar to emacs) in wgpu or skulpin.
28(similar to emacs) in wgpu or skulpin.
29
29
30−# Installation
31−
32Note: Only certain languages have indentation definitions at the moment. Check
30Note: Only certain languages have indentation definitions at the moment. Check
33`runtime/queries/<lang>/` for `indents.toml`.
31`runtime/queries/<lang>/` for `indents.toml`.
34
32
33+# Installation
34+
35We provide packaging for various distributions, but here's a quick method to
35We provide packaging for various distributions, but here's a quick method to
36build from source.
36build from source.
37
37
@@ -71,7 +71,7 @@ Some suggestions to get started:
71
71
72- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
72- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
73- Help with packaging on various distributions needed!
73- 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:
75 * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
75 * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
76 * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
76 * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
77- If your preferred language is missing, integrating a tree-sitter grammar for
77- If your preferred language is missing, integrating a tree-sitter grammar for
TODO.md−6
@@ -6,10 +6,6 @@
6 - clojure
6 - clojure
7 - erlang
7 - erlang
8
8
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
13- [ ] completion isIncomplete support
9- [ ] completion isIncomplete support
14
10
151
111
@@ -18,8 +14,6 @@ as you type completion!
18
14
19- [ ] = for auto indent line/selection
15- [ ] = for auto indent line/selection
20- [ ] :x for closing buffers
16- [ ] :x for closing buffers
21−- [ ] repeat selection
22−
23- [ ] lsp: signature help
17- [ ] lsp: signature help
24
18
252
192
book/src/SUMMARY.md+2
@@ -8,3 +8,5 @@
8 - [Keymap](./keymap.md)
8 - [Keymap](./keymap.md)
9 - [Key Remapping](./remapping.md)
9 - [Key Remapping](./remapping.md)
10 - [Hooks](./hooks.md)
10 - [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
21| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
21| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
22| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
22| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
23| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
23| `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` |
24
26
25## LSP
27## LSP
26
28
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 @@
6
6
7> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
7> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
8
8
9−| Key | Description | Command |
9+| Key | Description | Command |
10−| ----- | ----------- | ------- |
10+| ----- | ----------- | ------- |
11−| `h`, `Left` | Move left | `move_char_left` |
11+| `h`/`Left` | Move left | `move_char_left` |
12−| `j`, `Down` | Move down | `move_char_right` |
12+| `j`/`Down` | Move down | `move_line_down` |
13−| `k`, `Up` | Move up | `move_line_up` |
13+| `k`/`Up` | Move up | `move_line_up` |
14−| `l`, `Right` | Move right | `move_line_down` |
14+| `l`/`Right` | Move right | `move_char_right` |
15−| `w` | Move next word start | `move_next_word_start` |
15+| `w` | Move next word start | `move_next_word_start` |
16−| `b` | Move previous word start | `move_prev_word_start` |
16+| `b` | Move previous word start | `move_prev_word_start` |
17−| `e` | Move next word end | `move_next_word_end` |
17+| `e` | Move next word end | `move_next_word_end` |
18−| `W` | Move next WORD start | `move_next_long_word_start` |
18+| `W` | Move next WORD start | `move_next_long_word_start` |
19−| `B` | Move previous WORD start | `move_prev_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` |
20+| `E` | Move next WORD end | `move_next_long_word_end` |
21−| `t` | Find 'till next char | `find_till_char` |
21+| `t` | Find 'till next char | `find_till_char` |
22−| `f` | Find next char | `find_next_char` |
22+| `f` | Find next char | `find_next_char` |
23−| `T` | Find 'till previous char | `till_prev_char` |
23+| `T` | Find 'till previous char | `till_prev_char` |
24−| `F` | Find previous char | `find_prev_char` |
24+| `F` | Find previous char | `find_prev_char` |
25+| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
25−| `Home` | Move to the start of the line | `goto_line_start` |
26+| `Home` | Move to the start of the line | `goto_line_start` |
26−| `End` | Move to the end of the line | `goto_line_end` |
27+| `End` | Move to the end of the line | `goto_line_end` |
27−| `PageUp` | Move page up | `page_up` |
28+| `PageUp` | Move page up | `page_up` |
28−| `PageDown` | Move page down | `page_down` |
29+| `PageDown` | Move page down | `page_down` |
29−| `Ctrl-u` | Move half page up | `half_page_up` |
30+| `Ctrl-u` | Move half page up | `half_page_up` |
30−| `Ctrl-d` | Move half page down | `half_page_down` |
31+| `Ctrl-d` | Move half page down | `half_page_down` |
31−| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
32+| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
32−| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33+| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
33−| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34+| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
34−| `g` | Enter [goto mode](#goto-mode) | N/A |
35+| `g` | Enter [goto mode](#goto-mode) | N/A |
35−| `m` | Enter [match mode](#match-mode) | N/A |
36+| `m` | Enter [match mode](#match-mode) | N/A |
36−| `:` | Enter command mode | `command_mode` |
37+| `:` | Enter command mode | `command_mode` |
37−| `z` | Enter [view mode](#view-mode) | N/A |
38+| `z` | Enter [view mode](#view-mode) | N/A |
38−| `Z` | Enter sticky [view mode](#view-mode) | N/A |
39+| `Z` | Enter sticky [view mode](#view-mode) | N/A |
39−| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
40+| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
40−| `Space` | Enter [space mode](#space-mode) | N/A |
41+| `Space` | Enter [space mode](#space-mode) | N/A |
41
42
42### Changes
43### Changes
43
44
@@ -54,6 +55,7 @@
54| `A` | Insert at the end of the line | `append_to_line` |
55| `A` | Insert at the end of the line | `append_to_line` |
55| `o` | Open new line below selection | `open_below` |
56| `o` | Open new line below selection | `open_below` |
56| `O` | Open new line above selection | `open_above` |
57| `O` | Open new line above selection | `open_above` |
58+| `.` | Repeat last change | N/A |
57| `u` | Undo change | `undo` |
59| `u` | Undo change | `undo` |
58| `U` | Redo change | `redo` |
60| `U` | Redo change | `redo` |
59| `y` | Yank selection | `yank` |
61| `y` | Yank selection | `yank` |
@@ -86,8 +88,9 @@
86| `;` | Collapse selection onto a single cursor | `collapse_selection` |
88| `;` | Collapse selection onto a single cursor | `collapse_selection` |
87| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
89| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
88| `,` | Keep only the primary selection | `keep_primary_selection` |
90| `,` | Keep only the primary selection | `keep_primary_selection` |
91+| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
89−| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
92+| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
90−| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
93+| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
91| `(` | Rotate main selection backward | `rotate_selections_backward` |
94| `(` | Rotate main selection backward | `rotate_selections_backward` |
92| `)` | Rotate main selection forward | `rotate_selections_forward` |
95| `)` | Rotate main selection forward | `rotate_selections_forward` |
93| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
96| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
@@ -103,13 +106,13 @@
103
106
104### Search
107### Search
105
108
106−> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
107
109
108| Key | Description | Command |
110| Key | Description | Command |
109| ----- | ----------- | ------- |
111| ----- | ----------- | ------- |
110| `/` | Search for regex pattern | `search` |
112| `/` | Search for regex pattern | `search` |
113+| `?` | Search for previous pattern | `rsearch` |
111| `n` | Select next search match | `search_next` |
114| `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` |
113| `*` | Use current selection as the search pattern | `search_selection` |
116| `*` | Use current selection as the search pattern | `search_selection` |
114
117
115### Minor modes
118### Minor modes
@@ -158,6 +161,8 @@ Jumps to various locations.
158| `r` | Go to references | `goto_reference` |
161| `r` | Go to references | `goto_reference` |
159| `i` | Go to implementation | `goto_implementation` |
162| `i` | Go to implementation | `goto_implementation` |
160| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
163| `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` |
161
166
162#### Match mode
167#### Match mode
163
168
@@ -180,12 +185,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
180
185
181This layer is similar to vim keybindings as kakoune does not support window.
186This layer is similar to vim keybindings as kakoune does not support window.
182
187
183−| Key | Description | Command |
188+| Key | Description | Command |
184−| ----- | ------------- | ------- |
189+| ----- | ------------- | ------- |
185−| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
190+| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
186−| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
191+| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
187−| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
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` |
188−| `q`, `Ctrl-q` | Close current window | `wclose` |
197+| `q`, `Ctrl-q` | Close current window | `wclose` |
189
198
190#### Space mode
199#### Space mode
191
200
@@ -213,12 +222,12 @@ This layer is a kludge of mappings, mostly pickers.
213
222
214Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
223Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
215
224
216−| Key | Description | Command |
225+| Key | Description | Command |
217−| ----- | ----------- | ------- |
226+| ----- | ----------- | ------- |
218−| `[d` | Go to previous diagnostic | `goto_prev_diag` |
227+| `[d` | Go to previous diagnostic | `goto_prev_diag` |
219−| `]d` | Go to next diagnostic | `goto_next_diag` |
228+| `]d` | Go to next diagnostic | `goto_next_diag` |
220−| `[D` | Go to first diagnostic in document | `goto_first_diag` |
229+| `[D` | Go to first diagnostic in document | `goto_first_diag` |
221−| `]D` | Go to last diagnostic in document | `goto_last_diag` |
230+| `]D` | Go to last diagnostic in document | `goto_last_diag` |
222| `[space` | Add newline above | `add_newline_above` |
231| `[space` | Add newline above | `add_newline_above` |
223| `]space` | Add newline below | `add_newline_below` |
232| `]space` | Add newline below | `add_newline_below` |
224
233
@@ -242,12 +251,34 @@ commands (including goto) to extend the existing selection instead of replacing
242
251
243Keys to use within picker. Remapping currently not supported.
252Keys to use within picker. Remapping currently not supported.
244
253
245−| Key | Description |
254+| Key | Description |
246−| ----- | ------------- |
255+| ----- | ------------- |
247−| `Up`, `Ctrl-p` | Previous entry |
256+| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
248−| `Down`, `Ctrl-n` | Next entry |
257+| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
249−| `Ctrl-space` | Filter options |
258+| `Ctrl-space` | Filter options |
250−| `Enter` | Open selected |
259+| `Enter` | Open selected |
251−| `Ctrl-h` | Open horizontally |
260+| `Ctrl-s` | Open horizontally |
252−| `Ctrl-v` | Open vertically |
261+| `Ctrl-v` | Open vertically |
253−| `Escape`, `Ctrl-c` | Close picker |
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 @@
2
2
3One-way key remapping is temporarily supported via a simple TOML configuration
3One-way key remapping is temporarily supported via a simple TOML configuration
4file. (More powerful solutions such as rebinding via commands will be
4file. (More powerful solutions such as rebinding via commands will be
5−available in the feature).
5+available in the future).
6
6
7To remap keys, write a `config.toml` file in your `helix` configuration
7To remap keys, write a `config.toml` file in your `helix` configuration
8directory (default `~/.config/helix` in Linux systems) with a structure like
8directory (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
103[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
103[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
104[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
104[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
105
105
106−- `escape` (TODO: rename to (constant).character.escape)
107−
108- `type` - Types
106- `type` - Types
109 - `builtin` - Primitive types provided by the language (`int`, `usize`)
107 - `builtin` - Primitive types provided by the language (`int`, `usize`)
110
108
@@ -112,13 +110,17 @@ We use a similar set of scopes as
112 - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
110 - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
113 - `boolean`
111 - `boolean`
114 - `character`
112 - `character`
113+ - `escape`
114+ - `numeric` (numbers)
115+ - `integer`
116+ - `float`
115
117
116−- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
117- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
118- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
118 - `regexp` - Regular expressions
119 - `regexp` - Regular expressions
119 - `special`
120 - `special`
120 - `path`
121 - `path`
121 - `url`
122 - `url`
123+ - `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
122
124
123- `comment` - Code comments
125- `comment` - Code comments
124 - `line` - Single line comments (`//`)
126 - `line` - Single line comments (`//`)
@@ -128,7 +130,8 @@ We use a similar set of scopes as
128- `variable` - Variables
130- `variable` - Variables
129 - `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
131 - `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
130 - `parameter` - Function parameters
132 - `parameter` - Function parameters
131− - `property`
133+ - `other`
134+ - `member` - Fields of composite data types (e.g. structs, unions)
132 - `function` (TODO: ?)
135 - `function` (TODO: ?)
133
136
134- `label`
137- `label`
book/src/usage.md+12−3
@@ -2,6 +2,8 @@
2
2
3(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
3(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
4
4
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+
5## Registers
7## Registers
6
8
7Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
9Vim-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.
49
51
50## Textobjects
52## Textobjects
51
53
52−Currently supported: `word`, `surround`.
54+Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
53
55
54![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
56![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)
55
58
56- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
59- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
57- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
60- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
@@ -60,5 +63,11 @@ Currently supported: `word`, `surround`.
60| --- | --- |
63| --- | --- |
61| `w` | Word |
64| `w` | Word |
62| `(`, `[`, `'`, etc | Specified surround pairs |
65| `(`, `[`, `'`, etc | Specified surround pairs |
63−
66+| `f` | Function |
64−Textobjects based on treesitter, like `function`, `class`, etc are planned.
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 @@
2 "nodes": {
2 "nodes": {
3 "devshell": {
3 "devshell": {
4 "locked": {
4 "locked": {
5− "lastModified": 1630239564,
5+ "lastModified": 1632436039,
6− "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
6+ "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
7 "owner": "numtide",
7 "owner": "numtide",
8 "repo": "devshell",
8 "repo": "devshell",
9− "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
9+ "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
10 "type": "github"
10 "type": "github"
11 },
11 },
12 "original": {
12 "original": {
@@ -15,6 +15,21 @@
15 "type": "github"
15 "type": "github"
16 }
16 }
17 },
17 },
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+ },
18 "flakeCompat": {
33 "flakeCompat": {
19 "flake": false,
34 "flake": false,
20 "locked": {
35 "locked": {
@@ -37,14 +52,16 @@
37 "nixpkgs": [
52 "nixpkgs": [
38 "nixpkgs"
53 "nixpkgs"
39 ],
54 ],
40− "rustOverlay": "rustOverlay"
55+ "rustOverlay": [
56+ "rust-overlay"
57+ ]
41 },
58 },
42 "locked": {
59 "locked": {
43− "lastModified": 1631254163,
60+ "lastModified": 1634796585,
44− "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
61+ "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
45 "owner": "yusdacra",
62 "owner": "yusdacra",
46 "repo": "nix-cargo-integration",
63 "repo": "nix-cargo-integration",
47− "rev": "432d8504a32232e8d74710024d5bf5cc31767651",
64+ "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
48 "type": "github"
65 "type": "github"
49 },
66 },
50 "original": {
67 "original": {
@@ -55,11 +72,11 @@
55 },
72 },
56 "nixpkgs": {
73 "nixpkgs": {
57 "locked": {
74 "locked": {
58− "lastModified": 1631206977,
75+ "lastModified": 1634782485,
59− "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
76+ "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
60 "owner": "nixos",
77 "owner": "nixos",
61 "repo": "nixpkgs",
78 "repo": "nixpkgs",
62− "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
79+ "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
63 "type": "github"
80 "type": "github"
64 },
81 },
65 "original": {
82 "original": {
@@ -69,21 +86,40 @@
69 "type": "github"
86 "type": "github"
70 }
87 }
71 },
88 },
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+ },
72 "root": {
104 "root": {
73 "inputs": {
105 "inputs": {
74 "flakeCompat": "flakeCompat",
106 "flakeCompat": "flakeCompat",
75 "nixCargoIntegration": "nixCargoIntegration",
107 "nixCargoIntegration": "nixCargoIntegration",
76− "nixpkgs": "nixpkgs"
108+ "nixpkgs": "nixpkgs",
109+ "rust-overlay": "rust-overlay"
77 }
110 }
78 },
111 },
79− "rustOverlay": {
112+ "rust-overlay": {
80− "flake": false,
113+ "inputs": {
114+ "flake-utils": "flake-utils",
115+ "nixpkgs": "nixpkgs_2"
116+ },
81 "locked": {
117 "locked": {
82− "lastModified": 1631240108,
118+ "lastModified": 1634869268,
83− "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
119+ "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
84 "owner": "oxalica",
120 "owner": "oxalica",
85 "repo": "rust-overlay",
121 "repo": "rust-overlay",
86− "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
122+ "rev": "c02c2d86354327317546501af001886fbb53d374",
87 "type": "github"
123 "type": "github"
88 },
124 },
89 "original": {
125 "original": {
flake.nix+3−1
@@ -3,9 +3,11 @@
3
3
4 inputs = {
4 inputs = {
5 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
5 nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6+ rust-overlay.url = "github:oxalica/rust-overlay";
6 nixCargoIntegration = {
7 nixCargoIntegration = {
7 url = "github:yusdacra/nix-cargo-integration";
8 url = "github:yusdacra/nix-cargo-integration";
8 inputs.nixpkgs.follows = "nixpkgs";
9 inputs.nixpkgs.follows = "nixpkgs";
10+ inputs.rustOverlay.follows = "rust-overlay";
9 };
11 };
10 flakeCompat = {
12 flakeCompat = {
11 url = "github:edolstra/flake-compat";
13 url = "github:edolstra/flake-compat";
@@ -61,7 +63,7 @@
61 '';
63 '';
62 };
64 };
63 shell = common: prev: {
65 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 ]);
65 env = prev.env ++ [
67 env = prev.env ++ [
66 { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
68 { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
67 { name = "RUST_BACKTRACE"; value = "1"; }
69 { name = "RUST_BACKTRACE"; value = "1"; }
helix-core/Cargo.toml+4−3
@@ -1,8 +1,8 @@
1[package]
1[package]
2name = "helix-core"
2name = "helix-core"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
6license = "MPL-2.0"
6license = "MPL-2.0"
7description = "Helix editor core editing primitives"
7description = "Helix editor core editing primitives"
8categories = ["editor"]
8categories = ["editor"]
@@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
13[features]
13[features]
14
14
15[dependencies]
15[dependencies]
16−helix-syntax = { version = "0.4", path = "../helix-syntax" }
16+helix-syntax = { version = "0.5", path = "../helix-syntax" }
17
17
18ropey = "1.3"
18ropey = "1.3"
19smallvec = "1.7"
19smallvec = "1.7"
@@ -27,6 +27,7 @@ once_cell = "1.8"
27arc-swap = "1"
27arc-swap = "1"
28regex = "1"
28regex = "1"
29
29
30+log = "0.4"
30serde = { version = "1.0", features = ["derive"] }
31serde = { version = "1.0", features = ["derive"] }
31serde_json = "1.0"
32serde_json = "1.0"
32toml = "0.5"
33toml = "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+
1use crate::{Range, Rope, Selection, Tendril, Transaction};
4use crate::{Range, Rope, Selection, Tendril, Transaction};
2use smallvec::SmallVec;
5use smallvec::SmallVec;
3
6
helix-core/src/chars.rs+2
@@ -1,3 +1,5 @@
1+//! Utility functions to categorize a `char`.
2+
1use crate::LineEnding;
3use crate::LineEnding;
2
4
3#[derive(Debug, Eq, PartialEq)]
5#[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+
1use crate::{
4use crate::{
2 find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
5 find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
3};
6};
@@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
60 let token = token.unwrap_or("//");
63 let token = token.unwrap_or("//");
61 let comment = Tendril::from(format!("{} ", token));
64 let comment = Tendril::from(format!("{} ", token));
62
65
63− let mut lines: Vec<usize> = Vec::new();
66+ let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
64
67
65 let mut min_next_line = 0;
68 let mut min_next_line = 0;
66 for selection in selection {
69 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`].
1#[derive(Debug, Eq, PartialEq)]
4#[derive(Debug, Eq, PartialEq)]
2pub enum Severity {
5pub enum Severity {
3 Error,
6 Error,
@@ -6,12 +9,14 @@ pub enum Severity {
6 Hint,
9 Hint,
7}
10}
8
11
12+/// A range of `char`s within the text.
9−#[derive(Debug)]
13+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
10pub struct Range {
14pub struct Range {
11 pub start: usize,
15 pub start: usize,
12 pub end: usize,
16 pub end: usize,
13}
17}
14
18
19+/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
15#[derive(Debug)]
20#[derive(Debug)]
16pub struct Diagnostic {
21pub struct Diagnostic {
17 pub range: Range,
22 pub range: Range,
helix-core/src/graphemes.rs+3−1
@@ -1,4 +1,6 @@
1+//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
2+//!
1−// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
3+//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
2use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
4use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
3use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
5use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
4use unicode_width::UnicodeWidthStr;
6use unicode_width::UnicodeWidthStr;
helix-core/src/history.rs+65−40
@@ -4,48 +4,50 @@ use regex::Regex;
4use std::num::NonZeroUsize;
4use std::num::NonZeroUsize;
5use std::time::{Duration, Instant};
5use std::time::{Duration, Instant};
6
6
7−// Stores the history of changes to a buffer.
7+/// Stores the history of changes to a buffer.
8−//
8+///
9−// Currently the history is represented as a vector of revisions. The vector
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
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]
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,
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
13+/// and an inversion of that transaction to transition from the parent to its
14−// latest child.
14+/// latest child.
15−//
15+///
16−// When using `u` to undo a change, an inverse of the stored transaction will
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.
17+/// be applied which will transition the buffer to the parent state.
18−//
18+///
19−// Each revision with the exception of the last in the vector also has a
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
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.
21+/// will be applied to the current state of the buffer.
22−//
22+///
23−// The current revision is the one currently displayed in the buffer.
23+/// The current revision is the one currently displayed in the buffer.
24−//
24+///
25−// Commiting a new revision to the history will update the last child of the
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.
26+/// current revision, and push a new revision to the end of the vector.
27−//
27+///
28−// Revisions are commited with a timestamp. :earlier and :later can be used
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
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
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
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.
32+/// jump the given number of revisions in the vector.
33−//
33+///
34−// Limitations:
34+/// Limitations:
35−// * Changes in selections currently don't commit history changes. The selection
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.
36+/// will only be updated to the state after a commited buffer change.
37−// * The vector of history revisions is currently unbounded. This might
37+/// * The vector of history revisions is currently unbounded. This might
38−// cause the memory consumption to grow significantly large during long
38+/// cause the memory consumption to grow significantly large during long
39−// editing sessions.
39+/// editing sessions.
40−// * Because delete transactions currently don't store the text that they
40+/// * Because delete transactions currently don't store the text that they
41−// delete, we also store an inversion of the transaction.
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>
42#[derive(Debug)]
44#[derive(Debug)]
43pub struct History {
45pub struct History {
44 revisions: Vec<Revision>,
46 revisions: Vec<Revision>,
45 current: usize,
47 current: usize,
46}
48}
47
49
48−// A single point in history. See [History] for more information.
50+/// A single point in history. See [History] for more information.
49#[derive(Debug)]
51#[derive(Debug)]
50struct Revision {
52struct Revision {
51 parent: usize,
53 parent: usize,
@@ -111,6 +113,7 @@ impl History {
111 self.current == 0
113 self.current == 0
112 }
114 }
113
115
116+ /// Undo the last edit.
114 pub fn undo(&mut self) -> Option<&Transaction> {
117 pub fn undo(&mut self) -> Option<&Transaction> {
115 if self.at_root() {
118 if self.at_root() {
116 return None;
119 return None;
@@ -121,6 +124,7 @@ impl History {
121 Some(&current_revision.inversion)
124 Some(&current_revision.inversion)
122 }
125 }
123
126
127+ /// Redo the last edit.
124 pub fn redo(&mut self) -> Option<&Transaction> {
128 pub fn redo(&mut self) -> Option<&Transaction> {
125 let current_revision = &self.revisions[self.current];
129 let current_revision = &self.revisions[self.current];
126 let last_child = current_revision.last_child?;
130 let last_child = current_revision.last_child?;
@@ -147,8 +151,8 @@ impl History {
147 }
151 }
148 }
152 }
149
153
150− // List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
154+ /// 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`.
155+ /// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
152 fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
156 fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
153 let mut path = Vec::new();
157 let mut path = Vec::new();
154 while n != a {
158 while n != a {
@@ -158,6 +162,7 @@ impl History {
158 path
162 path
159 }
163 }
160
164
165+ /// Create a [`Transaction`] that will jump to a specific revision in the history.
161 fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
166 fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
162 let lca = self.lowest_common_ancestor(self.current, to);
167 let lca = self.lowest_common_ancestor(self.current, to);
163 let up = self.path_up(self.current, lca);
168 let up = self.path_up(self.current, lca);
@@ -171,10 +176,12 @@ impl History {
171 up_txns.chain(down_txns).collect()
176 up_txns.chain(down_txns).collect()
172 }
177 }
173
178
179+ /// Creates a [`Transaction`] that will undo `delta` revisions.
174 fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
180 fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
175 self.jump_to(self.current.saturating_sub(delta))
181 self.jump_to(self.current.saturating_sub(delta))
176 }
182 }
177
183
184+ /// Creates a [`Transaction`] that will redo `delta` revisions.
178 fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
185 fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
179 self.jump_to(
186 self.jump_to(
180 self.current
187 self.current
@@ -183,7 +190,7 @@ impl History {
183 )
190 )
184 }
191 }
185
192
186− // Helper for a binary search case below.
193+ /// Helper for a binary search case below.
187 fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
194 fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
188 let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
195 let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
189 let dur_i = self.revisions[i].timestamp.duration_since(instant);
196 let dur_i = self.revisions[i].timestamp.duration_since(instant);
@@ -194,6 +201,8 @@ impl History {
194 }
201 }
195 }
202 }
196
203
204+ /// Creates a [`Transaction`] that will match a revision created at around
205+ /// `instant`.
197 fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
206 fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
198 let search_result = self
207 let search_result = self
199 .revisions
208 .revisions
@@ -209,6 +218,8 @@ impl History {
209 self.jump_to(revision)
218 self.jump_to(revision)
210 }
219 }
211
220
221+ /// Creates a [`Transaction`] that will match a revision created `duration` ago
222+ /// from the timestamp of current revision.
212 fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
223 fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
213 match self.revisions[self.current].timestamp.checked_sub(duration) {
224 match self.revisions[self.current].timestamp.checked_sub(duration) {
214 Some(instant) => self.jump_instant(instant),
225 Some(instant) => self.jump_instant(instant),
@@ -216,6 +227,8 @@ impl History {
216 }
227 }
217 }
228 }
218
229
230+ /// Creates a [`Transaction`] that will match a revision created `duration` in
231+ /// the future from the timestamp of the current revision.
219 fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
232 fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
220 match self.revisions[self.current].timestamp.checked_add(duration) {
233 match self.revisions[self.current].timestamp.checked_add(duration) {
221 Some(instant) => self.jump_instant(instant),
234 Some(instant) => self.jump_instant(instant),
@@ -223,6 +236,7 @@ impl History {
223 }
236 }
224 }
237 }
225
238
239+ /// Creates an undo [`Transaction`].
226 pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
240 pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
227 use UndoKind::*;
241 use UndoKind::*;
228 match uk {
242 match uk {
@@ -231,6 +245,7 @@ impl History {
231 }
245 }
232 }
246 }
233
247
248+ /// Creates a redo [`Transaction`].
234 pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
249 pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
235 use UndoKind::*;
250 use UndoKind::*;
236 match uk {
251 match uk {
@@ -240,13 +255,14 @@ impl History {
240 }
255 }
241}
256}
242
257
258+/// Whether to undo by a number of edits or a duration of time.
243#[derive(Debug, PartialEq)]
259#[derive(Debug, PartialEq)]
244pub enum UndoKind {
260pub enum UndoKind {
245 Steps(usize),
261 Steps(usize),
246 TimePeriod(std::time::Duration),
262 TimePeriod(std::time::Duration),
247}
263}
248
264
249−// A subset of sytemd.time time span syntax units.
265+/// A subset of sytemd.time time span syntax units.
250const TIME_UNITS: &[(&[&str], &str, u64)] = &[
266const TIME_UNITS: &[(&[&str], &str, u64)] = &[
251 (&["seconds", "second", "sec", "s"], "seconds", 1),
267 (&["seconds", "second", "sec", "s"], "seconds", 1),
252 (&["minutes", "minute", "min", "m"], "minutes", 60),
268 (&["minutes", "minute", "min", "m"], "minutes", 60),
@@ -254,11 +270,20 @@ const TIME_UNITS: &[(&[&str], &str, u64)] = &[
254 (&["days", "day", "d"], "days", 24 * 60 * 60),
270 (&["days", "day", "d"], "days", 24 * 60 * 60),
255];
271];
256
272
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`
257static DURATION_VALIDATION_REGEX: Lazy<Regex> =
280static DURATION_VALIDATION_REGEX: Lazy<Regex> =
258 Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
281 Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
259
282
283+/// Captures both the number and unit as separate capture groups.
260static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
284static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
261
285
286+/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`].
262fn parse_human_duration(s: &str) -> Result<Duration, String> {
287fn parse_human_duration(s: &str) -> Result<Duration, String> {
263 if !DURATION_VALIDATION_REGEX.is_match(s) {
288 if !DURATION_VALIDATION_REGEX.is_match(s) {
264 return Err("duration should be composed \
289 return Err("duration should be composed \
helix-core/src/indent.rs+1
@@ -464,6 +464,7 @@ where
464 unit: String::from(" "),
464 unit: String::from(" "),
465 }),
465 }),
466 indent_query: OnceCell::new(),
466 indent_query: OnceCell::new(),
467+ textobject_query: OnceCell::new(),
467 debugger: None,
468 debugger: None,
468 }],
469 }],
469 });
470 });
helix-core/src/lib.rs+2−1
@@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
35 line.chars().position(|ch| !ch.is_whitespace())
35 line.chars().position(|ch| !ch.is_whitespace())
36}
36}
37
37
38+/// Find `.git` root.
38pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
39pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
39 let current_dir = std::env::current_dir().expect("unable to determine current directory");
40 let current_dir = std::env::current_dir().expect("unable to determine current directory");
40
41
@@ -193,7 +194,7 @@ pub use tendril::StrTendril as Tendril;
193pub use {regex, tree_sitter};
194pub use {regex, tree_sitter};
194
195
195pub use graphemes::RopeGraphemes;
196pub 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};
197pub use selection::{Range, Selection};
198pub use selection::{Range, Selection};
198pub use smallvec::{smallvec, SmallVec};
199pub use smallvec::{smallvec, SmallVec};
199pub use syntax::Syntax;
200pub use syntax::Syntax;
helix-core/src/line_ending.rs+3−3
@@ -20,7 +20,7 @@ pub enum LineEnding {
20
20
21impl LineEnding {
21impl LineEnding {
22 #[inline]
22 #[inline]
23− pub fn len_chars(&self) -> usize {
23+ pub const fn len_chars(&self) -> usize {
24 match self {
24 match self {
25 Self::Crlf => 2,
25 Self::Crlf => 2,
26 _ => 1,
26 _ => 1,
@@ -28,7 +28,7 @@ impl LineEnding {
28 }
28 }
29
29
30 #[inline]
30 #[inline]
31− pub fn as_str(&self) -> &'static str {
31+ pub const fn as_str(&self) -> &'static str {
32 match self {
32 match self {
33 Self::Crlf => "\u{000D}\u{000A}",
33 Self::Crlf => "\u{000D}\u{000A}",
34 Self::LF => "\u{000A}",
34 Self::LF => "\u{000A}",
@@ -42,7 +42,7 @@ impl LineEnding {
42 }
42 }
43
43
44 #[inline]
44 #[inline]
45− pub fn from_char(ch: char) -> Option<LineEnding> {
45+ pub const fn from_char(ch: char) -> Option<LineEnding> {
46 match ch {
46 match ch {
47 '\u{000A}' => Some(LineEnding::LF),
47 '\u{000A}' => Some(LineEnding::LF),
48 '\u{000B}' => Some(LineEnding::VT),
48 '\u{000B}' => Some(LineEnding::VT),
helix-core/src/movement.rs+4
@@ -53,6 +53,10 @@ pub fn move_vertically(
53 let pos = range.cursor(slice);
53 let pos = range.cursor(slice);
54
54
55 // Compute the current position's 2d coordinates.
55 // 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
56 let Position { row, col } = coords_at_pos(slice, pos);
60 let Position { row, col } = coords_at_pos(slice, pos);
57 let horiz = range.horiz.unwrap_or(col as u32);
61 let horiz = range.horiz.unwrap_or(col as u32);
58
62
helix-core/src/object.rs+7−2
@@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
13 let parent = match tree
13 let parent = match tree
14 .root_node()
14 .root_node()
15 .descendant_for_byte_range(from, to)
15 .descendant_for_byte_range(from, to)
16− .and_then(|node| node.parent())
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+ }
17− {
22+ }) {
18 Some(parent) => parent,
23 Some(parent) => parent,
19 None => return range,
24 None => return range,
20 };
25 };
helix-core/src/position.rs+73−8
@@ -2,6 +2,7 @@ use crate::{
2 chars::char_is_line_ending,
2 chars::char_is_line_ending,
3 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
3 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
4 line_ending::line_end_char_index,
4 line_ending::line_end_char_index,
5+ unicode::width::UnicodeWidthChar,
5 RopeSlice,
6 RopeSlice,
6};
7};
7
8
@@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
54}
55}
55/// Convert a character index to (line, column) coordinates.
56/// Convert a character index to (line, column) coordinates.
56///
57///
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.
62pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
60pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
63 let line = text.char_to_line(pos);
61 let line = text.char_to_line(pos);
64
62
@@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
69 Position::new(line, col)
67 Position::new(line, col)
70}
68}
71
69
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+
72/// Convert (line, column) coordinates to a character index.
92/// Convert (line, column) coordinates to a character index.
73///
93///
74/// If the `line` coordinate is beyond the end of the file, the EOF
94/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -130,7 +150,6 @@ mod test {
130 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
150 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
131
151
132 // Test with wide characters.
152 // Test with wide characters.
133− // TODO: account for character width.
134 let text = Rope::from("今日はいい\n");
153 let text = Rope::from("今日はいい\n");
135 let slice = text.slice(..);
154 let slice = text.slice(..);
136 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
155 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -151,7 +170,6 @@ mod test {
151 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
170 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
152
171
153 // Test with wide-character grapheme clusters.
172 // Test with wide-character grapheme clusters.
154− // TODO: account for character width.
155 let text = Rope::from("किमपि\n");
173 let text = Rope::from("किमपि\n");
156 let slice = text.slice(..);
174 let slice = text.slice(..);
157 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
175 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -161,7 +179,6 @@ mod test {
161 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
179 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
162
180
163 // Test with tabs.
181 // Test with tabs.
164− // Todo: account for tab stops.
165 let text = Rope::from("\tHello\n");
182 let text = Rope::from("\tHello\n");
166 let slice = text.slice(..);
183 let slice = text.slice(..);
167 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
184 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -169,6 +186,54 @@ mod test {
169 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
186 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
170 }
187 }
171
188
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+
172 #[test]
237 #[test]
173 fn test_pos_at_coords() {
238 fn test_pos_at_coords() {
174 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
239 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
helix-core/src/register.rs+2−2
@@ -7,7 +7,7 @@ pub struct Register {
7}
7}
8
8
9impl Register {
9impl Register {
10− pub fn new(name: char) -> Self {
10+ pub const fn new(name: char) -> Self {
11 Self {
11 Self {
12 name,
12 name,
13 values: Vec::new(),
13 values: Vec::new(),
@@ -18,7 +18,7 @@ impl Register {
18 Self { name, values }
18 Self { name, values }
19 }
19 }
20
20
21− pub fn name(&self) -> char {
21+ pub const fn name(&self) -> char {
22 self.name
22 self.name
23 }
23 }
24
24
helix-core/src/selection.rs+11
@@ -362,6 +362,11 @@ impl Selection {
362
362
363 /// Adds a new range to the selection and makes it the primary range.
363 /// Adds a new range to the selection and makes it the primary range.
364 pub fn remove(mut self, index: usize) -> Self {
364 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+
365 self.ranges.remove(index);
370 self.ranges.remove(index);
366 if index < self.primary_index || self.primary_index == self.ranges.len() {
371 if index < self.primary_index || self.primary_index == self.ranges.len() {
367 self.primary_index -= 1;
372 self.primary_index -= 1;
@@ -369,6 +374,12 @@ impl Selection {
369 self
374 self
370 }
375 }
371
376
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+
372 /// Map selections over a set of changes. Useful for adjusting the selection position after
383 /// Map selections over a set of changes. Useful for adjusting the selection position after
373 /// applying changes to a document.
384 /// applying changes to a document.
374 pub fn map(self, changes: &ChangeSet) -> Self {
385 pub fn map(self, changes: &ChangeSet) -> Self {
helix-core/src/syntax.rs+49−41
@@ -49,7 +49,7 @@ pub struct Configuration {
49#[serde(rename_all = "kebab-case")]
49#[serde(rename_all = "kebab-case")]
50pub struct LanguageConfiguration {
50pub struct LanguageConfiguration {
51 #[serde(rename = "name")]
51 #[serde(rename = "name")]
52− pub(crate) language_id: String,
52+ pub language_id: String,
53 pub scope: String, // source.rust
53 pub scope: String, // source.rust
54 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
54 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
55 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
55 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
76
76
77 #[serde(skip)]
77 #[serde(skip)]
78 pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
78 pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
79+ #[serde(skip)]
80+ pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 #[serde(skip_serializing_if = "Option::is_none")]
80 pub debugger: Option<DebugAdapterConfig>,
82 pub debugger: Option<DebugAdapterConfig>,
81}
83}
@@ -160,6 +162,32 @@ pub struct IndentQuery {
160 pub outdent: HashSet<String>,
162 pub outdent: HashSet<String>,
161}
163}
162
164
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+
163fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
191fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
164 let path = crate::RUNTIME_DIR
192 let path = crate::RUNTIME_DIR
165 .join("queries")
193 .join("queries")
@@ -208,13 +236,14 @@ impl LanguageConfiguration {
208 // highlights_query += "\n(ERROR) @error";
236 // highlights_query += "\n(ERROR) @error";
209
237
210 let injections_query = read_query(&language, "injections.scm");
238 let injections_query = read_query(&language, "injections.scm");
211−
212 let locals_query = read_query(&language, "locals.scm");
239 let locals_query = read_query(&language, "locals.scm");
213
240
214 if highlights_query.is_empty() {
241 if highlights_query.is_empty() {
215 None
242 None
216 } else {
243 } 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()?;
218 let config = HighlightConfiguration::new(
247 let config = HighlightConfiguration::new(
219 language,
248 language,
220 &highlights_query,
249 &highlights_query,
@@ -258,6 +287,18 @@ impl LanguageConfiguration {
258 .as_ref()
287 .as_ref()
259 }
288 }
260
289
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+
261 pub fn scope(&self) -> &str {
302 pub fn scope(&self) -> &str {
262 &self.scope
303 &self.scope
263 }
304 }
@@ -451,7 +492,7 @@ impl Syntax {
451
492
452 /// Iterate over the highlighted regions for a given slice of source code.
493 /// Iterate over the highlighted regions for a given slice of source code.
453 pub fn highlight_iter<'a>(
494 pub fn highlight_iter<'a>(
454− &self,
495+ &'a self,
455 source: RopeSlice<'a>,
496 source: RopeSlice<'a>,
456 range: Option<std::ops::Range<usize>>,
497 range: Option<std::ops::Range<usize>>,
457 cancellation_flag: Option<&'a AtomicUsize>,
498 cancellation_flag: Option<&'a AtomicUsize>,
@@ -466,11 +507,10 @@ impl Syntax {
466 let highlighter = &mut ts_parser.borrow_mut();
507 let highlighter = &mut ts_parser.borrow_mut();
467 highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
508 highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
468 });
509 });
469− let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
510+ let tree_ref = self.tree();
470 let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
511 let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
471− let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
512+ let query_ref = &self.config.query;
472− let config_ref =
513+ let config_ref = self.config.as_ref();
473− unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
474
514
475 // if reusing cursors & no range this resets to whole range
515 // if reusing cursors & no range this resets to whole range
476 cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
516 cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@@ -582,39 +622,7 @@ impl LanguageLayer {
582 self.tree.as_ref(),
622 self.tree.as_ref(),
583 )
623 )
584 .ok_or(Error::Cancelled)?;
624 .ok_or(Error::Cancelled)?;
585− // unsafe { syntax.parser.set_cancellation_flag(None) };
625+
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− // }
618 self.tree = Some(tree)
626 self.tree = Some(tree)
619 }
627 }
620 Ok(())
628 Ok(())
helix-core/src/textobject.rs+51
@@ -1,9 +1,13 @@
1+use std::fmt::Display;
2+
1use ropey::RopeSlice;
3use ropey::RopeSlice;
4+use tree_sitter::{Node, QueryCursor};
2
5
3use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
6use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
4use crate::graphemes::next_grapheme_boundary;
7use crate::graphemes::next_grapheme_boundary;
5use crate::movement::Direction;
8use crate::movement::Direction;
6use crate::surround;
9use crate::surround;
10+use crate::syntax::LanguageConfiguration;
7use crate::Range;
11use crate::Range;
8
12
9fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
13fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
@@ -51,6 +55,15 @@ pub enum TextObject {
51 Inside,
55 Inside,
52}
56}
53
57
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+
54// count doesn't do anything yet
67// count doesn't do anything yet
55pub fn textobject_word(
68pub fn textobject_word(
56 slice: RopeSlice,
69 slice: RopeSlice,
@@ -108,6 +121,44 @@ pub fn textobject_surround(
108 .unwrap_or(range)
121 .unwrap_or(range)
109}
122}
110
123
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+
111#[cfg(test)]
162#[cfg(test)]
112mod test {
163mod test {
113 use super::TextObject::*;
164 use super::TextObject::*;
helix-core/src/transaction.rs+10
@@ -132,6 +132,9 @@ impl ChangeSet {
132 if self.changes.is_empty() {
132 if self.changes.is_empty() {
133 return other;
133 return other;
134 }
134 }
135+ if other.changes.is_empty() {
136+ return self;
137+ }
135
138
136 let len = self.changes.len();
139 let len = self.changes.len();
137
140
@@ -465,6 +468,13 @@ impl Transaction {
465 }
468 }
466 }
469 }
467
470
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+
468 pub fn with_selection(mut self, selection: Selection) -> Self {
478 pub fn with_selection(mut self, selection: Selection) -> Self {
469 self.selection = Some(selection);
479 self.selection = Some(selection);
470 self
480 self
helix-dap/Cargo.toml+1−1
@@ -1,6 +1,6 @@
1[package]
1[package]
2name = "helix-dap"
2name = "helix-dap"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5edition = "2018"
5edition = "2018"
6license = "MPL-2.0"
6license = "MPL-2.0"
helix-lsp/Cargo.toml+6−6
@@ -1,8 +1,8 @@
1[package]
1[package]
2name = "helix-lsp"
2name = "helix-lsp"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
6license = "MPL-2.0"
6license = "MPL-2.0"
7description = "LSP client implementation for Helix project"
7description = "LSP client implementation for Helix project"
8categories = ["editor"]
8categories = ["editor"]
@@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
12# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13
13
14[dependencies]
14[dependencies]
15−helix-core = { version = "0.4", path = "../helix-core" }
15+helix-core = { version = "0.5", path = "../helix-core" }
16
16
17anyhow = "1.0"
17anyhow = "1.0"
18futures-executor = "0.3"
18futures-executor = "0.3"
19futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
19futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
20jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
20jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
21log = "0.4"
21log = "0.4"
22−lsp-types = { version = "0.90", features = ["proposed"] }
22+lsp-types = { version = "0.91", features = ["proposed"] }
23serde = { version = "1.0", features = ["derive"] }
23serde = { version = "1.0", features = ["derive"] }
24serde_json = "1.0"
24serde_json = "1.0"
25thiserror = "1.0"
25thiserror = "1.0"
26−tokio = { version = "1.12", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
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.7"
27+tokio-stream = "0.1.8"
helix-lsp/src/client.rs+4−3
@@ -461,7 +461,7 @@ impl Client {
461 };
461 };
462
462
463 let changes = match sync_capabilities {
463 let changes = match sync_capabilities {
464− lsp::TextDocumentSyncKind::Full => {
464+ lsp::TextDocumentSyncKind::FULL => {
465 vec![lsp::TextDocumentContentChangeEvent {
465 vec![lsp::TextDocumentContentChangeEvent {
466 // range = None -> whole document
466 // range = None -> whole document
467 range: None, //Some(Range)
467 range: None, //Some(Range)
@@ -469,10 +469,11 @@ impl Client {
469 text: new_text.to_string(),
469 text: new_text.to_string(),
470 }]
470 }]
471 }
471 }
472− lsp::TextDocumentSyncKind::Incremental => {
472+ lsp::TextDocumentSyncKind::INCREMENTAL => {
473 Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding)
473 Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding)
474 }
474 }
475− lsp::TextDocumentSyncKind::None => return None,
475+ lsp::TextDocumentSyncKind::NONE => return None,
476+ kind => unimplemented!("{:?}", kind),
476 };
477 };
477
478
478 Some(self.notify::<lsp::notification::DidChangeTextDocument>(
479 Some(self.notify::<lsp::notification::DidChangeTextDocument>(
helix-syntax/Cargo.toml+2−2
@@ -1,8 +1,8 @@
1[package]
1[package]
2name = "helix-syntax"
2name = "helix-syntax"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
6license = "MPL-2.0"
6license = "MPL-2.0"
7description = "Tree-sitter grammars support"
7description = "Tree-sitter grammars support"
8categories = ["editor"]
8categories = ["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 @@
1[package]
1[package]
2name = "helix-term"
2name = "helix-term"
3−version = "0.4.1"
3+version = "0.5.0"
4description = "A post-modern text editor."
4description = "A post-modern text editor."
5authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
6−edition = "2018"
6+edition = "2021"
7license = "MPL-2.0"
7license = "MPL-2.0"
8categories = ["editor", "command-line-utilities"]
8categories = ["editor", "command-line-utilities"]
9repository = "https://github.com/helix-editor/helix"
9repository = "https://github.com/helix-editor/helix"
@@ -21,10 +21,10 @@ name = "hx"
21path = "src/main.rs"
21path = "src/main.rs"
22
22
23[dependencies]
23[dependencies]
24−helix-core = { version = "0.4", path = "../helix-core" }
24+helix-core = { version = "0.5", path = "../helix-core" }
25−helix-view = { version = "0.4", path = "../helix-view" }
25+helix-view = { version = "0.5", path = "../helix-view" }
26−helix-lsp = { version = "0.4", path = "../helix-lsp" }
26+helix-lsp = { version = "0.5", path = "../helix-lsp" }
27−helix-dap = { version = "0.4", path = "../helix-dap" }
27+helix-dap = { version = "0.5", path = "../helix-dap" }
28
28
29anyhow = "1"
29anyhow = "1"
30once_cell = "1.8"
30once_cell = "1.8"
@@ -32,7 +32,7 @@ once_cell = "1.8"
32tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
32tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
33num_cpus = "1"
33num_cpus = "1"
34tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
34tui = { 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"] }
36signal-hook = "0.3"
36signal-hook = "0.3"
37tokio-stream = "0.1"
37tokio-stream = "0.1"
38futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
38futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -45,10 +45,10 @@ log = "0.4"
45# File picker
45# File picker
46fuzzy-matcher = "0.3"
46fuzzy-matcher = "0.3"
47ignore = "0.4"
47ignore = "0.4"
48−# shellexpand = "2.1"
49−# dirs-next = "2.0"
50# markdown doc rendering
48# markdown doc rendering
51pulldown-cmark = { version = "0.8", default-features = false }
49pulldown-cmark = { version = "0.8", default-features = false }
50+# file type detection
51+content_inspector = "0.2.4"
52
52
53# config
53# config
54toml = "0.5"
54toml = "0.5"
helix-term/src/application.rs+17−9
@@ -99,12 +99,17 @@ impl Application {
99 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
99 let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
100 compositor.push(editor_view);
100 compositor.push(editor_view);
101
101
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)?;
102− if !args.files.is_empty() {
107+ } else if !args.files.is_empty() {
103 let first = &args.files[0]; // we know it's not empty
108 let first = &args.files[0]; // we know it's not empty
104 if first.is_dir() {
109 if first.is_dir() {
105 std::env::set_current_dir(&first)?;
110 std::env::set_current_dir(&first)?;
106 editor.new_file(Action::VerticalSplit);
111 editor.new_file(Action::VerticalSplit);
107− compositor.push(Box::new(ui::file_picker(first.clone())));
112+ compositor.push(Box::new(ui::file_picker(".".into())));
108 } else {
113 } else {
109 let nr_of_files = args.files.len();
114 let nr_of_files = args.files.len();
110 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
115 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -240,7 +245,7 @@ impl Application {
240 }
245 }
241
246
242 pub fn handle_idle_timeout(&mut self) {
247 pub fn handle_idle_timeout(&mut self) {
243− use crate::commands::{completion, Context};
248+ use crate::commands::{insert::idle_completion, Context};
244 use helix_view::document::Mode;
249 use helix_view::document::Mode;
245
250
246 if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
251 if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
@@ -267,7 +272,7 @@ impl Application {
267 callback: None,
272 callback: None,
268 on_next_key_callback: None,
273 on_next_key_callback: None,
269 };
274 };
270− completion(&mut cx);
275+ idle_completion(&mut cx);
271 self.render();
276 self.render();
272 }
277 }
273
278
@@ -548,10 +553,11 @@ impl Application {
548 message: diagnostic.message,
553 message: diagnostic.message,
549 severity: diagnostic.severity.map(
554 severity: diagnostic.severity.map(
550 |severity| match severity {
555 |severity| match severity {
551− DiagnosticSeverity::Error => Error,
556+ DiagnosticSeverity::ERROR => Error,
552− DiagnosticSeverity::Warning => Warning,
557+ DiagnosticSeverity::WARNING => Warning,
553− DiagnosticSeverity::Information => Info,
558+ DiagnosticSeverity::INFORMATION => Info,
554− DiagnosticSeverity::Hint => Hint,
559+ DiagnosticSeverity::HINT => Hint,
560+ severity => unimplemented!("{:?}", severity),
555 },
561 },
556 ),
562 ),
557 // code
563 // code
@@ -727,7 +733,9 @@ impl Application {
727 let mut stdout = stdout();
733 let mut stdout = stdout();
728 // reset cursor shape
734 // reset cursor shape
729 write!(stdout, "\x1B[2 q")?;
735 write!(stdout, "\x1B[2 q")?;
736+ // Ignore errors on disabling, this might trigger on windows if we call
737+ // disable without calling enable previously
730− execute!(stdout, DisableMouseCapture)?;
738+ let _ = execute!(stdout, DisableMouseCapture);
731 execute!(stdout, terminal::LeaveAlternateScreen)?;
739 execute!(stdout, terminal::LeaveAlternateScreen)?;
732 terminal::disable_raw_mode()?;
740 terminal::disable_raw_mode()?;
733 Ok(())
741 Ok(())
helix-term/src/args.rs+2
@@ -5,6 +5,7 @@ use std::path::PathBuf;
5pub struct Args {
5pub struct Args {
6 pub display_help: bool,
6 pub display_help: bool,
7 pub display_version: bool,
7 pub display_version: bool,
8+ pub load_tutor: bool,
8 pub verbosity: u64,
9 pub verbosity: u64,
9 pub files: Vec<PathBuf>,
10 pub files: Vec<PathBuf>,
10}
11}
@@ -22,6 +23,7 @@ impl Args {
22 "--" => break, // stop parsing at this point treat the remaining as files
23 "--" => break, // stop parsing at this point treat the remaining as files
23 "--version" => args.display_version = true,
24 "--version" => args.display_version = true,
24 "--help" => args.display_help = true,
25 "--help" => args.display_help = true,
26+ "--tutor" => args.load_tutor = true,
25 arg if arg.starts_with("--") => {
27 arg if arg.starts_with("--") => {
26 return Err(Error::msg(format!(
28 return Err(Error::msg(format!(
27 "unexpected double dash argument: {}",
29 "unexpected double dash argument: {}",
helix-term/src/commands.rs+433−197
@@ -16,8 +16,13 @@ use helix_core::{
16};
16};
17
17
18use helix_view::{
18use helix_view::{
19− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
19+ clipboard::ClipboardType,
20+ document::Mode,
21+ editor::{Action, Motion},
22+ input::KeyEvent,
23+ keyboard::KeyCode,
24+ view::View,
20− view::View, Document, DocumentId, Editor, ViewId,
25+ Document, DocumentId, Editor, ViewId,
21};
26};
22
27
23use anyhow::{anyhow, bail, Context as _};
28use anyhow::{anyhow, bail, Context as _};
@@ -202,6 +207,7 @@ impl Command {
202 find_prev_char, "Move to previous occurance of char",
207 find_prev_char, "Move to previous occurance of char",
203 extend_till_prev_char, "Extend till previous occurance of char",
208 extend_till_prev_char, "Extend till previous occurance of char",
204 extend_prev_char, "Extend to previous occurance of char",
209 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...)",
205 replace, "Replace with new char",
211 replace, "Replace with new char",
206 switch_case, "Switch (toggle) case",
212 switch_case, "Switch (toggle) case",
207 switch_to_uppercase, "Switch to uppercase",
213 switch_to_uppercase, "Switch to uppercase",
@@ -215,8 +221,11 @@ impl Command {
215 split_selection, "Split selection into subselections on regex matches",
221 split_selection, "Split selection into subselections on regex matches",
216 split_selection_on_newline, "Split selection on newlines",
222 split_selection_on_newline, "Split selection on newlines",
217 search, "Search for regex pattern",
223 search, "Search for regex pattern",
224+ rsearch, "Reverse search for regex pattern",
218 search_next, "Select next search match",
225 search_next, "Select next search match",
226+ search_prev, "Select previous search match",
219 extend_search_next, "Add next search match to selection",
227 extend_search_next, "Add next search match to selection",
228+ extend_search_prev, "Add previous search match to selection",
220 search_selection, "Use current selection as search pattern",
229 search_selection, "Use current selection as search pattern",
221 global_search, "Global Search in workspace folder",
230 global_search, "Global Search in workspace folder",
222 extend_line, "Select current line, if already selected, extend to next line",
231 extend_line, "Select current line, if already selected, extend to next line",
@@ -260,6 +269,8 @@ impl Command {
260 goto_prev_diag, "Goto previous diagnostic",
269 goto_prev_diag, "Goto previous diagnostic",
261 goto_line_start, "Goto line start",
270 goto_line_start, "Goto line start",
262 goto_line_end, "Goto line end",
271 goto_line_end, "Goto line end",
272+ goto_next_buffer, "Goto next buffer",
273+ goto_previous_buffer, "Goto previous buffer",
263 // TODO: different description ?
274 // TODO: different description ?
264 goto_line_end_newline, "Goto line end",
275 goto_line_end_newline, "Goto line end",
265 goto_first_nonwhitespace, "Goto first non-blank in line",
276 goto_first_nonwhitespace, "Goto first non-blank in line",
@@ -305,6 +316,10 @@ impl Command {
305 expand_selection, "Expand selection to parent syntax node",
316 expand_selection, "Expand selection to parent syntax node",
306 jump_forward, "Jump forward on jumplist",
317 jump_forward, "Jump forward on jumplist",
307 jump_backward, "Jump backward on jumplist",
318 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",
308 rotate_view, "Goto next window",
323 rotate_view, "Goto next window",
309 hsplit, "Horizontal bottom split",
324 hsplit, "Horizontal bottom split",
310 vsplit, "Vertical right split",
325 vsplit, "Vertical right split",
@@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) {
528 )
543 )
529}
544}
530
545
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+
531fn extend_to_line_start(cx: &mut Context) {
579fn extend_to_line_start(cx: &mut Context) {
532 let (view, doc) = current!(cx.editor);
580 let (view, doc) = current!(cx.editor);
533 goto_line_start_impl(view, doc, Movement::Extend)
581 goto_line_start_impl(view, doc, Movement::Extend)
@@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) {
631 } else {
679 } else {
632 push_jump(cx.editor);
680 push_jump(cx.editor);
633 let (view, doc) = current!(cx.editor);
681 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);
635 }
688 }
636}
689}
637
690
638fn goto_file_end(cx: &mut Context) {
691fn goto_file_end(cx: &mut Context) {
639 push_jump(cx.editor);
692 push_jump(cx.editor);
640 let (view, doc) = current!(cx.editor);
693 let (view, doc) = current!(cx.editor);
694+ let text = doc.text().slice(..);
641− doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
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);
642}
701}
643
702
644fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
703fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
@@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
681 extend_word_impl(cx, movement::move_next_long_word_end)
740 extend_word_impl(cx, movement::move_next_long_word_end)
682}
741}
683
742
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)
686where
744where
687 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
745 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
688{
746{
@@ -704,13 +762,7 @@ where
704 // usually mix line endings. But we should fix it eventually
762 // usually mix line endings. But we should fix it eventually
705 // anyway.
763 // anyway.
706 {
764 {
707− current!(cx.editor)
708− .1
709− .line_ending
765+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
710− .as_str()
711− .chars()
712− .next()
713− .unwrap()
714 }
766 }
715
767
716 KeyEvent {
768 KeyEvent {
@@ -720,29 +772,48 @@ where
720 _ => return,
772 _ => return,
721 };
773 };
722
774
723− let (view, doc) = current!(cx.editor);
775+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
724− let text = doc.text().slice(..);
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+}
725
781
726− let selection = doc.selection(view.id).clone().transform(|range| {
782+//
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− };
735
783
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+
736− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
808+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
737− if extend {
809+ if extend {
738− range.put_cursor(text, pos, true)
810+ range.put_cursor(text, pos, true)
739− } else {
811+ } else {
740− Range::point(range.cursor(text)).put_cursor(text, pos, true)
812+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
741− }
813+ }
742− })
814+ })
743− });
815+ });
744− doc.set_selection(view.id, selection);
816+ doc.set_selection(view.id, selection);
745− })
746}
817}
747
818
748fn find_next_char_impl(
819fn find_next_char_impl(
@@ -756,6 +827,10 @@ fn find_next_char_impl(
756 if inclusive {
827 if inclusive {
757 search::find_nth_next(text, ch, pos, n)
828 search::find_nth_next(text, ch, pos, n)
758 } else {
829 } else {
830+ let n = match text.get_char(pos) {
831+ Some(next_ch) if next_ch == ch => n + 1,
832+ _ => n,
833+ };
759 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
834 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
760 }
835 }
761}
836}
@@ -770,80 +845,52 @@ fn find_prev_char_impl(
770 if inclusive {
845 if inclusive {
771 search::find_nth_prev(text, ch, pos, n)
846 search::find_nth_prev(text, ch, pos, n)
772 } else {
847 } 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+ };
773 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
852 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
774 }
853 }
775}
854}
776
855
777fn find_till_char(cx: &mut Context) {
856fn find_till_char(cx: &mut Context) {
778− find_char_impl(
779− cx,
780− find_next_char_impl,
857+ will_find_char(cx, find_next_char_impl, false, false)
781− false, /* inclusive */
782− false, /* extend */
783− )
784}
858}
785
859
786fn find_next_char(cx: &mut Context) {
860fn find_next_char(cx: &mut Context) {
787− find_char_impl(
788− cx,
789− find_next_char_impl,
861+ will_find_char(cx, find_next_char_impl, true, false)
790− true, /* inclusive */
791− false, /* extend */
792− )
793}
862}
794
863
795fn extend_till_char(cx: &mut Context) {
864fn extend_till_char(cx: &mut Context) {
796− find_char_impl(
797− cx,
798− find_next_char_impl,
865+ will_find_char(cx, find_next_char_impl, false, true)
799− false, /* inclusive */
800− true, /* extend */
801− )
802}
866}
803
867
804fn extend_next_char(cx: &mut Context) {
868fn extend_next_char(cx: &mut Context) {
805− find_char_impl(
806− cx,
807− find_next_char_impl,
869+ will_find_char(cx, find_next_char_impl, true, true)
808− true, /* inclusive */
809− true, /* extend */
810− )
811}
870}
812
871
813fn till_prev_char(cx: &mut Context) {
872fn till_prev_char(cx: &mut Context) {
814− find_char_impl(
815− cx,
816− find_prev_char_impl,
873+ will_find_char(cx, find_prev_char_impl, false, false)
817− false, /* inclusive */
818− false, /* extend */
819− )
820}
874}
821
875
822fn find_prev_char(cx: &mut Context) {
876fn find_prev_char(cx: &mut Context) {
823− find_char_impl(
824− cx,
825− find_prev_char_impl,
877+ will_find_char(cx, find_prev_char_impl, true, false)
826− true, /* inclusive */
827− false, /* extend */
828− )
829}
878}
830
879
831fn extend_till_prev_char(cx: &mut Context) {
880fn extend_till_prev_char(cx: &mut Context) {
832− find_char_impl(
833− cx,
834− find_prev_char_impl,
881+ will_find_char(cx, find_prev_char_impl, false, true)
835− false, /* inclusive */
836− true, /* extend */
837− )
838}
882}
839
883
840fn extend_prev_char(cx: &mut Context) {
884fn extend_prev_char(cx: &mut Context) {
841− find_char_impl(
842− cx,
843− find_prev_char_impl,
885+ will_find_char(cx, find_prev_char_impl, true, true)
844− true, /* inclusive */
886+}
845− true, /* extend */
887+
846− )
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+ }
847}
894}
848
895
849fn replace(cx: &mut Context) {
896fn replace(cx: &mut Context) {
@@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) {
1091 cx,
1138 cx,
1092 "select:".into(),
1139 "select:".into(),
1093 Some(reg),
1140 Some(reg),
1141+ |_input: &str| Vec::new(),
1094 move |view, doc, regex, event| {
1142 move |view, doc, regex, event| {
1095 if event != PromptEvent::Update {
1143 if event != PromptEvent::Update {
1096 return;
1144 return;
@@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) {
1113 cx,
1161 cx,
1114 "split:".into(),
1162 "split:".into(),
1115 Some(reg),
1163 Some(reg),
1164+ |_input: &str| Vec::new(),
1116 move |view, doc, regex, event| {
1165 move |view, doc, regex, event| {
1117 if event != PromptEvent::Update {
1166 if event != PromptEvent::Update {
1118 return;
1167 return;
@@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) {
1137 doc.set_selection(view.id, selection);
1186 doc.set_selection(view.id, selection);
1138}
1187}
1139
1188
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+) {
1141 let text = doc.text().slice(..);
1197 let text = doc.text().slice(..);
1142 let selection = doc.selection(view.id);
1198 let selection = doc.selection(view.id);
1143
1199
1144− // Get the right side of the primary block cursor.
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 {
1145− let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1203+ Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary(
1146− text,
1204+ text,
1205+ selection.primary().to(),
1206+ )),
1207+ Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary(
1208+ text,
1147− selection.primary().cursor(text),
1209+ selection.primary().from(),
1148− ));
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;
1149
1218
1150 // use find_at to find the next match after the cursor, loop around the end
1219 // use find_at to find the next match after the cursor, loop around the end
1151 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1220 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1152− let mat = regex
1221+ let mat = match direction {
1222+ Direction::Forward => regex
1153− .find_at(contents, start)
1223+ .find_at(contents, start)
1154− .or_else(|| regex.find(contents));
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+ };
1155 // TODO: message on wraparound
1230 // TODO: message on wraparound
1156 if let Some(mat) = mat {
1231 if let Some(mat) = mat {
1157− let start = text.byte_to_char(mat.start());
1232+ let start = text.byte_to_char(mat.start() + offset);
1158− let end = text.byte_to_char(mat.end());
1233+ let end = text.byte_to_char(mat.end() + offset);
1159
1234
1160 if end == 0 {
1235 if end == 0 {
1161 // skip empty matches that don't make sense
1236 // skip empty matches that don't make sense
1162 return;
1237 return;
1163 }
1238 }
1164
1239
1240+ // Determine range direction based on the primary range
1165− let selection = if extend {
1241+ let primary = selection.primary();
1242+ let range = if primary.head < primary.anchor {
1166− selection.clone().push(Range::new(start, end))
1243+ Range::new(end, start)
1167 } else {
1244 } 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),
1169 };
1251 };
1170
1252
1171 doc.set_selection(view.id, selection);
1253 doc.set_selection(view.id, selection);
@@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
1173 };
1255 };
1174}
1256}
1175
1257
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+
1176// TODO: use one function for search vs extend
1267// TODO: use one function for search vs extend
1177fn search(cx: &mut Context) {
1268fn 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) {
1178 let reg = cx.register.unwrap_or('/');
1277 let reg = cx.register.unwrap_or('/');
1179 let (_, doc) = current!(cx.editor);
1278 let (_, doc) = current!(cx.editor);
1180
1279
@@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) {
1183 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1282 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1184 // feed chunks into the regex yet
1283 // feed chunks into the regex yet
1185 let contents = doc.text().slice(..).to_string();
1284 let contents = doc.text().slice(..).to_string();
1285+ let completions = search_completions(cx, Some(reg));
1186
1286
1187 let prompt = ui::regex_prompt(
1287 let prompt = ui::regex_prompt(
1188 cx,
1288 cx,
1189 "search:".into(),
1289 "search:".into(),
1190 Some(reg),
1290 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+ },
1191 move |view, doc, regex, event| {
1298 move |view, doc, regex, event| {
1192 if event != PromptEvent::Update {
1299 if event != PromptEvent::Update {
1193 return;
1300 return;
1194 }
1301 }
1195− search_impl(doc, view, &contents, &regex, false);
1302+ search_impl(doc, view, &contents, &regex, Movement::Move, direction);
1196 },
1303 },
1197 );
1304 );
1198
1305
1199 cx.push_layer(Box::new(prompt));
1306 cx.push_layer(Box::new(prompt));
1200}
1307}
1201
1308
1202−fn search_next_impl(cx: &mut Context, extend: bool) {
1309+fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1203 let (view, doc) = current!(cx.editor);
1310 let (view, doc) = current!(cx.editor);
1204 let registers = &cx.editor.registers;
1311 let registers = &cx.editor.registers;
1205 if let Some(query) = registers.read('/') {
1312 if let Some(query) = registers.read('/') {
@@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
1214 .case_insensitive(case_insensitive)
1321 .case_insensitive(case_insensitive)
1215 .build()
1322 .build()
1216 {
1323 {
1217− search_impl(doc, view, &contents, &regex, extend);
1324+ search_impl(doc, view, &contents, &regex, movement, direction);
1218 } else {
1325 } else {
1219 // get around warning `mutable_borrow_reservation_conflict`
1326 // get around warning `mutable_borrow_reservation_conflict`
1220 // which will be a hard error in the future
1327 // which will be a hard error in the future
@@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
1226}
1333}
1227
1334
1228fn search_next(cx: &mut Context) {
1335fn search_next(cx: &mut Context) {
1229− search_next_impl(cx, false);
1336+ search_next_or_prev_impl(cx, Movement::Move, Direction::Forward);
1230}
1337}
1231
1338
1339+fn search_prev(cx: &mut Context) {
1340+ search_next_or_prev_impl(cx, Movement::Move, Direction::Backward);
1341+}
1232fn extend_search_next(cx: &mut Context) {
1342fn 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);
1234}
1348}
1235
1349
1236fn search_selection(cx: &mut Context) {
1350fn search_selection(cx: &mut Context) {
@@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) {
1247 let (all_matches_sx, all_matches_rx) =
1361 let (all_matches_sx, all_matches_rx) =
1248 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1362 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1249 let smart_case = cx.editor.config.smart_case;
1363 let smart_case = cx.editor.config.smart_case;
1364+
1365+ let completions = search_completions(cx, None);
1250 let prompt = ui::regex_prompt(
1366 let prompt = ui::regex_prompt(
1251 cx,
1367 cx,
1252 "global search:".into(),
1368 "global search:".into(),
1253 None,
1369 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+ },
1254 move |_view, _doc, regex, event| {
1377 move |_view, _doc, regex, event| {
1255 if event != PromptEvent::Validate {
1378 if event != PromptEvent::Validate {
1256 return;
1379 return;
@@ -1572,7 +1695,8 @@ mod cmd {
1572 let (_, doc) = current!(cx.editor);
1695 let (_, doc) = current!(cx.editor);
1573
1696
1574 if let Some(path) = path {
1697 if let Some(path) = path {
1698+ doc.set_path(Some(path.as_ref()))
1575− doc.set_path(path.as_ref()).context("invalid filepath")?;
1699+ .context("invalid filepath")?;
1576 }
1700 }
1577 if doc.path().is_none() {
1701 if doc.path().is_none() {
1578 bail!("cannot write a buffer without a filename");
1702 bail!("cannot write a buffer without a filename");
@@ -1635,7 +1759,7 @@ mod cmd {
1635
1759
1636 // If no argument, report current indent style.
1760 // If no argument, report current indent style.
1637 if args.is_empty() {
1761 if args.is_empty() {
1638− let style = current!(cx.editor).1.indent_style;
1762+ let style = doc!(cx.editor).indent_style;
1639 cx.editor.set_status(match style {
1763 cx.editor.set_status(match style {
1640 Tabs => "tabs".into(),
1764 Tabs => "tabs".into(),
1641 Spaces(1) => "1 space".into(),
1765 Spaces(1) => "1 space".into(),
@@ -1674,7 +1798,7 @@ mod cmd {
1674
1798
1675 // If no argument, report current line ending setting.
1799 // If no argument, report current line ending setting.
1676 if args.is_empty() {
1800 if args.is_empty() {
1677− let line_ending = current!(cx.editor).1.line_ending;
1801+ let line_ending = doc!(cx.editor).line_ending;
1678 cx.editor.set_status(match line_ending {
1802 cx.editor.set_status(match line_ending {
1679 Crlf => "crlf".into(),
1803 Crlf => "crlf".into(),
1680 LF => "line feed".into(),
1804 LF => "line feed".into(),
@@ -1790,7 +1914,7 @@ mod cmd {
1790 let mut errors = String::new();
1914 let mut errors = String::new();
1791
1915
1792 // save all documents
1916 // save all documents
1793− for (_, doc) in &mut cx.editor.documents {
1917+ for doc in &mut cx.editor.documents.values_mut() {
1794 if doc.path().is_none() {
1918 if doc.path().is_none() {
1795 errors.push_str("cannot write a buffer without a filename\n");
1919 errors.push_str("cannot write a buffer without a filename\n");
1796 continue;
1920 continue;
@@ -2085,8 +2209,7 @@ mod cmd {
2085 args: &[&str],
2209 args: &[&str],
2086 _event: PromptEvent,
2210 _event: PromptEvent,
2087 ) -> anyhow::Result<()> {
2211 ) -> anyhow::Result<()> {
2088− let (_, doc) = current!(cx.editor);
2212+ let id = view!(cx.editor).doc;
2089− let id = doc.id();
2090
2213
2091 if let Some(path) = args.get(0) {
2214 if let Some(path) = args.get(0) {
2092 cx.editor.open(path.into(), Action::VerticalSplit)?;
2215 cx.editor.open(path.into(), Action::VerticalSplit)?;
@@ -2102,8 +2225,7 @@ mod cmd {
2102 args: &[&str],
2225 args: &[&str],
2103 _event: PromptEvent,
2226 _event: PromptEvent,
2104 ) -> anyhow::Result<()> {
2227 ) -> anyhow::Result<()> {
2105− let (_, doc) = current!(cx.editor);
2228+ let id = view!(cx.editor).doc;
2106− let id = doc.id();
2107
2229
2108 if let Some(path) = args.get(0) {
2230 if let Some(path) = args.get(0) {
2109 cx.editor.open(path.into(), Action::HorizontalSplit)?;
2231 cx.editor.open(path.into(), Action::HorizontalSplit)?;
@@ -2188,6 +2310,18 @@ mod cmd {
2188 Ok(())
2310 Ok(())
2189 }
2311 }
2190
2312
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+
2191 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2325 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2192 TypableCommand {
2326 TypableCommand {
2193 name: "quit",
2327 name: "quit",
@@ -2199,7 +2333,7 @@ mod cmd {
2199 TypableCommand {
2333 TypableCommand {
2200 name: "quit!",
2334 name: "quit!",
2201 aliases: &["q!"],
2335 aliases: &["q!"],
2202− doc: "Close the current view.",
2336+ doc: "Close the current view forcefully (ignoring unsaved changes).",
2203 fun: force_quit,
2337 fun: force_quit,
2204 completer: None,
2338 completer: None,
2205 },
2339 },
@@ -2262,35 +2396,35 @@ mod cmd {
2262 TypableCommand {
2396 TypableCommand {
2263 name: "write-quit",
2397 name: "write-quit",
2264 aliases: &["wq", "x"],
2398 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)",
2266 fun: write_quit,
2400 fun: write_quit,
2267 completer: Some(completers::filename),
2401 completer: Some(completers::filename),
2268 },
2402 },
2269 TypableCommand {
2403 TypableCommand {
2270 name: "write-quit!",
2404 name: "write-quit!",
2271 aliases: &["wq!", "x!"],
2405 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)",
2273 fun: force_write_quit,
2407 fun: force_write_quit,
2274 completer: Some(completers::filename),
2408 completer: Some(completers::filename),
2275 },
2409 },
2276 TypableCommand {
2410 TypableCommand {
2277 name: "write-all",
2411 name: "write-all",
2278 aliases: &["wa"],
2412 aliases: &["wa"],
2279− doc: "Writes changes from all views to disk.",
2413+ doc: "Write changes from all views to disk.",
2280 fun: write_all,
2414 fun: write_all,
2281 completer: None,
2415 completer: None,
2282 },
2416 },
2283 TypableCommand {
2417 TypableCommand {
2284 name: "write-quit-all",
2418 name: "write-quit-all",
2285 aliases: &["wqa", "xa"],
2419 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.",
2287 fun: write_all_quit,
2421 fun: write_all_quit,
2288 completer: None,
2422 completer: None,
2289 },
2423 },
2290 TypableCommand {
2424 TypableCommand {
2291 name: "write-quit-all!",
2425 name: "write-quit-all!",
2292 aliases: &["wqa!", "xa!"],
2426 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).",
2294 fun: force_write_all_quit,
2428 fun: force_write_all_quit,
2295 completer: None,
2429 completer: None,
2296 },
2430 },
@@ -2461,7 +2595,14 @@ mod cmd {
2461 doc: "Open the file in a horizontal split.",
2595 doc: "Open the file in a horizontal split.",
2462 fun: hsplit,
2596 fun: hsplit,
2463 completer: Some(completers::filename),
2597 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+ },
2465 ];
2606 ];
2466
2607
2467 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2608 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) {
2561 cx.editor
2702 cx.editor
2562 .documents
2703 .documents
2563 .iter()
2704 .iter()
2564− .map(|(id, doc)| (id, doc.path().cloned()))
2705+ .map(|(id, doc)| (*id, doc.path().cloned()))
2565 .collect(),
2706 .collect(),
2566 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2707 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2567 let path = path.as_deref().map(helix_core::path::get_relative_path);
2708 let path = path.as_deref().map(helix_core::path::get_relative_path);
@@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) {
2580 editor.switch(*id, Action::Replace);
2721 editor.switch(*id, Action::Replace);
2581 },
2722 },
2582 |editor, (id, path)| {
2723 |editor, (id, path)| {
2583− let doc = &editor.documents.get(*id)?;
2724+ let doc = &editor.documents.get(id)?;
2584 let &view_id = doc.selections().keys().next()?;
2725 let &view_id = doc.selections().keys().next()?;
2585 let line = doc
2726 let line = doc
2586 .selection(view_id)
2727 .selection(view_id)
@@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) {
2996 doc.text().len_lines() - 1
3137 doc.text().len_lines() - 1
2997 };
3138 };
2998 let line_idx = std::cmp::min(count.get() - 1, max_line);
3139 let line_idx = std::cmp::min(count.get() - 1, max_line);
3140+ let text = doc.text().slice(..);
2999 let pos = doc.text().line_to_char(line_idx);
3141 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);
3001 }
3147 }
3002}
3148}
3003
3149
@@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) {
3011 } else {
3157 } else {
3012 doc.text().len_lines() - 1
3158 doc.text().len_lines() - 1
3013 };
3159 };
3160+ let text = doc.text().slice(..);
3014 let pos = doc.text().line_to_char(line_idx);
3161 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);
3016}
3167}
3017
3168
3018fn goto_last_accessed_file(cx: &mut Context) {
3169fn goto_last_accessed_file(cx: &mut Context) {
@@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) {
3306 let editor = &mut cx.editor;
3457 let editor = &mut cx.editor;
3307 let (_, doc) = current!(editor);
3458 let (_, doc) = current!(editor);
3308
3459
3309− let diag = if let Some(diag) = doc.diagnostics().first() {
3460+ let pos = match doc.diagnostics().first() {
3310− diag.range.start
3461+ Some(diag) => diag.range.start,
3311− } else {
3312− return;
3462+ None => return,
3313 };
3463 };
3314
3464
3315− goto_pos(editor, diag);
3465+ goto_pos(editor, pos);
3316}
3466}
3317
3467
3318fn goto_last_diag(cx: &mut Context) {
3468fn goto_last_diag(cx: &mut Context) {
3319 let editor = &mut cx.editor;
3469 let editor = &mut cx.editor;
3320 let (_, doc) = current!(editor);
3470 let (_, doc) = current!(editor);
3321
3471
3322− let diag = if let Some(diag) = doc.diagnostics().last() {
3472+ let pos = match doc.diagnostics().last() {
3323− diag.range.start
3473+ Some(diag) => diag.range.start,
3324− } else {
3325− return;
3474+ None => return,
3326 };
3475 };
3327
3476
3328− goto_pos(editor, diag);
3477+ goto_pos(editor, pos);
3329}
3478}
3330
3479
3331fn goto_next_diag(cx: &mut Context) {
3480fn goto_next_diag(cx: &mut Context) {
@@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) {
3336 .selection(view.id)
3485 .selection(view.id)
3337 .primary()
3486 .primary()
3338 .cursor(doc.text().slice(..));
3487 .cursor(doc.text().slice(..));
3488+
3339− let diag = if let Some(diag) = doc
3489+ let diag = doc
3340 .diagnostics()
3490 .diagnostics()
3341 .iter()
3491 .iter()
3342− .map(|diag| diag.range.start)
3343− .find(|&pos| pos > cursor_pos)
3492+ .find(|diag| diag.range.start > cursor_pos)
3344− {
3345− diag
3346− } else if let Some(diag) = doc.diagnostics().first() {
3493+ .or_else(|| doc.diagnostics().first());
3494+
3495+ let pos = match diag {
3347− diag.range.start
3496+ Some(diag) => diag.range.start,
3348− } else {
3349− return;
3497+ None => return,
3350 };
3498 };
3351
3499
3352− goto_pos(editor, diag);
3500+ goto_pos(editor, pos);
3353}
3501}
3354
3502
3355fn goto_prev_diag(cx: &mut Context) {
3503fn goto_prev_diag(cx: &mut Context) {
@@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) {
3360 .selection(view.id)
3508 .selection(view.id)
3361 .primary()
3509 .primary()
3362 .cursor(doc.text().slice(..));
3510 .cursor(doc.text().slice(..));
3511+
3363− let diag = if let Some(diag) = doc
3512+ let diag = doc
3364 .diagnostics()
3513 .diagnostics()
3365 .iter()
3514 .iter()
3366 .rev()
3515 .rev()
3367− .map(|diag| diag.range.start)
3368− .find(|&pos| pos < cursor_pos)
3516+ .find(|diag| diag.range.start < cursor_pos)
3369− {
3370− diag
3371− } else if let Some(diag) = doc.diagnostics().last() {
3517+ .or_else(|| doc.diagnostics().last());
3518+
3519+ let pos = match diag {
3372− diag.range.start
3520+ Some(diag) => diag.range.start,
3373− } else {
3374− return;
3521+ None => return,
3375 };
3522 };
3376
3523
3377− goto_pos(editor, diag);
3524+ goto_pos(editor, pos);
3378}
3525}
3379
3526
3380fn signature_help(cx: &mut Context) {
3527fn signature_help(cx: &mut Context) {
@@ -3423,7 +3570,26 @@ pub mod insert {
3423 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3570 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3424 pub type PostHook = fn(&mut Context, char);
3571 pub type PostHook = fn(&mut Context, char);
3425
3572
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+ }
3426− fn completion(cx: &mut Context, ch: char) {
3589+ super::completion(cx);
3590+ }
3591+
3592+ fn language_server_completion(cx: &mut Context, ch: char) {
3427 // if ch matches completion char, trigger completion
3593 // if ch matches completion char, trigger completion
3428 let doc = doc_mut!(cx.editor);
3594 let doc = doc_mut!(cx.editor);
3429 let language_server = match doc.language_server() {
3595 let language_server = match doc.language_server() {
@@ -3433,19 +3599,14 @@ pub mod insert {
3433
3599
3434 let capabilities = language_server.capabilities();
3600 let capabilities = language_server.capabilities();
3435
3601
3436− if let lsp::ServerCapabilities {
3602+ if let Some(lsp::CompletionOptions {
3437− completion_provider:
3438− Some(lsp::CompletionOptions {
3439− trigger_characters: Some(triggers),
3603+ trigger_characters: Some(triggers),
3440− ..
3441− }),
3442 ..
3604 ..
3443− } = capabilities
3605+ }) = &capabilities.completion_provider
3444 {
3606 {
3445 // TODO: what if trigger is multiple chars long
3607 // TODO: what if trigger is multiple chars long
3446− let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3608+ if triggers.iter().any(|trigger| trigger.contains(ch)) {
3447−
3609+ cx.editor.clear_idle_timer();
3448− if is_trigger {
3449 super::completion(cx);
3610 super::completion(cx);
3450 }
3611 }
3451 }
3612 }
@@ -3527,7 +3688,8 @@ pub mod insert {
3527 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3688 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3528 // this could also generically look at Transaction, but it's a bit annoying to look at
3689 // this could also generically look at Transaction, but it's a bit annoying to look at
3529 // Operation instead of Change.
3690 // 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] {
3531 hook(cx, c);
3693 hook(cx, c);
3532 }
3694 }
3533 }
3695 }
@@ -3668,13 +3830,19 @@ pub mod insert {
3668fn undo(cx: &mut Context) {
3830fn undo(cx: &mut Context) {
3669 let (view, doc) = current!(cx.editor);
3831 let (view, doc) = current!(cx.editor);
3670 let view_id = view.id;
3832 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+ }
3672}
3837}
3673
3838
3674fn redo(cx: &mut Context) {
3839fn redo(cx: &mut Context) {
3675 let (view, doc) = current!(cx.editor);
3840 let (view, doc) = current!(cx.editor);
3676 let view_id = view.id;
3841 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+ }
3678}
3846}
3679
3847
3680// Yank / Paste
3848// Yank / Paste
@@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl(
3735}
3903}
3736
3904
3737fn yank_joined_to_clipboard(cx: &mut Context) {
3905fn 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;
3739 let _ = yank_joined_to_clipboard_impl(
3907 let _ = yank_joined_to_clipboard_impl(
3740 &mut cx.editor,
3908 &mut cx.editor,
3741 line_ending.as_str(),
3909 line_ending.as_str(),
@@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) {
3769}
3937}
3770
3938
3771fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3939fn 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;
3773 let _ = yank_joined_to_clipboard_impl(
3941 let _ = yank_joined_to_clipboard_impl(
3774 &mut cx.editor,
3942 &mut cx.editor,
3775 line_ending.as_str(),
3943 line_ending.as_str(),
@@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) {
3882 let registers = &mut cx.editor.registers;
4050 let registers = &mut cx.editor.registers;
3883
4051
3884 if let Some(values) = registers.read(reg_name) {
4052 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);
3886 let selection = doc.selection(view.id);
4064 let selection = doc.selection(view.id);
3887 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4065 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3888 if !range.is_empty() {
4066 if !range.is_empty() {
3889− (range.from(), range.to(), Some(yank.as_str().into()))
4067+ (range.from(), range.to(), Some(values.next().unwrap()))
3890 } else {
4068 } else {
3891 (range.from(), range.to(), None)
4069 (range.from(), range.to(), None)
3892 }
4070 }
@@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) {
4128 cx,
4306 cx,
4129 "keep:".into(),
4307 "keep:".into(),
4130 Some(reg),
4308 Some(reg),
4309+ |_input: &str| Vec::new(),
4131 move |view, doc, regex, event| {
4310 move |view, doc, regex, event| {
4132 if event != PromptEvent::Update {
4311 if event != PromptEvent::Update {
4133 return;
4312 return;
@@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) {
4228 iter.reverse();
4407 iter.reverse();
4229 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4408 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4230 let start_offset = cursor.saturating_sub(offset);
4409 let start_offset = cursor.saturating_sub(offset);
4410+ let prefix = text.slice(start_offset..cursor).to_string();
4231
4411
4232 cx.callback(
4412 cx.callback(
4233 future,
4413 future,
@@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) {
4240 return;
4420 return;
4241 }
4421 }
4242
4422
4243− let items = match response {
4423+ let mut items = match response {
4244 Some(lsp::CompletionResponse::Array(items)) => items,
4424 Some(lsp::CompletionResponse::Array(items)) => items,
4245 // TODO: do something with is_incomplete
4425 // TODO: do something with is_incomplete
4246 Some(lsp::CompletionResponse::List(lsp::CompletionList {
4426 Some(lsp::CompletionResponse::List(lsp::CompletionList {
@@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) {
4250 None => Vec::new(),
4430 None => Vec::new(),
4251 };
4431 };
4252
4432
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+
4253 if items.is_empty() {
4445 if items.is_empty() {
4254 // editor.set_error("No completion available".to_string());
4446 // editor.set_error("No completion available".to_string());
4255 return;
4447 return;
@@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
4401// tree sitter node selection
4593// tree sitter node selection
4402
4594
4403fn expand_selection(cx: &mut Context) {
4595fn expand_selection(cx: &mut Context) {
4596+ let motion = |editor: &mut Editor| {
4404− let (view, doc) = current!(cx.editor);
4597+ let (view, doc) = current!(editor);
4405
4598
4406− if let Some(syntax) = doc.syntax() {
4599+ if let Some(syntax) = doc.syntax() {
4407− let text = doc.text().slice(..);
4600+ let text = doc.text().slice(..);
4408− let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4601+ let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4409− doc.set_selection(view.id, selection);
4602+ doc.set_selection(view.id, selection);
4603+ }
4410− }
4604+ };
4605+ motion(&mut cx.editor);
4606+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
4411}
4607}
4412
4608
4413fn match_brackets(cx: &mut Context) {
4609fn match_brackets(cx: &mut Context) {
4414 let (view, doc) = current!(cx.editor);
4610 let (view, doc) = current!(cx.editor);
4415
4611
4416 if let Some(syntax) = doc.syntax() {
4612 if let Some(syntax) = doc.syntax() {
4417− let pos = doc
4418− .selection(view.id)
4419− .primary()
4420− .cursor(doc.text().slice(..));
4613+ let text = doc.text().slice(..);
4614+ let selection = doc.selection(view.id).clone().transform(|range| {
4421− if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4615+ if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4422− let selection = Selection::point(pos);
4616+ range.put_cursor(text, pos, doc.mode == Mode::Select)
4617+ } else {
4618+ range
4619+ }
4620+ });
4423− doc.set_selection(view.id, selection);
4621+ doc.set_selection(view.id, selection);
4424− };
4425 }
4622 }
4426}
4623}
4427
4624
@@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) {
4429
4626
4430fn jump_forward(cx: &mut Context) {
4627fn jump_forward(cx: &mut Context) {
4431 let count = cx.count();
4628 let count = cx.count();
4432− let (view, _doc) = current!(cx.editor);
4629+ let view = view_mut!(cx.editor);
4433
4630
4434 if let Some((id, selection)) = view.jumps.forward(count) {
4631 if let Some((id, selection)) = view.jumps.forward(count) {
4435 view.doc = *id;
4632 view.doc = *id;
@@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) {
4463 cx.editor.focus_next()
4660 cx.editor.focus_next()
4464}
4661}
4465
4662
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+
4466// split helper, clear it later
4679// split helper, clear it later
4467fn split(cx: &mut Context, action: Action) {
4680fn split(cx: &mut Context, action: Action) {
4468 let (view, doc) = current!(cx.editor);
4681 let (view, doc) = current!(cx.editor);
@@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4552 let count = cx.count();
4765 let count = cx.count();
4553 cx.on_next_key(move |cx, event| {
4766 cx.on_next_key(move |cx, event| {
4554 if let Some(ch) = event.char() {
4767 if let Some(ch) = event.char() {
4768+ let textobject = move |editor: &mut Editor| {
4555− let (view, doc) = current!(cx.editor);
4769+ let (view, doc) = current!(editor);
4556− let text = doc.text().slice(..);
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+ };
4557
4787
4558− let selection = doc.selection(view.id).clone().transform(|range| {
4788+ let selection = doc.selection(view.id).clone().transform(|range| {
4559− match ch {
4789+ match ch {
4560− 'w' => textobject::textobject_word(text, range, objtype, count),
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),
4561− // TODO: cancel new ranges if inconsistent surround matches across lines
4794+ // TODO: cancel new ranges if inconsistent surround matches across lines
4562− ch if !ch.is_ascii_alphanumeric() => {
4795+ ch if !ch.is_ascii_alphanumeric() => {
4563− textobject::textobject_surround(text, range, objtype, ch, count)
4796+ textobject::textobject_surround(text, range, objtype, ch, count)
4797+ }
4798+ _ => range,
4564 }
4799 }
4565− _ => range,
4566− }
4567− });
4800+ });
4568− doc.set_selection(view.id, selection);
4801+ doc.set_selection(view.id, selection);
4802+ };
4803+ textobject(&mut cx.editor);
4804+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
4569 }
4805 }
4570 })
4806 })
4571}
4807}
@@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) {
4577 let selection = doc.selection(view.id);
4813 let selection = doc.selection(view.id);
4578 let (open, close) = surround::get_pair(ch);
4814 let (open, close) = surround::get_pair(ch);
4579
4815
4580− let mut changes = Vec::new();
4816+ let mut changes = Vec::with_capacity(selection.len() * 2);
4581 for range in selection.iter() {
4817 for range in selection.iter() {
4582 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4818 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4583 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
4819 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 {
207 ///
207 ///
208 /// ```rust
208 /// ```rust
209 /// use helix_term::{ui::Text, compositor::Component};
209 /// 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()));
211 /// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
211 /// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
212 /// ```
212 /// ```
213 fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
213 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};
5use serde::Deserialize;
5use serde::Deserialize;
6use std::{
6use std::{
7 borrow::Cow,
7 borrow::Cow,
8− collections::HashMap,
8+ collections::{BTreeSet, HashMap},
9 ops::{Deref, DerefMut},
9 ops::{Deref, DerefMut},
10};
10};
11
11
12#[macro_export]
12#[macro_export]
13macro_rules! key {
13macro_rules! key {
14 ($key:ident) => {
14 ($key:ident) => {
15− KeyEvent {
15+ ::helix_view::input::KeyEvent {
16 code: ::helix_view::keyboard::KeyCode::$key,
16 code: ::helix_view::keyboard::KeyCode::$key,
17 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
17 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
18 }
18 }
19 };
19 };
20 ($($ch:tt)*) => {
20 ($($ch:tt)*) => {
21− KeyEvent {
21+ ::helix_view::input::KeyEvent {
22 code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
22 code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
23 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
23 modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
24 }
24 }
@@ -78,19 +78,30 @@ macro_rules! keymap {
78 };
78 };
79}
79}
80
80
81−#[derive(Debug, Clone, Deserialize)]
81+#[derive(Debug, Clone)]
82pub struct KeyTrieNode {
82pub struct KeyTrieNode {
83 /// A label for keys coming under this node, like "Goto mode"
83 /// A label for keys coming under this node, like "Goto mode"
84− #[serde(skip)]
85 name: String,
84 name: String,
86− #[serde(flatten)]
87 map: HashMap<KeyEvent, KeyTrie>,
85 map: HashMap<KeyEvent, KeyTrie>,
88− #[serde(skip)]
89 order: Vec<KeyEvent>,
86 order: Vec<KeyEvent>,
90− #[serde(skip)]
91 pub is_sticky: bool,
87 pub is_sticky: bool,
92}
88}
93
89
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+
94impl KeyTrieNode {
105impl KeyTrieNode {
95 pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
106 pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
96 Self {
107 Self {
@@ -118,7 +129,6 @@ impl KeyTrieNode {
118 }
129 }
119 self.map.insert(key, trie);
130 self.map.insert(key, trie);
120 }
131 }
121−
122 for &key in self.map.keys() {
132 for &key in self.map.keys() {
123 if !self.order.contains(&key) {
133 if !self.order.contains(&key) {
124 self.order.push(key);
134 self.order.push(key);
@@ -127,20 +137,29 @@ impl KeyTrieNode {
127 }
137 }
128
138
129 pub fn infobox(&self) -> Info {
139 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());
131 for (&key, trie) in self.iter() {
141 for (&key, trie) in self.iter() {
132 let desc = match trie {
142 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+ }
134 KeyTrie::Node(n) => n.name(),
149 KeyTrie::Node(n) => n.name(),
135 };
150 };
136 match body.iter().position(|(d, _)| d == &desc) {
151 match body.iter().position(|(d, _)| d == &desc) {
137− // FIXME: multiple keys are ordered randomly (use BTreeSet)
152+ Some(pos) => {
138− Some(pos) => body[pos].1.push(key),
153+ body[pos].1.insert(key);
154+ }
139− None => body.push((desc, vec![key])),
155+ None => body.push((desc, BTreeSet::from([key]))),
140 }
156 }
141 }
157 }
142 body.sort_unstable_by_key(|(_, keys)| {
158 body.sort_unstable_by_key(|(_, keys)| {
159+ self.order
160+ .iter()
143− self.order.iter().position(|&k| k == keys[0]).unwrap()
161+ .position(|&k| k == *keys.iter().next().unwrap())
162+ .unwrap()
144 });
163 });
145 let prefix = format!("{} ", self.name());
164 let prefix = format!("{} ", self.name());
146 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
165 if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
@@ -151,6 +170,11 @@ impl KeyTrieNode {
151 }
170 }
152 Info::new(self.name(), body)
171 Info::new(self.name(), body)
153 }
172 }
173+
174+ /// Get a reference to the key trie node's order.
175+ pub fn order(&self) -> &[KeyEvent] {
176+ self.order.as_slice()
177+ }
154}
178}
155
179
156impl Default for KeyTrieNode {
180impl Default for KeyTrieNode {
@@ -235,6 +259,7 @@ pub enum KeymapResultKind {
235
259
236/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
260/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
237/// reference to the sticky node if one is currently active.
261/// reference to the sticky node if one is currently active.
262+#[derive(Debug)]
238pub struct KeymapResult<'a> {
263pub struct KeymapResult<'a> {
239 pub kind: KeymapResultKind,
264 pub kind: KeymapResultKind,
240 pub sticky: Option<&'a KeyTrieNode>,
265 pub sticky: Option<&'a KeyTrieNode>,
@@ -395,6 +420,7 @@ impl Default for Keymaps {
395 "F" => find_prev_char,
420 "F" => find_prev_char,
396 "r" => replace,
421 "r" => replace,
397 "R" => replace_with_yanked,
422 "R" => replace_with_yanked,
423+ "A-." => repeat_last_motion,
398
424
399 "~" => switch_case,
425 "~" => switch_case,
400 "`" => switch_to_lowercase,
426 "`" => switch_to_lowercase,
@@ -427,6 +453,8 @@ impl Default for Keymaps {
427 "m" => goto_window_middle,
453 "m" => goto_window_middle,
428 "b" => goto_window_bottom,
454 "b" => goto_window_bottom,
429 "a" => goto_last_accessed_file,
455 "a" => goto_last_accessed_file,
456+ "n" => goto_next_buffer,
457+ "p" => goto_previous_buffer,
430 },
458 },
431 ":" => command_mode,
459 ":" => command_mode,
432
460
@@ -476,10 +504,9 @@ impl Default for Keymaps {
476 },
504 },
477
505
478 "/" => search,
506 "/" => search,
479− // ? for search_reverse
507+ "?" => rsearch,
480 "n" => search_next,
508 "n" => search_next,
481− "N" => extend_search_next,
509+ "N" => search_prev,
482− // N for search_prev
483 "*" => search_selection,
510 "*" => search_selection,
484
511
485 "u" => undo,
512 "u" => undo,
@@ -520,9 +547,13 @@ impl Default for Keymaps {
520
547
521 "C-w" => { "Window"
548 "C-w" => { "Window"
522 "C-w" | "w" => rotate_view,
549 "C-w" | "w" => rotate_view,
523− "C-h" | "h" => hsplit,
550+ "C-s" | "s" => hsplit,
524 "C-v" | "v" => vsplit,
551 "C-v" | "v" => vsplit,
525 "C-q" | "q" => wclose,
552 "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,
526 },
557 },
527
558
528 // move under <space>c
559 // move under <space>c
@@ -621,6 +652,9 @@ impl Default for Keymaps {
621 "B" => extend_prev_long_word_start,
652 "B" => extend_prev_long_word_start,
622 "E" => extend_next_long_word_end,
653 "E" => extend_next_long_word_end,
623
654
655+ "n" => extend_search_next,
656+ "N" => extend_search_prev,
657+
624 "t" => extend_till_char,
658 "t" => extend_till_char,
625 "f" => extend_next_char,
659 "f" => extend_next_char,
626 "T" => extend_till_prev_char,
660 "T" => extend_till_prev_char,
@@ -669,63 +703,101 @@ pub fn merge_keys(mut config: Config) -> Config {
669 config
703 config
670}
704}
671
705
672−#[test]
706+#[cfg(test)]
707+mod tests {
708+ use super::*;
709+ #[test]
673−fn merge_partial_keys() {
710+ fn merge_partial_keys() {
674− let config = Config {
711+ let config = Config {
675− keys: Keymaps(hashmap! {
712+ keys: Keymaps(hashmap! {
676− Mode::Normal => Keymap::new(
713+ Mode::Normal => Keymap::new(
677− keymap!({ "Normal mode"
714+ keymap!({ "Normal mode"
678− "i" => normal_mode,
715+ "i" => normal_mode,
679− "无" => insert_mode,
716+ "无" => insert_mode,
680− "z" => jump_backward,
717+ "z" => jump_backward,
681− "g" => { "Merge into goto mode"
718+ "g" => { "Merge into goto mode"
682− "$" => goto_line_end,
719+ "$" => goto_line_end,
683− "g" => delete_char_forward,
720+ "g" => delete_char_forward,
684− },
721+ },
685− })
722+ })
686− )
723+ )
687− }),
724+ }),
688− ..Default::default()
725+ ..Default::default()
689− };
726+ };
690− let mut merged_config = merge_keys(config.clone());
727+ let mut merged_config = merge_keys(config.clone());
691− assert_ne!(config, merged_config);
728+ assert_ne!(config, merged_config);
692−
729+
693− let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
730+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
694− assert_eq!(
731+ assert_eq!(
695− keymap.get(key!('i')).kind,
732+ keymap.get(key!('i')).kind,
696− KeymapResultKind::Matched(Command::normal_mode),
733+ KeymapResultKind::Matched(Command::normal_mode),
697− "Leaf should replace leaf"
734+ "Leaf should replace leaf"
698− );
735+ );
699− assert_eq!(
736+ assert_eq!(
700− keymap.get(key!('无')).kind,
737+ keymap.get(key!('无')).kind,
701− KeymapResultKind::Matched(Command::insert_mode),
738+ KeymapResultKind::Matched(Command::insert_mode),
702− "New leaf should be present in merged keymap"
739+ "New leaf should be present in merged keymap"
703− );
740+ );
704− // Assumes that z is a node in the default keymap
741+ // Assumes that z is a node in the default keymap
705− assert_eq!(
742+ assert_eq!(
706− keymap.get(key!('z')).kind,
743+ keymap.get(key!('z')).kind,
707− KeymapResultKind::Matched(Command::jump_backward),
744+ KeymapResultKind::Matched(Command::jump_backward),
708− "Leaf should replace node"
745+ "Leaf should replace node"
709− );
746+ );
710− // Assumes that `g` is a node in default keymap
747+ // Assumes that `g` is a node in default keymap
711− assert_eq!(
748+ assert_eq!(
712− keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
749+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
713− &KeyTrie::Leaf(Command::goto_line_end),
750+ &KeyTrie::Leaf(Command::goto_line_end),
714− "Leaf should be present in merged subnode"
751+ "Leaf should be present in merged subnode"
715− );
752+ );
716− // Assumes that `gg` is in default keymap
753+ // Assumes that `gg` is in default keymap
717− assert_eq!(
754+ assert_eq!(
718− keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
755+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
719− &KeyTrie::Leaf(Command::delete_char_forward),
756+ &KeyTrie::Leaf(Command::delete_char_forward),
720− "Leaf should replace old leaf in merged subnode"
757+ "Leaf should replace old leaf in merged subnode"
721− );
758+ );
722− // Assumes that `ge` is in default keymap
759+ // Assumes that `ge` is in default keymap
723− assert_eq!(
760+ assert_eq!(
724− keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
761+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
725− &KeyTrie::Leaf(Command::goto_last_line),
762+ &KeyTrie::Leaf(Command::goto_last_line),
726− "Old leaves in subnode should be present in merged node"
763+ "Old leaves in subnode should be present in merged node"
727− );
764+ );
728−
765+
729− assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
766+ assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
730− assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
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+ }
731}
803}
helix-term/src/main.rs+7−1
@@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
16 };
16 };
17
17
18 // Separate file config so we can include year, month and day in file logs
18 // 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)?;
19 let file_config = fern::Dispatch::new()
24 let file_config = fern::Dispatch::new()
20 .format(|out, message, record| {
25 .format(|out, message, record| {
21 out.finish(format_args!(
26 out.finish(format_args!(
@@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
26 message
31 message
27 ))
32 ))
28 })
33 })
29− .chain(fern::log_file(logpath)?);
34+ .chain(file);
30
35
31 base_config.chain(file_config).apply()?;
36 base_config.chain(file_config).apply()?;
32
37
@@ -55,6 +60,7 @@ ARGS:
55
60
56FLAGS:
61FLAGS:
57 -h, --help Prints help information
62 -h, --help Prints help information
63+ --tutor Loads the tutorial
58 -v Increases logging verbosity each use for up to 3 times
64 -v Increases logging verbosity each use for up to 3 times
59 (default file: {})
65 (default file: {})
60 -V, --version Prints version information
66 -V, --version Prints version information
helix-term/src/ui/completion.rs+61−73
@@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
5use std::borrow::Cow;
5use std::borrow::Cow;
6
6
7use helix_core::Transaction;
7use helix_core::Transaction;
8−use helix_view::{graphics::Rect, Document, Editor, View};
8+use helix_view::{graphics::Rect, Document, Editor};
9
9
10use crate::commands;
10use crate::commands;
11use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
11use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -30,31 +30,32 @@ impl menu::Item for CompletionItem {
30 menu::Row::new(vec![
30 menu::Row::new(vec![
31 menu::Cell::from(self.label.as_str()),
31 menu::Cell::from(self.label.as_str()),
32 menu::Cell::from(match self.kind {
32 menu::Cell::from(match self.kind {
33− Some(lsp::CompletionItemKind::Text) => "text",
33+ Some(lsp::CompletionItemKind::TEXT) => "text",
34− Some(lsp::CompletionItemKind::Method) => "method",
34+ Some(lsp::CompletionItemKind::METHOD) => "method",
35− Some(lsp::CompletionItemKind::Function) => "function",
35+ Some(lsp::CompletionItemKind::FUNCTION) => "function",
36− Some(lsp::CompletionItemKind::Constructor) => "constructor",
36+ Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
37− Some(lsp::CompletionItemKind::Field) => "field",
37+ Some(lsp::CompletionItemKind::FIELD) => "field",
38− Some(lsp::CompletionItemKind::Variable) => "variable",
38+ Some(lsp::CompletionItemKind::VARIABLE) => "variable",
39− Some(lsp::CompletionItemKind::Class) => "class",
39+ Some(lsp::CompletionItemKind::CLASS) => "class",
40− Some(lsp::CompletionItemKind::Interface) => "interface",
40+ Some(lsp::CompletionItemKind::INTERFACE) => "interface",
41− Some(lsp::CompletionItemKind::Module) => "module",
41+ Some(lsp::CompletionItemKind::MODULE) => "module",
42− Some(lsp::CompletionItemKind::Property) => "property",
42+ Some(lsp::CompletionItemKind::PROPERTY) => "property",
43− Some(lsp::CompletionItemKind::Unit) => "unit",
43+ Some(lsp::CompletionItemKind::UNIT) => "unit",
44− Some(lsp::CompletionItemKind::Value) => "value",
44+ Some(lsp::CompletionItemKind::VALUE) => "value",
45− Some(lsp::CompletionItemKind::Enum) => "enum",
45+ Some(lsp::CompletionItemKind::ENUM) => "enum",
46− Some(lsp::CompletionItemKind::Keyword) => "keyword",
46+ Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
47− Some(lsp::CompletionItemKind::Snippet) => "snippet",
47+ Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
48− Some(lsp::CompletionItemKind::Color) => "color",
48+ Some(lsp::CompletionItemKind::COLOR) => "color",
49− Some(lsp::CompletionItemKind::File) => "file",
49+ Some(lsp::CompletionItemKind::FILE) => "file",
50− Some(lsp::CompletionItemKind::Reference) => "reference",
50+ Some(lsp::CompletionItemKind::REFERENCE) => "reference",
51− Some(lsp::CompletionItemKind::Folder) => "folder",
51+ Some(lsp::CompletionItemKind::FOLDER) => "folder",
52− Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
52+ Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
53− Some(lsp::CompletionItemKind::Constant) => "constant",
53+ Some(lsp::CompletionItemKind::CONSTANT) => "constant",
54− Some(lsp::CompletionItemKind::Struct) => "struct",
54+ Some(lsp::CompletionItemKind::STRUCT) => "struct",
55− Some(lsp::CompletionItemKind::Event) => "event",
55+ Some(lsp::CompletionItemKind::EVENT) => "event",
56− Some(lsp::CompletionItemKind::Operator) => "operator",
56+ Some(lsp::CompletionItemKind::OPERATOR) => "operator",
57− Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
57+ Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
58+ Some(kind) => unimplemented!("{:?}", kind),
58 None => "",
59 None => "",
59 }),
60 }),
60 // self.detail.as_deref().unwrap_or("")
61 // self.detail.as_deref().unwrap_or("")
@@ -83,13 +84,13 @@ impl Completion {
83 start_offset: usize,
84 start_offset: usize,
84 trigger_offset: usize,
85 trigger_offset: usize,
85 ) -> Self {
86 ) -> Self {
86− // let items: Vec<CompletionItem> = Vec::new();
87 let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
87 let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
88 fn item_to_transaction(
88 fn item_to_transaction(
89 doc: &Document,
89 doc: &Document,
90− view: &View,
91 item: &CompletionItem,
90 item: &CompletionItem,
92 offset_encoding: helix_lsp::OffsetEncoding,
91 offset_encoding: helix_lsp::OffsetEncoding,
92+ start_offset: usize,
93+ trigger_offset: usize,
93 ) -> Transaction {
94 ) -> Transaction {
94 if let Some(edit) = &item.text_edit {
95 if let Some(edit) = &item.text_edit {
95 let edit = match edit {
96 let edit = match edit {
@@ -105,63 +106,52 @@ impl Completion {
105 )
106 )
106 } else {
107 } else {
107 let text = item.insert_text.as_ref().unwrap_or(&item.label);
108 let text = item.insert_text.as_ref().unwrap_or(&item.label);
108− let cursor = doc
109+ // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
109− .selection(view.id)
110+ // in these cases we need to check for a common prefix and remove it
110− .primary()
111+ let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
111− .cursor(doc.text().slice(..));
112+ let text = text.trim_start_matches::<&str>(&prefix);
112 Transaction::change(
113 Transaction::change(
113 doc.text(),
114 doc.text(),
114− vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
115+ vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
115 )
116 )
116 }
117 }
117 }
118 }
118
119
120+ let (view, doc) = current!(editor);
121+
122+ // if more text was entered, remove it
123+ doc.restore(view.id);
124+
119 match event {
125 match event {
120 PromptEvent::Abort => {}
126 PromptEvent::Abort => {}
121 PromptEvent::Update => {
127 PromptEvent::Update => {
122− let (view, doc) = current!(editor);
123−
124 // always present here
128 // always present here
125 let item = item.unwrap();
129 let item = item.unwrap();
126
130
127− // if more text was entered, remove it
131+ let transaction = item_to_transaction(
128− // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
132+ doc,
129− let cursor = doc
133+ item,
130− .selection(view.id)
134+ offset_encoding,
131− .primary()
135+ start_offset,
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(),
136+ trigger_offset,
137− );
137+ );
138− doc.apply(&remove, view.id);
138+
139− }
139+ // initialize a savepoint
140+ doc.savepoint();
140
141
141− let transaction = item_to_transaction(doc, view, item, offset_encoding);
142 doc.apply(&transaction, view.id);
142 doc.apply(&transaction, view.id);
143 }
143 }
144 PromptEvent::Validate => {
144 PromptEvent::Validate => {
145− let (view, doc) = current!(editor);
146−
147 // always present here
145 // always present here
148 let item = item.unwrap();
146 let item = item.unwrap();
149
147
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+ );
165 doc.apply(&transaction, view.id);
155 doc.apply(&transaction, view.id);
166
156
167 if let Some(additional_edits) = &item.additional_text_edits {
157 if let Some(additional_edits) = &item.additional_text_edits {
@@ -210,7 +200,7 @@ impl Completion {
210 .selection(view.id)
200 .selection(view.id)
211 .primary()
201 .primary()
212 .cursor(doc.text().slice(..));
202 .cursor(doc.text().slice(..));
213− if self.start_offset <= cursor {
203+ if self.trigger_offset <= cursor {
214 let fragment = doc.text().slice(self.start_offset..cursor);
204 let fragment = doc.text().slice(self.start_offset..cursor);
215 let text = Cow::from(fragment);
205 let text = Cow::from(fragment);
216 // TODO: logic is same as ui/picker
206 // TODO: logic is same as ui/picker
@@ -274,12 +264,10 @@ impl Component for Completion {
274 .language()
264 .language()
275 .and_then(|scope| scope.strip_prefix("source."))
265 .and_then(|scope| scope.strip_prefix("source."))
276 .unwrap_or("");
266 .unwrap_or("");
277− let cursor_pos = doc
267+ let text = doc.text().slice(..);
278− .selection(view.id)
268+ let cursor_pos = doc.selection(view.id).primary().cursor(text);
279− .primary()
269+ let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
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;
270+ let cursor_pos = (coords.row - view.offset.row) as u16;
283 let mut markdown_doc = match &option.documentation {
271 let mut markdown_doc = match &option.documentation {
284 Some(lsp::Documentation::String(contents))
272 Some(lsp::Documentation::String(contents))
285 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
273 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
helix-term/src/ui/editor.rs+94−40
@@ -689,6 +689,8 @@ impl EditorView {
689 theme: &Theme,
689 theme: &Theme,
690 is_focused: bool,
690 is_focused: bool,
691 ) {
691 ) {
692+ use tui::text::{Span, Spans};
693+
692 //-------------------------------
694 //-------------------------------
693 // Left side of the status line.
695 // Left side of the status line.
694 //-------------------------------
696 //-------------------------------
@@ -707,17 +709,17 @@ impl EditorView {
707 })
709 })
708 .unwrap_or("");
710 .unwrap_or("");
709
711
710− let style = if is_focused {
712+ let base_style = if is_focused {
711 theme.get("ui.statusline")
713 theme.get("ui.statusline")
712 } else {
714 } else {
713 theme.get("ui.statusline.inactive")
715 theme.get("ui.statusline.inactive")
714 };
716 };
715 // statusline
717 // statusline
716− surface.set_style(viewport.with_height(1), style);
718+ surface.set_style(viewport.with_height(1), base_style);
717 if is_focused {
719 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);
719 }
721 }
720− surface.set_string(viewport.x + 5, viewport.y, progress, style);
722+ surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
721
723
722 if let Some(path) = doc.relative_path() {
724 if let Some(path) = doc.relative_path() {
723 let path = path.to_string_lossy();
725 let path = path.to_string_lossy();
@@ -728,7 +730,7 @@ impl EditorView {
728 viewport.y,
730 viewport.y,
729 title,
731 title,
730 viewport.width.saturating_sub(6) as usize,
732 viewport.width.saturating_sub(6) as usize,
731− style,
733+ base_style,
732 );
734 );
733 }
735 }
734
736
@@ -736,8 +738,50 @@ impl EditorView {
736 // Right side of the status line.
738 // Right side of the status line.
737 //-------------------------------
739 //-------------------------------
738
740
741+ let mut right_side_text = Spans::default();
742+
739− // Compute the individual info strings.
743+ // Compute the individual info strings and add them to `right_side_text`.
744+
745+ // Diagnostics
740− let diag_count = format!("{}", doc.diagnostics().len());
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+
741 // let indent_info = match doc.indent_style {
785 // let indent_info = match doc.indent_style {
742 // IndentStyle::Tabs => "tabs",
786 // IndentStyle::Tabs => "tabs",
743 // IndentStyle::Spaces(1) => "spaces:1",
787 // IndentStyle::Spaces(1) => "spaces:1",
@@ -750,29 +794,28 @@ impl EditorView {
750 // IndentStyle::Spaces(8) => "spaces:8",
794 // IndentStyle::Spaces(8) => "spaces:8",
751 // _ => "indent:ERROR",
795 // _ => "indent:ERROR",
752 // };
796 // };
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− };
762
797
763− // Render them to the status line together.
798+ // Position
764− let right_side_text = format!(
799+ let pos = coords_at_pos(
765− "{} {} ",
800+ doc.text().slice(..),
766− &diag_count[..diag_count.len().min(4)],
801+ doc.selection(view.id)
767− // indent_info,
802+ .primary()
768− position_info
803+ .cursor(doc.text().slice(..)),
769 );
804 );
770− let text_len = right_side_text.len() as u16;
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.
771− surface.set_string(
811+ surface.set_spans(
772− viewport.x + viewport.width.saturating_sub(text_len),
812+ viewport.x
813+ + viewport
814+ .width
815+ .saturating_sub(right_side_text.width() as u16),
773 viewport.y,
816 viewport.y,
774− right_side_text,
817+ &right_side_text,
775− style,
818+ right_side_text.width() as u16,
776 );
819 );
777 }
820 }
778
821
@@ -984,7 +1027,7 @@ impl EditorView {
984
1027
985 pub fn set_completion(
1028 pub fn set_completion(
986 &mut self,
1029 &mut self,
987− editor: &Editor,
1030+ editor: &mut Editor,
988 items: Vec<helix_lsp::lsp::CompletionItem>,
1031 items: Vec<helix_lsp::lsp::CompletionItem>,
989 offset_encoding: helix_lsp::OffsetEncoding,
1032 offset_encoding: helix_lsp::OffsetEncoding,
990 start_offset: usize,
1033 start_offset: usize,
@@ -999,10 +1042,21 @@ impl EditorView {
999 return;
1042 return;
1000 }
1043 }
1001
1044
1045+ // Immediately initialize a savepoint
1046+ doc_mut!(editor).savepoint();
1047+
1002 // TODO : propagate required size on resize to completion too
1048 // TODO : propagate required size on resize to completion too
1003 completion.required_size((size.width, size.height));
1049 completion.required_size((size.width, size.height));
1004 self.completion = Some(completion);
1050 self.completion = Some(completion);
1005 }
1051 }
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+ }
1006}
1060}
1007
1061
1008impl EditorView {
1062impl EditorView {
@@ -1022,12 +1076,12 @@ impl EditorView {
1022 let editor = &mut cxt.editor;
1076 let editor = &mut cxt.editor;
1023
1077
1024 let result = editor.tree.views().find_map(|(view, _focus)| {
1078 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)
1026 .map(|pos| (pos, view.id))
1080 .map(|pos| (pos, view.id))
1027 });
1081 });
1028
1082
1029 if let Some((pos, view_id)) = result {
1083 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();
1031
1085
1032 if modifiers == crossterm::event::KeyModifiers::ALT {
1086 if modifiers == crossterm::event::KeyModifiers::ALT {
1033 let selection = doc.selection(view_id).clone();
1087 let selection = doc.selection(view_id).clone();
@@ -1096,7 +1150,7 @@ impl EditorView {
1096 };
1150 };
1097
1151
1098 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
1152 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)
1100 .map(|_| view.id)
1154 .map(|_| view.id)
1101 });
1155 });
1102
1156
@@ -1182,12 +1236,12 @@ impl EditorView {
1182 }
1236 }
1183
1237
1184 let result = editor.tree.views().find_map(|(view, _focus)| {
1238 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)
1186 .map(|pos| (pos, view.id))
1240 .map(|pos| (pos, view.id))
1187 });
1241 });
1188
1242
1189 if let Some((pos, view_id)) = result {
1243 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();
1191 doc.set_selection(view_id, Selection::point(pos));
1245 doc.set_selection(view_id, Selection::point(pos));
1192 editor.tree.focus = view_id;
1246 editor.tree.focus = view_id;
1193 commands::Command::paste_primary_clipboard_before.execute(cxt);
1247 commands::Command::paste_primary_clipboard_before.execute(cxt);
@@ -1254,8 +1308,7 @@ impl Component for EditorView {
1254
1308
1255 if callback.is_some() {
1309 if callback.is_some() {
1256 // assume close_fn
1310 // assume close_fn
1257− self.completion = None;
1311+ self.clear_completion(cxt.editor);
1258− cxt.editor.clear_idle_timer(); // don't retrigger
1259 }
1312 }
1260 }
1313 }
1261 }
1314 }
@@ -1268,8 +1321,7 @@ impl Component for EditorView {
1268 if let Some(completion) = &mut self.completion {
1321 if let Some(completion) = &mut self.completion {
1269 completion.update(&mut cxt);
1322 completion.update(&mut cxt);
1270 if completion.is_empty() {
1323 if completion.is_empty() {
1271− self.completion = None;
1324+ self.clear_completion(cxt.editor);
1272− cxt.editor.clear_idle_timer(); // don't retrigger
1273 }
1325 }
1274 }
1326 }
1275 }
1327 }
@@ -1397,8 +1449,10 @@ impl Component for EditorView {
1397 info.render(area, surface, cx);
1449 info.render(area, surface, cx);
1398 }
1450 }
1399
1451
1452+ if cx.editor.config.auto_info {
1400− if let Some(ref mut info) = self.autoinfo {
1453+ if let Some(ref mut info) = self.autoinfo {
1401− info.render(area, surface, cx);
1454+ info.render(area, surface, cx);
1455+ }
1402 }
1456 }
1403
1457
1404 let key_width = 15u16; // for showing pending keys
1458 let key_width = 15u16; // for showing pending keys
@@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
1469}
1523}
1470
1524
1471#[inline]
1525#[inline]
1472−fn abs_diff(a: usize, b: usize) -> usize {
1526+const fn abs_diff(a: usize, b: usize) -> usize {
1473 if a > b {
1527 if a > b {
1474 a - b
1528 a - b
1475 } else {
1529 } else {
helix-term/src/ui/menu.rs+25−18
@@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
64 }
64 }
65
65
66 pub fn score(&mut self, pattern: &str) {
66 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−
75 // reuse the matches allocation
67 // reuse the matches allocation
76− matches.clear();
68+ self.matches.clear();
69+ self.matches.extend(
70+ self.options
71+ .iter()
72+ .enumerate()
77− matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
73+ .filter_map(|(index, option)| {
78− let text = option.filter_text();
74+ let text = option.filter_text();
79− // TODO: using fuzzy_indices could give us the char idx for match highlighting
75+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
80− matcher
76+ self.matcher
81− .fuzzy_match(text, pattern)
77+ .fuzzy_match(text, pattern)
82− .map(|score| (index, score))
78+ .map(|score| (index, score))
79+ }),
83− }));
80+ );
84 // matches.sort_unstable_by_key(|(_, score)| -score);
81 // matches.sort_unstable_by_key(|(_, score)| -score);
82+ self.matches
85− matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
83+ .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
86
84
87 // reset cursor position
85 // reset cursor position
88 self.cursor = None;
86 self.cursor = None;
@@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
100
98
101 pub fn move_up(&mut self) {
99 pub fn move_up(&mut self) {
102 let len = self.matches.len();
100 let len = self.matches.len();
101+ let max_index = len.saturating_sub(1);
103− let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
102+ let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
104 self.cursor = Some(pos);
103 self.cursor = Some(pos);
105 self.adjust_scroll();
104 self.adjust_scroll();
106 }
105 }
@@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
216 | KeyEvent {
215 | KeyEvent {
217 code: KeyCode::Char('p'),
216 code: KeyCode::Char('p'),
218 modifiers: KeyModifiers::CONTROL,
217 modifiers: KeyModifiers::CONTROL,
218+ }
219+ | KeyEvent {
220+ code: KeyCode::Char('k'),
221+ modifiers: KeyModifiers::CONTROL,
219 } => {
222 } => {
220 self.move_up();
223 self.move_up();
221 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
224 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
233 | KeyEvent {
236 | KeyEvent {
234 code: KeyCode::Char('n'),
237 code: KeyCode::Char('n'),
235 modifiers: KeyModifiers::CONTROL,
238 modifiers: KeyModifiers::CONTROL,
239+ }
240+ | KeyEvent {
241+ code: KeyCode::Char('j'),
242+ modifiers: KeyModifiers::CONTROL,
236 } => {
243 } => {
237 self.move_down();
244 self.move_down();
238 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
245 (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(
29 cx: &mut crate::commands::Context,
29 cx: &mut crate::commands::Context,
30 prompt: std::borrow::Cow<'static, str>,
30 prompt: std::borrow::Cow<'static, str>,
31 history_register: Option<char>,
31 history_register: Option<char>,
32+ completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
32 fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
33 fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
33) -> Prompt {
34) -> Prompt {
34 let (view, doc) = current!(cx.editor);
35 let (view, doc) = current!(cx.editor);
@@ -38,7 +39,7 @@ pub fn regex_prompt(
38 Prompt::new(
39 Prompt::new(
39 prompt,
40 prompt,
40 history_register,
41 history_register,
41− |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
42+ completion_fn,
42 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
43 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
43 match event {
44 match event {
44 PromptEvent::Abort => {
45 PromptEvent::Abort => {
@@ -92,9 +93,25 @@ pub fn regex_prompt(
92}
93}
93
94
94pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
95pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
95− use ignore::Walk;
96+ use ignore::{types::TypesBuilder, WalkBuilder};
96 use std::time;
97 use std::time;
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+
97− let files = Walk::new(&root).filter_map(|entry| {
114+ let files = walk_builder.build().filter_map(|entry| {
98 let entry = entry.ok()?;
115 let entry = entry.ok()?;
99 // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
116 // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
100 if entry.path().is_dir() {
117 if entry.path().is_dir() {
helix-term/src/ui/picker.rs+120−37
@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12use fuzzy_matcher::FuzzyMatcher;
12use fuzzy_matcher::FuzzyMatcher;
13use tui::widgets::Widget;
13use tui::widgets::Widget;
14
14
15+use std::{
16+ borrow::Cow,
15−use std::{borrow::Cow, collections::HashMap, path::PathBuf};
17+ collections::HashMap,
18+ io::Read,
19+ path::{Path, PathBuf},
20+};
16
21
17use crate::ui::{Prompt, PromptEvent};
22use crate::ui::{Prompt, PromptEvent};
18use helix_core::Position;
23use helix_core::Position;
@@ -23,18 +28,58 @@ use helix_view::{
23};
28};
24
29
25pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
30pub 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;
26
33
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)
28type FileLocation = (PathBuf, Option<(usize, usize)>);
35type FileLocation = (PathBuf, Option<(usize, usize)>);
29
36
30pub struct FilePicker<T> {
37pub struct FilePicker<T> {
31 picker: Picker<T>,
38 picker: Picker<T>,
32 /// Caches paths to documents
39 /// Caches paths to documents
33− preview_cache: HashMap<PathBuf, Document>,
40+ preview_cache: HashMap<PathBuf, CachedPreview>,
41+ read_buffer: Vec<u8>,
34 /// Given an item in the picker, return the file path and line number to display.
42 /// Given an item in the picker, return the file path and line number to display.
35 file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
43 file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
36}
44}
37
45
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+
38impl<T> FilePicker<T> {
83impl<T> FilePicker<T> {
39 pub fn new(
84 pub fn new(
40 options: Vec<T>,
85 options: Vec<T>,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
45 Self {
90 Self {
46 picker: Picker::new(false, options, format_fn, callback_fn),
91 picker: Picker::new(false, options, format_fn, callback_fn),
47 preview_cache: HashMap::new(),
92 preview_cache: HashMap::new(),
93+ read_buffer: Vec::with_capacity(1024),
48 file_fn: Box::new(preview_fn),
94 file_fn: Box::new(preview_fn),
49 }
95 }
50 }
96 }
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
60 })
106 })
61 }
107 }
62
108
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,
63− fn calculate_preview(&mut self, editor: &Editor) {
114+ editor: &'editor Editor,
64− if let Some((path, _line)) = self.current_file(editor) {
115+ ) -> Preview<'picker, 'editor> {
65− if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
116+ if let Some(doc) = editor.document_by_path(path) {
66− // TODO: enable syntax highlighting; blocked by async rendering
117+ return Preview::EditorDocument(doc);
67− let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
68− self.preview_cache.insert(path, doc);
69− }
118+ }
119+
120+ if self.preview_cache.contains_key(path) {
121+ return Preview::Cached(&self.preview_cache[path]);
70 }
122 }
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])
71 }
148 }
72}
149}
73
150
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
79 // |picker | | |
156 // |picker | | |
80 // | | | |
157 // | | | |
81 // +---------+ +---------+
158 // +---------+ +---------+
82− self.calculate_preview(cx.editor);
83 let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
159 let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
84 let area = inner_rect(area);
160 let area = inner_rect(area);
85 // -- Render the frame:
161 // -- Render the frame:
86 // clear area
162 // clear area
87 let background = cx.editor.theme.get("ui.background");
163 let background = cx.editor.theme.get("ui.background");
164+ let text = cx.editor.theme.get("ui.text");
88 surface.clear_with(area, background);
165 surface.clear_with(area, background);
89
166
90 let picker_width = if render_preview {
167 let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
113 horizontal: 1,
190 horizontal: 1,
114 };
191 };
115 let inner = inner.inner(&margin);
192 let inner = inner.inner(&margin);
116−
117 block.render(preview_area, surface);
193 block.render(preview_area, surface);
118
194
119− if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
195+ if let Some((path, range)) = self.current_file(cx.editor) {
120− cx.editor
196+ let preview = self.get_preview(&path, cx.editor);
121− .document_by_path(&path)
197+ let doc = match preview.document() {
122− .or_else(|| self.preview_cache.get(&path))
198+ Some(doc) => doc,
123− .zip(Some(range))
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+ }
124− }) {
206+ };
207+
125 // align to middle
208 // align to middle
126− let first_line = line
209+ let first_line = range
127 .map(|(start, end)| {
210 .map(|(start, end)| {
128 let height = end.saturating_sub(start) + 1;
211 let height = end.saturating_sub(start) + 1;
129 let middle = start + (height.saturating_sub(1) / 2);
212 let middle = start + (height.saturating_sub(1) / 2);
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
150 );
233 );
151
234
152 // highlight the line
235 // highlight the line
153− if let Some((start, end)) = line {
236+ if let Some((start, end)) = range {
154 let offset = start.saturating_sub(first_line) as u16;
237 let offset = start.saturating_sub(first_line) as u16;
155 surface.set_style(
238 surface.set_style(
156 Rect::new(
239 Rect::new(
@@ -234,37 +317,28 @@ impl<T> Picker<T> {
234 }
317 }
235
318
236 pub fn score(&mut self) {
319 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−
246 let pattern = &self.prompt.line;
320 let pattern = &self.prompt.line;
247
321
248 // reuse the matches allocation
322 // reuse the matches allocation
249− matches.clear();
323+ self.matches.clear();
250− matches.extend(
324+ self.matches.extend(
251 self.options
325 self.options
252 .iter()
326 .iter()
253 .enumerate()
327 .enumerate()
254 .filter_map(|(index, option)| {
328 .filter_map(|(index, option)| {
255 // filter options first before matching
329 // filter options first before matching
256− if !filters.is_empty() {
330+ if !self.filters.is_empty() {
257− filters.binary_search(&index).ok()?;
331+ self.filters.binary_search(&index).ok()?;
258 }
332 }
259 // TODO: maybe using format_fn isn't the best idea here
333 // TODO: maybe using format_fn isn't the best idea here
260− let text = (format_fn)(option);
334+ let text = (self.format_fn)(option);
261 // TODO: using fuzzy_indices could give us the char idx for match highlighting
335 // TODO: using fuzzy_indices could give us the char idx for match highlighting
262− matcher
336+ self.matcher
263 .fuzzy_match(&text, pattern)
337 .fuzzy_match(&text, pattern)
264 .map(|score| (index, score))
338 .map(|score| (index, score))
265 }),
339 }),
266 );
340 );
267− matches.sort_unstable_by_key(|(_, score)| -score);
341+ self.matches.sort_unstable_by_key(|(_, score)| -score);
268
342
269 // reset cursor position
343 // reset cursor position
270 self.cursor = 0;
344 self.cursor = 0;
@@ -337,6 +411,10 @@ impl<T: 'static> Component for Picker<T> {
337 code: KeyCode::BackTab,
411 code: KeyCode::BackTab,
338 ..
412 ..
339 }
413 }
414+ | KeyEvent {
415+ code: KeyCode::Char('k'),
416+ modifiers: KeyModifiers::CONTROL,
417+ }
340 | KeyEvent {
418 | KeyEvent {
341 code: KeyCode::Char('p'),
419 code: KeyCode::Char('p'),
342 modifiers: KeyModifiers::CONTROL,
420 modifiers: KeyModifiers::CONTROL,
@@ -350,6 +428,10 @@ impl<T: 'static> Component for Picker<T> {
350 | KeyEvent {
428 | KeyEvent {
351 code: KeyCode::Tab, ..
429 code: KeyCode::Tab, ..
352 }
430 }
431+ | KeyEvent {
432+ code: KeyCode::Char('j'),
433+ modifiers: KeyModifiers::CONTROL,
434+ }
353 | KeyEvent {
435 | KeyEvent {
354 code: KeyCode::Char('n'),
436 code: KeyCode::Char('n'),
355 modifiers: KeyModifiers::CONTROL,
437 modifiers: KeyModifiers::CONTROL,
@@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
375 return close_fn;
457 return close_fn;
376 }
458 }
377 KeyEvent {
459 KeyEvent {
378− code: KeyCode::Char('h'),
460+ code: KeyCode::Char('s'),
379 modifiers: KeyModifiers::CONTROL,
461 modifiers: KeyModifiers::CONTROL,
380 } => {
462 } => {
381 if let Some(option) = self.selection() {
463 if let Some(option) = self.selection() {
@@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
485 text_style
567 text_style
486 },
568 },
487 true,
569 true,
570+ true,
488 );
571 );
489 }
572 }
490 }
573 }
helix-term/src/ui/prompt.rs+35−2
@@ -186,6 +186,11 @@ impl Prompt {
186 self.exit_selection();
186 self.exit_selection();
187 }
187 }
188
188
189+ pub fn insert_str(&mut self, s: &str) {
190+ self.line.insert_str(self.cursor, s);
191+ self.cursor += s.len();
192+ }
193+
189 pub fn move_cursor(&mut self, movement: Movement) {
194 pub fn move_cursor(&mut self, movement: Movement) {
190 let pos = self.eval_movement(movement);
195 let pos = self.eval_movement(movement);
191 self.cursor = pos
196 self.cursor = pos
@@ -474,6 +479,26 @@ impl Component for Prompt {
474 self.delete_char_backwards();
479 self.delete_char_backwards();
475 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
480 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
476 }
481 }
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+ }
477 KeyEvent {
502 KeyEvent {
478 code: KeyCode::Enter,
503 code: KeyCode::Enter,
479 ..
504 ..
@@ -502,6 +527,7 @@ impl Component for Prompt {
502 if let Some(register) = self.history_register {
527 if let Some(register) = self.history_register {
503 let register = cx.editor.registers.get_mut(register);
528 let register = cx.editor.registers.get_mut(register);
504 self.change_history(register.read(), CompletionDirection::Backward);
529 self.change_history(register.read(), CompletionDirection::Backward);
530+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
505 }
531 }
506 }
532 }
507 KeyEvent {
533 KeyEvent {
@@ -515,15 +541,22 @@ impl Component for Prompt {
515 if let Some(register) = self.history_register {
541 if let Some(register) = self.history_register {
516 let register = cx.editor.registers.get_mut(register);
542 let register = cx.editor.registers.get_mut(register);
517 self.change_history(register.read(), CompletionDirection::Forward);
543 self.change_history(register.read(), CompletionDirection::Forward);
544+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
518 }
545 }
519 }
546 }
520 KeyEvent {
547 KeyEvent {
521 code: KeyCode::Tab, ..
548 code: KeyCode::Tab, ..
549+ } => {
522− } => self.change_completion_selection(CompletionDirection::Forward),
550+ self.change_completion_selection(CompletionDirection::Forward);
551+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
552+ }
523 KeyEvent {
553 KeyEvent {
524 code: KeyCode::BackTab,
554 code: KeyCode::BackTab,
525 ..
555 ..
556+ } => {
526− } => self.change_completion_selection(CompletionDirection::Backward),
557+ self.change_completion_selection(CompletionDirection::Backward);
558+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
559+ }
527 KeyEvent {
560 KeyEvent {
528 code: KeyCode::Char('q'),
561 code: KeyCode::Char('q'),
529 modifiers: KeyModifiers::CONTROL,
562 modifiers: KeyModifiers::CONTROL,
helix-tui/Cargo.toml+5−5
@@ -1,11 +1,11 @@
1[package]
1[package]
2name = "helix-tui"
2name = "helix-tui"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5description = """
5description = """
6A library to build rich terminal user interfaces or dashboards
6A library to build rich terminal user interfaces or dashboards
7"""
7"""
8−edition = "2018"
8+edition = "2021"
9license = "MPL-2.0"
9license = "MPL-2.0"
10categories = ["editor"]
10categories = ["editor"]
11repository = "https://github.com/helix-editor/helix"
11repository = "https://github.com/helix-editor/helix"
@@ -19,7 +19,7 @@ default = ["crossterm"]
19bitflags = "1.3"
19bitflags = "1.3"
20cassowary = "0.3"
20cassowary = "0.3"
21unicode-segmentation = "1.8"
21unicode-segmentation = "1.8"
22−crossterm = { version = "0.21", optional = true }
22+crossterm = { version = "0.22", optional = true }
23serde = { version = "1", "optional" = true, features = ["derive"]}
23serde = { version = "1", "optional" = true, features = ["derive"]}
24−helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
24+helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
25−helix-core = { version = "0.4", path = "../helix-core" }
25+helix-core = { version = "0.5", path = "../helix-core" }
helix-tui/src/buffer.rs+56−22
@@ -266,12 +266,14 @@ impl Buffer {
266 where
266 where
267 S: AsRef<str>,
267 S: AsRef<str>,
268 {
268 {
269− self.set_string_truncated(x, y, string, width, style, false)
269+ self.set_string_truncated(x, y, string, width, style, false, false)
270 }
270 }
271
271
272 /// Print at most the first `width` characters of a string if enough space is available
272 /// 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
273+ /// until the end of the line. If `ellipsis` is true appends a `…` at the end of
274− /// truncated lines.
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)]
275 pub fn set_string_truncated<S>(
277 pub fn set_string_truncated<S>(
276 &mut self,
278 &mut self,
277 x: u16,
279 x: u16,
@@ -280,6 +282,7 @@ impl Buffer {
280 width: usize,
282 width: usize,
281 style: Style,
283 style: Style,
282 ellipsis: bool,
284 ellipsis: bool,
285+ truncate_start: bool,
283 ) -> (u16, u16)
286 ) -> (u16, u16)
284 where
287 where
285 S: AsRef<str>,
288 S: AsRef<str>,
@@ -289,28 +292,59 @@ impl Buffer {
289 let width = if ellipsis { width - 1 } else { width };
292 let width = if ellipsis { width - 1 } else { width };
290 let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
293 let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
291 let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
294 let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
295+ if !truncate_start {
292− for s in graphemes {
296+ for s in graphemes {
293− let width = s.width();
297+ let width = s.width();
294− if width == 0 {
298+ if width == 0 {
295− continue;
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;
296 }
315 }
297− // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
316+ if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
298− // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
317+ self.content[index].set_symbol("…");
299− if width > max_offset.saturating_sub(x_offset) {
300− break;
301 }
318 }
302−
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+ }
303− self.content[index].set_symbol(s);
341+ self.content[start].set_symbol(s);
304− self.content[index].set_style(style);
342+ self.content[start].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 {
343+ for i in start + 1..index {
307− self.content[i].reset();
344+ self.content[i].reset();
345+ }
346+ index -= width;
308 }
347 }
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("…");
314 }
348 }
315 (x_offset as u16, y)
349 (x_offset as u16, y)
316 }
350 }
helix-view/Cargo.toml+6−6
@@ -1,8 +1,8 @@
1[package]
1[package]
2name = "helix-view"
2name = "helix-view"
3−version = "0.4.1"
3+version = "0.5.0"
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
4authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5−edition = "2018"
5+edition = "2021"
6license = "MPL-2.0"
6license = "MPL-2.0"
7description = "UI abstractions for use in backends"
7description = "UI abstractions for use in backends"
8categories = ["editor"]
8categories = ["editor"]
@@ -16,10 +16,10 @@ term = ["crossterm"]
16[dependencies]
16[dependencies]
17bitflags = "1.3"
17bitflags = "1.3"
18anyhow = "1"
18anyhow = "1"
19−helix-core = { version = "0.4", path = "../helix-core" }
19+helix-core = { version = "0.5", path = "../helix-core" }
20−helix-lsp = { version = "0.4", path = "../helix-lsp"}
20+helix-lsp = { version = "0.5", path = "../helix-lsp"}
21−helix-dap = { version = "0.4", path = "../helix-dap"}
21+helix-dap = { version = "0.5", path = "../helix-dap"}
22−crossterm = { version = "0.21", optional = true }
22+crossterm = { version = "0.22", optional = true }
23
23
24# Conversion traits
24# Conversion traits
25once_cell = "1.8"
25once_cell = "1.8"
helix-view/src/clipboard.rs+6−16
@@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
116 }
116 }
117 } else {
117 } else {
118 #[cfg(target_os = "windows")]
118 #[cfg(target_os = "windows")]
119− return Box::new(provider::WindowsProvider::new());
119+ return Box::new(provider::WindowsProvider::default());
120
120
121 #[cfg(not(target_os = "windows"))]
121 #[cfg(not(target_os = "windows"))]
122 return Box::new(provider::NopProvider::new());
122 return Box::new(provider::NopProvider::new());
@@ -145,15 +145,15 @@ mod provider {
145 use anyhow::{bail, Context as _, Result};
145 use anyhow::{bail, Context as _, Result};
146 use std::borrow::Cow;
146 use std::borrow::Cow;
147
147
148+ #[cfg(not(target_os = "windows"))]
148 #[derive(Debug)]
149 #[derive(Debug)]
149 pub struct NopProvider {
150 pub struct NopProvider {
150 buf: String,
151 buf: String,
151 primary_buf: String,
152 primary_buf: String,
152 }
153 }
153
154
155+ #[cfg(not(target_os = "windows"))]
154 impl NopProvider {
156 impl NopProvider {
155− #[allow(dead_code)]
156− // Only dead_code on Windows.
157 pub fn new() -> Self {
157 pub fn new() -> Self {
158 Self {
158 Self {
159 buf: String::new(),
159 buf: String::new(),
@@ -162,6 +162,7 @@ mod provider {
162 }
162 }
163 }
163 }
164
164
165+ #[cfg(not(target_os = "windows"))]
165 impl ClipboardProvider for NopProvider {
166 impl ClipboardProvider for NopProvider {
166 fn name(&self) -> Cow<str> {
167 fn name(&self) -> Cow<str> {
167 Cow::Borrowed("none")
168 Cow::Borrowed("none")
@@ -186,19 +187,8 @@ mod provider {
186 }
187 }
187
188
188 #[cfg(target_os = "windows")]
189 #[cfg(target_os = "windows")]
189− #[derive(Debug)]
190+ #[derive(Default, Debug)]
190− pub struct WindowsProvider {
191+ 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− }
202
192
203 #[cfg(target_os = "windows")]
193 #[cfg(target_os = "windows")]
204 impl ClipboardProvider for WindowsProvider {
194 impl ClipboardProvider for WindowsProvider {
helix-view/src/document.rs+54−27
@@ -23,6 +23,8 @@ use crate::{DocumentId, Theme, ViewId};
23/// 8kB of buffer space for encoding and decoding `Rope`s.
23/// 8kB of buffer space for encoding and decoding `Rope`s.
24const BUF_SIZE: usize = 8192;
24const BUF_SIZE: usize = 8192;
25
25
26+const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
27+
26#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
27pub enum Mode {
29pub enum Mode {
28 Normal,
30 Normal,
@@ -95,6 +97,9 @@ pub struct Document {
95 // it back as it separated from the edits. We could split out the parts manually but that will
97 // it back as it separated from the edits. We could split out the parts manually but that will
96 // be more troublesome.
98 // be more troublesome.
97 history: Cell<History>,
99 history: Cell<History>,
100+
101+ pub savepoint: Option<Transaction>,
102+
98 last_saved_revision: usize,
103 last_saved_revision: usize,
99 version: i32, // should be usize?
104 version: i32, // should be usize?
100
105
@@ -306,8 +311,7 @@ where
306 T: Default,
311 T: Default,
307 F: FnOnce(T) -> T,
312 F: FnOnce(T) -> T,
308{
313{
309− let t = mem::take(mut_ref);
314+ *mut_ref = f(mem::take(mut_ref));
310− let _ = mem::replace(mut_ref, f(t));
311}
315}
312
316
313use helix_lsp::lsp;
317use helix_lsp::lsp;
@@ -325,7 +329,8 @@ impl Document {
325 encoding,
329 encoding,
326 text,
330 text,
327 selections: HashMap::default(),
331 selections: HashMap::default(),
328− indent_style: IndentStyle::Spaces(4),
332+ indent_style: DEFAULT_INDENT,
333+ line_ending: DEFAULT_LINE_ENDING,
329 mode: Mode::Normal,
334 mode: Mode::Normal,
330 restore_cursor: false,
335 restore_cursor: false,
331 syntax: None,
336 syntax: None,
@@ -335,9 +340,9 @@ impl Document {
335 diagnostics: Vec::new(),
340 diagnostics: Vec::new(),
336 version: 0,
341 version: 0,
337 history: Cell::new(History::default()),
342 history: Cell::new(History::default()),
343+ savepoint: None,
338 last_saved_revision: 0,
344 last_saved_revision: 0,
339 language_server: None,
345 language_server: None,
340− line_ending: DEFAULT_LINE_ENDING,
341 }
346 }
342 }
347 }
343
348
@@ -363,7 +368,7 @@ impl Document {
363 let mut doc = Self::from(rope, Some(encoding));
368 let mut doc = Self::from(rope, Some(encoding));
364
369
365 // set the path and try detecting the language
370 // set the path and try detecting the language
366− doc.set_path(path)?;
371+ doc.set_path(Some(path))?;
367 if let Some(loader) = config_loader {
372 if let Some(loader) = config_loader {
368 doc.detect_language(theme, loader);
373 doc.detect_language(theme, loader);
369 }
374 }
@@ -495,17 +500,15 @@ impl Document {
495 }
500 }
496
501
497 /// Detect the indentation used in the file, or otherwise defaults to the language indentation
502 /// 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
499 /// specified. Line ending is likewise auto-detected, and will fallback to the default OS
504 /// specified. Line ending is likewise auto-detected, and will fallback to the default OS
500 /// line ending.
505 /// line ending.
501 pub fn detect_indent_and_line_ending(&mut self) {
506 pub fn detect_indent_and_line_ending(&mut self) {
502 self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
507 self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
503− IndentStyle::from_str(
504− self.language
508+ self.language
505− .as_ref()
509+ .as_ref()
506− .and_then(|config| config.indent.as_ref())
510+ .and_then(|config| config.indent.as_ref())
507− .map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces.
511+ .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
508− )
509 });
512 });
510 self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
513 self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
511 }
514 }
@@ -550,12 +553,14 @@ impl Document {
550 self.encoding
553 self.encoding
551 }
554 }
552
555
553− pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
556+ pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
557+ let path = path
554− let path = helix_core::path::get_canonicalized_path(path)?;
558+ .map(helix_core::path::get_canonicalized_path)
559+ .transpose()?;
555
560
556 // if parent doesn't exist we still want to open the document
561 // if parent doesn't exist we still want to open the document
557 // and error out when document is saved
562 // and error out when document is saved
558− self.path = Some(path);
563+ self.path = path;
559
564
560 Ok(())
565 Ok(())
561 }
566 }
@@ -635,6 +640,14 @@ impl Document {
635 if !transaction.changes().is_empty() {
640 if !transaction.changes().is_empty() {
636 self.version += 1;
641 self.version += 1;
637
642
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+
638 // update tree-sitter syntax tree
651 // update tree-sitter syntax tree
639 if let Some(syntax) = &mut self.syntax {
652 if let Some(syntax) = &mut self.syntax {
640 // TODO: no unwrap
653 // TODO: no unwrap
@@ -644,14 +657,13 @@ impl Document {
644 }
657 }
645
658
646 // map state.diagnostics over changes::map_pos too
659 // 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 {
660+ for diagnostic in &mut self.diagnostics {
649− // use helix_core::Assoc;
661+ use helix_core::Assoc;
650− // let changes = transaction.changes();
662+ let changes = transaction.changes();
651− // diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
663+ diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
652− // diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
664+ diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
653− // diagnostic.line = self.text.char_to_line(diagnostic.range.start);
665+ diagnostic.line = self.text.char_to_line(diagnostic.range.start);
654− // }
666+ }
655
667
656 // emit lsp notification
668 // emit lsp notification
657 if let Some(language_server) = self.language_server() {
669 if let Some(language_server) = self.language_server() {
@@ -692,8 +704,8 @@ impl Document {
692 success
704 success
693 }
705 }
694
706
695− /// Undo the last modification to the [`Document`].
707+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
696− pub fn undo(&mut self, view_id: ViewId) {
708+ pub fn undo(&mut self, view_id: ViewId) -> bool {
697 let mut history = self.history.take();
709 let mut history = self.history.take();
698 let success = if let Some(transaction) = history.undo() {
710 let success = if let Some(transaction) = history.undo() {
699 self.apply_impl(transaction, view_id)
711 self.apply_impl(transaction, view_id)
@@ -706,10 +718,11 @@ impl Document {
706 // reset changeset to fix len
718 // reset changeset to fix len
707 self.changes = ChangeSet::new(self.text());
719 self.changes = ChangeSet::new(self.text());
708 }
720 }
721+ success
709 }
722 }
710
723
711− /// Redo the last modification to the [`Document`].
724+ /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
712− pub fn redo(&mut self, view_id: ViewId) {
725+ pub fn redo(&mut self, view_id: ViewId) -> bool {
713 let mut history = self.history.take();
726 let mut history = self.history.take();
714 let success = if let Some(transaction) = history.redo() {
727 let success = if let Some(transaction) = history.redo() {
715 self.apply_impl(transaction, view_id)
728 self.apply_impl(transaction, view_id)
@@ -722,6 +735,17 @@ impl Document {
722 // reset changeset to fix len
735 // reset changeset to fix len
723 self.changes = ChangeSet::new(self.text());
736 self.changes = ChangeSet::new(self.text());
724 }
737 }
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+ }
725 }
749 }
726
750
727 /// Undo modifications to the [`Document`] according to `uk`.
751 /// Undo modifications to the [`Document`] according to `uk`.
@@ -894,6 +918,9 @@ impl Document {
894
918
895 pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
919 pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
896 self.diagnostics = diagnostics;
920 self.diagnostics = diagnostics;
921+ // sort by range
922+ self.diagnostics
923+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
897 }
924 }
898}
925}
899
926
helix-view/src/editor.rs+94−30
@@ -2,7 +2,7 @@ use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 graphics::{CursorKind, Rect},
3 graphics::{CursorKind, Rect},
4 theme::{self, Theme},
4 theme::{self, Theme},
5− tree::Tree,
5+ tree::{self, Tree},
6 Document, DocumentId, View, ViewId,
6 Document, DocumentId, View, ViewId,
7};
7};
8
8
@@ -12,6 +12,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
12
12
13use std::{
13use std::{
14 collections::HashMap,
14 collections::HashMap,
15+ collections::BTreeMap,
15 path::{Path, PathBuf},
16 path::{Path, PathBuf},
16 pin::Pin,
17 pin::Pin,
17 sync::Arc,
18 sync::Arc,
@@ -19,8 +20,6 @@ use std::{
19
20
20use tokio::time::{sleep, Duration, Instant, Sleep};
21use tokio::time::{sleep, Duration, Instant, Sleep};
21
22
22−use slotmap::SlotMap;
23−
24use anyhow::Error;
23use anyhow::Error;
25
24
26pub use helix_core::diagnostic::Severity;
25pub use helix_core::diagnostic::Severity;
@@ -63,6 +62,9 @@ pub struct Config {
63 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
62 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
64 #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
63 #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
65 pub idle_timeout: Duration,
64 pub idle_timeout: Duration,
65+ pub completion_trigger_len: u8,
66+ /// Whether to display infoboxes. Defaults to true.
67+ pub auto_info: bool,
66}
68}
67
69
68#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
70#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -92,14 +94,29 @@ impl Default for Config {
92 auto_pairs: true,
94 auto_pairs: true,
93 auto_completion: true,
95 auto_completion: true,
94 idle_timeout: Duration::from_millis(400),
96 idle_timeout: Duration::from_millis(400),
97+ completion_trigger_len: 2,
98+ auto_info: true,
95 }
99 }
96 }
100 }
97}
101}
98
102
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+
99#[derive(Debug)]
115#[derive(Debug)]
100pub struct Editor {
116pub struct Editor {
101 pub tree: Tree,
117 pub tree: Tree,
118+ pub next_document_id: usize,
102− pub documents: SlotMap<DocumentId, Document>,
119+ pub documents: BTreeMap<DocumentId, Document>,
103 pub count: Option<std::num::NonZeroUsize>,
120 pub count: Option<std::num::NonZeroUsize>,
104 pub selected_register: Option<char>,
121 pub selected_register: Option<char>,
105 pub registers: Registers,
122 pub registers: Registers,
@@ -124,6 +141,7 @@ pub struct Editor {
124 pub config: Config,
141 pub config: Config,
125
142
126 pub idle_timer: Pin<Box<Sleep>>,
143 pub idle_timer: Pin<Box<Sleep>>,
144+ pub last_motion: Option<Motion>,
127}
145}
128
146
129#[derive(Debug, Copy, Clone)]
147#[derive(Debug, Copy, Clone)]
@@ -148,7 +166,8 @@ impl Editor {
148
166
149 Self {
167 Self {
150 tree: Tree::new(area),
168 tree: Tree::new(area),
169+ next_document_id: 0,
151− documents: SlotMap::with_key(),
170+ documents: BTreeMap::new(),
152 count: None,
171 count: None,
153 selected_register: None,
172 selected_register: None,
154 theme: themes.default(),
173 theme: themes.default(),
@@ -166,6 +185,7 @@ impl Editor {
166 clipboard_provider: get_clipboard_provider(),
185 clipboard_provider: get_clipboard_provider(),
167 status_msg: None,
186 status_msg: None,
168 idle_timer: Box::pin(sleep(config.idle_timeout)),
187 idle_timer: Box::pin(sleep(config.idle_timeout)),
188+ last_motion: None,
169 config,
189 config,
170 }
190 }
171 }
191 }
@@ -221,7 +241,7 @@ impl Editor {
221
241
222 fn _refresh(&mut self) {
242 fn _refresh(&mut self) {
223 for (view, _) in self.tree.views_mut() {
243 for (view, _) in self.tree.views_mut() {
224− let doc = &self.documents[view.doc];
244+ let doc = &self.documents[&view.doc];
225 view.ensure_cursor_in_view(doc, self.config.scrolloff)
245 view.ensure_cursor_in_view(doc, self.config.scrolloff)
226 }
246 }
227 }
247 }
@@ -230,22 +250,38 @@ impl Editor {
230 use crate::tree::Layout;
250 use crate::tree::Layout;
231 use helix_core::Selection;
251 use helix_core::Selection;
232
252
233− if !self.documents.contains_key(id) {
253+ if !self.documents.contains_key(&id) {
234 log::error!("cannot switch to document that does not exist (anymore)");
254 log::error!("cannot switch to document that does not exist (anymore)");
235 return;
255 return;
236 }
256 }
237
257
238 match action {
258 match action {
239 Action::Replace => {
259 Action::Replace => {
240− let view = view!(self);
260+ let (view, doc) = current_ref!(self);
241− let jump = (
261+ // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
242− view.doc,
262+ // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
243− self.documents[view.doc].selection(view.id).clone(),
263+ // of `self.tree`, which is mutably borrowed when `view_mut` is called.
244− );
264+ let remove_empty_scratch = !doc.is_modified()
245−
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);
246 let view = view_mut!(self);
274 let view = view_mut!(self);
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());
247− view.jumps.push(jump);
282+ view.jumps.push(jump);
248− view.last_accessed_doc = Some(view.doc);
283+ view.last_accessed_doc = Some(view.doc);
284+ }
249 view.doc = id;
285 view.doc = id;
250 view.offset = Position::default();
286 view.offset = Position::default();
251
287
@@ -272,14 +308,14 @@ impl Editor {
272 let view = View::new(id);
308 let view = View::new(id);
273 let view_id = self.tree.split(view, Layout::Horizontal);
309 let view_id = self.tree.split(view, Layout::Horizontal);
274 // initialize selection for view
310 // initialize selection for view
275− let doc = &mut self.documents[id];
311+ let doc = self.documents.get_mut(&id).unwrap();
276 doc.selections.insert(view_id, Selection::point(0));
312 doc.selections.insert(view_id, Selection::point(0));
277 }
313 }
278 Action::VerticalSplit => {
314 Action::VerticalSplit => {
279 let view = View::new(id);
315 let view = View::new(id);
280 let view_id = self.tree.split(view, Layout::Vertical);
316 let view_id = self.tree.split(view, Layout::Vertical);
281 // initialize selection for view
317 // initialize selection for view
282− let doc = &mut self.documents[id];
318+ let doc = self.documents.get_mut(&id).unwrap();
283 doc.selections.insert(view_id, Selection::point(0));
319 doc.selections.insert(view_id, Selection::point(0));
284 }
320 }
285 }
321 }
@@ -288,9 +324,11 @@ impl Editor {
288 }
324 }
289
325
290 pub fn new_file(&mut self, action: Action) -> DocumentId {
326 pub fn new_file(&mut self, action: Action) -> DocumentId {
327+ let id = DocumentId(self.next_document_id);
328+ self.next_document_id += 1;
291− let doc = Document::default();
329+ let mut doc = Document::default();
330+ doc.id = id;
292− let id = self.documents.insert(doc);
331+ self.documents.insert(id, doc);
293− self.documents[id].id = id;
294 self.switch(id, action);
332 self.switch(id, action);
295 id
333 id
296 }
334 }
@@ -313,7 +351,11 @@ impl Editor {
313 self.language_servers
351 self.language_servers
314 .get(language)
352 .get(language)
315 .map_err(|e| {
353 .map_err(|e| {
354+ log::error!(
355+ "Failed to initialize the LSP for `{}` {{ {} }}",
316− log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
356+ language.scope(),
357+ e
358+ )
317 })
359 })
318 .ok()
360 .ok()
319 });
361 });
@@ -336,8 +378,10 @@ impl Editor {
336 doc.set_language_server(Some(language_server));
378 doc.set_language_server(Some(language_server));
337 }
379 }
338
380
381+ let id = DocumentId(self.next_document_id);
382+ self.next_document_id += 1;
383+ doc.id = id;
339− let id = self.documents.insert(doc);
384+ self.documents.insert(id, doc);
340− self.documents[id].id = id;
341 id
385 id
342 };
386 };
343
387
@@ -348,16 +392,20 @@ impl Editor {
348 pub fn close(&mut self, id: ViewId, close_buffer: bool) {
392 pub fn close(&mut self, id: ViewId, close_buffer: bool) {
349 let view = self.tree.get(self.tree.focus);
393 let view = self.tree.get(self.tree.focus);
350 // remove selection
394 // 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);
352
400
353 if close_buffer {
401 if close_buffer {
354 // get around borrowck issues
402 // get around borrowck issues
355− let doc = &self.documents[view.doc];
403+ let doc = &self.documents[&view.doc];
356
404
357 if let Some(language_server) = doc.language_server() {
405 if let Some(language_server) = doc.language_server() {
358 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
406 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
359 }
407 }
360− self.documents.remove(view.doc);
408+ self.documents.remove(&view.doc);
361 }
409 }
362
410
363 self.tree.remove(id);
411 self.tree.remove(id);
@@ -374,24 +422,40 @@ impl Editor {
374 self.tree.focus_next();
422 self.tree.focus_next();
375 }
423 }
376
424
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+
377 pub fn should_close(&self) -> bool {
441 pub fn should_close(&self) -> bool {
378 self.tree.is_empty()
442 self.tree.is_empty()
379 }
443 }
380
444
381 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
445 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
382 let view = self.tree.get_mut(id);
446 let view = self.tree.get_mut(id);
383− let doc = &self.documents[view.doc];
447+ let doc = &self.documents[&view.doc];
384 view.ensure_cursor_in_view(doc, self.config.scrolloff)
448 view.ensure_cursor_in_view(doc, self.config.scrolloff)
385 }
449 }
386
450
387 #[inline]
451 #[inline]
388 pub fn document(&self, id: DocumentId) -> Option<&Document> {
452 pub fn document(&self, id: DocumentId) -> Option<&Document> {
389− self.documents.get(id)
453+ self.documents.get(&id)
390 }
454 }
391
455
392 #[inline]
456 #[inline]
393 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
457 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
394− self.documents.get_mut(id)
458+ self.documents.get_mut(&id)
395 }
459 }
396
460
397 #[inline]
461 #[inline]
@@ -416,7 +480,7 @@ impl Editor {
416
480
417 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
481 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
418 let view = view!(self);
482 let view = view!(self);
419− let doc = &self.documents[view.doc];
483+ let doc = &self.documents[&view.doc];
420 let cursor = doc
484 let cursor = doc
421 .selection(view.id)
485 .selection(view.id)
422 .primary()
486 .primary()
helix-view/src/info.rs+2−2
@@ -1,6 +1,6 @@
1use crate::input::KeyEvent;
1use crate::input::KeyEvent;
2use helix_core::unicode::width::UnicodeWidthStr;
2use helix_core::unicode::width::UnicodeWidthStr;
3−use std::fmt::Write;
3+use std::{collections::BTreeSet, fmt::Write};
4
4
5#[derive(Debug)]
5#[derive(Debug)]
6/// Info box used in editor. Rendering logic will be in other crate.
6/// Info box used in editor. Rendering logic will be in other crate.
@@ -16,7 +16,7 @@ pub struct Info {
16}
16}
17
17
18impl Info {
18impl 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 {
20 let body = body
20 let body = body
21 .into_iter()
21 .into_iter()
22 .map(|(desc, events)| {
22 .map(|(desc, events)| {
helix-view/src/input.rs+1−1
@@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers};
8
8
9/// Represents a key event.
9/// Represents a key event.
10// We use a newtype here because we want to customize Deserialize and Display.
10// 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)]
12pub struct KeyEvent {
12pub struct KeyEvent {
13 pub code: KeyCode,
13 pub code: KeyCode,
14 pub modifiers: KeyModifiers,
14 pub modifiers: KeyModifiers,
helix-view/src/keyboard.rs+1−1
@@ -54,7 +54,7 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
54}
54}
55
55
56/// Represents a key.
56/// Represents a key.
57−#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
57+#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59pub enum KeyCode {
59pub enum KeyCode {
60 /// Backspace key.
60 /// Backspace key.
helix-view/src/lib.rs+3−1
@@ -12,8 +12,10 @@ pub mod theme;
12pub mod tree;
12pub mod tree;
13pub mod view;
13pub mod view;
14
14
15+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
16+pub struct DocumentId(usize);
17+
15slotmap::new_key_type! {
18slotmap::new_key_type! {
16− pub struct DocumentId;
17 pub struct ViewId;
19 pub struct ViewId;
18}
20}
19
21
helix-view/src/macros.rs+3−2
@@ -13,7 +13,8 @@
13macro_rules! current {
13macro_rules! current {
14 ( $( $editor:ident ).+ ) => {{
14 ( $( $editor:ident ).+ ) => {{
15 let view = $crate::view_mut!( $( $editor ).+ );
15 let view = $crate::view_mut!( $( $editor ).+ );
16+ let id = view.doc;
16− let doc = &mut $( $editor ).+ .documents[view.doc];
17+ let doc = $( $editor ).+ .documents.get_mut(&id).unwrap();
17 (view, doc)
18 (view, doc)
18 }};
19 }};
19}
20}
@@ -56,7 +57,7 @@ macro_rules! doc {
56macro_rules! current_ref {
57macro_rules! current_ref {
57 ( $( $editor:ident ).+ ) => {{
58 ( $( $editor:ident ).+ ) => {{
58 let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
59 let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
59− let doc = &$( $editor ).+ .documents[view.doc];
60+ let doc = &$( $editor ).+ .documents[&view.doc];
60 (view, doc)
61 (view, doc)
61 }};
62 }};
62}
63}
helix-view/src/theme.rs−1
@@ -1,6 +1,5 @@
1use std::{
1use std::{
2 collections::HashMap,
2 collections::HashMap,
3− convert::TryFrom,
4 path::{Path, PathBuf},
3 path::{Path, PathBuf},
5};
4};
6
5
helix-view/src/tree.rs+182−9
@@ -47,13 +47,21 @@ impl Node {
47
47
48// TODO: screen coord to container + container coordinate helpers
48// TODO: screen coord to container + container coordinate helpers
49
49
50−#[derive(Debug, PartialEq, Eq)]
50+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Layout {
51pub enum Layout {
52 Horizontal,
52 Horizontal,
53 Vertical,
53 Vertical,
54 // could explore stacked/tabbed
54 // could explore stacked/tabbed
55}
55}
56
56
57+#[derive(Debug, Clone, Copy)]
58+pub enum Direction {
59+ Up,
60+ Down,
61+ Left,
62+ Right,
63+}
64+
57#[derive(Debug)]
65#[derive(Debug)]
58pub struct Container {
66pub struct Container {
59 layout: Layout,
67 layout: Layout,
@@ -150,7 +158,6 @@ impl Tree {
150 } => container,
158 } => container,
151 _ => unreachable!(),
159 _ => unreachable!(),
152 };
160 };
153−
154 if container.layout == layout {
161 if container.layout == layout {
155 // insert node after the current item if there is children already
162 // insert node after the current item if there is children already
156 let pos = if container.children.is_empty() {
163 let pos = if container.children.is_empty() {
@@ -393,6 +400,112 @@ impl Tree {
393 Traverse::new(self)
400 Traverse::new(self)
394 }
401 }
395
402
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+
396 pub fn focus_next(&mut self) {
509 pub fn focus_next(&mut self) {
397 // This function is very dumb, but that's because we don't store any parent links.
510 // This function is very dumb, but that's because we don't store any parent links.
398 // (we'd be able to go parent.next_sibling() recursively until we find something)
511 // (we'd be able to go parent.next_sibling() recursively until we find something)
@@ -420,13 +533,12 @@ impl Tree {
420 // if found = container -> found = first child
533 // if found = container -> found = first child
421 // }
534 // }
422
535
536+ let mut views = self
423− let iter = self.traverse();
537+ .traverse()
424−
425− let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
538+ .skip_while(|&(id, _view)| id != self.focus)
426− iter.next(); // take the focused value
539+ .skip(1); // Skip focused value
427−
428− if let Some((key, _)) = iter.next() {
540+ if let Some((id, _)) = views.next() {
429− self.focus = key;
541+ self.focus = id;
430 } else {
542 } else {
431 // extremely crude, take the first item again
543 // extremely crude, take the first item again
432 let (key, _) = self.traverse().next().unwrap();
544 let (key, _) = self.traverse().next().unwrap();
@@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
472 }
584 }
473 }
585 }
474}
586}
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;
2
2
3use crate::{graphics::Rect, Document, DocumentId, ViewId};
3use crate::{graphics::Rect, Document, DocumentId, ViewId};
4use helix_core::{
4use helix_core::{
5− coords_at_pos,
6 graphemes::{grapheme_width, RopeGraphemes},
5 graphemes::{grapheme_width, RopeGraphemes},
7 line_ending::line_end_char_index,
6 line_ending::line_end_char_index,
8− Position, RopeSlice, Selection,
7+ visual_coords_at_pos, Position, RopeSlice, Selection,
9};
8};
10
9
11type Jump = (DocumentId, Selection);
10type Jump = (DocumentId, Selection);
@@ -91,7 +90,10 @@ impl View {
91 .selection(self.id)
90 .selection(self.id)
92 .primary()
91 .primary()
93 .cursor(doc.text().slice(..));
92 .cursor(doc.text().slice(..));
93+
94− let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
94+ let Position { col, row: line } =
95+ visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
96+
95 let inner_area = self.inner_area();
97 let inner_area = self.inner_area();
96 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
98 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
97
99
languages.toml+10−1
@@ -312,7 +312,7 @@ injection-regex = "php"
312file-types = ["php"]
312file-types = ["php"]
313roots = []
313roots = []
314
314
315−indent = { tab-width = 2, unit = " " }
315+indent = { tab-width = 4, unit = " " }
316
316
317[[language]]
317[[language]]
318name = "latex"
318name = "latex"
@@ -458,3 +458,12 @@ file-types = ["scm"]
458roots = []
458roots = []
459comment-token = ";"
459comment-token = ";"
460indent = { tab-width = 2, unit = " " }
460indent = { 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 @@
7
7
8(command_name) @function
8(command_name) @function
9
9
10−(variable_name) @property
10+(variable_name) @variable.other.member
11
11
12[
12[
13 "case"
13 "case"
@@ -31,7 +31,7 @@
31
31
32(function_definition name: (word) @function)
32(function_definition name: (word) @function)
33
33
34−(file_descriptor) @number
34+(file_descriptor) @constant.numeric.integer
35
35
36[
36[
37 (command_substitution)
37 (command_substitution)
runtime/queries/c-sharp/highlights.scm+5−5
@@ -20,16 +20,16 @@
20] @type.builtin
20] @type.builtin
21
21
22;; Enum
22;; Enum
23−(enum_member_declaration (identifier) @variable.property)
23+(enum_member_declaration (identifier) @variable.other.member)
24
24
25;; Literals
25;; Literals
26[
26[
27 (real_literal)
27 (real_literal)
28 (integer_literal)
28 (integer_literal)
29−] @number
29+] @constant.numeric.integer
30
30
31+(character_literal) @constant.character
31[
32[
32− (character_literal)
33 (string_literal)
33 (string_literal)
34 (verbatim_string_literal)
34 (verbatim_string_literal)
35 (interpolated_string_text)
35 (interpolated_string_text)
@@ -40,8 +40,8 @@
40 "$@\""
40 "$@\""
41 ] @string
41 ] @string
42
42
43+(boolean_literal) @constant.builtin.boolean
43[
44[
44− (boolean_literal)
45 (null_literal)
45 (null_literal)
46 (void_keyword)
46 (void_keyword)
47] @constant.builtin
47] @constant.builtin
@@ -98,7 +98,7 @@
98;; Keywords
98;; Keywords
99(modifier) @keyword
99(modifier) @keyword
100(this_expression) @keyword
100(this_expression) @keyword
101−(escape_sequence) @keyword
101+(escape_sequence) @constant.character.escape
102
102
103[
103[
104 "as"
104 "as"
runtime/queries/c/highlights.scm+3−3
@@ -60,8 +60,8 @@
60(system_lib_string) @string
60(system_lib_string) @string
61
61
62(null) @constant
62(null) @constant
63−(number_literal) @number
63+(number_literal) @constant.numeric.integer
64−(char_literal) @string
64+(char_literal) @constant.character
65
65
66(call_expression
66(call_expression
67 function: (identifier) @function)
67 function: (identifier) @function)
@@ -73,7 +73,7 @@
73(preproc_function_def
73(preproc_function_def
74 name: (identifier) @function.special)
74 name: (identifier) @function.special)
75
75
76−(field_identifier) @property
76+(field_identifier) @variable.other.member
77(statement_identifier) @label
77(statement_identifier) @label
78(type_identifier) @type
78(type_identifier) @type
79(primitive_type) @type
79(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 @@
3; Functions
3; Functions
4
4
5(call_expression
5(call_expression
6− function: (scoped_identifier
6+ function: (qualified_identifier
7 name: (identifier) @function))
7 name: (identifier) @function))
8
8
9(template_function
9(template_function
@@ -13,15 +13,14 @@
13 name: (field_identifier) @function)
13 name: (field_identifier) @function)
14
14
15(template_function
15(template_function
16− name: (scoped_identifier
17− name: (identifier) @function))
16+ name: (identifier) @function)
18
17
19(function_declarator
18(function_declarator
20− declarator: (scoped_identifier
19+ declarator: (qualified_identifier
21 name: (identifier) @function))
20 name: (identifier) @function))
22
21
23(function_declarator
22(function_declarator
24− declarator: (scoped_identifier
23+ declarator: (qualified_identifier
25 name: (identifier) @function))
24 name: (identifier) @function))
26
25
27(function_declarator
26(function_declarator
runtime/queries/css/highlights.scm+7−7
@@ -26,11 +26,11 @@
26(pseudo_element_selector (tag_name) @attribute)
26(pseudo_element_selector (tag_name) @attribute)
27(pseudo_class_selector (class_name) @attribute)
27(pseudo_class_selector (class_name) @attribute)
28
28
29−(class_name) @property
29+(class_name) @variable.other.member
30−(id_name) @property
30+(id_name) @variable.other.member
31−(namespace_name) @property
31+(namespace_name) @variable.other.member
32−(property_name) @property
32+(property_name) @variable.other.member
33−(feature_name) @property
33+(feature_name) @variable.other.member
34
34
35(attribute_name) @attribute
35(attribute_name) @attribute
36
36
@@ -55,8 +55,8 @@
55(string_value) @string
55(string_value) @string
56(color_value) @string.special
56(color_value) @string.special
57
57
58−(integer_value) @number
58+(integer_value) @constant.numeric.integer
59−(float_value) @number
59+(float_value) @constant.numeric.float
60(unit) @type
60(unit) @type
61
61
62"#" @punctuation.delimiter
62"#" @punctuation.delimiter
runtime/queries/elixir/highlights.scm+203−120
@@ -1,125 +1,210 @@
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+
1−["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
20+["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
2−
21+
3−[(true) (false) (nil)] @constant.builtin
22+; Operators
4−
23+
5−(keyword
24+; * doc string
6− [(keyword_literal)
25+(unary_operator
7− ":"] @tag)
26+ operator: "@" @comment.block.documentation
8−
27+ operand: (call
9−(keyword
28+ target: (identifier) @comment.block.documentation.__attribute__
10− (keyword_string
29+ (arguments
11− [(string_start)
30+ [
12− (string_content)
31+ (string) @comment.block.documentation
13− (string_end)] @tag))
32+ (charlist) @comment.block.documentation
14−
33+ (sigil
15−[(atom_literal)
34+ quoted_start: _ @comment.block.documentation
16− (atom_start)
35+ quoted_end: _ @comment.block.documentation) @comment.block.documentation
17− (atom_content)
36+ (boolean) @comment.block.documentation
18− (atom_end)] @tag
37+ ]))
19−
38+ (#match? @comment.block.documentation.__attribute__ "^(moduledoc|typedoc|doc)$"))
20−[(comment)
39+
21− (unused_identifier)] @comment
40+; * module attribute
22−
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+
23−(escape_sequence) @escape
97+(escape_sequence) @constant.character.escape
24−
98+
25−(call function: (function_identifier) @keyword
99+[
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)$"))
100+ (atom)
27−
101+ (quoted_atom)
28−(call function: (function_identifier) @keyword
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
29− [(call
143+(call
30− function: (function_identifier) @function
144+ target: [
31− (arguments
145+ ; local
32− [(identifier) @variable.parameter
33− (_ (identifier) @variable.parameter)
34− (_ (_ (identifier) @variable.parameter))
146+ (identifier) @function
35− (_ (_ (_ (identifier) @variable.parameter)))
147+ ; remote
36− (_ (_ (_ (_ (identifier) @variable.parameter))))
148+ (dot
37− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
149+ right: (identifier) @function)
38− (binary_op
150+ ])
39− left:
151+
152+; * just identifier in function definition
40− (call
153+(call
41− function: (function_identifier) @function
154+ target: (identifier) @keyword
42− (arguments
155+ (arguments
43− [(identifier) @variable.parameter
156+ [
44− (_ (identifier) @variable.parameter)
45− (_ (_ (identifier) @variable.parameter))
46− (_ (_ (_ (identifier) @variable.parameter)))
157+ (identifier) @function
47− (_ (_ (_ (_ (identifier) @variable.parameter))))
158+ (binary_operator
48− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
159+ left: (identifier) @function
49 operator: "when")
160 operator: "when")
50− (binary_op
161+ ])
51− left: (identifier) @variable.parameter
162+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
52− operator: _ @function
163+
53− right: (identifier) @variable.parameter)]
164+; * pipe into identifier (definition)
54− (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
55−
56−(call (function_identifier) @keyword
57− [(call
165+(call
58− function: (function_identifier) @function)
59− (identifier) @function
166+ target: (identifier) @keyword
60− (binary_op
167+ (arguments
61− left:
168+ (binary_operator
62− [(call
169+ operator: "|>"
63− function: (function_identifier) @function)
64− (identifier) @function]
170+ right: (identifier) @variable))
65− operator: "when")]
171+ (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
66− (#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
172+
67−
173+; * pipe into identifier (function call)
68−(anonymous_function
174+(binary_operator
69− (stab_expression
175+ operator: "|>"
70− left: (bare_arguments
176+ right: (identifier) @function)
71− [(identifier) @variable.parameter
177+
72− (_ (identifier) @variable.parameter)
178+; Identifiers
73− (_ (_ (identifier) @variable.parameter))
179+
74− (_ (_ (_ (identifier) @variable.parameter)))
180+; * special
75− (_ (_ (_ (_ (identifier) @variable.parameter))))
181+(
76− (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
182+ (identifier) @constant.builtin
77−
183+ (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
78−(unary_op
184+)
79− operator: "@"
185+
80− (call (identifier) @attribute
186+; * unused
81− (heredoc
187+(
82− [(heredoc_start)
188+ (identifier) @comment
83− (heredoc_content)
189+ (#match? @comment "^_")
84− (heredoc_end)] @doc))
190+)
85− (#match? @attribute "^(doc|moduledoc)$"))
191+
86−
192+; * regular
87−(module) @type
193+(identifier) @variable
88−
194+
89−(unary_op
195+; Comment
90− operator: "@" @attribute
196+
91− [(call
197+(comment) @comment
92− function: (function_identifier) @attribute)
198+
93− (identifier) @attribute])
199+; Punctuation
94−
200+
95−(unary_op
201+[
96− operator: _ @operator)
202+ "%"
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)
203+] @punctuation
118
204
119[
205[
120 ","
206 ","
121− "->"
207+ ";"
122− "."
123] @punctuation.delimiter
208] @punctuation.delimiter
124
209
125[
210[
@@ -133,6 +218,4 @@
133 ">>"
218 ">>"
134] @punctuation.bracket
219] @punctuation.bracket
135
220
136−(special_identifier) @function.special
137−
138(ERROR) @warning
221(ERROR) @warning
runtime/queries/go/highlights.scm+3−3
@@ -25,7 +25,7 @@
25(variadic_parameter_declaration (identifier) @variable.parameter)
25(variadic_parameter_declaration (identifier) @variable.parameter)
26
26
27(type_identifier) @type
27(type_identifier) @type
28−(field_identifier) @property
28+(field_identifier) @variable.other.member
29(identifier) @variable
29(identifier) @variable
30(package_identifier) @variable
30(package_identifier) @variable
31
31
@@ -130,13 +130,13 @@
130 (rune_literal)
130 (rune_literal)
131] @string
131] @string
132
132
133−(escape_sequence) @escape
133+(escape_sequence) @constant.character.escape
134
134
135[
135[
136 (int_literal)
136 (int_literal)
137 (float_literal)
137 (float_literal)
138 (imaginary_literal)
138 (imaginary_literal)
139−] @number
139+] @constant.numeric.integer
140
140
141[
141[
142 (true)
142 (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 @@
13(constraint class: (class_name (type)) @class)
13(constraint class: (class_name (type)) @class)
14(class (class_head class: (class_name (type)) @class))
14(class (class_head class: (class_name (type)) @class))
15(instance (instance_head class: (class_name (type)) @class))
15(instance (instance_head class: (class_name (type)) @class))
16−(integer) @number
16+(integer) @constant.numeric.integer
17−(exp_literal (float)) @number
17+(exp_literal (float)) @constant.numeric.float
18−(char) @literal
18+(char) @constant.character
19(con_unit) @literal
19(con_unit) @literal
20(con_list) @literal
20(con_list) @literal
21(tycon_arrow) @operator
21(tycon_arrow) @operator
runtime/queries/java/highlights.scm+6−5
@@ -59,14 +59,15 @@
59 (hex_integer_literal)
59 (hex_integer_literal)
60 (decimal_integer_literal)
60 (decimal_integer_literal)
61 (octal_integer_literal)
61 (octal_integer_literal)
62+] @constant.numeric.integer
63+
64+[
62 (decimal_floating_point_literal)
65 (decimal_floating_point_literal)
63 (hex_floating_point_literal)
66 (hex_floating_point_literal)
64−] @number
67+] @constant.numeric.float
65
68
66−[
67− (character_literal)
69+(character_literal) @constant.character
68− (string_literal)
70+(string_literal) @string
69−] @string
70
71
71[
72[
72 (true)
73 (true)
runtime/queries/javascript/highlights.scm+2−2
@@ -65,7 +65,7 @@
65; Properties
65; Properties
66;-----------
66;-----------
67
67
68−(property_identifier) @property
68+(property_identifier) @variable.other.member
69
69
70; Literals
70; Literals
71;---------
71;---------
@@ -88,7 +88,7 @@
88] @string
88] @string
89
89
90(regex) @string.regexp
90(regex) @string.regexp
91−(number) @number
91+(number) @constant.numeric.integer
92
92
93; Tokens
93; Tokens
94;-------
94;-------
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
1(pair
7(pair
2 key: (_) @keyword)
8 key: (_) @keyword)
3
9
4(string) @string
10(string) @string
11+(escape_sequence) @constant.character.escape
12+(ERROR) @error
5
13
6−(object
14+"," @punctuation.delimiter
15+[
16+ "["
17+ "]"
7− "{" @escape
18+ "{"
8− (_)
9− "}" @escape)
19+ "}"
20+] @punctuation.bracket
runtime/queries/julia/highlights.scm+6−6
@@ -15,7 +15,7 @@
15
15
16(field_expression
16(field_expression
17 (identifier)
17 (identifier)
18− (identifier) @field .)
18+ (identifier) @variable.other.member .)
19
19
20(function_definition
20(function_definition
21 name: (identifier) @function)
21 name: (identifier) @function)
@@ -80,14 +80,14 @@
80(struct_definition
80(struct_definition
81 name: (identifier) @type)
81 name: (identifier) @type)
82
82
83−(number) @number
83+(number) @constant.numeric.integer
84(range_expression
84(range_expression
85− (identifier) @number
85+ (identifier) @constant.numeric.integer
86− (eq? @number "end"))
86+ (eq? @constant.numeric.integer "end"))
87(range_expression
87(range_expression
88 (_
88 (_
89− (identifier) @number
89+ (identifier) @constant.numeric.integer
90− (eq? @number "end")))
90+ (eq? @constant.numeric.integer "end")))
91(coefficient_expression
91(coefficient_expression
92 (number)
92 (number)
93 (identifier) @constant.builtin)
93 (identifier) @constant.builtin)
runtime/queries/ledger/highlights.scm+2−2
@@ -7,9 +7,9 @@
7 (date)
7 (date)
8 (interval)
8 (interval)
9 (quantity)
9 (quantity)
10−] @number
10+] @constant.numeric.integer
11
11
12−((account) @field)
12+((account) @variable.other.member)
13((commodity) @text.literal)
13((commodity) @text.literal)
14
14
15"include" @include
15"include" @include
runtime/queries/lua/highlights.scm+3−3
@@ -150,14 +150,14 @@
150(table ["{" "}"] @constructor)
150(table ["{" "}"] @constructor)
151(comment) @comment
151(comment) @comment
152(string) @string
152(string) @string
153−(number) @number
153+(number) @constant.numeric.integer
154(label_statement) @label
154(label_statement) @label
155; A bit of a tricky one, this will only match field names
155; A bit of a tricky one, this will only match field names
156−(field . (identifier) @property (_))
156+(field . (identifier) @variable.other.member (_))
157(shebang) @comment
157(shebang) @comment
158
158
159;; Property
159;; Property
160−(property_identifier) @property
160+(property_identifier) @variable.other.member
161
161
162;; Variable
162;; Variable
163(identifier) @variable
163(identifier) @variable
runtime/queries/nix/highlights.scm+5−7
@@ -33,16 +33,14 @@
33
33
34(uri) @string.special.uri
34(uri) @string.special.uri
35
35
36−[
36+(integer) @constant.numeric.integer
37− (integer)
37+(float) @constant.numeric.float
38− (float)
39−] @number
40
38
41(interpolation
39(interpolation
42 "${" @punctuation.special
40 "${" @punctuation.special
43 "}" @punctuation.special) @embedded
41 "}" @punctuation.special) @embedded
44
42
45−(escape_sequence) @escape
43+(escape_sequence) @constant.character.escape
46
44
47(function
45(function
48 universal: (identifier) @variable.parameter
46 universal: (identifier) @variable.parameter
@@ -66,8 +64,8 @@
66(binary
64(binary
67 operator: _ @operator)
65 operator: _ @operator)
68
66
69−(attr_identifier) @property
67+(attr_identifier) @variable.other.member
70−(inherit attrs: (attrs_inherited (identifier) @property) )
68+(inherit attrs: (attrs_inherited (identifier) @variable.other.member) )
71
69
72[
70[
73 ";"
71 ";"
runtime/queries/ocaml/highlights.scm+4−4
@@ -51,14 +51,14 @@
51; Properties
51; Properties
52;-----------
52;-----------
53
53
54−[(label_name) (field_name) (instance_variable_name)] @property
54+[(label_name) (field_name) (instance_variable_name)] @variable.other.member
55
55
56; Constants
56; Constants
57;----------
57;----------
58
58
59[(boolean) (unit)] @constant
59[(boolean) (unit)] @constant
60
60
61−[(number) (signed_number)] @number
61+[(number) (signed_number)] @constant.numeric.integer
62
62
63(character) @constant.character
63(character) @constant.character
64
64
@@ -66,7 +66,7 @@
66
66
67(quoted_string "{" @string "}" @string) @string
67(quoted_string "{" @string "}" @string) @string
68
68
69−(escape_sequence) @string.escape
69+(escape_sequence) @constant.character.escape
70
70
71[
71[
72 (conversion_specification)
72 (conversion_specification)
@@ -145,7 +145,7 @@
145; Attributes
145; Attributes
146;-----------
146;-----------
147
147
148−(attribute_id) @property
148+(attribute_id) @variable.other.member
149
149
150; Comments
150; Comments
151;---------
151;---------
runtime/queries/php/highlights.scm+6−6
@@ -30,12 +30,12 @@
30; Member
30; Member
31
31
32(property_element
32(property_element
33− (variable_name) @property)
33+ (variable_name) @variable.other.member)
34
34
35(member_access_expression
35(member_access_expression
36− name: (variable_name (name)) @property)
36+ name: (variable_name (name)) @variable.other.member)
37(member_access_expression
37(member_access_expression
38− name: (name) @property)
38+ name: (name) @variable.other.member)
39
39
40; Variables
40; Variables
41
41
@@ -56,10 +56,10 @@
56
56
57(string) @string
57(string) @string
58(heredoc) @string
58(heredoc) @string
59−(boolean) @constant.builtin
59+(boolean) @constant.builtin.boolean
60(null) @constant.builtin
60(null) @constant.builtin
61−(integer) @number
61+(integer) @constant.numeric.integer
62−(float) @number
62+(float) @constant.numeric.float
63(comment) @comment
63(comment) @comment
64
64
65"$" @operator
65"$" @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 @@
34[
34[
35 (fieldName)
35 (fieldName)
36 (optionName)
36 (optionName)
37−] @property
37+] @variable.other.member
38(enumVariantName) @type.enum.variant
38(enumVariantName) @type.enum.variant
39
39
40(fullIdent) @namespace
40(fullIdent) @namespace
41
41
42−[
42+(intLit) @constant.numeric.integer
43− (intLit)
44− (floatLit)
43+(floatLit) @constant.numeric.float
45−] @number
46−(boolLit) @constant.builtin
44+(boolLit) @constant.builtin.boolean
47(strLit) @string
45(strLit) @string
48
46
49(constant) @constant
47(constant) @constant
runtime/queries/python/highlights.scm+4−7
@@ -29,7 +29,7 @@
29 name: (identifier) @function)
29 name: (identifier) @function)
30
30
31(identifier) @variable
31(identifier) @variable
32−(attribute attribute: (identifier) @property)
32+(attribute attribute: (identifier) @variable.other.member)
33(type (identifier) @type)
33(type (identifier) @type)
34
34
35; Literals
35; Literals
@@ -40,14 +40,11 @@
40 (false)
40 (false)
41] @constant.builtin
41] @constant.builtin
42
42
43−[
43+(integer) @constant.numeric.integer
44− (integer)
44+(float) @constant.numeric.float
45− (float)
46−] @number
47−
48(comment) @comment
45(comment) @comment
49(string) @string
46(string) @string
50−(escape_sequence) @escape
47+(escape_sequence) @constant.character.escape
51
48
52(interpolation
49(interpolation
53 "{" @punctuation.special
50 "{" @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 @@
55[
55[
56 (class_variable)
56 (class_variable)
57 (instance_variable)
57 (instance_variable)
58−] @property
58+] @variable.other.member
59
59
60((identifier) @constant.builtin
60((identifier) @constant.builtin
61 (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
61 (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
@@ -101,12 +101,12 @@
101] @string.special.symbol
101] @string.special.symbol
102
102
103(regex) @string.regexp
103(regex) @string.regexp
104−(escape_sequence) @escape
104+(escape_sequence) @constant.character.escape
105
105
106[
106[
107 (integer)
107 (integer)
108 (float)
108 (float)
109−] @number
109+] @constant.numeric.integer
110
110
111[
111[
112 (nil)
112 (nil)
runtime/queries/rust/highlights.scm+24−11
@@ -15,15 +15,13 @@
15; Primitives
15; Primitives
16; ---
16; ---
17
17
18−(escape_sequence) @escape
18+(escape_sequence) @constant.character.escape
19(primitive_type) @type.builtin
19(primitive_type) @type.builtin
20(boolean_literal) @constant.builtin.boolean
20(boolean_literal) @constant.builtin.boolean
21+(integer_literal) @constant.numeric.integer
22+(float_literal) @constant.numeric.float
23+(char_literal) @constant.character
21[
24[
22− (integer_literal)
23− (float_literal)
24−] @number
25−[
26− (char_literal)
27 (string_literal)
25 (string_literal)
28 (raw_string_literal)
26 (raw_string_literal)
29] @string
27] @string
@@ -40,10 +38,10 @@
40(enum_variant (identifier) @type.enum.variant)
38(enum_variant (identifier) @type.enum.variant)
41
39
42(field_initializer
40(field_initializer
43− (field_identifier) @property)
41+ (field_identifier) @variable.other.member)
44(shorthand_field_initializer
42(shorthand_field_initializer
45− (identifier) @variable.property)
43+ (identifier) @variable.other.member)
46−(shorthand_field_identifier) @variable.property
44+(shorthand_field_identifier) @variable.other.member
47
45
48(lifetime
46(lifetime
49 "'" @label
47 "'" @label
@@ -81,9 +79,24 @@
81 ] @punctuation.bracket)
79 ] @punctuation.bracket)
82
80
83; ---
81; ---
84−; Parameters
82+; Variables
85; ---
83; ---
86
84
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)
87(parameter
100(parameter
88 pattern: (identifier) @variable.parameter)
101 pattern: (identifier) @variable.parameter)
89(closure_parameters
102(closure_parameters
@@ -336,4 +349,4 @@
336
349
337(type_identifier) @type
350(type_identifier) @type
338(identifier) @variable
351(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 @@
29 (#match? @_attr "^(href|src)$"))
29 (#match? @_attr "^(href|src)$"))
30
30
31(tag_name) @tag
31(tag_name) @tag
32−(attribute_name) @property
32+(attribute_name) @variable.other.member
33(erroneous_end_tag_name) @error
33(erroneous_end_tag_name) @error
34(comment) @comment
34(comment) @comment
35
35
runtime/queries/toml/highlights.scm+4−4
@@ -1,17 +1,17 @@
1; Properties
1; Properties
2;-----------
2;-----------
3
3
4−(bare_key) @property
4+(bare_key) @variable.other.member
5(quoted_key) @string
5(quoted_key) @string
6
6
7; Literals
7; Literals
8;---------
8;---------
9
9
10−(boolean) @constant.builtin
10+(boolean) @constant.builtin.boolean
11(comment) @comment
11(comment) @comment
12(string) @string
12(string) @string
13−(integer) @number
13+(integer) @constant.numeric.integer
14−(float) @number
14+(float) @constant.numeric.float
15(offset_date_time) @string.special
15(offset_date_time) @string.special
16(local_date_time) @string.special
16(local_date_time) @string.special
17(local_date) @string.special
17(local_date) @string.special
runtime/queries/tsq/highlights.scm+2−2
@@ -35,12 +35,12 @@
35
35
36(comment) @comment
36(comment) @comment
37
37
38−(field_name) @property
38+(field_name) @variable.other.member
39
39
40(capture) @label
40(capture) @label
41
41
42(predicate_name) @function
42(predicate_name) @function
43
43
44−(escape_sequence) @escape
44+(escape_sequence) @constant.character.escape
45
45
46(node_name) @variable
46(node_name) @variable
runtime/queries/yaml/highlights.scm+5−5
@@ -1,12 +1,12 @@
1−(block_mapping_pair key: (_) @property)
1+(block_mapping_pair key: (_) @variable.other.member)
2−(flow_mapping (_ key: (_) @property))
2+(flow_mapping (_ key: (_) @variable.other.member))
3(boolean_scalar) @constant.builtin.boolean
3(boolean_scalar) @constant.builtin.boolean
4(null_scalar) @constant.builtin
4(null_scalar) @constant.builtin
5(double_quote_scalar) @string
5(double_quote_scalar) @string
6(single_quote_scalar) @string
6(single_quote_scalar) @string
7−(escape_sequence) @string.escape
7+(escape_sequence) @constant.character.escape
8−(integer_scalar) @number
8+(integer_scalar) @constant.numeric.integer
9−(float_scalar) @number
9+(float_scalar) @constant.numeric.float
10(comment) @comment
10(comment) @comment
11(anchor_name) @type
11(anchor_name) @type
12(alias_name) @type
12(alias_name) @type
runtime/queries/zig/highlights.scm+4−4
@@ -14,7 +14,7 @@ parameter: (IDENTIFIER) @variable.parameter
14[
14[
15 field_member: (IDENTIFIER)
15 field_member: (IDENTIFIER)
16 field_access: (IDENTIFIER)
16 field_access: (IDENTIFIER)
17−] @variable.property
17+] @variable.other.member
18
18
19;; assume TitleCase is a type
19;; assume TitleCase is a type
20(
20(
@@ -75,9 +75,9 @@ field_constant: (IDENTIFIER) @constant
75((BUILTINIDENTIFIER) @keyword.control.import
75((BUILTINIDENTIFIER) @keyword.control.import
76 (#any-of? @keyword.control.import "@import" "@cImport"))
76 (#any-of? @keyword.control.import "@import" "@cImport"))
77
77
78−(INTEGER) @number
78+(INTEGER) @constant.numeric.integer
79
79
80−(FLOAT) @number
80+(FLOAT) @constant.numeric.float
81
81
82[
82[
83 (LINESTRING)
83 (LINESTRING)
@@ -85,7 +85,7 @@ field_constant: (IDENTIFIER) @constant
85] @string
85] @string
86
86
87(CHAR_LITERAL) @constant.character
87(CHAR_LITERAL) @constant.character
88−(EscapeSequence) @escape
88+(EscapeSequence) @constant.character.escape
89(FormatSequence) @string.special
89(FormatSequence) @string.special
90
90
91[
91[
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 @@
8"punctuation.delimiter" = "#dc7759"
8"punctuation.delimiter" = "#dc7759"
9"operator" = { fg = "#dc7759", modifiers = ["bold"] }
9"operator" = { fg = "#dc7759", modifiers = ["bold"] }
10"special" = "#7fdc59"
10"special" = "#7fdc59"
11−"property" = "#c6b8ad"
11+"variable.other.member" = "#c6b8ad"
12"variable" = "#c6b8ad"
12"variable" = "#c6b8ad"
13"variable.parameter" = "#c6b8ad"
13"variable.parameter" = "#c6b8ad"
14"type" = "#dc597f"
14"type" = "#dc597f"
@@ -22,8 +22,8 @@
22"constant" = "#59dcb7"
22"constant" = "#59dcb7"
23"constant.builtin" = "#59dcb7"
23"constant.builtin" = "#59dcb7"
24"string" = "#59dcb7"
24"string" = "#59dcb7"
25−"number" = "#59c0dc"
25+"constant.numeric" = "#59c0dc"
26−"escape" = { fg = "#7fdc59", modifiers = ["bold"] }
26+"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] }
27"label" = "#59c0dc"
27"label" = "#59c0dc"
28
28
29"module" = "#d32c5d"
29"module" = "#d32c5d"
runtime/themes/dark_plus.toml+35−18
@@ -7,7 +7,7 @@
7"type.builtin" = { fg = "type" }
7"type.builtin" = { fg = "type" }
8"type.enum.variant" = { fg = "constant" }
8"type.enum.variant" = { fg = "constant" }
9"constructor" = { fg = "constant" }
9"constructor" = { fg = "constant" }
10−"property" = { fg = "variable" }
10+"variable.other.member" = { fg = "variable" }
11
11
12"keyword" = { fg = "keyword" }
12"keyword" = { fg = "keyword" }
13"keyword.directive" = { fg = "keyword" }
13"keyword.directive" = { fg = "keyword" }
@@ -31,47 +31,64 @@
31"function.macro" = { fg = "keyword" }
31"function.macro" = { fg = "keyword" }
32"attribute" = { fg = "fn_declaration" }
32"attribute" = { fg = "fn_declaration" }
33
33
34−"comment" = { fg = "#6A9955" }
34+"comment" = { fg = "dark_green" }
35
35
36−"string" = { fg = "#ce9178" }
36+"string" = { fg = "orange" }
37+"constant.character" = { fg = "orange" }
37−"string.regexp" = { fg = "regex" }
38+"string.regexp" = { fg = "gold" }
38−"number" = { fg = "#b5cea8" }
39+"constant.numeric" = { fg = "pale_green" }
39−"escape" = { fg = "#d7ba7d" }
40+"constant.character.escape" = { fg = "gold" }
40
41
41−"ui.background" = { fg = "#d4d4d4", bg = "#1e1e1e" }
42+"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
42
43
43"ui.window" = { bg = "widget" }
44"ui.window" = { bg = "widget" }
44"ui.popup" = { bg = "widget" }
45"ui.popup" = { bg = "widget" }
45"ui.help" = { bg = "widget" }
46"ui.help" = { bg = "widget" }
46"ui.menu.selected" = { bg = "widget" }
47"ui.menu.selected" = { bg = "widget" }
47
48
49+# TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`.
48"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
50"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
49"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
51"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
50"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
52"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
51
53
52"ui.selection" = { bg = "#3a3d41" }
54"ui.selection" = { bg = "#3a3d41" }
53−"ui.selection.primary" = { bg = "#264f78" }
55+"ui.selection.primary" = { bg = "dark_blue" }
54
56
55−"ui.linenr" = { fg = "#858585" }
57+"ui.linenr" = { fg = "dark_gray" }
56−"ui.linenr.selected" = { fg = "#c6c6c6" }
58+"ui.linenr.selected" = { fg = "light_gray2" }
57
59
58−"ui.statusline" = { fg = "#ffffff", bg = "#007acc" }
60+"ui.statusline" = { fg = "white", bg = "blue" }
59−"ui.statusline.inactive" = { fg = "#ffffff", bg = "#007acc" }
61+"ui.statusline.inactive" = { fg = "white", bg = "blue" }
60
62
61"ui.text" = { fg = "text", bg = "background" }
63"ui.text" = { fg = "text", bg = "background" }
62−"ui.text.focus" = { fg = "#ffffff" }
64+"ui.text.focus" = { fg = "white" }
63
65
64−"warning" = { fg = "#cca700" }
66+"warning" = { fg = "gold2" }
65−"error" = { fg = "#ff1212" }
67+"error" = { fg = "red" }
66−"info" = { fg = "#75beff" }
68+"info" = { fg = "light_blue" }
67−"hint" = { fg = "#eeeeeeb3" }
69+"hint" = { fg = "light_gray3" }
68
70
69diagnostic = { modifiers = ["underlined"] }
71diagnostic = { modifiers = ["underlined"] }
70
72
71[palette]
73[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+
72type = "#4EC9B0"
90type = "#4EC9B0"
73keyword = "#569CD6"
91keyword = "#569CD6"
74−regex = "#CE9178"
75special = "#C586C0"
92special = "#C586C0"
76variable = "#9CDCFE"
93variable = "#9CDCFE"
77fn_declaration = "#DCDCAA"
94fn_declaration = "#DCDCAA"
runtime/themes/everforest_dark.toml+3−4
@@ -8,16 +8,16 @@
8# Email: sainnhe@gmail.com
8# Email: sainnhe@gmail.com
9# License: MIT License
9# License: MIT License
10
10
11−"escape" = "orange"
11+"constant.character.escape" = "orange"
12"type" = "yellow"
12"type" = "yellow"
13"constant" = "purple"
13"constant" = "purple"
14−"number" = "purple"
14+"constant.numeric" = "purple"
15"string" = "grey2"
15"string" = "grey2"
16"comment" = "grey0"
16"comment" = "grey0"
17"variable" = "fg"
17"variable" = "fg"
18"variable.builtin" = "blue"
18"variable.builtin" = "blue"
19"variable.parameter" = "fg"
19"variable.parameter" = "fg"
20−"variable.property" = "fg"
20+"variable.other.member" = "fg"
21"label" = "aqua"
21"label" = "aqua"
22"punctuation" = "grey2"
22"punctuation" = "grey2"
23"punctuation.delimiter" = "grey2"
23"punctuation.delimiter" = "grey2"
@@ -32,7 +32,6 @@
32"attribute" = "aqua"
32"attribute" = "aqua"
33"constructor" = "yellow"
33"constructor" = "yellow"
34"module" = "blue"
34"module" = "blue"
35−"property" = "fg"
36"special" = "orange"
35"special" = "orange"
37
36
38"ui.background" = { bg = "bg0" }
37"ui.background" = { bg = "bg0" }
runtime/themes/gruvbox.toml+3−4
@@ -9,8 +9,7 @@
9"punctuation.delimiter" = "orange1"
9"punctuation.delimiter" = "orange1"
10"operator" = "purple1"
10"operator" = "purple1"
11"special" = "purple0"
11"special" = "purple0"
12−"property" = "blue1"
13−"variable.property" = "blue1"
12+"variable.other.member" = "blue1"
14"variable" = "fg1"
13"variable" = "fg1"
15"variable.builtin" = "orange1"
14"variable.builtin" = "orange1"
16"variable.parameter" = "fg2"
15"variable.parameter" = "fg2"
@@ -24,8 +23,8 @@
24"constant" = { fg = "purple1" }
23"constant" = { fg = "purple1" }
25"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
24"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
26"string" = "green1"
25"string" = "green1"
27−"number" = "purple1"
26+"constant.numeric" = "purple1"
28−"escape" = { fg = "fg2", modifiers = ["bold"] }
27+"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] }
29"label" = "aqua1"
28"label" = "aqua1"
30"module" = "aqua1"
29"module" = "aqua1"
31
30
runtime/themes/ingrid.toml+3−3
@@ -8,7 +8,7 @@
8"punctuation.delimiter" = "#C97270"
8"punctuation.delimiter" = "#C97270"
9"operator" = { fg = "#D74E50", modifiers = ["bold"] }
9"operator" = { fg = "#D74E50", modifiers = ["bold"] }
10"special" = "#D68482"
10"special" = "#D68482"
11−"property" = "#89BEB7"
11+"variable.other.member" = "#89BEB7"
12"variable" = "#A6B6CE"
12"variable" = "#A6B6CE"
13"variable.parameter" = "#89BEB7"
13"variable.parameter" = "#89BEB7"
14"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
14"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
@@ -22,8 +22,8 @@
22"constant" = "#D4A520"
22"constant" = "#D4A520"
23"constant.builtin" = "#D4A520"
23"constant.builtin" = "#D4A520"
24"string" = "#D74E50"
24"string" = "#D74E50"
25−"number" = "#D74E50"
25+"constant.numeric" = "#D74E50"
26−"escape" = { fg = "#D74E50", modifiers = ["bold"] }
26+"constant.character.escape" = { fg = "#D74E50", modifiers = ["bold"] }
27"label" = "#D68482"
27"label" = "#D68482"
28
28
29"module" = "#839A53"
29"module" = "#839A53"
runtime/themes/monokai.toml+4−3
@@ -7,7 +7,7 @@
7"type.builtin" = { fg = "#66D9EF" }
7"type.builtin" = { fg = "#66D9EF" }
8"type.enum.variant" = { fg = "text" }
8"type.enum.variant" = { fg = "text" }
9"constructor" = { fg = "text" }
9"constructor" = { fg = "text" }
10−"property" = { fg = "variable" }
10+"variable.other.member" = { fg = "variable" }
11
11
12"keyword" = { fg = "keyword" }
12"keyword" = { fg = "keyword" }
13"keyword.directive" = { fg = "keyword" }
13"keyword.directive" = { fg = "keyword" }
@@ -34,9 +34,10 @@
34"comment" = { fg = "#88846F" }
34"comment" = { fg = "#88846F" }
35
35
36"string" = { fg = "#e6db74" }
36"string" = { fg = "#e6db74" }
37+"constant.character" = { fg = "#e6db74" }
37"string.regexp" = { fg = "regex" }
38"string.regexp" = { fg = "regex" }
38−"number" = { fg = "#ae81ff" }
39+"constant.numeric" = { fg = "#ae81ff" }
39−"escape" = { fg = "#ae81ff" }
40+"constant.character.escape" = { fg = "#ae81ff" }
40
41
41"ui.background" = { fg = "text", bg = "background" }
42"ui.background" = { fg = "text", bg = "background" }
42
43
runtime/themes/nord.toml+61−43
@@ -1,84 +1,102 @@
1# Author : RayGervais<raygervais@hotmail.ca>
1# Author : RayGervais<raygervais@hotmail.ca>
2
2
3−# "ui.linenr.selected" = { fg = "#d8dee9" }
3+"ui.linenr.selected" = { fg = "nord4" }
4−"ui.text.focus" = { fg = "#88c0d0", modifiers= ["bold"] }
4+"ui.text.focus" = { fg = "nord8", modifiers= ["bold"] }
5−# "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
5+"ui.menu.selected" = { fg = "nord8", bg = "nord2" }
6
6
7−# "info" = "#b48ead"
7+"info" = "nord8"
8−# "hint" = "#a3be8c"
8+"hint" = "nord8"
9
9
10# Polar Night
10# Polar Night
11# nord0 - background color
11# nord0 - background color
12−"ui.background" = { bg = "#2e3440" }
12+"ui.background" = { bg = "nord0" }
13−"ui.statusline.inactive" = { fg = "#d8dEE9", bg = "#2e3440" }
13+"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" }
14
14
15
15
16# nord1 - status bars, panels, modals, autocompletion
16# nord1 - status bars, panels, modals, autocompletion
17−"ui.statusline" = { fg = "#88c0d0", bg = "#3b4252" }
17+"ui.statusline" = { fg = "nord8", bg = "nord1" }
18"ui.popup" = { bg = "#232d38" }
18"ui.popup" = { bg = "#232d38" }
19"ui.window" = { bg = "#232d38" }
19"ui.window" = { bg = "#232d38" }
20−"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
20+"ui.help" = { bg = "#232d38", fg = "nord4" }
21
21
22# nord2 - active line, highlighting
22# nord2 - active line, highlighting
23−"ui.selection" = { bg = "#434c5e" }
23+"ui.selection" = { bg = "nord2" }
24−"ui.cursor.match" = { bg = "434c5e" }
24+"ui.cursor.match" = { bg = "nord2" }
25
25
26−# nord3 - comments
26+# nord3 - comments, nord3 based lighter color
27+# relative: https://github.com/arcticicestudio/nord/issues/94
27−"comment" = "#616E88"
28+"comment" = "gray"
28−"ui.linenr" = { fg = "#616E88" }
29+"ui.linenr" = { fg = "gray" }
29
30
30# Snow Storm
31# Snow Storm
31# nord4 - cursor, variables, constants, attributes, fields
32# nord4 - cursor, variables, constants, attributes, fields
32−"ui.cursor.primary" = { fg = "#d8dee9", modifiers = ["reversed"] }
33+"ui.cursor.primary" = { fg = "nord4", modifiers = ["reversed"] }
33−"attribute" = "#d8dee9"
34+"attribute" = "nord4"
34−"variable" = "#d8dee9"
35+"variable" = "nord4"
35−"constant" = "#d8dee9"
36+"constant" = "nord4"
36−"variable.builtin" = "#d8dee9"
37+"variable.builtin" = "nord4"
37−"constant.builtin" = "#d8dee9"
38+"constant.builtin" = "nord4"
38−"namespace" = "#d8dee9"
39+"namespace" = "nord4"
39
40
40# nord5 - suble UI text
41# nord5 - suble UI text
41
42
42# nord6 - base text, punctuation
43# nord6 - base text, punctuation
43−"ui.text" = { fg = "#eceff4" }
44+"ui.text" = { fg = "nord6" }
44−"punctuation" = "#eceff4"
45+"punctuation" = "nord6"
45
46
46# Frost
47# Frost
47# nord7 - classes, types, primiatives
48# nord7 - classes, types, primiatives
48−"type" = "#8fbcbb"
49+"type" = "nord7"
49−"type.builtin" = { fg = "#8fbcbb"}
50+"type.builtin" = { fg = "nord7"}
50−"label" = "#8fbcbb"
51+"label" = "nord7"
51
52
52# nord8 - declaration, methods, routines
53# nord8 - declaration, methods, routines
53−"constructor" = "#88c0d0"
54+"constructor" = "nord8"
54−"function" = "#88c0d0"
55+"function" = "nord8"
55−"function.macro" = { fg = "#88c0d0" }
56+"function.macro" = { fg = "nord8" }
56−"function.builtin" = { fg = "#88c0d0" }
57+"function.builtin" = { fg = "nord8" }
57
58
58# nord9 - operator, tags, units, punctuations
59# nord9 - operator, tags, units, punctuations
59−"punctuation.delimiter" = "#81a1c1"
60+"punctuation.delimiter" = "nord9"
60−"operator" = { fg = "#81a1c1" }
61+"operator" = { fg = "nord9" }
61−"property" = "#81a1c1"
62+"variable.other.member" = "nord9"
62
63
63# nord10 - keywords, special
64# nord10 - keywords, special
64−"keyword" = { fg = "#5e81ac" }
65+"keyword" = { fg = "nord10" }
65−"keyword.directive" = "#5e81ac"
66+"keyword.directive" = "nord10"
66−"variable.parameter" = "#5e81ac"
67+"variable.parameter" = "nord10"
67
68
68# Aurora
69# Aurora
69# nord11 - error
70# nord11 - error
70−"error" = "#bf616a"
71+"error" = "nord11"
71
72
72# nord12 - annotations, decorators
73# nord12 - annotations, decorators
73−"special" = "#d08770"
74+"special" = "nord12"
74−"module" = "#d08770"
75+"module" = "nord12"
75
76
76# nord13 - warnings, escape characters, regex
77# nord13 - warnings, escape characters, regex
77−"warning" = "#ebcb8b"
78+"warning" = "nord13"
78−"escape" = { fg = "#ebcb8b" }
79+"constant.character.escape" = { fg = "nord13" }
79
80
80# nord14 - strings
81# nord14 - strings
81−"string" = "#a3be8c"
82+"string" = "nord14"
82
83
83# nord15 - integer, floating point
84# nord15 - integer, floating point
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"
84−"number" = "#b48ead"
101+nord15 = "#b48ead"
102+gray = "#616e88"
runtime/themes/onedark.toml+60−43
@@ -1,54 +1,71 @@
1# Author : Gokul Soumya <gokulps15@gmail.com>
1# Author : Gokul Soumya <gokulps15@gmail.com>
2
2
3−"attribute" = { fg = "#E5C07B" }
3+"attribute" = { fg = "yellow" }
4−"comment" = { fg = "#5C6370", modifiers = ['italic'] }
4+"comment" = { fg = "light-gray", modifiers = ["italic"] }
5−"constant" = { fg = "#56B6C2" }
5+"constant" = { fg = "cyan" }
6−"constant.builtin" = { fg = "#61AFEF" }
6+"constant.builtin" = { fg = "blue" }
7−"constructor" = { fg = "#61AFEF" }
7+"constructor" = { fg = "blue" }
8−"escape" = { fg = "#D19A66" }
8+"escape" = { fg = "gold" }
9−"function" = { fg = "#61AFEF" }
9+"function" = { fg = "blue" }
10−"function.builtin" = { fg = "#61AFEF" }
10+"function.builtin" = { fg = "blue" }
11−"function.macro" = { fg = "#C678DD" }
11+"function.macro" = { fg = "purple" }
12+"keyword" = { fg = "red" }
12−"keyword" = { fg = "#E06C75" }
13+"keyword.control" = { fg = "purple" }
13−"keyword.directive" = { fg = "#C678DD" }
14+"keyword.directive" = { fg = "purple" }
14−"label" = { fg = "#C678DD" }
15+"label" = { fg = "purple" }
15−"namespace" = { fg = "#61AFEF" }
16+"namespace" = { fg = "blue" }
16−"number" = { fg = "#D19A66" }
17+"number" = { fg = "gold" }
17−"operator" = { fg = "#C678DD" }
18+"operator" = { fg = "purple" }
18−"property" = { fg = "#E06C75" }
19+"property" = { fg = "red" }
19−"special" = { fg = "#61AFEF" }
20+"special" = { fg = "blue" }
20−"string" = { fg = "#98C379" }
21+"string" = { fg = "green" }
21−"type" = { fg = "#E5C07B" }
22+"type" = { fg = "yellow" }
22−"type.builtin" = { fg = "#E5C07B" }
23+"type.builtin" = { fg = "yellow" }
23−"variable" = { fg = "#61AFEF" }
24+# "variable" = { fg = "blue" }
24−"variable.builtin" = { fg = "#61AFEF" }
25+"variable.builtin" = { fg = "blue" }
25−"variable.parameter" = { fg = "#E06C75" }
26+"variable.parameter" = { fg = "red" }
26
27
27diagnostic = { modifiers = ["underlined"] }
28diagnostic = { modifiers = ["underlined"] }
28−"info" = { fg = "#61afef", modifiers = ['bold'] }
29+"info" = { fg = "blue", modifiers = ["bold"] }
29−"hint" = { fg = "#98c379", modifiers = ['bold'] }
30+"hint" = { fg = "green", modifiers = ["bold"] }
30−"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
31+"warning" = { fg = "yellow", modifiers = ["bold"] }
31−"error" = { fg = "#e06c75", modifiers = ['bold'] }
32+"error" = { fg = "red", modifiers = ["bold"] }
32
33
33−"ui.background" = { bg = "#282C34" }
34+"ui.background" = { bg = "black" }
34
35
35−"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] }
36+"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
36−"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] }
37+"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }
37−"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']}
38+"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]}
38
39
39−"ui.selection" = { bg = "#5C6370" }
40+"ui.selection" = { bg = "light-gray" }
40−"ui.selection.primary" = { bg = "#3E4452" }
41+"ui.selection.primary" = { bg = "gray" }
41
42
42−"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
43+"ui.linenr" = { fg = "linenr", modifiers = ["dim"] }
43−"ui.linenr.selected" = { fg = "#ABB2BF" }
44+"ui.linenr.selected" = { fg = "white" }
44
45
45−"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
46+"ui.statusline" = { fg = "white", bg = "light-black" }
46−"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" }
47+"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" }
47
48
48−"ui.text" = { fg = "#ABB2BF" }
49+"ui.text" = { fg = "white" }
49−"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
50+"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] }
50
51
51−"ui.help" = { bg = "#3E4452" }
52+"ui.help" = { bg = "gray" }
52−"ui.popup" = { bg = "#3E4452" }
53+"ui.popup" = { bg = "gray" }
53−"ui.window" = { bg = "#3E4452" }
54+"ui.window" = { bg = "gray" }
54−"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
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"
6"punctuation.delimiter" = "lavender"
6"punctuation.delimiter" = "lavender"
7operator = "lilac"
7operator = "lilac"
8special = "honey"
8special = "honey"
9−property = "white"
9+variable.other.member = "white"
10variable = "lavender"
10variable = "lavender"
11# variable = "almond" # TODO: metavariables only
11# variable = "almond" # TODO: metavariables only
12# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }
12# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }