on master help sign in
Merge branch 'master' into cursor-shape-new
commitd4fb1d06333315a3c6e9dc7f0ad8055d91551e3a authorGokul Soumya <gokulps15@gmail.com> dateSat 18 Dec 2021, 08:33 (5 years ago) parent016640f3ef115d
94 files changed+3864−919
.cargo/config+2 .github/ISSUE_TEMPLATE/bug_report.md+1−1 .github/workflows/build.yml+57−9 .github/workflows/release.yml+1−1 .gitmodules+12 Cargo.lock+32−23 Cargo.toml+1 README.md+3−17 base16_theme.toml+38 book/src/SUMMARY.md+3−1 book/src/commands.md+5 book/src/configuration.md+1 book/src/generated/lang-support.md+42 book/src/generated/typable-cmd.md+43 book/src/guides/adding_languages.md+1−1 book/src/install.md+10−3 book/src/keymap.md+46−37 book/src/lang-support.md+10 book/src/remapping.md+6−4 book/src/themes.md+17−2 book/src/usage.md+2 docs/CONTRIBUTING.md+37 flake.lock+19−35 flake.nix+1−5 helix-core/Cargo.toml+2 helix-core/src/auto_pairs.rs+346−75 helix-core/src/diagnostic.rs+2−2 helix-core/src/increment/date_time.rs+490 helix-core/src/increment/mod.rs+8 helix-core/src/numbers.rs → helix-core/src/increment/number.rs+19−11 helix-core/src/lib.rs+3−2 helix-core/src/register.rs+11−3 helix-core/src/selection.rs+3−3 helix-core/src/shellwords.rs+164 helix-core/src/surround.rs+83−65 helix-core/src/syntax.rs+4−3 helix-core/src/textobject.rs+6−4 helix-core/src/transaction.rs+3−13 helix-lsp/src/lib.rs+4−1 helix-syntax/languages/tree-sitter-markdown+1 helix-syntax/languages/tree-sitter-wgsl+1 helix-term/Cargo.toml+1 helix-term/src/application.rs+20−10 helix-term/src/commands.rs+546−214 helix-term/src/compositor.rs+22−4 helix-term/src/keymap.rs+32−23 helix-term/src/lib.rs+11 helix-term/src/ui/completion.rs+3−3 helix-term/src/ui/editor.rs+58−82 helix-term/src/ui/markdown.rs+1−5 helix-term/src/ui/menu.rs+2−2 helix-term/src/ui/mod.rs+1 helix-term/src/ui/picker.rs+7−7 helix-term/src/ui/popup.rs+24−5 helix-term/src/ui/prompt.rs+2−2 helix-tui/src/buffer.rs+1−10 helix-tui/src/text.rs+2−14 helix-tui/src/widgets/table.rs+1−10 helix-view/src/document.rs+6 helix-view/src/editor.rs+102−85 helix-view/src/graphics.rs+1−12 helix-view/src/gutter.rs+96 helix-view/src/input.rs+34−10 helix-view/src/keyboard.rs+1−4 helix-view/src/lib.rs+13−2 helix-view/src/theme.rs+45−6 helix-view/src/view.rs+26−4 languages.toml+35−1 runtime/queries/haskell/highlights.scm+1−1 runtime/queries/julia/locals.scm+11−11 runtime/queries/latex/highlights.scm+14−14 runtime/queries/ledger/highlights.scm+1−1 runtime/queries/llvm/highlights.scm+14 runtime/queries/markdown/highlights.scm+35 runtime/queries/markdown/injections.scm+8 runtime/queries/ocaml/highlights.scm+1−1 runtime/queries/perl/indents.toml+17 runtime/queries/svelte/highlights.scm+1−1 runtime/queries/wgsl/highlights.scm+102 runtime/themes/base16_default_dark.toml+19−18 runtime/themes/base16_default_light.toml+60 runtime/themes/base16_terminal.toml+39 runtime/themes/monokai_pro.toml+102 runtime/themes/monokai_pro_machine.toml+102 runtime/themes/monokai_pro_octagon.toml+102 runtime/themes/monokai_pro_ristretto.toml+102 runtime/themes/monokai_pro_spectrum.toml+102 runtime/themes/rose_pine.toml+1 runtime/themes/rose_pine_dawn.toml+63 runtime/themes/solarized_dark.toml+16−14 runtime/themes/solarized_light.toml+24−22 theme.toml+6 xtask/Cargo.toml+11 xtask/src/main.rs+277
.cargo/confignew file+2
@@ -0,0 +1,2 @@
1+[alias]
2+xtask = "run --package xtask --"
.github/ISSUE_TEMPLATE/bug_report.md+1−1
@@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
1717### Environment
1818
1919- Platform: <!-- macOS / Windows / Linux -->
20−- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
20+- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
2121
2222<details><summary>~/.cache/helix/helix.log</summary>
2323
.github/workflows/build.yml+57−9
@@ -25,19 +25,19 @@ jobs:
2525 override: true
2626
2727 - name: Cache cargo registry
28− uses: actions/cache@v2.1.6
28+ uses: actions/cache@v2.1.7
2929 with:
3030 path: ~/.cargo/registry
3131 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
3232
3333 - name: Cache cargo index
34− uses: actions/cache@v2.1.6
34+ uses: actions/cache@v2.1.7
3535 with:
3636 path: ~/.cargo/git
3737 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
3838
3939 - name: Cache cargo target dir
40− uses: actions/cache@v2.1.6
40+ uses: actions/cache@v2.1.7
4141 with:
4242 path: target
4343 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
6464 override: true
6565
6666 - name: Cache cargo registry
67− uses: actions/cache@v2.1.6
67+ uses: actions/cache@v2.1.7
6868 with:
6969 path: ~/.cargo/registry
7070 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
7171
7272 - name: Cache cargo index
73− uses: actions/cache@v2.1.6
73+ uses: actions/cache@v2.1.7
7474 with:
7575 path: ~/.cargo/git
7676 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
7777
7878 - name: Cache cargo target dir
79− uses: actions/cache@v2.1.6
79+ uses: actions/cache@v2.1.7
8080 with:
8181 path: target
8282 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -109,19 +109,19 @@ jobs:
109109 components: rustfmt, clippy
110110
111111 - name: Cache cargo registry
112− uses: actions/cache@v2.1.6
112+ uses: actions/cache@v2.1.7
113113 with:
114114 path: ~/.cargo/registry
115115 key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
116116
117117 - name: Cache cargo index
118− uses: actions/cache@v2.1.6
118+ uses: actions/cache@v2.1.7
119119 with:
120120 path: ~/.cargo/git
121121 key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
122122
123123 - name: Cache cargo target dir
124− uses: actions/cache@v2.1.6
124+ uses: actions/cache@v2.1.7
125125 with:
126126 path: target
127127 key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -137,3 +137,51 @@ jobs:
137137 with:
138138 command: clippy
139139 args: -- -D warnings
140+
141+ docs:
142+ name: Docs
143+ runs-on: ubuntu-latest
144+ steps:
145+ - name: Checkout sources
146+ uses: actions/checkout@v2
147+ with:
148+ submodules: true
149+
150+ - name: Install stable toolchain
151+ uses: actions-rs/toolchain@v1
152+ with:
153+ profile: minimal
154+ toolchain: stable
155+ override: true
156+
157+ - name: Cache cargo registry
158+ uses: actions/cache@v2.1.6
159+ with:
160+ path: ~/.cargo/registry
161+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
162+
163+ - name: Cache cargo index
164+ uses: actions/cache@v2.1.6
165+ with:
166+ path: ~/.cargo/git
167+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
168+
169+ - name: Cache cargo target dir
170+ uses: actions/cache@v2.1.6
171+ with:
172+ path: target
173+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
174+
175+ - name: Generate docs
176+ uses: actions-rs/cargo@v1
177+ with:
178+ command: xtask
179+ args: docgen
180+
181+ - name: Check uncommitted documentation changes
182+ run: |
183+ git diff
184+ git diff-files --quiet \
185+ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
186+ && exit 1)
187+
.github/workflows/release.yml+1−1
@@ -102,7 +102,7 @@ jobs:
102102 fi
103103 cp -r runtime dist
104104
105− - uses: actions/upload-artifact@v2.2.4
105+ - uses: actions/upload-artifact@v2.3.0
106106 with:
107107 name: bins-${{ matrix.build }}
108108 path: dist
.gitmodules+12
@@ -142,3 +142,15 @@
142142 path = helix-syntax/languages/tree-sitter-perl
143143 url = https://github.com/ganezdragon/tree-sitter-perl
144144 shallow = true
145+[submodule "helix-syntax/languages/tree-sitter-wgsl"]
146+ path = helix-syntax/languages/tree-sitter-wgsl
147+ url = https://github.com/szebniok/tree-sitter-wgsl
148+ shallow = true
149+[submodule "helix-syntax/tree-sitter-llvm"]
150+ path = helix-syntax/languages/tree-sitter-llvm
151+ url = https://github.com/benwilliamgraham/tree-sitter-llvm
152+ shallow = true
153+[submodule "helix-syntax/languages/tree-sitter-markdown"]
154+ path = helix-syntax/languages/tree-sitter-markdown
155+ url = https://github.com/MDeiml/tree-sitter-markdown
156+ shallow = true
Cargo.lock+32−23
@@ -13,9 +13,9 @@ dependencies = [
1313
1414[[package]]
1515name = "anyhow"
16−version = "1.0.48"
16+version = "1.0.51"
1717source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
18+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
1919
2020[[package]]
2121name = "arc-swap"
@@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
184184
185185[[package]]
186186name = "encoding_rs"
187−version = "0.8.29"
187+version = "0.8.30"
188188source = "registry+https://github.com/rust-lang/crates.io-index"
189−checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
189+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
190190dependencies = [
191191 "cfg-if",
192192]
@@ -258,15 +258,15 @@ dependencies = [
258258
259259[[package]]
260260name = "futures-core"
261−version = "0.3.17"
261+version = "0.3.18"
262262source = "registry+https://github.com/rust-lang/crates.io-index"
263−checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
263+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
264264
265265[[package]]
266266name = "futures-executor"
267−version = "0.3.17"
267+version = "0.3.18"
268268source = "registry+https://github.com/rust-lang/crates.io-index"
269−checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
269+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
270270dependencies = [
271271 "futures-core",
272272 "futures-task",
@@ -275,17 +275,16 @@ dependencies = [
275275
276276[[package]]
277277name = "futures-task"
278−version = "0.3.17"
278+version = "0.3.18"
279279source = "registry+https://github.com/rust-lang/crates.io-index"
280−checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
280+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
281281
282282[[package]]
283283name = "futures-util"
284−version = "0.3.17"
284+version = "0.3.18"
285285source = "registry+https://github.com/rust-lang/crates.io-index"
286−checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
286+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
287287dependencies = [
288− "autocfg",
289288 "futures-core",
290289 "futures-task",
291290 "pin-project-lite",
@@ -370,6 +369,7 @@ name = "helix-core"
370369version = "0.5.0"
371370dependencies = [
372371 "arc-swap",
372+ "chrono",
373373 "etcetera",
374374 "helix-syntax",
375375 "log",
@@ -535,9 +535,9 @@ dependencies = [
535535
536536[[package]]
537537name = "itoa"
538−version = "0.4.8"
538+version = "1.0.1"
539539source = "registry+https://github.com/rust-lang/crates.io-index"
540−checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
540+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
541541
542542[[package]]
543543name = "jsonrpc-core"
@@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
877877
878878[[package]]
879879name = "serde"
880−version = "1.0.130"
880+version = "1.0.131"
881881source = "registry+https://github.com/rust-lang/crates.io-index"
882−checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
882+checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
883883dependencies = [
884884 "serde_derive",
885885]
886886
887887[[package]]
888888name = "serde_derive"
889−version = "1.0.130"
889+version = "1.0.131"
890890source = "registry+https://github.com/rust-lang/crates.io-index"
891−checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
891+checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
892892dependencies = [
893893 "proc-macro2",
894894 "quote",
@@ -897,9 +897,9 @@ dependencies = [
897897
898898[[package]]
899899name = "serde_json"
900−version = "1.0.71"
900+version = "1.0.73"
901901source = "registry+https://github.com/rust-lang/crates.io-index"
902−checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
902+checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
903903dependencies = [
904904 "itoa",
905905 "ryu",
@@ -919,9 +919,9 @@ dependencies = [
919919
920920[[package]]
921921name = "signal-hook"
922−version = "0.3.10"
922+version = "0.3.12"
923923source = "registry+https://github.com/rust-lang/crates.io-index"
924−checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
924+checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
925925dependencies = [
926926 "libc",
927927 "signal-hook-registry",
@@ -1259,3 +1259,12 @@ name = "winapi-x86_64-pc-windows-gnu"
12591259version = "0.4.0"
12601260source = "registry+https://github.com/rust-lang/crates.io-index"
12611261checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1262+
1263+[[package]]
1264+name = "xtask"
1265+version = "0.5.0"
1266+dependencies = [
1267+ "helix-core",
1268+ "helix-term",
1269+ "toml",
1270+]
Cargo.toml+1
@@ -6,6 +6,7 @@ members = [
66 "helix-tui",
77 "helix-syntax",
88 "helix-lsp",
9+ "xtask",
910]
1011
1112# Build helix-syntax in release mode to make the code path faster in development.
README.md+3−17
@@ -44,8 +44,8 @@ cargo install --path helix-term
4444This will install the `hx` binary to `$HOME/.cargo/bin`.
4545
4646Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
47−config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
48−via the `HELIX_RUNTIME` environment variable.
47+config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
48+This location can be overriden via the `HELIX_RUNTIME` environment variable.
4949
5050Packages already solve this for you by wrapping the `hx` binary with a wrapper
5151that sets the variable to the install dir.
@@ -65,21 +65,7 @@ brew install helix
6565
6666# Contributing
6767
68−Contributors are very welcome! **No contribution is too small and all contributions are valued.**
69−
70−Some suggestions to get started:
71−
72−- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
73−- Help with packaging on various distributions needed!
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!")`)
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
78− it and defining syntax highlight queries for it is straight forward and
79− doesn't require much knowledge of the internals.
80−
81−We provide an [architecture.md](./docs/architecture.md) that should give you
82−a good overview of the internals.
68+Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
8369
8470# Getting help
8571
base16_theme.tomlnew file+38
@@ -0,0 +1,38 @@
1+# Author: NNB <nnbnh@protonmail.com>
2+
3+"ui.menu" = "black"
4+"ui.menu.selected" = { modifiers = ["reversed"] }
5+"ui.linenr" = { fg = "gray", bg = "black" }
6+"ui.popup" = { modifiers = ["reversed"] }
7+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
8+"ui.selection" = { fg = "black", bg = "blue" }
9+"ui.selection.primary" = { fg = "white", bg = "blue" }
10+"comment" = { fg = "gray" }
11+"ui.statusline" = { fg = "black", bg = "white" }
12+"ui.statusline.inactive" = { fg = "gray", bg = "white" }
13+"ui.help" = { modifiers = ["reversed"] }
14+"ui.cursor" = { modifiers = ["reversed"] }
15+"variable" = "red"
16+"constant.numeric" = "yellow"
17+"constant" = "yellow"
18+"attributes" = "yellow"
19+"type" = "yellow"
20+"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] }
21+"string" = "green"
22+"variable.other.member" = "green"
23+"constant.character.escape" = "cyan"
24+"function" = "blue"
25+"constructor" = "blue"
26+"special" = "blue"
27+"keyword" = "magenta"
28+"label" = "magenta"
29+"namespace" = "magenta"
30+"ui.help" = { fg = "white", bg = "black" }
31+
32+"diagnostic" = { modifiers = ["underlined"] }
33+"ui.gutter" = { bg = "black" }
34+"info" = "blue"
35+"hint" = "gray"
36+"debug" = "gray"
37+"warning" = "yellow"
38+"error" = "red"
book/src/SUMMARY.md+3−1
@@ -2,10 +2,12 @@
22
33- [Installation](./install.md)
44- [Usage](./usage.md)
5+ - [Keymap](./keymap.md)
6+ - [Commands](./commands.md)
7+ - [Language Support](./lang-support.md)
58- [Migrating from Vim](./from-vim.md)
69- [Configuration](./configuration.md)
710 - [Themes](./themes.md)
8− - [Keymap](./keymap.md)
911 - [Key Remapping](./remapping.md)
1012 - [Hooks](./hooks.md)
1113 - [Languages](./languages.md)
book/src/commands.mdnew file+5
@@ -0,0 +1,5 @@
1+# Commands
2+
3+Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
4+
5+{{#include ./generated/typable-cmd.md}}
book/src/configuration.md+1
@@ -41,6 +41,7 @@ hidden = false
4141| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
4242| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
4343| `auto-info` | Whether to display infoboxes | `true` |
44+| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
4445
4546### `[editor.cursor-shape]` Section
4647
book/src/generated/lang-support.mdnew file+42
@@ -0,0 +1,42 @@
1+| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
2+| --- | --- | --- | --- | --- |
3+| bash | ✓ | | | `bash-language-server` |
4+| c | ✓ | | | `clangd` |
5+| c-sharp | ✓ | | | |
6+| cmake | ✓ | | | `cmake-language-server` |
7+| cpp | ✓ | | | `clangd` |
8+| css | ✓ | | | |
9+| elixir | ✓ | | | `elixir-ls` |
10+| glsl | ✓ | | ✓ | |
11+| go | ✓ | ✓ | ✓ | `gopls` |
12+| html | ✓ | | | |
13+| java | ✓ | | | |
14+| javascript | ✓ | | ✓ | |
15+| json | ✓ | | ✓ | |
16+| julia | ✓ | | | `julia` |
17+| latex | ✓ | | | |
18+| ledger | ✓ | | | |
19+| llvm | ✓ | | | |
20+| lua | ✓ | | ✓ | |
21+| markdown | ✓ | | | |
22+| mint | | | | `mint` |
23+| nix | ✓ | | ✓ | `rnix-lsp` |
24+| ocaml | ✓ | | ✓ | |
25+| ocaml-interface | ✓ | | | |
26+| perl | ✓ | ✓ | ✓ | |
27+| php | ✓ | | ✓ | |
28+| prolog | | | | `swipl` |
29+| protobuf | ✓ | | ✓ | |
30+| python | ✓ | ✓ | ✓ | `pylsp` |
31+| racket | | | | `racket` |
32+| ruby | ✓ | | | `solargraph` |
33+| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
34+| svelte | ✓ | | ✓ | `svelteserver` |
35+| toml | ✓ | | | |
36+| tsq | ✓ | | | |
37+| tsx | ✓ | | | `typescript-language-server` |
38+| typescript | ✓ | | ✓ | `typescript-language-server` |
39+| vue | ✓ | | | |
40+| wgsl | ✓ | | | |
41+| yaml | ✓ | | ✓ | |
42+| zig | ✓ | | ✓ | `zls` |
book/src/generated/typable-cmd.mdnew file+43
@@ -0,0 +1,43 @@
1+| Name | Description |
2+| --- | --- |
3+| `:quit`, `:q` | Close the current view. |
4+| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
5+| `:open`, `:o` | Open a file from disk into the current view. |
6+| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
7+| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
8+| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
9+| `:new`, `:n` | Create a new scratch buffer. |
10+| `:format`, `:fmt` | Format the file using the LSP formatter. |
11+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
12+| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
13+| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
14+| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
15+| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
16+| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
17+| `:write-all`, `:wa` | Write changes from all views to disk. |
18+| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
19+| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
20+| `:quit-all`, `:qa` | Close all views. |
21+| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
22+| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
23+| `:theme` | Change the editor theme. |
24+| `:clipboard-yank` | Yank main selection into system clipboard. |
25+| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
26+| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
27+| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
28+| `:clipboard-paste-after` | Paste system clipboard after selections. |
29+| `:clipboard-paste-before` | Paste system clipboard before selections. |
30+| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
31+| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
32+| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
33+| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
34+| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
35+| `:change-current-directory`, `:cd` | Change the current working directory. |
36+| `:show-directory`, `:pwd` | Show the current working directory. |
37+| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
38+| `:reload` | Discard changes and reload from the source file. |
39+| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
40+| `:vsplit`, `:vs` | Open the file in a vertical split. |
41+| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
42+| `:tutor` | Open the tutorial. |
43+| `:goto`, `:g` | Go to line number. |
book/src/guides/adding_languages.md+1−1
@@ -2,7 +2,7 @@
22
33## Submodules
44
5−To add a new langauge, you should first add a tree-sitter submodule. To do this,
5+To add a new language, you should first add a tree-sitter submodule. To do this,
66you can run the command
77```sh
88git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
book/src/install.md+10−3
@@ -25,9 +25,16 @@ shell for working on Helix.
2525
2626Releases are available in the `community` repository.
2727
28−Packages are also available on AUR:
29−- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
30−- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
28+A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
29+
30+### Fedora Linux
31+
32+You can install the COPR package for Helix via
33+
34+```
35+sudo dnf copr enable varlad/helix
36+sudo dnf install helix
37+```
3138
3239## Build from source
3340
book/src/keymap.md+46−37
@@ -34,6 +34,7 @@
3434| `Ctrl-d` | Move half page down | `half_page_down` |
3535| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
3636| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
37+| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
3738| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
3839| `g` | Enter [goto mode](#goto-mode) | N/A |
3940| `m` | Enter [match mode](#match-mode) | N/A |
@@ -45,44 +46,48 @@
4546
4647### Changes
4748
48−| Key | Description | Command |
49−| ----- | ----------- | ------- |
50−| `r` | Replace with a character | `replace` |
51−| `R` | Replace with yanked text | `replace_with_yanked` |
52−| `~` | Switch case of the selected text | `switch_case` |
53−| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
54−| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
55−| `i` | Insert before selection | `insert_mode` |
56−| `a` | Insert after selection (append) | `append_mode` |
57−| `I` | Insert at the start of the line | `prepend_to_line` |
58−| `A` | Insert at the end of the line | `append_to_line` |
59−| `o` | Open new line below selection | `open_below` |
60−| `O` | Open new line above selection | `open_above` |
61−| `.` | Repeat last change | N/A |
62−| `u` | Undo change | `undo` |
63−| `U` | Redo change | `redo` |
64−| `Alt-u` | Move backward in history | `earlier` |
65−| `Alt-U` | Move forward in history | `later` |
66−| `y` | Yank selection | `yank` |
67−| `p` | Paste after selection | `paste_after` |
68−| `P` | Paste before selection | `paste_before` |
69−| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
70−| `>` | Indent selection | `indent` |
71−| `<` | Unindent selection | `unindent` |
72−| `=` | Format selection (**LSP**) | `format_selections` |
73−| `d` | Delete selection | `delete_selection` |
74−| `c` | Change selection (delete and enter insert mode) | `change_selection` |
75−| `Ctrl-a` | Increment object (number) under cursor | `increment` |
76−| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
49+| Key | Description | Command |
50+| ----- | ----------- | ------- |
51+| `r` | Replace with a character | `replace` |
52+| `R` | Replace with yanked text | `replace_with_yanked` |
53+| `~` | Switch case of the selected text | `switch_case` |
54+| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
55+| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
56+| `i` | Insert before selection | `insert_mode` |
57+| `a` | Insert after selection (append) | `append_mode` |
58+| `I` | Insert at the start of the line | `prepend_to_line` |
59+| `A` | Insert at the end of the line | `append_to_line` |
60+| `o` | Open new line below selection | `open_below` |
61+| `O` | Open new line above selection | `open_above` |
62+| `.` | Repeat last change | N/A |
63+| `u` | Undo change | `undo` |
64+| `U` | Redo change | `redo` |
65+| `Alt-u` | Move backward in history | `earlier` |
66+| `Alt-U` | Move forward in history | `later` |
67+| `y` | Yank selection | `yank` |
68+| `p` | Paste after selection | `paste_after` |
69+| `P` | Paste before selection | `paste_before` |
70+| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
71+| `>` | Indent selection | `indent` |
72+| `<` | Unindent selection | `unindent` |
73+| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
74+| `d` | Delete selection | `delete_selection` |
75+| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
76+| `c` | Change selection (delete and enter insert mode) | `change_selection` |
77+| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
78+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
79+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
80+| `q` | Start/stop macro recording to the selected register | `record_macro` |
81+| `Q` | Play back a recorded macro from the selected register | `play_macro` |
7782
7883#### Shell
7984
80−| Key | Description | Command |
81−| ------ | ----------- | ------- |
82−| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
83−| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
84−| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
85−| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
85+| Key | Description | Command |
86+| ------ | ----------- | ------- |
87+| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
88+| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
89+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
90+| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
8691
8792
8893### Selection manipulation
@@ -158,17 +163,19 @@ Jumps to various locations.
158163| ----- | ----------- | ------- |
159164| `g` | Go to the start of the file | `goto_file_start` |
160165| `e` | Go to the end of the file | `goto_last_line` |
166+| `f` | Go to files in the selection | `goto_file` |
161167| `h` | Go to the start of the line | `goto_line_start` |
162168| `l` | Go to the end of the line | `goto_line_end` |
163169| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
164170| `t` | Go to the top of the screen | `goto_window_top` |
165−| `m` | Go to the middle of the screen | `goto_window_middle` |
171+| `c` | Go to the middle of the screen | `goto_window_center` |
166172| `b` | Go to the bottom of the screen | `goto_window_bottom` |
167173| `d` | Go to definition (**LSP**) | `goto_definition` |
168174| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
169175| `r` | Go to references (**LSP**) | `goto_reference` |
170176| `i` | Go to implementation (**LSP**) | `goto_implementation` |
171177| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
178+| `m` | Go to the last modified/alternate file | `goto_last_modified_file` |
172179| `n` | Go to next buffer | `goto_next_buffer` |
173180| `p` | Go to previous buffer | `goto_previous_buffer` |
174181| `.` | Go to last modification in current file | `goto_last_modification` |
@@ -200,6 +207,8 @@ This layer is similar to vim keybindings as kakoune does not support window.
200207| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
201208| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
202209| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
210+| `f` | Go to files in the selection in horizontal splits | `goto_file` |
211+| `F` | Go to files in the selection in vertical splits | `goto_file` |
203212| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
204213| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
205214| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
@@ -315,7 +324,7 @@ Keys to use within prompt, Remapping currently not supported.
315324| `Ctrl-u` | Delete to start of line |
316325| `Ctrl-k` | Delete to end of line |
317326| `backspace`, `Ctrl-h` | Delete previous char |
318−| `delete`, `Ctrl-d` | Delete previous char |
327+| `delete`, `Ctrl-d` | Delete next char |
319328| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
320329| `Ctrl-p`, `Up` | Select previous history |
321330| `Ctrl-n`, `Down` | Select next history |
book/src/lang-support.mdnew file+10
@@ -0,0 +1,10 @@
1+# Language Support
2+
3+For more information like arguments passed to default LSP server,
4+extensions assosciated with a filetype, custom LSP settings, filetype
5+specific indent settings, etc see the default
6+[`languages.toml`][languages.toml] file.
7+
8+{{#include ./generated/lang-support.md}}
9+
10+[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
book/src/remapping.md+6−4
@@ -11,6 +11,8 @@ this:
1111```toml
1212# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
1313[keys.normal]
14+C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
15+C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
1416a = "move_char_left" # Maps the 'a' key to the move_char_left command
1517w = "move_line_up" # Maps the 'w' key move_line_up
1618"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
@@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
2123"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
2224j = { k = "normal_mode" } # Maps `jk` to exit insert mode
2325```
26+> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
2427
2528Control, Shift and Alt modifiers are encoded respectively with the prefixes
2629`C-`, `S-` and `A-`. Special keys are encoded as follows:
@@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
4245| Down | `"down"` |
4346| Home | `"home"` |
4447| End | `"end"` |
45−| Page | `"pageup"` |
46−| Page | `"pagedown"` |
48+| Page Up | `"pageup"` |
49+| Page Down | `"pagedown"` |
4750| Tab | `"tab"` |
48−| Back | `"backtab"` |
4951| Delete | `"del"` |
5052| Insert | `"ins"` |
5153| Null | `"null"` |
@@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
5456Keys can be disabled by binding them to the `no_op` command.
5557
5658Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
57−> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
59+> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.
book/src/themes.md+17−2
@@ -145,11 +145,12 @@ We use a similar set of scopes as
145145 - `conditional` - `if`, `else`
146146 - `repeat` - `for`, `while`, `loop`
147147 - `import` - `import`, `export`
148− - (TODO: return?)
148+ - `return`
149+ - `operator` - `or`, `in`
149150 - `directive` - Preprocessor directives (`#if` in C)
150151 - `function` - `fn`, `func`
151152
152−- `operator` - `||`, `+=`, `>`, `or`
153+- `operator` - `||`, `+=`, `>`
153154
154155- `function`
155156 - `builtin`
@@ -161,6 +162,20 @@ We use a similar set of scopes as
161162
162163- `namespace`
163164
165+- `markup`
166+ - `heading`
167+ - `list`
168+ - `unnumbered`
169+ - `numbered`
170+ - `bold`
171+ - `italic`
172+ - `underline`
173+ - `link`
174+ - `quote`
175+ - `raw`
176+ - `inline`
177+ - `block`
178+
164179#### Interface
165180
166181These scopes are used for theming the editor interface.
book/src/usage.md+2
@@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
2323| `/` | Last search |
2424| `:` | Last executed command |
2525| `"` | Last yanked text |
26+| `_` | Black hole |
2627
2728> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
29+> The black hole register works as a no-op register, meaning no data will be written to / read from it.
2830
2931## Surround
3032
docs/CONTRIBUTING.mdnew file+37
@@ -0,0 +1,37 @@
1+# Contributing
2+
3+Contributors are very welcome! **No contribution is too small and all contributions are valued.**
4+
5+Some suggestions to get started:
6+
7+- You can look at the [good first issue][good-first-issue] label on the issue tracker.
8+- Help with packaging on various distributions needed!
9+- To use print debugging to the [Helix log file][log-file], you must:
10+ * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
11+ * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
12+- If your preferred language is missing, integrating a tree-sitter grammar for
13+ it and defining syntax highlight queries for it is straight forward and
14+ doesn't require much knowledge of the internals.
15+
16+We provide an [architecture.md][architecture.md] that should give you
17+a good overview of the internals.
18+
19+# Auto generated documentation
20+
21+Some parts of [the book][docs] are autogenerated from the code itself,
22+like the list of `:commands` and supported languages. To generate these
23+files, run
24+
25+```shell
26+cargo xtask docgen
27+```
28+
29+inside the project. We use [xtask][xtask] as an ad-hoc task runner and
30+thus do not require any dependencies other than `cargo` (You don't have
31+to `cargo install` anything either).
32+
33+[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
34+[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
35+[architecture.md]: ./architecture.md
36+[docs]: https://docs.helix-editor.com/
37+[xtask]: https://github.com/matklad/cargo-xtask
flake.lock+19−35
@@ -2,11 +2,11 @@
22 "nodes": {
33 "devshell": {
44 "locked": {
5− "lastModified": 1632436039,
6− "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
5+ "lastModified": 1637575296,
6+ "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=",
77 "owner": "numtide",
88 "repo": "devshell",
9− "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
9+ "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618",
1010 "type": "github"
1111 },
1212 "original": {
@@ -17,11 +17,11 @@
1717 },
1818 "flake-utils": {
1919 "locked": {
20− "lastModified": 1623875721,
21− "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
20+ "lastModified": 1637014545,
21+ "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
2222 "owner": "numtide",
2323 "repo": "flake-utils",
24− "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
24+ "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
2525 "type": "github"
2626 },
2727 "original": {
@@ -30,22 +30,6 @@
3030 "type": "github"
3131 }
3232 },
33− "flakeCompat": {
34− "flake": false,
35− "locked": {
36− "lastModified": 1627913399,
37− "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
38− "owner": "edolstra",
39− "repo": "flake-compat",
40− "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
41− "type": "github"
42− },
43− "original": {
44− "owner": "edolstra",
45− "repo": "flake-compat",
46− "type": "github"
47− }
48− },
4933 "nixCargoIntegration": {
5034 "inputs": {
5135 "devshell": "devshell",
@@ -57,11 +41,11 @@
5741 ]
5842 },
5943 "locked": {
60− "lastModified": 1634796585,
61− "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
44+ "lastModified": 1638425401,
45+ "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=",
6246 "owner": "yusdacra",
6347 "repo": "nix-cargo-integration",
64− "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
48+ "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37",
6549 "type": "github"
6650 },
6751 "original": {
@@ -72,11 +56,11 @@
7256 },
7357 "nixpkgs": {
7458 "locked": {
75− "lastModified": 1634782485,
76− "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
59+ "lastModified": 1638376152,
60+ "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=",
7761 "owner": "nixos",
7862 "repo": "nixpkgs",
79− "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
63+ "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f",
8064 "type": "github"
8165 },
8266 "original": {
@@ -88,22 +72,22 @@
8872 },
8973 "nixpkgs_2": {
9074 "locked": {
91− "lastModified": 1628186154,
92− "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
75+ "lastModified": 1637453606,
76+ "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
9377 "owner": "NixOS",
9478 "repo": "nixpkgs",
95− "rev": "06552b72346632b6943c8032e57e702ea12413bf",
79+ "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
9680 "type": "github"
9781 },
9882 "original": {
9983 "owner": "NixOS",
84+ "ref": "nixpkgs-unstable",
10085 "repo": "nixpkgs",
10186 "type": "github"
10287 }
10388 },
10489 "root": {
10590 "inputs": {
106− "flakeCompat": "flakeCompat",
10791 "nixCargoIntegration": "nixCargoIntegration",
10892 "nixpkgs": "nixpkgs",
10993 "rust-overlay": "rust-overlay"
@@ -115,11 +99,11 @@
11599 "nixpkgs": "nixpkgs_2"
116100 },
117101 "locked": {
118− "lastModified": 1634869268,
119− "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
102+ "lastModified": 1638497756,
103+ "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=",
120104 "owner": "oxalica",
121105 "repo": "rust-overlay",
122− "rev": "c02c2d86354327317546501af001886fbb53d374",
106+ "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a",
123107 "type": "github"
124108 },
125109 "original": {
flake.nix+1−5
@@ -9,10 +9,6 @@
99 inputs.nixpkgs.follows = "nixpkgs";
1010 inputs.rustOverlay.follows = "rust-overlay";
1111 };
12− flakeCompat = {
13− url = "github:edolstra/flake-compat";
14− flake = false;
15− };
1612 };
1713
1814 outputs = inputs@{ self, nixCargoIntegration, ... }:
@@ -63,7 +59,7 @@
6359 '';
6460 };
6561 shell = common: prev: {
66− packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
62+ packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
6763 env = prev.env ++ [
6864 { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
6965 { name = "RUST_BACKTRACE"; value = "1"; }
helix-core/Cargo.toml+2
@@ -36,5 +36,7 @@ similar = "2.1"
3636
3737etcetera = "0.3"
3838
39+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
40+
3941[dev-dependencies]
4042quickcheck = { version = "1", default-features = false }
helix-core/src/auto_pairs.rs+346−75
@@ -2,6 +2,7 @@
22//! this module provides the functionality to insert the paired closing character.
33
44use crate::{Range, Rope, Selection, Tendril, Transaction};
5+use log::debug;
56use smallvec::SmallVec;
67
78// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
1516 ('`', '`'),
1617];
1718
18−const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
19+// [TODO] build this dynamically in language config. see #992
20+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
21+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
1922
2023// insert hook:
2124// Fn(doc, selection, char) => Option<Transaction>
@@ -25,40 +28,44 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
2528//
2629// to simplify, maybe return Option<Transaction> and just reimplement the default
2730
28−// TODO: delete implementation where it erases the whole bracket (|) -> |
31+// [TODO]
32+// * delete implementation where it erases the whole bracket (|) -> |
33+// * do not reduce to cursors; use whole selections, and surround with pair
34+// * change to multi character pairs to handle cases like placing the cursor in the
35+// middle of triple quotes, and more exotic pairs like Jinja's {% %}
2936
3037#[must_use]
3138pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
39+ debug!("autopairs hook selection: {:#?}", selection);
40+
41+ let cursors = selection.clone().cursors(doc.slice(..));
42+
3243 for &(open, close) in PAIRS {
3344 if open == ch {
3445 if open == close {
35− return handle_same(doc, selection, open);
46+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
3647 } else {
37− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
48+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
3849 }
3950 }
4051
4152 if close == ch {
4253 // && char_at pos == close
43− return Some(handle_close(doc, selection, open, close));
54+ return Some(handle_close(doc, &cursors, open, close));
4455 }
4556 }
4657
4758 None
4859}
4960
50−// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
51−// for example "&'a mut", or "fn<'a>"
52−
53−fn next_char(doc: &Rope, pos: usize) -> Option<char> {
54− if pos >= doc.len_chars() {
61+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
62+ if pos == 0 {
5563 return None;
5664 }
57− Some(doc.char(pos))
65+
66+ doc.get_char(pos - 1)
5867}
59−// TODO: selections should be extended if range, moved if point.
6068
61−// TODO: if not cursor but selection, wrap on both sides of selection (surround)
6269fn handle_open(
6370 doc: &Rope,
6471 selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
6673 close: char,
6774 close_before: &str,
6875) -> Transaction {
69− let mut ranges = SmallVec::with_capacity(selection.len());
76+ let mut end_ranges = SmallVec::with_capacity(selection.len());
7077
7178 let mut offs = 0;
7279
73− let transaction = Transaction::change_by_selection(doc, selection, |range| {
74− let pos = range.head;
75− let next = next_char(doc, pos);
80+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
81+ let start_head = start_range.head;
7682
77− let head = pos + offs + open.len_utf8();
78− // if selection, retain anchor, if cursor, move over
79− ranges.push(Range::new(
80− if range.is_empty() {
81− head
82− } else {
83− range.anchor + offs
84− },
85− head,
86− ));
83+ let next = doc.get_char(start_head);
84+ let end_head = start_head + offs + open.len_utf8();
85+
86+ let end_anchor = if start_range.is_empty() {
87+ end_head
88+ } else {
89+ start_range.anchor + offs
90+ };
91+
92+ end_ranges.push(Range::new(end_anchor, end_head));
8793
8894 match next {
8995 Some(ch) if !close_before.contains(ch) => {
90− offs += 1;
91− // TODO: else return (use default handler that inserts open)
92− (pos, pos, Some(Tendril::from_char(open)))
96+ offs += open.len_utf8();
97+ (start_head, start_head, Some(Tendril::from_char(open)))
9398 }
9499 // None | Some(ch) if close_before.contains(ch) => {}
95100 _ => {
96101 // insert open & close
97− let mut pair = Tendril::with_capacity(2);
98− pair.push_char(open);
99− pair.push_char(close);
100−
101− offs += 2;
102−
103− (pos, pos, Some(pair))
102+ let pair = Tendril::from_iter([open, close]);
103+ offs += open.len_utf8() + close.len_utf8();
104+ (start_head, start_head, Some(pair))
104105 }
105106 }
106107 });
107108
108− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
109+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
110+ debug!("auto pair transaction: {:#?}", t);
111+ t
109112}
110113
111114fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
112− let mut ranges = SmallVec::with_capacity(selection.len());
115+ let mut end_ranges = SmallVec::with_capacity(selection.len());
113116
114117 let mut offs = 0;
115118
116− let transaction = Transaction::change_by_selection(doc, selection, |range| {
117− let pos = range.head;
118− let next = next_char(doc, pos);
119+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
120+ let start_head = start_range.head;
121+ let next = doc.get_char(start_head);
122+ let end_head = start_head + offs + close.len_utf8();
119123
120− let head = pos + offs + close.len_utf8();
121− // if selection, retain anchor, if cursor, move over
122− ranges.push(Range::new(
123− if range.is_empty() {
124− head
125− } else {
126− range.anchor + offs
127− },
128− head,
129− ));
124+ let end_anchor = if start_range.is_empty() {
125+ end_head
126+ } else {
127+ start_range.anchor + offs
128+ };
129+
130+ end_ranges.push(Range::new(end_anchor, end_head));
130131
131132 if next == Some(close) {
132− // return transaction that moves past close
133− (pos, pos, None) // no-op
133+ // return transaction that moves past close
134+ (start_head, start_head, None) // no-op
134135 } else {
135136 offs += close.len_utf8();
137+ (start_head, start_head, Some(Tendril::from_char(close)))
138+ }
139+ });
140+
141+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
142+}
136143
137− // TODO: else return (use default handler that inserts close)
138− (pos, pos, Some(Tendril::from_char(close)))
144+/// handle cases where open and close is the same, or in triples ("""docstring""")
145+fn handle_same(
146+ doc: &Rope,
147+ selection: &Selection,
148+ token: char,
149+ close_before: &str,
150+ open_before: &str,
151+) -> Transaction {
152+ let mut end_ranges = SmallVec::with_capacity(selection.len());
153+
154+ let mut offs = 0;
155+
156+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
157+ let start_head = start_range.head;
158+ let end_head = start_head + offs + token.len_utf8();
159+
160+ // if selection, retain anchor, if cursor, move over
161+ let end_anchor = if start_range.is_empty() {
162+ end_head
163+ } else {
164+ start_range.anchor + offs
165+ };
166+
167+ end_ranges.push(Range::new(end_anchor, end_head));
168+
169+ let next = doc.get_char(start_head);
170+ let prev = prev_char(doc, start_head);
171+
172+ if next == Some(token) {
173+ // return transaction that moves past close
174+ (start_head, start_head, None) // no-op
175+ } else {
176+ let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
177+ pair.push_char(token);
178+
179+ // for equal pairs, don't insert both open and close if either
180+ // side has a non-pair char
181+ if (next.is_none() || close_before.contains(next.unwrap()))
182+ && (prev.is_none() || open_before.contains(prev.unwrap()))
183+ {
184+ pair.push_char(token);
185+ }
186+
187+ offs += pair.len();
188+ (start_head, start_head, Some(pair))
139189 }
140190 });
141191
142− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
192+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
143193}
144194
145−// handle cases where open and close is the same, or in triples ("""docstring""")
146−fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
147− // if not cursor but selection, wrap
148− // let next = next char
149−
150− // if next == bracket {
151− // // if start of syntax node, insert token twice (new pair because node is complete)
152− // // elseif colsedBracketAt
153− // // is_triple == allow triple && next 3 is equal
154− // // cursor jump over
155− // }
156− //} else if allow_triple && followed by triple {
157− //}
158− //} else if next != word char && prev != bracket && prev != word char {
159− // // condition checks for cases like I' where you don't want I'' (or I'm)
160− // insert pair ("")
161− //}
162− None
195+#[cfg(test)]
196+mod test {
197+ use super::*;
198+ use smallvec::smallvec;
199+
200+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
201+ PAIRS.iter().filter(|(open, close)| open != close)
202+ }
203+
204+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
205+ PAIRS.iter().filter(|(open, close)| open == close)
206+ }
207+
208+ fn test_hooks(
209+ in_doc: &Rope,
210+ in_sel: &Selection,
211+ ch: char,
212+ expected_doc: &Rope,
213+ expected_sel: &Selection,
214+ ) {
215+ let trans = hook(&in_doc, &in_sel, ch).unwrap();
216+ let mut actual_doc = in_doc.clone();
217+ assert!(trans.apply(&mut actual_doc));
218+ assert_eq!(expected_doc, &actual_doc);
219+ assert_eq!(expected_sel, trans.selection().unwrap());
220+ }
221+
222+ fn test_hooks_with_pairs<I, F, R>(
223+ in_doc: &Rope,
224+ in_sel: &Selection,
225+ pairs: I,
226+ get_expected_doc: F,
227+ actual_sel: &Selection,
228+ ) where
229+ I: IntoIterator<Item = &'static (char, char)>,
230+ F: Fn(char, char) -> R,
231+ R: Into<Rope>,
232+ Rope: From<R>,
233+ {
234+ pairs.into_iter().for_each(|(open, close)| {
235+ test_hooks(
236+ in_doc,
237+ in_sel,
238+ *open,
239+ &Rope::from(get_expected_doc(*open, *close)),
240+ actual_sel,
241+ )
242+ });
243+ }
244+
245+ // [] indicates range
246+
247+ /// [] -> insert ( -> ([])
248+ #[test]
249+ fn test_insert_blank() {
250+ test_hooks_with_pairs(
251+ &Rope::new(),
252+ &Selection::single(1, 0),
253+ PAIRS,
254+ |open, close| format!("{}{}", open, close),
255+ &Selection::single(1, 1),
256+ );
257+ }
258+
259+ /// [] ([])
260+ /// [] -> insert -> ([])
261+ /// [] ([])
262+ #[test]
263+ fn test_insert_blank_multi_cursor() {
264+ test_hooks_with_pairs(
265+ &Rope::from("\n\n\n"),
266+ &Selection::new(
267+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
268+ 0,
269+ ),
270+ PAIRS,
271+ |open, close| {
272+ format!(
273+ "{open}{close}\n{open}{close}\n{open}{close}\n",
274+ open = open,
275+ close = close
276+ )
277+ },
278+ &Selection::new(
279+ smallvec!(Range::point(1), Range::point(4), Range::point(7),),
280+ 0,
281+ ),
282+ );
283+ }
284+
285+ // [TODO] broken until it works with selections
286+ /// fo[o] -> append ( -> fo[o(])
287+ #[ignore]
288+ #[test]
289+ fn test_append() {
290+ test_hooks_with_pairs(
291+ &Rope::from("foo"),
292+ &Selection::single(2, 4),
293+ PAIRS,
294+ |open, close| format!("foo{}{}", open, close),
295+ &Selection::single(2, 5),
296+ );
297+ }
298+
299+ /// ([]) -> insert ) -> ()[]
300+ #[test]
301+ fn test_insert_close_inside_pair() {
302+ for (open, close) in PAIRS {
303+ let doc = Rope::from(format!("{}{}", open, close));
304+
305+ test_hooks(
306+ &doc,
307+ &Selection::single(2, 1),
308+ *close,
309+ &doc,
310+ &Selection::point(2),
311+ );
312+ }
313+ }
314+
315+ /// ([]) ()[]
316+ /// ([]) -> insert ) -> ()[]
317+ /// ([]) ()[]
318+ #[test]
319+ fn test_insert_close_inside_pair_multi_cursor() {
320+ let sel = Selection::new(
321+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
322+ 0,
323+ );
324+
325+ let expected_sel = Selection::new(
326+ // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
327+ smallvec!(Range::point(2), Range::point(5), Range::point(8),),
328+ 0,
329+ );
330+
331+ for (open, close) in PAIRS {
332+ let doc = Rope::from(format!(
333+ "{open}{close}\n{open}{close}\n{open}{close}\n",
334+ open = open,
335+ close = close
336+ ));
337+
338+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
339+ }
340+ }
341+
342+ /// ([]) -> insert ( -> (([]))
343+ #[test]
344+ fn test_insert_open_inside_pair() {
345+ let sel = Selection::single(2, 1);
346+ let expected_sel = Selection::point(2);
347+
348+ for (open, close) in differing_pairs() {
349+ let doc = Rope::from(format!("{}{}", open, close));
350+ let expected_doc = Rope::from(format!(
351+ "{open}{open}{close}{close}",
352+ open = open,
353+ close = close
354+ ));
355+
356+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
357+ }
358+ }
359+
360+ /// ([]) -> insert " -> ("[]")
361+ #[test]
362+ fn test_insert_nested_open_inside_pair() {
363+ let sel = Selection::single(2, 1);
364+ let expected_sel = Selection::point(2);
365+
366+ for (outer_open, outer_close) in differing_pairs() {
367+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
368+
369+ for (inner_open, inner_close) in matching_pairs() {
370+ let expected_doc = Rope::from(format!(
371+ "{}{}{}{}",
372+ outer_open, inner_open, inner_close, outer_close
373+ ));
374+
375+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
376+ }
377+ }
378+ }
379+
380+ /// []word -> insert ( -> ([]word
381+ #[test]
382+ fn test_insert_open_before_non_pair() {
383+ test_hooks_with_pairs(
384+ &Rope::from("word"),
385+ &Selection::single(1, 0),
386+ PAIRS,
387+ |open, _| format!("{}word", open),
388+ &Selection::point(1),
389+ )
390+ }
391+
392+ // [TODO] broken until it works with selections
393+ /// [wor]d -> insert ( -> ([wor]d
394+ #[test]
395+ #[ignore]
396+ fn test_insert_open_with_selection() {
397+ test_hooks_with_pairs(
398+ &Rope::from("word"),
399+ &Selection::single(0, 4),
400+ PAIRS,
401+ |open, _| format!("{}word", open),
402+ &Selection::single(1, 5),
403+ )
404+ }
405+
406+ /// we want pairs that are *not* the same char to be inserted after
407+ /// a non-pair char, for cases like functions, but for pairs that are
408+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
409+ ///
410+ /// word[] -> insert ( -> word([])
411+ /// word[] -> insert ' -> word'[]
412+ #[test]
413+ fn test_insert_open_after_non_pair() {
414+ let doc = Rope::from("word");
415+ let sel = Selection::single(5, 4);
416+ let expected_sel = Selection::point(5);
417+
418+ test_hooks_with_pairs(
419+ &doc,
420+ &sel,
421+ differing_pairs(),
422+ |open, close| format!("word{}{}", open, close),
423+ &expected_sel,
424+ );
425+
426+ test_hooks_with_pairs(
427+ &doc,
428+ &sel,
429+ matching_pairs(),
430+ |open, _| format!("word{}", open),
431+ &expected_sel,
432+ );
433+ }
163434}
helix-core/src/diagnostic.rs+2−2
@@ -1,7 +1,7 @@
11//! LSP diagnostic utility types.
22
33/// Describes the severity level of a [`Diagnostic`].
4−#[derive(Debug, Eq, PartialEq)]
4+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55pub enum Severity {
66 Error,
77 Warning,
@@ -17,7 +17,7 @@ pub struct Range {
1717}
1818
1919/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
20−#[derive(Debug)]
20+#[derive(Debug, Clone)]
2121pub struct Diagnostic {
2222 pub range: Range,
2323 pub line: usize,
helix-core/src/increment/date_time.rsnew file+490
@@ -0,0 +1,490 @@
1+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
2+use once_cell::sync::Lazy;
3+use regex::Regex;
4+use ropey::RopeSlice;
5+
6+use std::borrow::Cow;
7+use std::cmp;
8+
9+use super::Increment;
10+use crate::{Range, Tendril};
11+
12+#[derive(Debug, PartialEq, Eq)]
13+pub struct DateTimeIncrementor {
14+ date_time: NaiveDateTime,
15+ range: Range,
16+ fmt: &'static str,
17+ field: DateField,
18+}
19+
20+impl DateTimeIncrementor {
21+ pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
22+ let range = if range.is_empty() {
23+ if range.anchor < text.len_chars() {
24+ // Treat empty range as a cursor range.
25+ range.put_cursor(text, range.anchor + 1, true)
26+ } else {
27+ // The range is empty and at the end of the text.
28+ return None;
29+ }
30+ } else {
31+ range
32+ };
33+
34+ FORMATS.iter().find_map(|format| {
35+ let from = range.from().saturating_sub(format.max_len);
36+ let to = (range.from() + format.max_len).min(text.len_chars());
37+
38+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
39+ let text: Cow<str> = text.slice(from..to).into();
40+
41+ let captures = format.regex.captures(&text)?;
42+ if captures.len() - 1 != format.fields.len() {
43+ return None;
44+ }
45+
46+ let date_time = captures.get(0)?;
47+ let offset = range.from() - from_in_text;
48+ let range = Range::new(date_time.start() + offset, date_time.end() + offset);
49+
50+ let field = captures
51+ .iter()
52+ .skip(1)
53+ .enumerate()
54+ .find_map(|(i, capture)| {
55+ let capture = capture?;
56+ let capture_range = capture.range();
57+
58+ if capture_range.contains(&from_in_text)
59+ && capture_range.contains(&(to_in_text - 1))
60+ {
61+ Some(format.fields[i])
62+ } else {
63+ None
64+ }
65+ })?;
66+
67+ let has_date = format.fields.iter().any(|f| f.unit.is_date());
68+ let has_time = format.fields.iter().any(|f| f.unit.is_time());
69+
70+ let date_time = &text[date_time.start()..date_time.end()];
71+ let date_time = match (has_date, has_time) {
72+ (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
73+ (true, false) => {
74+ let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
75+
76+ date.and_hms(0, 0, 0)
77+ }
78+ (false, true) => {
79+ let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
80+
81+ NaiveDate::from_ymd(0, 1, 1).and_time(time)
82+ }
83+ (false, false) => return None,
84+ };
85+
86+ Some(DateTimeIncrementor {
87+ date_time,
88+ range,
89+ fmt: format.fmt,
90+ field,
91+ })
92+ })
93+ }
94+}
95+
96+impl Increment for DateTimeIncrementor {
97+ fn increment(&self, amount: i64) -> (Range, Tendril) {
98+ let date_time = match self.field.unit {
99+ DateUnit::Years => add_years(self.date_time, amount),
100+ DateUnit::Months => add_months(self.date_time, amount),
101+ DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
102+ DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
103+ DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
104+ DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
105+ DateUnit::AmPm => toggle_am_pm(self.date_time),
106+ }
107+ .unwrap_or(self.date_time);
108+
109+ (self.range, date_time.format(self.fmt).to_string().into())
110+ }
111+}
112+
113+static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
114+ vec![
115+ Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
116+ Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
117+ Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12
118+ Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12
119+ Format::new("%Y-%m-%d"), // 2021-11-24
120+ Format::new("%Y/%m/%d"), // 2021/11/24
121+ Format::new("%a %b %d %Y"), // Wed Nov 24 2021
122+ Format::new("%d-%b-%Y"), // 24-Nov-2021
123+ Format::new("%Y %b %d"), // 2021 Nov 24
124+ Format::new("%b %d, %Y"), // Nov 24, 2021
125+ Format::new("%-I:%M:%S %P"), // 7:21:53 am
126+ Format::new("%-I:%M %P"), // 7:21 am
127+ Format::new("%-I:%M:%S %p"), // 7:21:53 AM
128+ Format::new("%-I:%M %p"), // 7:21 AM
129+ Format::new("%H:%M:%S"), // 23:24:23
130+ Format::new("%H:%M"), // 23:24
131+ ]
132+});
133+
134+#[derive(Debug)]
135+struct Format {
136+ fmt: &'static str,
137+ fields: Vec<DateField>,
138+ regex: Regex,
139+ max_len: usize,
140+}
141+
142+impl Format {
143+ fn new(fmt: &'static str) -> Self {
144+ let mut remaining = fmt;
145+ let mut fields = Vec::new();
146+ let mut regex = String::new();
147+ let mut max_len = 0;
148+
149+ while let Some(i) = remaining.find('%') {
150+ let after = &remaining[i + 1..];
151+ let mut chars = after.chars();
152+ let c = chars.next().unwrap();
153+
154+ let spec_len = if c == '-' {
155+ 1 + chars.next().unwrap().len_utf8()
156+ } else {
157+ c.len_utf8()
158+ };
159+
160+ let specifier = &after[..spec_len];
161+ let field = DateField::from_specifier(specifier).unwrap();
162+ fields.push(field);
163+ max_len += field.max_len + remaining[..i].len();
164+ regex += &remaining[..i];
165+ regex += &format!("({})", field.regex);
166+ remaining = &after[spec_len..];
167+ }
168+
169+ let regex = Regex::new(&regex).unwrap();
170+
171+ Self {
172+ fmt,
173+ fields,
174+ regex,
175+ max_len,
176+ }
177+ }
178+}
179+
180+impl PartialEq for Format {
181+ fn eq(&self, other: &Self) -> bool {
182+ self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
183+ }
184+}
185+
186+impl Eq for Format {}
187+
188+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
189+struct DateField {
190+ regex: &'static str,
191+ unit: DateUnit,
192+ max_len: usize,
193+}
194+
195+impl DateField {
196+ fn from_specifier(specifier: &str) -> Option<Self> {
197+ match specifier {
198+ "Y" => Some(DateField {
199+ regex: r"\d{4}",
200+ unit: DateUnit::Years,
201+ max_len: 5,
202+ }),
203+ "y" => Some(DateField {
204+ regex: r"\d\d",
205+ unit: DateUnit::Years,
206+ max_len: 2,
207+ }),
208+ "m" => Some(DateField {
209+ regex: r"[0-1]\d",
210+ unit: DateUnit::Months,
211+ max_len: 2,
212+ }),
213+ "d" => Some(DateField {
214+ regex: r"[0-3]\d",
215+ unit: DateUnit::Days,
216+ max_len: 2,
217+ }),
218+ "-d" => Some(DateField {
219+ regex: r"[1-3]?\d",
220+ unit: DateUnit::Days,
221+ max_len: 2,
222+ }),
223+ "a" => Some(DateField {
224+ regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
225+ unit: DateUnit::Days,
226+ max_len: 3,
227+ }),
228+ "A" => Some(DateField {
229+ regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
230+ unit: DateUnit::Days,
231+ max_len: 9,
232+ }),
233+ "b" | "h" => Some(DateField {
234+ regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
235+ unit: DateUnit::Months,
236+ max_len: 3,
237+ }),
238+ "B" => Some(DateField {
239+ regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
240+ unit: DateUnit::Months,
241+ max_len: 9,
242+ }),
243+ "H" => Some(DateField {
244+ regex: r"[0-2]\d",
245+ unit: DateUnit::Hours,
246+ max_len: 2,
247+ }),
248+ "M" => Some(DateField {
249+ regex: r"[0-5]\d",
250+ unit: DateUnit::Minutes,
251+ max_len: 2,
252+ }),
253+ "S" => Some(DateField {
254+ regex: r"[0-5]\d",
255+ unit: DateUnit::Seconds,
256+ max_len: 2,
257+ }),
258+ "I" => Some(DateField {
259+ regex: r"[0-1]\d",
260+ unit: DateUnit::Hours,
261+ max_len: 2,
262+ }),
263+ "-I" => Some(DateField {
264+ regex: r"1?\d",
265+ unit: DateUnit::Hours,
266+ max_len: 2,
267+ }),
268+ "P" => Some(DateField {
269+ regex: r"am|pm",
270+ unit: DateUnit::AmPm,
271+ max_len: 2,
272+ }),
273+ "p" => Some(DateField {
274+ regex: r"AM|PM",
275+ unit: DateUnit::AmPm,
276+ max_len: 2,
277+ }),
278+ _ => None,
279+ }
280+ }
281+}
282+
283+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
284+enum DateUnit {
285+ Years,
286+ Months,
287+ Days,
288+ Hours,
289+ Minutes,
290+ Seconds,
291+ AmPm,
292+}
293+
294+impl DateUnit {
295+ fn is_date(self) -> bool {
296+ matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
297+ }
298+
299+ fn is_time(self) -> bool {
300+ matches!(
301+ self,
302+ DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
303+ )
304+ }
305+}
306+
307+fn ndays_in_month(year: i32, month: u32) -> u32 {
308+ // The first day of the next month...
309+ let (y, m) = if month == 12 {
310+ (year + 1, 1)
311+ } else {
312+ (year, month + 1)
313+ };
314+ let d = NaiveDate::from_ymd(y, m, 1);
315+
316+ // ...is preceded by the last day of the original month.
317+ d.pred().day()
318+}
319+
320+fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
321+ let month = (date_time.month0() as i64).checked_add(amount)?;
322+ let year = date_time.year() + i32::try_from(month / 12).ok()?;
323+ let year = if month.is_negative() { year - 1 } else { year };
324+
325+ // Normalize month
326+ let month = month % 12;
327+ let month = if month.is_negative() {
328+ month + 12
329+ } else {
330+ month
331+ } as u32
332+ + 1;
333+
334+ let day = cmp::min(date_time.day(), ndays_in_month(year, month));
335+
336+ Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
337+}
338+
339+fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
340+ let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
341+ let ndays = ndays_in_month(year, date_time.month());
342+
343+ if date_time.day() > ndays {
344+ let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
345+ Some(d.succ().and_time(date_time.time()))
346+ } else {
347+ date_time.with_year(year)
348+ }
349+}
350+
351+fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
352+ date_time.checked_add_signed(duration)
353+}
354+
355+fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
356+ if date_time.hour() < 12 {
357+ add_duration(date_time, Duration::hours(12))
358+ } else {
359+ add_duration(date_time, Duration::hours(-12))
360+ }
361+}
362+
363+#[cfg(test)]
364+mod test {
365+ use super::*;
366+ use crate::Rope;
367+
368+ #[test]
369+ fn test_increment_date_times() {
370+ let tests = [
371+ // (original, cursor, amount, expected)
372+ ("2020-02-28", 0, 1, "2021-02-28"),
373+ ("2020-02-29", 0, 1, "2021-03-01"),
374+ ("2020-01-31", 5, 1, "2020-02-29"),
375+ ("2020-01-20", 5, 1, "2020-02-20"),
376+ ("2021-01-01", 5, -1, "2020-12-01"),
377+ ("2021-01-31", 5, -2, "2020-11-30"),
378+ ("2020-02-28", 8, 1, "2020-02-29"),
379+ ("2021-02-28", 8, 1, "2021-03-01"),
380+ ("2021-02-28", 0, -1, "2020-02-28"),
381+ ("2021-03-01", 0, -1, "2020-03-01"),
382+ ("2020-02-29", 5, -1, "2020-01-29"),
383+ ("2020-02-20", 5, -1, "2020-01-20"),
384+ ("2020-02-29", 8, -1, "2020-02-28"),
385+ ("2021-03-01", 8, -1, "2021-02-28"),
386+ ("1980/12/21", 8, 100, "1981/03/31"),
387+ ("1980/12/21", 8, -100, "1980/09/12"),
388+ ("1980/12/21", 8, 1000, "1983/09/17"),
389+ ("1980/12/21", 8, -1000, "1978/03/27"),
390+ ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
391+ ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
392+ ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
393+ ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
394+ ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
395+ ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
396+ ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
397+ ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
398+ ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
399+ ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
400+ ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
401+ ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
402+ ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
403+ ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
404+ ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
405+ ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
406+ ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
407+ ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
408+ ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
409+ ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
410+ ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
411+ ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
412+ ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
413+ ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
414+ ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
415+ ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
416+ ("24-Nov-2021", 0, 1, "25-Nov-2021"),
417+ ("24-Nov-2021", 3, 1, "24-Dec-2021"),
418+ ("24-Nov-2021", 7, 1, "24-Nov-2022"),
419+ ("2021 Nov 24", 0, 1, "2022 Nov 24"),
420+ ("2021 Nov 24", 5, 1, "2021 Dec 24"),
421+ ("2021 Nov 24", 9, 1, "2021 Nov 25"),
422+ ("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
423+ ("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
424+ ("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
425+ ("7:21:53 am", 0, 1, "8:21:53 am"),
426+ ("7:21:53 am", 3, 1, "7:22:53 am"),
427+ ("7:21:53 am", 5, 1, "7:21:54 am"),
428+ ("7:21:53 am", 8, 1, "7:21:53 pm"),
429+ ("7:21:53 AM", 0, 1, "8:21:53 AM"),
430+ ("7:21:53 AM", 3, 1, "7:22:53 AM"),
431+ ("7:21:53 AM", 5, 1, "7:21:54 AM"),
432+ ("7:21:53 AM", 8, 1, "7:21:53 PM"),
433+ ("7:21 am", 0, 1, "8:21 am"),
434+ ("7:21 am", 3, 1, "7:22 am"),
435+ ("7:21 am", 5, 1, "7:21 pm"),
436+ ("7:21 AM", 0, 1, "8:21 AM"),
437+ ("7:21 AM", 3, 1, "7:22 AM"),
438+ ("7:21 AM", 5, 1, "7:21 PM"),
439+ ("23:24:23", 1, 1, "00:24:23"),
440+ ("23:24:23", 3, 1, "23:25:23"),
441+ ("23:24:23", 6, 1, "23:24:24"),
442+ ("23:24", 1, 1, "00:24"),
443+ ("23:24", 3, 1, "23:25"),
444+ ];
445+
446+ for (original, cursor, amount, expected) in tests {
447+ let rope = Rope::from_str(original);
448+ let range = Range::new(cursor, cursor + 1);
449+ assert_eq!(
450+ DateTimeIncrementor::from_range(rope.slice(..), range)
451+ .unwrap()
452+ .increment(amount)
453+ .1,
454+ expected.into()
455+ );
456+ }
457+ }
458+
459+ #[test]
460+ fn test_invalid_date_times() {
461+ let tests = [
462+ "0000-00-00",
463+ "1980-2-21",
464+ "1980-12-1",
465+ "12345",
466+ "2020-02-30",
467+ "1999-12-32",
468+ "19-12-32",
469+ "1-2-3",
470+ "0000/00/00",
471+ "1980/2/21",
472+ "1980/12/1",
473+ "12345",
474+ "2020/02/30",
475+ "1999/12/32",
476+ "19/12/32",
477+ "1/2/3",
478+ "123:456:789",
479+ "11:61",
480+ "2021-55-12 08:12:54",
481+ ];
482+
483+ for invalid in tests {
484+ let rope = Rope::from_str(invalid);
485+ let range = Range::new(0, 1);
486+
487+ assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
488+ }
489+ }
490+}
helix-core/src/increment/mod.rsnew file+8
@@ -0,0 +1,8 @@
1+pub mod date_time;
2+pub mod number;
3+
4+use crate::{Range, Tendril};
5+
6+pub trait Increment {
7+ fn increment(&self, amount: i64) -> (Range, Tendril);
8+}
helix-core/src/numbers.rs → helix-core/src/increment/number.rsrenamed+19−11
@@ -2,6 +2,8 @@ use std::borrow::Cow;
22
33use ropey::RopeSlice;
44
5+use super::Increment;
6+
57use crate::{
68 textobject::{textobject_word, TextObject},
79 Range, Tendril,
@@ -9,9 +11,9 @@ use crate::{
911
1012#[derive(Debug, PartialEq, Eq)]
1113pub struct NumberIncrementor<'a> {
12− pub range: Range,
13− pub value: i64,
14− pub radix: u32,
14+ value: i64,
15+ radix: u32,
16+ range: Range,
1517
1618 text: RopeSlice<'a>,
1719}
@@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
7173 text,
7274 })
7375 }
76+}
7477
75− /// Add `amount` to the number and return the formatted text.
76− pub fn incremented_text(&self, amount: i64) -> Tendril {
78+impl<'a> Increment for NumberIncrementor<'a> {
79+ fn increment(&self, amount: i64) -> (Range, Tendril) {
7780 let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
7881 let old_length = old_text.len();
7982 let new_value = self.value.wrapping_add(amount);
@@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
144147 }
145148 }
146149
147− new_text.into()
150+ (self.range, new_text.into())
148151 }
149152}
150153
@@ -366,7 +369,8 @@ mod test {
366369 assert_eq!(
367370 NumberIncrementor::from_range(rope.slice(..), range)
368371 .unwrap()
369− .incremented_text(amount),
372+ .increment(amount)
373+ .1,
370374 expected.into()
371375 );
372376 }
@@ -392,7 +396,8 @@ mod test {
392396 assert_eq!(
393397 NumberIncrementor::from_range(rope.slice(..), range)
394398 .unwrap()
395− .incremented_text(amount),
399+ .increment(amount)
400+ .1,
396401 expected.into()
397402 );
398403 }
@@ -419,7 +424,8 @@ mod test {
419424 assert_eq!(
420425 NumberIncrementor::from_range(rope.slice(..), range)
421426 .unwrap()
422− .incremented_text(amount),
427+ .increment(amount)
428+ .1,
423429 expected.into()
424430 );
425431 }
@@ -464,7 +470,8 @@ mod test {
464470 assert_eq!(
465471 NumberIncrementor::from_range(rope.slice(..), range)
466472 .unwrap()
467− .incremented_text(amount),
473+ .increment(amount)
474+ .1,
468475 expected.into()
469476 );
470477 }
@@ -491,7 +498,8 @@ mod test {
491498 assert_eq!(
492499 NumberIncrementor::from_range(rope.slice(..), range)
493500 .unwrap()
494− .incremented_text(amount),
501+ .increment(amount)
502+ .1,
495503 expected.into()
496504 );
497505 }
helix-core/src/lib.rs+3−2
@@ -5,18 +5,19 @@ pub mod diagnostic;
55pub mod diff;
66pub mod graphemes;
77pub mod history;
8+pub mod increment;
89pub mod indent;
910pub mod line_ending;
1011pub mod macros;
1112pub mod match_brackets;
1213pub mod movement;
13−pub mod numbers;
1414pub mod object;
1515pub mod path;
1616mod position;
1717pub mod register;
1818pub mod search;
1919pub mod selection;
20+pub mod shellwords;
2021mod state;
2122pub mod surround;
2223pub mod syntax;
@@ -158,7 +159,7 @@ mod merge_toml_tests {
158159 ";
159160
160161 let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
161− .expect("Couldn't parse built-in langauges config");
162+ .expect("Couldn't parse built-in languages config");
162163 let user: Value = toml::from_str(USER).unwrap();
163164
164165 let merged = merge_toml_values(base, user);
helix-core/src/register.rs+11−3
@@ -15,7 +15,11 @@ impl Register {
1515 }
1616
1717 pub fn new_with_values(name: char, values: Vec<String>) -> Self {
18− Self { name, values }
18+ if name == '_' {
19+ Self::new(name)
20+ } else {
21+ Self { name, values }
22+ }
1923 }
2024
2125 pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
2731 }
2832
2933 pub fn write(&mut self, values: Vec<String>) {
30− self.values = values;
34+ if self.name != '_' {
35+ self.values = values;
36+ }
3137 }
3238
3339 pub fn push(&mut self, value: String) {
34− self.values.push(value);
40+ if self.name != '_' {
41+ self.values.push(value);
42+ }
3543 }
3644}
3745
helix-core/src/selection.rs+3−3
@@ -308,10 +308,10 @@ impl Range {
308308}
309309
310310impl From<(usize, usize)> for Range {
311− fn from(tuple: (usize, usize)) -> Self {
311+ fn from((anchor, head): (usize, usize)) -> Self {
312312 Self {
313− anchor: tuple.0,
314− head: tuple.1,
313+ anchor,
314+ head,
315315 horiz: None,
316316 }
317317 }
helix-core/src/shellwords.rsnew file+164
@@ -0,0 +1,164 @@
1+use std::borrow::Cow;
2+
3+/// Get the vec of escaped / quoted / doublequoted filenames from the input str
4+pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
5+ enum State {
6+ Normal,
7+ NormalEscaped,
8+ Quoted,
9+ QuoteEscaped,
10+ Dquoted,
11+ DquoteEscaped,
12+ }
13+
14+ use State::*;
15+
16+ let mut state = Normal;
17+ let mut args: Vec<Cow<str>> = Vec::new();
18+ let mut escaped = String::with_capacity(input.len());
19+
20+ let mut start = 0;
21+ let mut end = 0;
22+
23+ for (i, c) in input.char_indices() {
24+ state = match state {
25+ Normal => match c {
26+ '\\' => {
27+ escaped.push_str(&input[start..i]);
28+ start = i + 1;
29+ NormalEscaped
30+ }
31+ '"' => {
32+ end = i;
33+ Dquoted
34+ }
35+ '\'' => {
36+ end = i;
37+ Quoted
38+ }
39+ c if c.is_ascii_whitespace() => {
40+ end = i;
41+ Normal
42+ }
43+ _ => Normal,
44+ },
45+ NormalEscaped => Normal,
46+ Quoted => match c {
47+ '\\' => {
48+ escaped.push_str(&input[start..i]);
49+ start = i + 1;
50+ QuoteEscaped
51+ }
52+ '\'' => {
53+ end = i;
54+ Normal
55+ }
56+ _ => Quoted,
57+ },
58+ QuoteEscaped => Quoted,
59+ Dquoted => match c {
60+ '\\' => {
61+ escaped.push_str(&input[start..i]);
62+ start = i + 1;
63+ DquoteEscaped
64+ }
65+ '"' => {
66+ end = i;
67+ Normal
68+ }
69+ _ => Dquoted,
70+ },
71+ DquoteEscaped => Dquoted,
72+ };
73+
74+ if i >= input.len() - 1 && end == 0 {
75+ end = i + 1;
76+ }
77+
78+ if end > 0 {
79+ let esc_trim = escaped.trim();
80+ let inp = &input[start..end];
81+
82+ if !(esc_trim.is_empty() && inp.trim().is_empty()) {
83+ if esc_trim.is_empty() {
84+ args.push(inp.into());
85+ } else {
86+ args.push([escaped, inp.into()].concat().into());
87+ escaped = "".to_string();
88+ }
89+ }
90+ start = i + 1;
91+ end = 0;
92+ }
93+ }
94+ args
95+}
96+
97+#[cfg(test)]
98+mod test {
99+ use super::*;
100+
101+ #[test]
102+ fn test_normal() {
103+ let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
104+ let result = shellwords(input);
105+ let expected = vec![
106+ Cow::from(":o"),
107+ Cow::from("single_word"),
108+ Cow::from("twó"),
109+ Cow::from("wörds"),
110+ Cow::from(r#"three "with escaping\"#),
111+ ];
112+ // TODO test is_owned and is_borrowed, once they get stabilized.
113+ assert_eq!(expected, result);
114+ }
115+
116+ #[test]
117+ fn test_quoted() {
118+ let quoted =
119+ r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
120+ let result = shellwords(quoted);
121+ let expected = vec![
122+ Cow::from(":o"),
123+ Cow::from("single_word"),
124+ Cow::from("twó wörds"),
125+ Cow::from(r#"three' "with escaping\"#),
126+ Cow::from("quote incomplete"),
127+ ];
128+ assert_eq!(expected, result);
129+ }
130+
131+ #[test]
132+ fn test_dquoted() {
133+ let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
134+ let result = shellwords(dquoted);
135+ let expected = vec![
136+ Cow::from(":o"),
137+ Cow::from("single_word"),
138+ Cow::from("twó wörds"),
139+ Cow::from(r#"three' "with escaping\"#),
140+ Cow::from("dquote incomplete"),
141+ ];
142+ assert_eq!(expected, result);
143+ }
144+
145+ #[test]
146+ fn test_mixed() {
147+ let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
148+ let result = shellwords(dquoted);
149+ let expected = vec![
150+ Cow::from(":o"),
151+ Cow::from("single_word"),
152+ Cow::from("twó wörds"),
153+ Cow::from("three' \"with escaping\\"),
154+ Cow::from("no space before"),
155+ Cow::from("and after"),
156+ Cow::from("$#%^@"),
157+ Cow::from("%^&(%^"),
158+ Cow::from(")(*&^%"),
159+ Cow::from(r#"a\\b"#),
160+ //last ' just changes to quoted but since we dont have anything after it, it should be ignored
161+ ];
162+ assert_eq!(expected, result);
163+ }
164+}
helix-core/src/surround.rs+83−65
@@ -1,4 +1,4 @@
1−use crate::{search, Selection};
1+use crate::{search, Range, Selection};
22use ropey::RopeSlice;
33
44pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
3535pub fn find_nth_pairs_pos(
3636 text: RopeSlice,
3737 ch: char,
38− pos: usize,
38+ range: Range,
3939 n: usize,
4040) -> Option<(usize, usize)> {
41− let (open, close) = get_pair(ch);
42−
43− if text.len_chars() < 2 || pos >= text.len_chars() {
41+ if text.len_chars() < 2 || range.to() >= text.len_chars() {
4442 return None;
4543 }
4644
45+ let (open, close) = get_pair(ch);
46+ let pos = range.cursor(text);
47+
4748 if open == close {
4849 if Some(open) == text.get_char(pos) {
49− // Special case: cursor is directly on a matching char.
50− match pos {
51− 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
52− _ if (pos + 1) == text.len_chars() => {
53− Some((search::find_nth_prev(text, open, pos, n)?, pos))
54− }
55− // We return no match because there's no way to know which
56− // side of the char we should be searching on.
57− _ => None,
58− }
59− } else {
60− Some((
61− search::find_nth_prev(text, open, pos, n)?,
62− search::find_nth_next(text, close, pos, n)?,
63− ))
50+ // Cursor is directly on match char. We return no match
51+ // because there's no way to know which side of the char
52+ // we should be searching on.
53+ return None;
6454 }
55+ Some((
56+ search::find_nth_prev(text, open, pos, n)?,
57+ search::find_nth_next(text, close, pos, n)?,
58+ ))
6559 } else {
6660 Some((
6761 find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
160154) -> Option<Vec<usize>> {
161155 let mut change_pos = Vec::new();
162156
163− for range in selection {
164− let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
157+ for &range in selection {
158+ let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
165159 if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
166160 return None;
167161 }
@@ -178,67 +172,91 @@ mod test {
178172 use ropey::Rope;
179173 use smallvec::SmallVec;
180174
181− #[test]
182− fn test_find_nth_pairs_pos() {
183− let doc = Rope::from("some (text) here");
175+ fn check_find_nth_pair_pos(
176+ text: &str,
177+ cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
178+ ) {
179+ let doc = Rope::from(text);
184180 let slice = doc.slice(..);
185181
186− // cursor on [t]ext
187− assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
188− assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
189− // cursor on so[m]e
190− assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
191− // cursor on bracket itself
192− assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
193− assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
182+ for (cursor_pos, ch, n, expected_range) in cases {
183+ let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
184+ assert_eq!(
185+ range, expected_range,
186+ "Expected {:?}, got {:?}",
187+ expected_range, range
188+ );
189+ }
194190 }
195191
196192 #[test]
197− fn test_find_nth_pairs_pos_skip() {
198− let doc = Rope::from("(so (many (good) text) here)");
199− let slice = doc.slice(..);
193+ fn test_find_nth_pairs_pos() {
194+ check_find_nth_pair_pos(
195+ "some (text) here",
196+ vec![
197+ // cursor on [t]ext
198+ (6, '(', 1, Some((5, 10))),
199+ (6, ')', 1, Some((5, 10))),
200+ // cursor on so[m]e
201+ (2, '(', 1, None),
202+ // cursor on bracket itself
203+ (5, '(', 1, Some((5, 10))),
204+ (10, '(', 1, Some((5, 10))),
205+ ],
206+ );
207+ }
200208
201− // cursor on go[o]d
202− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
203− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
204− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
209+ #[test]
210+ fn test_find_nth_pairs_pos_skip() {
211+ check_find_nth_pair_pos(
212+ "(so (many (good) text) here)",
213+ vec![
214+ // cursor on go[o]d
215+ (13, '(', 1, Some((10, 15))),
216+ (13, '(', 2, Some((4, 21))),
217+ (13, '(', 3, Some((0, 27))),
218+ ],
219+ );
205220 }
206221
207222 #[test]
208223 fn test_find_nth_pairs_pos_same() {
209− let doc = Rope::from("'so 'many 'good' text' here'");
210− let slice = doc.slice(..);
211−
212− // cursor on go[o]d
213− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
214− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
215− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
216− // cursor on the quotes
217− assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
218− // this is the best we can do since opening and closing pairs are same
219− assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
220− assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
224+ check_find_nth_pair_pos(
225+ "'so 'many 'good' text' here'",
226+ vec![
227+ // cursor on go[o]d
228+ (13, '\'', 1, Some((10, 15))),
229+ (13, '\'', 2, Some((4, 21))),
230+ (13, '\'', 3, Some((0, 27))),
231+ // cursor on the quotes
232+ (10, '\'', 1, None),
233+ ],
234+ )
221235 }
222236
223237 #[test]
224238 fn test_find_nth_pairs_pos_step() {
225− let doc = Rope::from("((so)((many) good (text))(here))");
226− let slice = doc.slice(..);
227−
228− // cursor on go[o]d
229− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
230− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
239+ check_find_nth_pair_pos(
240+ "((so)((many) good (text))(here))",
241+ vec![
242+ // cursor on go[o]d
243+ (15, '(', 1, Some((5, 24))),
244+ (15, '(', 2, Some((0, 31))),
245+ ],
246+ )
231247 }
232248
233249 #[test]
234250 fn test_find_nth_pairs_pos_mixed() {
235− let doc = Rope::from("(so [many {good} text] here)");
236− let slice = doc.slice(..);
237−
238− // cursor on go[o]d
239− assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
240− assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
241− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
251+ check_find_nth_pair_pos(
252+ "(so [many {good} text] here)",
253+ vec![
254+ // cursor on go[o]d
255+ (13, '{', 1, Some((10, 15))),
256+ (13, '[', 1, Some((4, 21))),
257+ (13, '(', 1, Some((0, 27))),
258+ ],
259+ )
242260 }
243261
244262 #[test]
helix-core/src/syntax.rs+4−3
@@ -50,7 +50,7 @@ pub struct Configuration {
5050#[serde(rename_all = "kebab-case", deny_unknown_fields)]
5151pub struct LanguageConfiguration {
5252 #[serde(rename = "name")]
53− pub language_id: String,
53+ pub language_id: String, // c-sharp, rust
5454 pub scope: String, // source.rust
5555 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
5656 #[serde(default)]
@@ -310,8 +310,9 @@ impl Loader {
310310
311311 pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
312312 let line = Cow::from(source.line(0));
313− static SHEBANG_REGEX: Lazy<Regex> =
314− Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
313+ static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
314+ Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
315+ });
315316 let configuration_id = SHEBANG_REGEX
316317 .captures(&line)
317318 .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
helix-core/src/textobject.rs+6−4
@@ -114,7 +114,7 @@ pub fn textobject_surround(
114114 ch: char,
115115 count: usize,
116116) -> Range {
117− surround::find_nth_pairs_pos(slice, ch, range.head, count)
117+ surround::find_nth_pairs_pos(slice, ch, range, count)
118118 .map(|(anchor, head)| match textobject {
119119 TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
120120 TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
170170
171171 #[test]
172172 fn test_textobject_word() {
173− // (text, [(cursor position, textobject, final range), ...])
173+ // (text, [(char position, textobject, final range), ...])
174174 let tests = &[
175175 (
176176 "cursor at beginning of doc",
@@ -269,7 +269,9 @@ mod test {
269269 let slice = doc.slice(..);
270270 for &case in scenario {
271271 let (pos, objtype, expected_range) = case;
272− let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
272+ // cursor is a single width selection
273+ let range = Range::new(pos, pos + 1);
274+ let result = textobject_word(slice, range, objtype, 1, false);
273275 assert_eq!(
274276 result,
275277 expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
283285
284286 #[test]
285287 fn test_textobject_surround() {
286− // (text, [(cursor position, textobject, final range, count), ...])
288+ // (text, [(cursor position, textobject, final range, surround char, count), ...])
287289 let tests = &[
288290 (
289291 "simple (single) surround pairs",
helix-core/src/transaction.rs+3−13
@@ -22,7 +22,7 @@ pub enum Assoc {
2222}
2323
2424// ChangeSpec = Change | ChangeSet | Vec<Change>
25−#[derive(Debug, Clone, PartialEq, Eq)]
25+#[derive(Debug, Default, Clone, PartialEq, Eq)]
2626pub struct ChangeSet {
2727 pub(crate) changes: Vec<Operation>,
2828 /// The required document length. Will refuse to apply changes unless it matches.
@@ -30,16 +30,6 @@ pub struct ChangeSet {
3030 len_after: usize,
3131}
3232
33−impl Default for ChangeSet {
34− fn default() -> Self {
35− Self {
36− changes: Vec::new(),
37− len: 0,
38− len_after: 0,
39− }
40− }
41−}
42−
4333impl ChangeSet {
4434 pub fn with_capacity(capacity: usize) -> Self {
4535 Self {
@@ -330,7 +320,7 @@ impl ChangeSet {
330320 /// `true` when the set is empty.
331321 #[inline]
332322 pub fn is_empty(&self) -> bool {
333− self.changes.is_empty()
323+ self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
334324 }
335325
336326 /// Map a position through the changes.
@@ -419,7 +409,7 @@ impl ChangeSet {
419409
420410/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
421411/// a single transaction.
422−#[derive(Debug, Default, Clone)]
412+#[derive(Debug, Default, Clone, PartialEq, Eq)]
423413pub struct Transaction {
424414 changes: ChangeSet,
425415 selection: Option<Selection>,
helix-lsp/src/lib.rs+4−1
@@ -337,7 +337,10 @@ impl Registry {
337337 })
338338 .await;
339339
340− value.expect("failed to initialize capabilities");
340+ if let Err(e) = value {
341+ log::error!("failed to initialize language server: {}", e);
342+ return;
343+ }
341344
342345 // next up, notify<initialized>
343346 _client
helix-syntax/languages/tree-sitter-markdownnew file+1

Submodule changed.

helix-syntax/languages/tree-sitter-wgslnew file+1

Submodule changed.

helix-term/Cargo.toml+1
@@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
99repository = "https://github.com/helix-editor/helix"
1010homepage = "https://helix-editor.com"
1111include = ["src/**/*", "README.md"]
12+default-run = "hx"
1213
1314[package.metadata.nix]
1415build = true
helix-term/src/application.rs+20−10
@@ -76,17 +76,27 @@ impl Application {
7676 None => Ok(def_lang_conf),
7777 };
7878
79− let theme = if let Some(theme) = &config.theme {
80− match theme_loader.load(theme) {
81− Ok(theme) => theme,
82− Err(e) => {
83− log::warn!("failed to load theme `{}` - {}", theme, e);
79+ let true_color = config.editor.true_color || crate::true_color();
80+ let theme = config
81+ .theme
82+ .as_ref()
83+ .and_then(|theme| {
84+ theme_loader
85+ .load(theme)
86+ .map_err(|e| {
87+ log::warn!("failed to load theme `{}` - {}", theme, e);
88+ e
89+ })
90+ .ok()
91+ .filter(|theme| (true_color || theme.is_16_color()))
92+ })
93+ .unwrap_or_else(|| {
94+ if true_color {
8495 theme_loader.default()
96+ } else {
97+ theme_loader.base16_default()
8598 }
86− }
87− } else {
88− theme_loader.default()
89− };
99+ });
90100
91101 let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
92102 .and_then(|conf| conf.try_into())
@@ -265,7 +275,7 @@ impl Application {
265275 use crate::commands::{insert::idle_completion, Context};
266276 use helix_view::document::Mode;
267277
268− if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
278+ if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
269279 return;
270280 }
271281 let editor_view = self
helix-term/src/commands.rs+546−214
@@ -1,16 +1,16 @@
11use helix_core::{
22 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
33 history::UndoKind,
4+ increment::date_time::DateTimeIncrementor,
5+ increment::{number::NumberIncrementor, Increment},
46 indent,
57 indent::IndentStyle,
68 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
79 match_brackets,
810 movement::{self, Direction},
9− numbers::NumberIncrementor,
1011 object, pos_at_coords,
1112 regex::{self, Regex, RegexBuilder},
12− register::Register,
13− search, selection, surround, textobject,
13+ search, selection, shellwords, surround, textobject,
1414 unicode::width::UnicodeWidthChar,
1515 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
1616 Transaction,
@@ -25,7 +25,7 @@ use helix_view::{
2525 Document, DocumentId, Editor, ViewId,
2626};
2727
28−use anyhow::{anyhow, bail, Context as _};
28+use anyhow::{anyhow, bail, ensure, Context as _};
2929use helix_lsp::{
3030 block_on, lsp,
3131 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -41,7 +41,7 @@ use crate::{
4141
4242use crate::job::{self, Job, Jobs};
4343use futures_util::{FutureExt, StreamExt};
44−use std::num::NonZeroUsize;
44+use std::{collections::HashSet, num::NonZeroUsize};
4545use std::{fmt, future::Future};
4646
4747use std::{
@@ -70,7 +70,7 @@ pub struct Context<'a> {
7070impl<'a> Context<'a> {
7171 /// Push a new component onto the compositor.
7272 pub fn push_layer(&mut self, component: Box<dyn Component>) {
73− self.callback = Some(Box::new(|compositor: &mut Compositor| {
73+ self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
7474 compositor.push(component)
7575 }));
7676 }
@@ -135,47 +135,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
135135 view.offset.row = line.saturating_sub(relative);
136136}
137137
138−/// A command is composed of a static name, and a function that takes the current state plus a count,
139−/// and does a side-effect on the state (usually by creating and applying a transaction).
140−#[derive(Copy, Clone)]
141−pub struct Command {
142− name: &'static str,
143− fun: fn(cx: &mut Context),
144− doc: &'static str,
145−}
146−
147−macro_rules! commands {
138+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
139+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
140+/// Both of these types of commands can be mapped with keybindings in the config.toml.
141+#[derive(Clone)]
142+pub enum MappableCommand {
143+ Typable {
144+ name: String,
145+ args: Vec<String>,
146+ doc: String,
147+ },
148+ Static {
149+ name: &'static str,
150+ fun: fn(cx: &mut Context),
151+ doc: &'static str,
152+ },
153+}
154+
155+macro_rules! static_commands {
148156 ( $($name:ident, $doc:literal,)* ) => {
149157 $(
150158 #[allow(non_upper_case_globals)]
151− pub const $name: Self = Self {
159+ pub const $name: Self = Self::Static {
152160 name: stringify!($name),
153161 fun: $name,
154162 doc: $doc
155163 };
156164 )*
157165
158− pub const COMMAND_LIST: &'static [Self] = &[
166+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
159167 $( Self::$name, )*
160168 ];
161169 }
162170}
163171
164−impl Command {
172+impl MappableCommand {
165173 pub fn execute(&self, cx: &mut Context) {
166− (self.fun)(cx);
174+ match &self {
175+ MappableCommand::Typable { name, args, doc: _ } => {
176+ let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
177+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
178+ let mut cx = compositor::Context {
179+ editor: cx.editor,
180+ jobs: cx.jobs,
181+ scroll: None,
182+ };
183+ if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
184+ cx.editor.set_error(format!("{}", e));
185+ }
186+ }
187+ }
188+ MappableCommand::Static { fun, .. } => (fun)(cx),
189+ }
167190 }
168191
169− pub fn name(&self) -> &'static str {
170− self.name
192+ pub fn name(&self) -> &str {
193+ match &self {
194+ MappableCommand::Typable { name, .. } => name,
195+ MappableCommand::Static { name, .. } => name,
196+ }
171197 }
172198
173− pub fn doc(&self) -> &'static str {
174− self.doc
199+ pub fn doc(&self) -> &str {
200+ match &self {
201+ MappableCommand::Typable { doc, .. } => doc,
202+ MappableCommand::Static { doc, .. } => doc,
203+ }
175204 }
176205
177206 #[rustfmt::skip]
178− commands!(
207+ static_commands!(
179208 no_op, "Do nothing",
180209 move_char_left, "Move left",
181210 move_char_right, "Move right",
@@ -232,7 +261,9 @@ impl Command {
232261 extend_line, "Select current line, if already selected, extend to next line",
233262 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
234263 delete_selection, "Delete selection",
264+ delete_selection_noyank, "Delete selection, without yanking",
235265 change_selection, "Change selection (delete and enter insert mode)",
266+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
236267 collapse_selection, "Collapse selection onto a single cursor",
237268 flip_selections, "Flip selection cursor and anchor",
238269 insert_mode, "Insert before selection",
@@ -258,11 +289,15 @@ impl Command {
258289 goto_implementation, "Goto implementation",
259290 goto_file_start, "Goto file start/line",
260291 goto_file_end, "Goto file end",
292+ goto_file, "Goto files in selection",
293+ goto_file_hsplit, "Goto files in selection (hsplit)",
294+ goto_file_vsplit, "Goto files in selection (vsplit)",
261295 goto_reference, "Goto references",
262296 goto_window_top, "Goto window top",
263− goto_window_middle, "Goto window middle",
297+ goto_window_center, "Goto window center",
264298 goto_window_bottom, "Goto window bottom",
265299 goto_last_accessed_file, "Goto last accessed file",
300+ goto_last_modified_file, "Goto last modified file",
266301 goto_last_modification, "Goto last modification",
267302 goto_line, "Goto line",
268303 goto_last_line, "Goto last line",
@@ -327,6 +362,7 @@ impl Command {
327362 expand_selection, "Expand selection to parent syntax node",
328363 jump_forward, "Jump forward on jumplist",
329364 jump_backward, "Jump backward on jumplist",
365+ save_selection, "Save the current selection to the jumplist",
330366 jump_view_right, "Jump to the split to the right",
331367 jump_view_left, "Jump to the split to the left",
332368 jump_view_up, "Jump to the split above",
@@ -359,36 +395,56 @@ impl Command {
359395 rename_symbol, "Rename symbol",
360396 increment, "Increment",
361397 decrement, "Decrement",
398+ record_macro, "Record macro",
399+ play_macro, "Play macro",
362400 );
363401}
364402
365−impl fmt::Debug for Command {
403+impl fmt::Debug for MappableCommand {
366404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367− let Command { name, .. } = self;
368− f.debug_tuple("Command").field(name).finish()
405+ f.debug_tuple("MappableCommand")
406+ .field(&self.name())
407+ .finish()
369408 }
370409}
371410
372−impl fmt::Display for Command {
411+impl fmt::Display for MappableCommand {
373412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374− let Command { name, .. } = self;
375− f.write_str(name)
413+ f.write_str(self.name())
376414 }
377415}
378416
379−impl std::str::FromStr for Command {
417+impl std::str::FromStr for MappableCommand {
380418 type Err = anyhow::Error;
381419
382420 fn from_str(s: &str) -> Result<Self, Self::Err> {
383− Command::COMMAND_LIST
384− .iter()
385− .copied()
386− .find(|cmd| cmd.name == s)
387− .ok_or_else(|| anyhow!("No command named '{}'", s))
421+ if let Some(suffix) = s.strip_prefix(':') {
422+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
423+ let name = typable_command
424+ .next()
425+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
426+ let args = typable_command
427+ .map(|s| s.to_owned())
428+ .collect::<Vec<String>>();
429+ cmd::TYPABLE_COMMAND_MAP
430+ .get(name)
431+ .map(|cmd| MappableCommand::Typable {
432+ name: cmd.name.to_owned(),
433+ doc: format!(":{} {:?}", cmd.name, args),
434+ args,
435+ })
436+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
437+ } else {
438+ MappableCommand::STATIC_COMMAND_LIST
439+ .iter()
440+ .cloned()
441+ .find(|cmd| cmd.name() == s)
442+ .ok_or_else(|| anyhow!("No command named '{}'", s))
443+ }
388444 }
389445}
390446
391−impl<'de> Deserialize<'de> for Command {
447+impl<'de> Deserialize<'de> for MappableCommand {
392448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
393449 where
394450 D: Deserializer<'de>,
@@ -398,9 +454,27 @@ impl<'de> Deserialize<'de> for Command {
398454 }
399455}
400456
401−impl PartialEq for Command {
457+impl PartialEq for MappableCommand {
402458 fn eq(&self, other: &Self) -> bool {
403− self.name() == other.name()
459+ match (self, other) {
460+ (
461+ MappableCommand::Typable {
462+ name: first_name, ..
463+ },
464+ MappableCommand::Typable {
465+ name: second_name, ..
466+ },
467+ ) => first_name == second_name,
468+ (
469+ MappableCommand::Static {
470+ name: first_name, ..
471+ },
472+ MappableCommand::Static {
473+ name: second_name, ..
474+ },
475+ ) => first_name == second_name,
476+ _ => false,
477+ }
404478 }
405479}
406480
@@ -599,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) {
599673
600674 let selection = doc.selection(view.id).clone().transform(|range| {
601675 let line = range.cursor_line(text);
602− let pos = line_end_char_index(&text, line);
603− range.put_cursor(text, pos, true)
676+ let line_end_pos = line_end_char_index(&text, line);
677+ let pos = range.cursor(text);
678+
679+ let mut new_range = range.put_cursor(text, line_end_pos, true);
680+ // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
681+ if pos != line_end_pos {
682+ new_range.head = line_end_pos;
683+ }
684+ new_range
604685 });
605686 delete_selection_insert_mode(doc, view, &selection);
606687}
@@ -729,10 +810,12 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) ->
729810}
730811
731812fn goto_window(cx: &mut Context, align: Align) {
813+ let count = cx.count() - 1;
732814 let (view, doc) = current!(cx.editor);
733815
734816 let height = view.inner_area().height as usize;
735817
818+ // respect user given count if any
736819 // - 1 so we have at least one gap in the middle.
737820 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
738821 // as we type
@@ -741,10 +824,11 @@ fn goto_window(cx: &mut Context, align: Align) {
741824 let last_line = view.last_line(doc);
742825
743826 let line = match align {
744− Align::Top => (view.offset.row + scrolloff),
745− Align::Center => (view.offset.row + (height / 2)),
746− Align::Bottom => last_line.saturating_sub(scrolloff),
827+ Align::Top => (view.offset.row + scrolloff + count),
828+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
829+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
747830 }
831+ .max(view.offset.row + scrolloff)
748832 .min(last_line.saturating_sub(scrolloff));
749833
750834 let pos = doc.text().line_to_char(line);
@@ -756,7 +840,7 @@ fn goto_window_top(cx: &mut Context) {
756840 goto_window(cx, Align::Top)
757841}
758842
759−fn goto_window_middle(cx: &mut Context) {
843+fn goto_window_center(cx: &mut Context) {
760844 goto_window(cx, Align::Center)
761845}
762846
@@ -834,6 +918,49 @@ fn goto_file_end(cx: &mut Context) {
834918 doc.set_selection(view.id, selection);
835919}
836920
921+fn goto_file(cx: &mut Context) {
922+ goto_file_impl(cx, Action::Replace);
923+}
924+
925+fn goto_file_hsplit(cx: &mut Context) {
926+ goto_file_impl(cx, Action::HorizontalSplit);
927+}
928+
929+fn goto_file_vsplit(cx: &mut Context) {
930+ goto_file_impl(cx, Action::VerticalSplit);
931+}
932+
933+fn goto_file_impl(cx: &mut Context, action: Action) {
934+ let (view, doc) = current_ref!(cx.editor);
935+ let text = doc.text();
936+ let selections = doc.selection(view.id);
937+ let mut paths: Vec<_> = selections
938+ .iter()
939+ .map(|r| text.slice(r.from()..r.to()).to_string())
940+ .collect();
941+ let primary = selections.primary();
942+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
943+ let current_word = movement::move_next_long_word_start(
944+ text.slice(..),
945+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
946+ 1,
947+ );
948+ paths.clear();
949+ paths.push(
950+ text.slice(current_word.from()..current_word.to())
951+ .to_string(),
952+ );
953+ }
954+ for sel in paths {
955+ let p = sel.trim();
956+ if !p.is_empty() {
957+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
958+ cx.editor.set_error(format!("Open file failed: {:?}", e));
959+ }
960+ }
961+ }
962+}
963+
837964fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
838965where
839966 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1693,19 +1820,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
16931820 );
16941821}
16951822
1696−fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1823+enum Operation {
1824+ Delete,
1825+ Change,
1826+}
1827+
1828+fn delete_selection_impl(cx: &mut Context, op: Operation) {
1829+ let (view, doc) = current!(cx.editor);
1830+
16971831 let text = doc.text().slice(..);
1698− let selection = doc.selection(view_id);
1832+ let selection = doc.selection(view.id);
16991833
1700− // first yank the selection
1701− let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1702− reg.write(values);
1834+ if cx.register != Some('_') {
1835+ // first yank the selection
1836+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1837+ let reg_name = cx.register.unwrap_or('"');
1838+ let registers = &mut cx.editor.registers;
1839+ let reg = registers.get_mut(reg_name);
1840+ reg.write(values);
1841+ };
17031842
17041843 // then delete
17051844 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
17061845 (range.from(), range.to(), None)
17071846 });
1708− doc.apply(&transaction, view_id);
1847+ doc.apply(&transaction, view.id);
1848+
1849+ match op {
1850+ Operation::Delete => {
1851+ doc.append_changes_to_history(view.id);
1852+ // exit select mode, if currently in select mode
1853+ exit_select_mode(cx);
1854+ }
1855+ Operation::Change => {
1856+ enter_insert_mode(doc);
1857+ }
1858+ }
17091859}
17101860
17111861#[inline]
@@ -1720,25 +1870,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
17201870}
17211871
17221872fn delete_selection(cx: &mut Context) {
1723− let reg_name = cx.register.unwrap_or('"');
1724− let (view, doc) = current!(cx.editor);
1725− let registers = &mut cx.editor.registers;
1726− let reg = registers.get_mut(reg_name);
1727− delete_selection_impl(reg, doc, view.id);
1728−
1729− doc.append_changes_to_history(view.id);
1873+ delete_selection_impl(cx, Operation::Delete);
1874+}
17301875
1731− // exit select mode, if currently in select mode
1732− exit_select_mode(cx);
1876+fn delete_selection_noyank(cx: &mut Context) {
1877+ cx.register = Some('_');
1878+ delete_selection_impl(cx, Operation::Delete);
17331879}
17341880
17351881fn change_selection(cx: &mut Context) {
1736− let reg_name = cx.register.unwrap_or('"');
1737− let (view, doc) = current!(cx.editor);
1738− let registers = &mut cx.editor.registers;
1739− let reg = registers.get_mut(reg_name);
1740− delete_selection_impl(reg, doc, view.id);
1741− enter_insert_mode(doc);
1882+ delete_selection_impl(cx, Operation::Change);
1883+}
1884+
1885+fn change_selection_noyank(cx: &mut Context) {
1886+ cx.register = Some('_');
1887+ delete_selection_impl(cx, Operation::Change);
17421888}
17431889
17441890fn collapse_selection(cx: &mut Context) {
@@ -1806,7 +1952,7 @@ fn append_mode(cx: &mut Context) {
18061952 doc.set_selection(view.id, selection);
18071953}
18081954
1809−mod cmd {
1955+pub mod cmd {
18101956 use super::*;
18111957 use std::collections::HashMap;
18121958
@@ -1819,13 +1965,13 @@ mod cmd {
18191965 pub aliases: &'static [&'static str],
18201966 pub doc: &'static str,
18211967 // params, flags, helper, completer
1822− pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
1968+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
18231969 pub completer: Option<Completer>,
18241970 }
18251971
18261972 fn quit(
18271973 cx: &mut compositor::Context,
1828− _args: &[&str],
1974+ _args: &[Cow<str>],
18291975 _event: PromptEvent,
18301976 ) -> anyhow::Result<()> {
18311977 // last view and we have unsaved changes
@@ -1840,7 +1986,7 @@ mod cmd {
18401986
18411987 fn force_quit(
18421988 cx: &mut compositor::Context,
1843− _args: &[&str],
1989+ _args: &[Cow<str>],
18441990 _event: PromptEvent,
18451991 ) -> anyhow::Result<()> {
18461992 cx.editor.close(view!(cx.editor).id);
@@ -1850,17 +1996,19 @@ mod cmd {
18501996
18511997 fn open(
18521998 cx: &mut compositor::Context,
1853− args: &[&str],
1999+ args: &[Cow<str>],
18542000 _event: PromptEvent,
18552001 ) -> anyhow::Result<()> {
1856− let path = args.get(0).context("wrong argument count")?;
1857− let _ = cx.editor.open(path.into(), Action::Replace)?;
2002+ ensure!(!args.is_empty(), "wrong argument count");
2003+ for arg in args {
2004+ let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?;
2005+ }
18582006 Ok(())
18592007 }
18602008
18612009 fn buffer_close(
18622010 cx: &mut compositor::Context,
1863− _args: &[&str],
2011+ _args: &[Cow<str>],
18642012 _event: PromptEvent,
18652013 ) -> anyhow::Result<()> {
18662014 let view = view!(cx.editor);
@@ -1871,7 +2019,7 @@ mod cmd {
18712019
18722020 fn force_buffer_close(
18732021 cx: &mut compositor::Context,
1874− _args: &[&str],
2022+ _args: &[Cow<str>],
18752023 _event: PromptEvent,
18762024 ) -> anyhow::Result<()> {
18772025 let view = view!(cx.editor);
@@ -1880,15 +2028,12 @@ mod cmd {
18802028 Ok(())
18812029 }
18822030
1883− fn write_impl<P: AsRef<Path>>(
1884− cx: &mut compositor::Context,
1885− path: Option<P>,
1886− ) -> anyhow::Result<()> {
2031+ fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
18872032 let jobs = &mut cx.jobs;
18882033 let (_, doc) = current!(cx.editor);
18892034
1890− if let Some(path) = path {
1891− doc.set_path(Some(path.as_ref()))
2035+ if let Some(ref path) = path {
2036+ doc.set_path(Some(path.as_ref().as_ref()))
18922037 .context("invalid filepath")?;
18932038 }
18942039 if doc.path().is_none() {
@@ -1907,12 +2052,17 @@ mod cmd {
19072052 });
19082053 let future = doc.format_and_save(fmt);
19092054 cx.jobs.add(Job::new(future).wait_before_exiting());
2055+
2056+ if path.is_some() {
2057+ let id = doc.id();
2058+ let _ = cx.editor.refresh_language_server(id);
2059+ }
19102060 Ok(())
19112061 }
19122062
19132063 fn write(
19142064 cx: &mut compositor::Context,
1915− args: &[&str],
2065+ args: &[Cow<str>],
19162066 _event: PromptEvent,
19172067 ) -> anyhow::Result<()> {
19182068 write_impl(cx, args.first())
@@ -1920,7 +2070,7 @@ mod cmd {
19202070
19212071 fn new_file(
19222072 cx: &mut compositor::Context,
1923− _args: &[&str],
2073+ _args: &[Cow<str>],
19242074 _event: PromptEvent,
19252075 ) -> anyhow::Result<()> {
19262076 cx.editor.new_file(Action::Replace);
@@ -1930,7 +2080,7 @@ mod cmd {
19302080
19312081 fn format(
19322082 cx: &mut compositor::Context,
1933− _args: &[&str],
2083+ _args: &[Cow<str>],
19342084 _event: PromptEvent,
19352085 ) -> anyhow::Result<()> {
19362086 let (_, doc) = current!(cx.editor);
@@ -1945,7 +2095,7 @@ mod cmd {
19452095 }
19462096 fn set_indent_style(
19472097 cx: &mut compositor::Context,
1948− args: &[&str],
2098+ args: &[Cow<str>],
19492099 _event: PromptEvent,
19502100 ) -> anyhow::Result<()> {
19512101 use IndentStyle::*;
@@ -1965,7 +2115,7 @@ mod cmd {
19652115 // Attempt to parse argument as an indent style.
19662116 let style = match args.get(0) {
19672117 Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
1968− Some(&"0") => Some(Tabs),
2118+ Some(Cow::Borrowed("0")) => Some(Tabs),
19692119 Some(arg) => arg
19702120 .parse::<u8>()
19712121 .ok()
@@ -1984,7 +2134,7 @@ mod cmd {
19842134 /// Sets or reports the current document's line ending setting.
19852135 fn set_line_ending(
19862136 cx: &mut compositor::Context,
1987− args: &[&str],
2137+ args: &[Cow<str>],
19882138 _event: PromptEvent,
19892139 ) -> anyhow::Result<()> {
19902140 use LineEnding::*;
@@ -2028,7 +2178,7 @@ mod cmd {
20282178
20292179 fn earlier(
20302180 cx: &mut compositor::Context,
2031− args: &[&str],
2181+ args: &[Cow<str>],
20322182 _event: PromptEvent,
20332183 ) -> anyhow::Result<()> {
20342184 let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2044,7 +2194,7 @@ mod cmd {
20442194
20452195 fn later(
20462196 cx: &mut compositor::Context,
2047− args: &[&str],
2197+ args: &[Cow<str>],
20482198 _event: PromptEvent,
20492199 ) -> anyhow::Result<()> {
20502200 let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2059,7 +2209,7 @@ mod cmd {
20592209
20602210 fn write_quit(
20612211 cx: &mut compositor::Context,
2062− args: &[&str],
2212+ args: &[Cow<str>],
20632213 event: PromptEvent,
20642214 ) -> anyhow::Result<()> {
20652215 write_impl(cx, args.first())?;
@@ -2068,7 +2218,7 @@ mod cmd {
20682218
20692219 fn force_write_quit(
20702220 cx: &mut compositor::Context,
2071− args: &[&str],
2221+ args: &[Cow<str>],
20722222 event: PromptEvent,
20732223 ) -> anyhow::Result<()> {
20742224 write_impl(cx, args.first())?;
@@ -2099,7 +2249,7 @@ mod cmd {
20992249
21002250 fn write_all_impl(
21012251 cx: &mut compositor::Context,
2102− _args: &[&str],
2252+ _args: &[Cow<str>],
21032253 _event: PromptEvent,
21042254 quit: bool,
21052255 force: bool,
@@ -2135,7 +2285,7 @@ mod cmd {
21352285
21362286 fn write_all(
21372287 cx: &mut compositor::Context,
2138− args: &[&str],
2288+ args: &[Cow<str>],
21392289 event: PromptEvent,
21402290 ) -> anyhow::Result<()> {
21412291 write_all_impl(cx, args, event, false, false)
@@ -2143,7 +2293,7 @@ mod cmd {
21432293
21442294 fn write_all_quit(
21452295 cx: &mut compositor::Context,
2146− args: &[&str],
2296+ args: &[Cow<str>],
21472297 event: PromptEvent,
21482298 ) -> anyhow::Result<()> {
21492299 write_all_impl(cx, args, event, true, false)
@@ -2151,7 +2301,7 @@ mod cmd {
21512301
21522302 fn force_write_all_quit(
21532303 cx: &mut compositor::Context,
2154− args: &[&str],
2304+ args: &[Cow<str>],
21552305 event: PromptEvent,
21562306 ) -> anyhow::Result<()> {
21572307 write_all_impl(cx, args, event, true, true)
@@ -2159,7 +2309,7 @@ mod cmd {
21592309
21602310 fn quit_all_impl(
21612311 editor: &mut Editor,
2162− _args: &[&str],
2312+ _args: &[Cow<str>],
21632313 _event: PromptEvent,
21642314 force: bool,
21652315 ) -> anyhow::Result<()> {
@@ -2178,23 +2328,23 @@ mod cmd {
21782328
21792329 fn quit_all(
21802330 cx: &mut compositor::Context,
2181− args: &[&str],
2331+ args: &[Cow<str>],
21822332 event: PromptEvent,
21832333 ) -> anyhow::Result<()> {
2184− quit_all_impl(&mut cx.editor, args, event, false)
2334+ quit_all_impl(cx.editor, args, event, false)
21852335 }
21862336
21872337 fn force_quit_all(
21882338 cx: &mut compositor::Context,
2189− args: &[&str],
2339+ args: &[Cow<str>],
21902340 event: PromptEvent,
21912341 ) -> anyhow::Result<()> {
2192− quit_all_impl(&mut cx.editor, args, event, true)
2342+ quit_all_impl(cx.editor, args, event, true)
21932343 }
21942344
21952345 fn cquit(
21962346 cx: &mut compositor::Context,
2197− args: &[&str],
2347+ args: &[Cow<str>],
21982348 _event: PromptEvent,
21992349 ) -> anyhow::Result<()> {
22002350 let exit_code = args
@@ -2213,85 +2363,91 @@ mod cmd {
22132363
22142364 fn theme(
22152365 cx: &mut compositor::Context,
2216− args: &[&str],
2366+ args: &[Cow<str>],
22172367 _event: PromptEvent,
22182368 ) -> anyhow::Result<()> {
2219− let theme = args.first().context("theme not provided")?;
2220− cx.editor.set_theme_from_name(theme)
2369+ let theme = args.first().context("Theme not provided")?;
2370+ let theme = cx
2371+ .editor
2372+ .theme_loader
2373+ .load(theme)
2374+ .with_context(|| format!("Failed setting theme {}", theme))?;
2375+ let true_color = cx.editor.config.true_color || crate::true_color();
2376+ if !(true_color || theme.is_16_color()) {
2377+ bail!("Unsupported theme: theme requires true color support");
2378+ }
2379+ cx.editor.set_theme(theme);
2380+ Ok(())
22212381 }
22222382
22232383 fn yank_main_selection_to_clipboard(
22242384 cx: &mut compositor::Context,
2225− _args: &[&str],
2385+ _args: &[Cow<str>],
22262386 _event: PromptEvent,
22272387 ) -> anyhow::Result<()> {
2228− yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
2388+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
22292389 }
22302390
22312391 fn yank_joined_to_clipboard(
22322392 cx: &mut compositor::Context,
2233− args: &[&str],
2393+ args: &[Cow<str>],
22342394 _event: PromptEvent,
22352395 ) -> anyhow::Result<()> {
22362396 let (_, doc) = current!(cx.editor);
2237− let separator = args
2238− .first()
2239− .copied()
2240− .unwrap_or_else(|| doc.line_ending.as_str());
2241− yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
2397+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2398+ let separator = args.first().unwrap_or(&default_sep);
2399+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
22422400 }
22432401
22442402 fn yank_main_selection_to_primary_clipboard(
22452403 cx: &mut compositor::Context,
2246− _args: &[&str],
2404+ _args: &[Cow<str>],
22472405 _event: PromptEvent,
22482406 ) -> anyhow::Result<()> {
2249− yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
2407+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
22502408 }
22512409
22522410 fn yank_joined_to_primary_clipboard(
22532411 cx: &mut compositor::Context,
2254− args: &[&str],
2412+ args: &[Cow<str>],
22552413 _event: PromptEvent,
22562414 ) -> anyhow::Result<()> {
22572415 let (_, doc) = current!(cx.editor);
2258− let separator = args
2259− .first()
2260− .copied()
2261− .unwrap_or_else(|| doc.line_ending.as_str());
2262− yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
2416+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2417+ let separator = args.first().unwrap_or(&default_sep);
2418+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
22632419 }
22642420
22652421 fn paste_clipboard_after(
22662422 cx: &mut compositor::Context,
2267− _args: &[&str],
2423+ _args: &[Cow<str>],
22682424 _event: PromptEvent,
22692425 ) -> anyhow::Result<()> {
2270− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
2426+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
22712427 }
22722428
22732429 fn paste_clipboard_before(
22742430 cx: &mut compositor::Context,
2275− _args: &[&str],
2431+ _args: &[Cow<str>],
22762432 _event: PromptEvent,
22772433 ) -> anyhow::Result<()> {
2278− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
2434+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
22792435 }
22802436
22812437 fn paste_primary_clipboard_after(
22822438 cx: &mut compositor::Context,
2283− _args: &[&str],
2439+ _args: &[Cow<str>],
22842440 _event: PromptEvent,
22852441 ) -> anyhow::Result<()> {
2286− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
2442+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
22872443 }
22882444
22892445 fn paste_primary_clipboard_before(
22902446 cx: &mut compositor::Context,
2291− _args: &[&str],
2447+ _args: &[Cow<str>],
22922448 _event: PromptEvent,
22932449 ) -> anyhow::Result<()> {
2294− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
2450+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
22952451 }
22962452
22972453 fn replace_selections_with_clipboard_impl(
@@ -2318,7 +2474,7 @@ mod cmd {
23182474
23192475 fn replace_selections_with_clipboard(
23202476 cx: &mut compositor::Context,
2321− _args: &[&str],
2477+ _args: &[Cow<str>],
23222478 _event: PromptEvent,
23232479 ) -> anyhow::Result<()> {
23242480 replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2326,7 +2482,7 @@ mod cmd {
23262482
23272483 fn replace_selections_with_primary_clipboard(
23282484 cx: &mut compositor::Context,
2329− _args: &[&str],
2485+ _args: &[Cow<str>],
23302486 _event: PromptEvent,
23312487 ) -> anyhow::Result<()> {
23322488 replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2334,7 +2490,7 @@ mod cmd {
23342490
23352491 fn show_clipboard_provider(
23362492 cx: &mut compositor::Context,
2337− _args: &[&str],
2493+ _args: &[Cow<str>],
23382494 _event: PromptEvent,
23392495 ) -> anyhow::Result<()> {
23402496 cx.editor
@@ -2344,12 +2500,13 @@ mod cmd {
23442500
23452501 fn change_current_directory(
23462502 cx: &mut compositor::Context,
2347− args: &[&str],
2503+ args: &[Cow<str>],
23482504 _event: PromptEvent,
23492505 ) -> anyhow::Result<()> {
23502506 let dir = helix_core::path::expand_tilde(
23512507 args.first()
23522508 .context("target directory not provided")?
2509+ .as_ref()
23532510 .as_ref(),
23542511 );
23552512
@@ -2367,7 +2524,7 @@ mod cmd {
23672524
23682525 fn show_current_directory(
23692526 cx: &mut compositor::Context,
2370− _args: &[&str],
2527+ _args: &[Cow<str>],
23712528 _event: PromptEvent,
23722529 ) -> anyhow::Result<()> {
23732530 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2379,7 +2536,7 @@ mod cmd {
23792536 /// Sets the [`Document`]'s encoding..
23802537 fn set_encoding(
23812538 cx: &mut compositor::Context,
2382− args: &[&str],
2539+ args: &[Cow<str>],
23832540 _event: PromptEvent,
23842541 ) -> anyhow::Result<()> {
23852542 let (_, doc) = current!(cx.editor);
@@ -2395,7 +2552,7 @@ mod cmd {
23952552 /// Reload the [`Document`] from its source file.
23962553 fn reload(
23972554 cx: &mut compositor::Context,
2398− _args: &[&str],
2555+ _args: &[Cow<str>],
23992556 _event: PromptEvent,
24002557 ) -> anyhow::Result<()> {
24012558 let (view, doc) = current!(cx.editor);
@@ -2404,7 +2561,7 @@ mod cmd {
24042561
24052562 fn tree_sitter_scopes(
24062563 cx: &mut compositor::Context,
2407− _args: &[&str],
2564+ _args: &[Cow<str>],
24082565 _event: PromptEvent,
24092566 ) -> anyhow::Result<()> {
24102567 let (view, doc) = current!(cx.editor);
@@ -2418,15 +2575,18 @@ mod cmd {
24182575
24192576 fn vsplit(
24202577 cx: &mut compositor::Context,
2421− args: &[&str],
2578+ args: &[Cow<str>],
24222579 _event: PromptEvent,
24232580 ) -> anyhow::Result<()> {
24242581 let id = view!(cx.editor).doc;
24252582
2426− if let Some(path) = args.get(0) {
2427− cx.editor.open(path.into(), Action::VerticalSplit)?;
2428− } else {
2583+ if args.is_empty() {
24292584 cx.editor.switch(id, Action::VerticalSplit);
2585+ } else {
2586+ for arg in args {
2587+ cx.editor
2588+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
2589+ }
24302590 }
24312591
24322592 Ok(())
@@ -2434,15 +2594,18 @@ mod cmd {
24342594
24352595 fn hsplit(
24362596 cx: &mut compositor::Context,
2437− args: &[&str],
2597+ args: &[Cow<str>],
24382598 _event: PromptEvent,
24392599 ) -> anyhow::Result<()> {
24402600 let id = view!(cx.editor).doc;
24412601
2442− if let Some(path) = args.get(0) {
2443− cx.editor.open(path.into(), Action::HorizontalSplit)?;
2444− } else {
2602+ if args.is_empty() {
24452603 cx.editor.switch(id, Action::HorizontalSplit);
2604+ } else {
2605+ for arg in args {
2606+ cx.editor
2607+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
2608+ }
24462609 }
24472610
24482611 Ok(())
@@ -2450,7 +2613,7 @@ mod cmd {
24502613
24512614 fn tutor(
24522615 cx: &mut compositor::Context,
2453− _args: &[&str],
2616+ _args: &[Cow<str>],
24542617 _event: PromptEvent,
24552618 ) -> anyhow::Result<()> {
24562619 let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2460,6 +2623,24 @@ mod cmd {
24602623 Ok(())
24612624 }
24622625
2626+ pub(super) fn goto_line_number(
2627+ cx: &mut compositor::Context,
2628+ args: &[Cow<str>],
2629+ _event: PromptEvent,
2630+ ) -> anyhow::Result<()> {
2631+ ensure!(!args.is_empty(), "Line number required");
2632+
2633+ let line = args[0].parse::<usize>()?;
2634+
2635+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
2636+
2637+ let (view, doc) = current!(cx.editor);
2638+
2639+ view.ensure_cursor_in_view(doc, line);
2640+
2641+ Ok(())
2642+ }
2643+
24632644 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
24642645 TypableCommand {
24652646 name: "quit",
@@ -2513,7 +2694,7 @@ mod cmd {
25132694 TypableCommand {
25142695 name: "format",
25152696 aliases: &["fmt"],
2516− doc: "Format the file using a formatter.",
2697+ doc: "Format the file using the LSP formatter.",
25172698 fun: format,
25182699 completer: None,
25192700 },
@@ -2604,7 +2785,7 @@ mod cmd {
26042785 TypableCommand {
26052786 name: "theme",
26062787 aliases: &[],
2607− doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
2788+ doc: "Change the editor theme.",
26082789 fun: theme,
26092790 completer: Some(completers::theme),
26102791 },
@@ -2688,7 +2869,7 @@ mod cmd {
26882869 TypableCommand {
26892870 name: "change-current-directory",
26902871 aliases: &["cd"],
2691− doc: "Change the current working directory (:cd <dir>).",
2872+ doc: "Change the current working directory.",
26922873 fun: change_current_directory,
26932874 completer: Some(completers::directory),
26942875 },
@@ -2741,17 +2922,25 @@ mod cmd {
27412922 fun: tutor,
27422923 completer: None,
27432924 },
2925+ TypableCommand {
2926+ name: "goto",
2927+ aliases: &["g"],
2928+ doc: "Go to line number.",
2929+ fun: goto_line_number,
2930+ completer: None,
2931+ }
27442932 ];
27452933
2746− pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2747− TYPABLE_COMMAND_LIST
2748− .iter()
2749− .flat_map(|cmd| {
2750− std::iter::once((cmd.name, cmd))
2751− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2752− })
2753− .collect()
2754− });
2934+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
2935+ Lazy::new(|| {
2936+ TYPABLE_COMMAND_LIST
2937+ .iter()
2938+ .flat_map(|cmd| {
2939+ std::iter::once((cmd.name, cmd))
2940+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2941+ })
2942+ .collect()
2943+ });
27552944}
27562945
27572946fn command_mode(cx: &mut Context) {
@@ -2777,7 +2966,7 @@ fn command_mode(cx: &mut Context) {
27772966 if let Some(cmd::TypableCommand {
27782967 completer: Some(completer),
27792968 ..
2780− }) = cmd::COMMANDS.get(parts[0])
2969+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
27812970 {
27822971 completer(part)
27832972 .into_iter()
@@ -2803,8 +2992,18 @@ fn command_mode(cx: &mut Context) {
28032992 return;
28042993 }
28052994
2806− if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2807− if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2995+ // If command is numeric, interpret as line number and go there.
2996+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
2997+ if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
2998+ cx.editor.set_error(format!("{}", e));
2999+ }
3000+ return;
3001+ }
3002+
3003+ // Handle typable commands
3004+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
3005+ let args = shellwords::shellwords(input);
3006+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
28083007 cx.editor.set_error(format!("{}", e));
28093008 }
28103009 } else {
@@ -2816,7 +3015,7 @@ fn command_mode(cx: &mut Context) {
28163015 prompt.doc_fn = Box::new(|input: &str| {
28173016 let part = input.split(' ').next().unwrap_or_default();
28183017
2819− if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
3018+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
28203019 return Some(doc);
28213020 }
28223021
@@ -3254,7 +3453,7 @@ fn apply_workspace_edit(
32543453
32553454fn last_picker(cx: &mut Context) {
32563455 // TODO: last picker does not seem to work well with buffer_picker
3257− cx.callback = Some(Box::new(|compositor: &mut Compositor| {
3456+ cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
32583457 if let Some(picker) = compositor.last_picker.take() {
32593458 compositor.push(picker);
32603459 }
@@ -3436,10 +3635,14 @@ fn push_jump(editor: &mut Editor) {
34363635}
34373636
34383637fn goto_line(cx: &mut Context) {
3439− if let Some(count) = cx.count {
3440− push_jump(cx.editor);
3638+ goto_line_impl(cx.editor, cx.count)
3639+}
34413640
3442− let (view, doc) = current!(cx.editor);
3641+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
3642+ if let Some(count) = count {
3643+ push_jump(editor);
3644+
3645+ let (view, doc) = current!(editor);
34433646 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
34443647 // If the last line is blank, don't jump to it.
34453648 doc.text().len_lines().saturating_sub(2)
@@ -3498,6 +3701,20 @@ fn goto_last_modification(cx: &mut Context) {
34983701 }
34993702}
35003703
3704+fn goto_last_modified_file(cx: &mut Context) {
3705+ let view = view!(cx.editor);
3706+ let alternate_file = view
3707+ .last_modified_docs
3708+ .into_iter()
3709+ .flatten()
3710+ .find(|&id| id != view.doc);
3711+ if let Some(alt) = alternate_file {
3712+ cx.editor.switch(alt, Action::Replace);
3713+ } else {
3714+ cx.editor.set_error("no last modified buffer".to_owned())
3715+ }
3716+}
3717+
35013718fn select_mode(cx: &mut Context) {
35023719 let (view, doc) = current!(cx.editor);
35033720 let text = doc.text().slice(..);
@@ -3982,8 +4199,9 @@ pub mod insert {
39824199 // The default insert hook: simply insert the character
39834200 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
39844201 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
4202+ let cursors = selection.clone().cursors(doc.slice(..));
39854203 let t = Tendril::from_char(ch);
3986− let transaction = Transaction::insert(doc, selection, t);
4204+ let transaction = Transaction::insert(doc, &cursors, t);
39874205 Some(transaction)
39884206 }
39894207
@@ -3998,11 +4216,11 @@ pub mod insert {
39984216 };
39994217
40004218 let text = doc.text();
4001− let selection = doc.selection(view.id).clone().cursors(text.slice(..));
4219+ let selection = doc.selection(view.id);
40024220
40034221 // run through insert hooks, stopping on the first one that returns Some(t)
40044222 for hook in hooks {
4005− if let Some(transaction) = hook(text, &selection, c) {
4223+ if let Some(transaction) = hook(text, selection, c) {
40064224 doc.apply(&transaction, view.id);
40074225 break;
40084226 }
@@ -4317,11 +4535,8 @@ fn yank_joined_to_clipboard_impl(
43174535
43184536fn yank_joined_to_clipboard(cx: &mut Context) {
43194537 let line_ending = doc!(cx.editor).line_ending;
4320− let _ = yank_joined_to_clipboard_impl(
4321− &mut cx.editor,
4322− line_ending.as_str(),
4323− ClipboardType::Clipboard,
4324− );
4538+ let _ =
4539+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
43254540 exit_select_mode(cx);
43264541}
43274542
@@ -4346,20 +4561,17 @@ fn yank_main_selection_to_clipboard_impl(
43464561}
43474562
43484563fn yank_main_selection_to_clipboard(cx: &mut Context) {
4349− let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
4564+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
43504565}
43514566
43524567fn yank_joined_to_primary_clipboard(cx: &mut Context) {
43534568 let line_ending = doc!(cx.editor).line_ending;
4354− let _ = yank_joined_to_clipboard_impl(
4355− &mut cx.editor,
4356− line_ending.as_str(),
4357− ClipboardType::Selection,
4358− );
4569+ let _ =
4570+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
43594571}
43604572
43614573fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4362− let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
4574+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
43634575 exit_select_mode(cx);
43644576}
43654577
@@ -4374,11 +4586,12 @@ fn paste_impl(
43744586 doc: &mut Document,
43754587 view: &View,
43764588 action: Paste,
4589+ count: usize,
43774590) -> Option<Transaction> {
43784591 let repeat = std::iter::repeat(
43794592 values
43804593 .last()
4381− .map(|value| Tendril::from_slice(value))
4594+ .map(|value| Tendril::from(value.repeat(count)))
43824595 .unwrap(),
43834596 );
43844597
@@ -4393,7 +4606,7 @@ fn paste_impl(
43934606 let mut values = values
43944607 .iter()
43954608 .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
4396− .map(|value| Tendril::from(value.as_ref()))
4609+ .map(|value| Tendril::from(value.as_ref().repeat(count)))
43974610 .chain(repeat);
43984611
43994612 let text = doc.text();
@@ -4413,7 +4626,7 @@ fn paste_impl(
44134626 // paste append
44144627 (Paste::After, false) => range.to(),
44154628 };
4416− (pos, pos, Some(values.next().unwrap()))
4629+ (pos, pos, values.next())
44174630 });
44184631
44194632 Some(transaction)
@@ -4423,13 +4636,14 @@ fn paste_clipboard_impl(
44234636 editor: &mut Editor,
44244637 action: Paste,
44254638 clipboard_type: ClipboardType,
4639+ count: usize,
44264640) -> anyhow::Result<()> {
44274641 let (view, doc) = current!(editor);
44284642
44294643 match editor
44304644 .clipboard_provider
44314645 .get_contents(clipboard_type)
4432− .map(|contents| paste_impl(&[contents], doc, view, action))
4646+ .map(|contents| paste_impl(&[contents], doc, view, action, count))
44334647 {
44344648 Ok(Some(transaction)) => {
44354649 doc.apply(&transaction, view.id);
@@ -4442,22 +4656,43 @@ fn paste_clipboard_impl(
44424656}
44434657
44444658fn paste_clipboard_after(cx: &mut Context) {
4445− let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
4659+ let _ = paste_clipboard_impl(
4660+ cx.editor,
4661+ Paste::After,
4662+ ClipboardType::Clipboard,
4663+ cx.count(),
4664+ );
44464665}
44474666
44484667fn paste_clipboard_before(cx: &mut Context) {
4449− let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
4668+ let _ = paste_clipboard_impl(
4669+ cx.editor,
4670+ Paste::Before,
4671+ ClipboardType::Clipboard,
4672+ cx.count(),
4673+ );
44504674}
44514675
44524676fn paste_primary_clipboard_after(cx: &mut Context) {
4453− let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
4677+ let _ = paste_clipboard_impl(
4678+ cx.editor,
4679+ Paste::After,
4680+ ClipboardType::Selection,
4681+ cx.count(),
4682+ );
44544683}
44554684
44564685fn paste_primary_clipboard_before(cx: &mut Context) {
4457− let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
4686+ let _ = paste_clipboard_impl(
4687+ cx.editor,
4688+ Paste::Before,
4689+ ClipboardType::Selection,
4690+ cx.count(),
4691+ );
44584692}
44594693
44604694fn replace_with_yanked(cx: &mut Context) {
4695+ let count = cx.count();
44614696 let reg_name = cx.register.unwrap_or('"');
44624697 let (view, doc) = current!(cx.editor);
44634698 let registers = &mut cx.editor.registers;
@@ -4467,12 +4702,12 @@ fn replace_with_yanked(cx: &mut Context) {
44674702 let repeat = std::iter::repeat(
44684703 values
44694704 .last()
4470− .map(|value| Tendril::from_slice(value))
4705+ .map(|value| Tendril::from_slice(&value.repeat(count)))
44714706 .unwrap(),
44724707 );
44734708 let mut values = values
44744709 .iter()
4475− .map(|value| Tendril::from_slice(value))
4710+ .map(|value| Tendril::from_slice(&value.repeat(count)))
44764711 .chain(repeat);
44774712 let selection = doc.selection(view.id);
44784713 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4492,6 +4727,7 @@ fn replace_with_yanked(cx: &mut Context) {
44924727fn replace_selections_with_clipboard_impl(
44934728 editor: &mut Editor,
44944729 clipboard_type: ClipboardType,
4730+ count: usize,
44954731) -> anyhow::Result<()> {
44964732 let (view, doc) = current!(editor);
44974733
@@ -4499,7 +4735,11 @@ fn replace_selections_with_clipboard_impl(
44994735 Ok(contents) => {
45004736 let selection = doc.selection(view.id);
45014737 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4502− (range.from(), range.to(), Some(contents.as_str().into()))
4738+ (
4739+ range.from(),
4740+ range.to(),
4741+ Some(contents.repeat(count).as_str().into()),
4742+ )
45034743 });
45044744
45054745 doc.apply(&transaction, view.id);
@@ -4511,21 +4751,22 @@ fn replace_selections_with_clipboard_impl(
45114751}
45124752
45134753fn replace_selections_with_clipboard(cx: &mut Context) {
4514− let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
4754+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
45154755}
45164756
45174757fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4518− let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
4758+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
45194759}
45204760
45214761fn paste_after(cx: &mut Context) {
4762+ let count = cx.count();
45224763 let reg_name = cx.register.unwrap_or('"');
45234764 let (view, doc) = current!(cx.editor);
45244765 let registers = &mut cx.editor.registers;
45254766
45264767 if let Some(transaction) = registers
45274768 .read(reg_name)
4528− .and_then(|values| paste_impl(values, doc, view, Paste::After))
4769+ .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
45294770 {
45304771 doc.apply(&transaction, view.id);
45314772 doc.append_changes_to_history(view.id);
@@ -4533,13 +4774,14 @@ fn paste_after(cx: &mut Context) {
45334774}
45344775
45354776fn paste_before(cx: &mut Context) {
4777+ let count = cx.count();
45364778 let reg_name = cx.register.unwrap_or('"');
45374779 let (view, doc) = current!(cx.editor);
45384780 let registers = &mut cx.editor.registers;
45394781
45404782 if let Some(transaction) = registers
45414783 .read(reg_name)
4542− .and_then(|values| paste_impl(values, doc, view, Paste::Before))
4784+ .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
45434785 {
45444786 doc.apply(&transaction, view.id);
45454787 doc.append_changes_to_history(view.id);
@@ -4935,8 +5177,12 @@ fn hover(cx: &mut Context) {
49355177 // skip if contents empty
49365178
49375179 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
4938− let popup = Popup::new(contents);
4939− compositor.push(Box::new(popup));
5180+ let popup = Popup::new("documentation", contents);
5181+ if let Some(doc_popup) = compositor.find_id("documentation") {
5182+ *doc_popup = popup;
5183+ } else {
5184+ compositor.push(Box::new(popup));
5185+ }
49405186 }
49415187 },
49425188 );
@@ -5030,7 +5276,7 @@ fn expand_selection(cx: &mut Context) {
50305276 doc.set_selection(view.id, selection);
50315277 }
50325278 };
5033− motion(&mut cx.editor);
5279+ motion(cx.editor);
50345280 cx.editor.last_motion = Some(Motion(Box::new(motion)));
50355281}
50365282
@@ -5086,6 +5332,12 @@ fn jump_backward(cx: &mut Context) {
50865332 };
50875333}
50885334
5335+fn save_selection(cx: &mut Context) {
5336+ push_jump(cx.editor);
5337+ cx.editor
5338+ .set_status("Selection saved to jumplist".to_owned());
5339+}
5340+
50895341fn rotate_view(cx: &mut Context) {
50905342 cx.editor.focus_next()
50915343}
@@ -5262,7 +5514,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
52625514 });
52635515 doc.set_selection(view.id, selection);
52645516 };
5265− textobject(&mut cx.editor);
5517+ textobject(cx.editor);
52665518 cx.editor.last_motion = Some(Motion(Box::new(textobject)));
52675519 }
52685520 })
@@ -5428,9 +5680,7 @@ fn shell_impl(
54285680) -> anyhow::Result<(Tendril, bool)> {
54295681 use std::io::Write;
54305682 use std::process::{Command, Stdio};
5431− if shell.is_empty() {
5432− bail!("No shell set");
5433− }
5683+ ensure!(!shell.is_empty(), "No shell set");
54345684
54355685 let mut process = match Command::new(&shell[0])
54365686 .args(&shell[1..])
@@ -5594,7 +5844,7 @@ fn rename_symbol(cx: &mut Context) {
55945844 let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
55955845 let edits = block_on(task).unwrap_or_default();
55965846 log::debug!("Edits from LSP: {:?}", edits);
5597− apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
5847+ apply_workspace_edit(cx.editor, offset_encoding, &edits);
55985848 },
55995849 );
56005850 cx.push_layer(Box::new(prompt));
@@ -5614,16 +5864,45 @@ fn decrement(cx: &mut Context) {
56145864fn increment_impl(cx: &mut Context, amount: i64) {
56155865 let (view, doc) = current!(cx.editor);
56165866 let selection = doc.selection(view.id);
5617− let text = doc.text();
5867+ let text = doc.text().slice(..);
5868+
5869+ let changes: Vec<_> = selection
5870+ .ranges()
5871+ .iter()
5872+ .filter_map(|range| {
5873+ let incrementor: Box<dyn Increment> =
5874+ if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
5875+ Box::new(incrementor)
5876+ } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
5877+ Box::new(incrementor)
5878+ } else {
5879+ return None;
5880+ };
5881+
5882+ let (range, new_text) = incrementor.increment(amount);
5883+
5884+ Some((range.from(), range.to(), Some(new_text)))
5885+ })
5886+ .collect();
56185887
5619− let changes = selection.ranges().iter().filter_map(|range| {
5620− let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
5621− let new_text = incrementor.incremented_text(amount);
5622− Some((
5623− incrementor.range.from(),
5624− incrementor.range.to(),
5625− Some(new_text),
5626− ))
5888+ // Overlapping changes in a transaction will panic, so we need to find and remove them.
5889+ // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
5890+ // incrementing will give overlapping changes, with each change incrementing a different part of
5891+ // the date. Since these conflict with each other we remove these changes from the transaction
5892+ // so nothing happens.
5893+ let mut overlapping_indexes = HashSet::new();
5894+ for (i, changes) in changes.windows(2).enumerate() {
5895+ if changes[0].1 > changes[1].0 {
5896+ overlapping_indexes.insert(i);
5897+ overlapping_indexes.insert(i + 1);
5898+ }
5899+ }
5900+ let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
5901+ if overlapping_indexes.contains(&i) {
5902+ None
5903+ } else {
5904+ Some(change)
5905+ }
56275906 });
56285907
56295908 if changes.clone().count() > 0 {
@@ -5634,3 +5913,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
56345913 doc.append_changes_to_history(view.id);
56355914 }
56365915}
5916+
5917+fn record_macro(cx: &mut Context) {
5918+ if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
5919+ // Remove the keypress which ends the recording
5920+ keys.pop();
5921+ let s = keys
5922+ .into_iter()
5923+ .map(|key| format!("{}", key))
5924+ .collect::<Vec<_>>()
5925+ .join(" ");
5926+ cx.editor.registers.get_mut(reg).write(vec![s]);
5927+ cx.editor
5928+ .set_status(format!("Recorded to register {}", reg));
5929+ } else {
5930+ let reg = cx.register.take().unwrap_or('@');
5931+ cx.editor.macro_recording = Some((reg, Vec::new()));
5932+ cx.editor
5933+ .set_status(format!("Recording to register {}", reg));
5934+ }
5935+}
5936+
5937+fn play_macro(cx: &mut Context) {
5938+ let reg = cx.register.unwrap_or('@');
5939+ let keys = match cx
5940+ .editor
5941+ .registers
5942+ .get(reg)
5943+ .and_then(|reg| reg.read().get(0))
5944+ .context("Register empty")
5945+ .and_then(|s| {
5946+ s.split_whitespace()
5947+ .map(str::parse::<KeyEvent>)
5948+ .collect::<Result<Vec<_>, _>>()
5949+ .context("Failed to parse macro")
5950+ }) {
5951+ Ok(keys) => keys,
5952+ Err(e) => {
5953+ cx.editor.set_error(format!("{}", e));
5954+ return;
5955+ }
5956+ };
5957+ let count = cx.count();
5958+
5959+ cx.callback = Some(Box::new(
5960+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
5961+ for _ in 0..count {
5962+ for &key in keys.iter() {
5963+ compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
5964+ }
5965+ }
5966+ },
5967+ ));
5968+}
helix-term/src/compositor.rs+22−4
@@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
77use crossterm::event::Event;
88use tui::buffer::Buffer as Surface;
99
10−pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
10+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
1111
1212// --> EventResult should have a callback that takes a context with methods like .popup(),
1313// .prompt() etc. That way we can abstract it from the renderer.
@@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
5555
5656 /// May be used by the parent component to compute the child area.
5757 /// viewport is the maximum allowed area, and the child should stay within those bounds.
58+ ///
59+ /// The returned size might be larger than the viewport if the child is too big to fit.
60+ /// In this case the parent can use the values to calculate scroll.
5861 fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
59− // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
60− // that way render can use it
6162 None
6263 }
6364
6465 fn type_name(&self) -> &'static str {
6566 std::any::type_name::<Self>()
6667 }
68+
69+ fn id(&self) -> Option<&'static str> {
70+ None
71+ }
6772}
6873
6974use anyhow::Error;
@@ -126,12 +131,17 @@ impl Compositor {
126131 }
127132
128133 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
134+ // If it is a key event and a macro is being recorded, push the key event to the recording.
135+ if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
136+ keys.push(key.into());
137+ }
138+
129139 // propagate events through the layers until we either find a layer that consumes it or we
130140 // run out of layers (event bubbling)
131141 for layer in self.layers.iter_mut().rev() {
132142 match layer.handle_event(event, cx) {
133143 EventResult::Consumed(Some(callback)) => {
134− callback(self);
144+ callback(self, cx);
135145 return true;
136146 }
137147 EventResult::Consumed(None) => return true,
@@ -184,6 +194,14 @@ impl Compositor {
184194 .find(|component| component.type_name() == type_name)
185195 .and_then(|component| component.as_any_mut().downcast_mut())
186196 }
197+
198+ pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
199+ let type_name = std::any::type_name::<T>();
200+ self.layers
201+ .iter_mut()
202+ .find(|component| component.type_name() == type_name && component.id() == Some(id))
203+ .and_then(|component| component.as_any_mut().downcast_mut())
204+ }
187205}
188206
189207// View casting, taken straight from Cursive
helix-term/src/keymap.rs+32−23
@@ -1,4 +1,4 @@
1−pub use crate::commands::Command;
1+pub use crate::commands::MappableCommand;
22use crate::config::Config;
33use helix_core::hashmap;
44use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
9292#[macro_export]
9393macro_rules! keymap {
9494 (@trie $cmd:ident) => {
95− $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
95+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
9696 };
9797
9898 (@trie
@@ -120,7 +120,7 @@ macro_rules! keymap {
120120 _key,
121121 keymap!(@trie $value)
122122 );
123− debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
123+ assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
124124 _order.push(_key);
125125 )+
126126 )*
@@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
260260#[derive(Debug, Clone, PartialEq, Deserialize)]
261261#[serde(untagged)]
262262pub enum KeyTrie {
263− Leaf(Command),
264− Sequence(Vec<Command>),
263+ Leaf(MappableCommand),
264+ Sequence(Vec<MappableCommand>),
265265 Node(KeyTrieNode),
266266}
267267
@@ -304,9 +304,9 @@ impl KeyTrie {
304304pub enum KeymapResultKind {
305305 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
306306 Pending(KeyTrieNode),
307− Matched(Command),
307+ Matched(MappableCommand),
308308 /// Matched a sequence of commands to execute.
309− MatchedSequence(Vec<Command>),
309+ MatchedSequence(Vec<MappableCommand>),
310310 /// Key was not found in the root keymap
311311 NotFound,
312312 /// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -386,10 +386,10 @@ impl Keymap {
386386 };
387387
388388 let trie = match trie_node.search(&[*first]) {
389− Some(&KeyTrie::Leaf(cmd)) => {
390− return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
389+ Some(KeyTrie::Leaf(ref cmd)) => {
390+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
391391 }
392− Some(&KeyTrie::Sequence(ref cmds)) => {
392+ Some(KeyTrie::Sequence(ref cmds)) => {
393393 return KeymapResult::new(
394394 KeymapResultKind::MatchedSequence(cmds.clone()),
395395 self.sticky(),
@@ -408,9 +408,9 @@ impl Keymap {
408408 }
409409 KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
410410 }
411− Some(&KeyTrie::Leaf(cmd)) => {
411+ Some(&KeyTrie::Leaf(ref cmd)) => {
412412 self.state.clear();
413− return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
413+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
414414 }
415415 Some(&KeyTrie::Sequence(ref cmds)) => {
416416 self.state.clear();
@@ -512,6 +512,7 @@ impl Default for Keymaps {
512512 "g" => { "Goto"
513513 "g" => goto_file_start,
514514 "e" => goto_last_line,
515+ "f" => goto_file,
515516 "h" => goto_line_start,
516517 "l" => goto_line_end,
517518 "s" => goto_first_nonwhitespace,
@@ -520,9 +521,10 @@ impl Default for Keymaps {
520521 "r" => goto_reference,
521522 "i" => goto_implementation,
522523 "t" => goto_window_top,
523− "m" => goto_window_middle,
524+ "c" => goto_window_center,
524525 "b" => goto_window_bottom,
525526 "a" => goto_last_accessed_file,
527+ "m" => goto_last_modified_file,
526528 "n" => goto_next_buffer,
527529 "p" => goto_previous_buffer,
528530 "." => goto_last_modification,
@@ -537,9 +539,9 @@ impl Default for Keymaps {
537539 "O" => open_above,
538540
539541 "d" => delete_selection,
540− // TODO: also delete without yanking
542+ "A-d" => delete_selection_noyank,
541543 "c" => change_selection,
542− // TODO: also change delete without yanking
544+ "A-c" => change_selection_noyank,
543545
544546 "C" => copy_selection_on_next_line,
545547 "A-C" => copy_selection_on_prev_line,
@@ -591,6 +593,9 @@ impl Default for Keymaps {
591593 // paste_all
592594 "P" => paste_before,
593595
596+ "q" => record_macro,
597+ "Q" => play_macro,
598+
594599 ">" => indent,
595600 "<" => unindent,
596601 "=" => format_selections,
@@ -622,6 +627,8 @@ impl Default for Keymaps {
622627 "C-w" | "w" => rotate_view,
623628 "C-s" | "s" => hsplit,
624629 "C-v" | "v" => vsplit,
630+ "f" => goto_file_hsplit,
631+ "F" => goto_file_vsplit,
625632 "C-q" | "q" => wclose,
626633 "C-o" | "o" => wonly,
627634 "C-h" | "h" | "left" => jump_view_left,
@@ -637,7 +644,7 @@ impl Default for Keymaps {
637644
638645 "tab" => jump_forward, // tab == <C-i>
639646 "C-o" => jump_backward,
640− // "C-s" => save_selection,
647+ "C-s" => save_selection,
641648
642649 "space" => { "Space"
643650 "f" => file_picker,
@@ -650,6 +657,8 @@ impl Default for Keymaps {
650657 "C-w" | "w" => rotate_view,
651658 "C-s" | "s" => hsplit,
652659 "C-v" | "v" => vsplit,
660+ "f" => goto_file_hsplit,
661+ "F" => goto_file_vsplit,
653662 "C-q" | "q" => wclose,
654663 "C-o" | "o" => wonly,
655664 "C-h" | "h" | "left" => jump_view_left,
@@ -827,36 +836,36 @@ mod tests {
827836 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
828837 assert_eq!(
829838 keymap.get(key!('i')).kind,
830− KeymapResultKind::Matched(Command::normal_mode),
839+ KeymapResultKind::Matched(MappableCommand::normal_mode),
831840 "Leaf should replace leaf"
832841 );
833842 assert_eq!(
834843 keymap.get(key!('无')).kind,
835− KeymapResultKind::Matched(Command::insert_mode),
844+ KeymapResultKind::Matched(MappableCommand::insert_mode),
836845 "New leaf should be present in merged keymap"
837846 );
838847 // Assumes that z is a node in the default keymap
839848 assert_eq!(
840849 keymap.get(key!('z')).kind,
841− KeymapResultKind::Matched(Command::jump_backward),
850+ KeymapResultKind::Matched(MappableCommand::jump_backward),
842851 "Leaf should replace node"
843852 );
844853 // Assumes that `g` is a node in default keymap
845854 assert_eq!(
846855 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
847− &KeyTrie::Leaf(Command::goto_line_end),
856+ &KeyTrie::Leaf(MappableCommand::goto_line_end),
848857 "Leaf should be present in merged subnode"
849858 );
850859 // Assumes that `gg` is in default keymap
851860 assert_eq!(
852861 keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
853− &KeyTrie::Leaf(Command::delete_char_forward),
862+ &KeyTrie::Leaf(MappableCommand::delete_char_forward),
854863 "Leaf should replace old leaf in merged subnode"
855864 );
856865 // Assumes that `ge` is in default keymap
857866 assert_eq!(
858867 keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
859− &KeyTrie::Leaf(Command::goto_last_line),
868+ &KeyTrie::Leaf(MappableCommand::goto_last_line),
860869 "Old leaves in subnode should be present in merged node"
861870 );
862871
@@ -890,7 +899,7 @@ mod tests {
890899 .root()
891900 .search(&[key!(' '), key!('s'), key!('v')])
892901 .unwrap(),
893− &KeyTrie::Leaf(Command::vsplit),
902+ &KeyTrie::Leaf(MappableCommand::vsplit),
894903 "Leaf should be present in merged subnode"
895904 );
896905 // Make sure an order was set during merge
helix-term/src/lib.rs+11
@@ -9,3 +9,14 @@ pub mod config;
99pub mod job;
1010pub mod keymap;
1111pub mod ui;
12+
13+#[cfg(not(windows))]
14+fn true_color() -> bool {
15+ std::env::var("COLORTERM")
16+ .map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
17+ .unwrap_or(false)
18+}
19+#[cfg(windows)]
20+fn true_color() -> bool {
21+ true
22+}
helix-term/src/ui/completion.rs+3−3
@@ -168,7 +168,7 @@ impl Completion {
168168 }
169169 };
170170 });
171− let popup = Popup::new(menu);
171+ let popup = Popup::new("completion", menu);
172172 let mut completion = Self {
173173 popup,
174174 start_offset,
@@ -328,8 +328,8 @@ impl Component for Completion {
328328 let y = popup_y;
329329
330330 if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
331− width = rel_width;
332− height = rel_height;
331+ width = rel_width.min(width);
332+ height = rel_height.min(height);
333333 }
334334 Rect::new(x, y, width, height)
335335 } else {
helix-term/src/ui/editor.rs+58−82
@@ -17,7 +17,6 @@ use helix_core::{
1717};
1818use helix_view::{
1919 document::{Mode, SCRATCH_BUFFER_NAME},
20− editor::LineNumber,
2120 graphics::{CursorKind, Modifier, Rect, Style},
2221 info::Info,
2322 input::KeyEvent,
@@ -32,7 +31,7 @@ use tui::buffer::Buffer as Surface;
3231pub struct EditorView {
3332 keymaps: Keymaps,
3433 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
35− last_insert: (commands::Command, Vec<KeyEvent>),
34+ last_insert: (commands::MappableCommand, Vec<KeyEvent>),
3635 pub(crate) completion: Option<Completion>,
3736 spinners: ProgressSpinners,
3837 autoinfo: Option<Info>,
@@ -49,7 +48,7 @@ impl EditorView {
4948 Self {
5049 keymaps,
5150 on_next_key: None,
52− last_insert: (commands::Command::normal_mode, Vec::new()),
51+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
5352 completion: None,
5453 spinners: ProgressSpinners::default(),
5554 autoinfo: None,
@@ -310,17 +309,16 @@ impl EditorView {
310309
311310 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
312311
313− let style = spans.iter().fold(text_style, |acc, span| {
314− let style = theme.get(theme.scopes()[span.0].as_str());
315− acc.patch(style)
316− });
317−
318312 for grapheme in RopeGraphemes::new(text) {
319313 let out_of_bounds = visual_x < offset.col as u16
320314 || visual_x >= viewport.width + offset.col as u16;
321315
322316 if LineEnding::from_rope_slice(&grapheme).is_some() {
323317 if !out_of_bounds {
318+ let style = spans.iter().fold(text_style, |acc, span| {
319+ acc.patch(theme.highlight(span.0))
320+ });
321+
324322 // we still want to render an empty cell with the style
325323 surface.set_string(
326324 viewport.x + visual_x - offset.col as u16,
@@ -351,6 +349,10 @@ impl EditorView {
351349 };
352350
353351 if !out_of_bounds {
352+ let style = spans.iter().fold(text_style, |acc, span| {
353+ acc.patch(theme.highlight(span.0))
354+ });
355+
354356 // if we're offscreen just keep going until we hit a new line
355357 surface.set_string(
356358 viewport.x + visual_x - offset.col as u16,
@@ -417,22 +419,6 @@ impl EditorView {
417419 let text = doc.text().slice(..);
418420 let last_line = view.last_line(doc);
419421
420− let linenr = theme.get("ui.linenr");
421− let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
422−
423− let warning = theme.get("warning");
424− let error = theme.get("error");
425− let info = theme.get("info");
426− let hint = theme.get("hint");
427−
428− // Whether to draw the line number for the last line of the
429− // document or not. We only draw it if it's not an empty line.
430− let draw_last = text.line_to_byte(last_line) < text.len_bytes();
431−
432− let current_line = doc
433− .text()
434− .char_to_line(doc.selection(view.id).primary().cursor(text));
435−
436422 // it's used inside an iterator so the collect isn't needless:
437423 // https://github.com/rust-lang/rust-clippy/issues/6164
438424 #[allow(clippy::needless_collect)]
@@ -442,51 +428,31 @@ impl EditorView {
442428 .map(|range| range.cursor_line(text))
443429 .collect();
444430
445− for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
446− use helix_core::diagnostic::Severity;
447− if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
448− surface.set_stringn(
449− viewport.x,
450− viewport.y + i as u16,
451− "●",
452− 1,
453− match diagnostic.severity {
454− Some(Severity::Error) => error,
455− Some(Severity::Warning) | None => warning,
456− Some(Severity::Info) => info,
457− Some(Severity::Hint) => hint,
458− },
459− );
460− }
431+ let mut offset = 0;
461432
462− let selected = cursors.contains(&line);
433+ let gutter_style = theme.get("ui.gutter");
463434
464− let text = if line == last_line && !draw_last {
465− " ~".into()
466− } else {
467− let line = match config.line_number {
468− LineNumber::Absolute => line + 1,
469− LineNumber::Relative => {
470− if current_line == line {
471− line + 1
472− } else {
473− abs_diff(current_line, line)
474− }
475− }
476− };
477− format!("{:>5}", line)
478− };
479− surface.set_stringn(
480− viewport.x + 1,
481− viewport.y + i as u16,
482− text,
483− 5,
484− if selected && is_focused {
485− linenr_select
486− } else {
487− linenr
488− },
489− );
435+ // avoid lots of small allocations by reusing a text buffer for each line
436+ let mut text = String::with_capacity(8);
437+
438+ for (constructor, width) in view.gutters() {
439+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
440+ text.reserve(*width); // ensure there's enough space for the gutter
441+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
442+ let selected = cursors.contains(&line);
443+
444+ if let Some(style) = gutter(line, selected, &mut text) {
445+ surface.set_stringn(
446+ viewport.x + offset,
447+ viewport.y + i as u16,
448+ &text,
449+ *width,
450+ gutter_style.patch(style),
451+ );
452+ }
453+ text.clear();
454+ }
455+ offset += *width as u16;
490456 }
491457 }
492458
@@ -916,7 +882,7 @@ impl EditorView {
916882 return EventResult::Ignored;
917883 }
918884
919− commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
885+ commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
920886
921887 EventResult::Consumed(None)
922888 }
@@ -934,7 +900,8 @@ impl EditorView {
934900 }
935901
936902 if modifiers == crossterm::event::KeyModifiers::ALT {
937− commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
903+ commands::MappableCommand::replace_selections_with_primary_clipboard
904+ .execute(cxt);
938905
939906 return EventResult::Consumed(None);
940907 }
@@ -948,7 +915,7 @@ impl EditorView {
948915 let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
949916 doc.set_selection(view_id, Selection::point(pos));
950917 editor.tree.focus = view_id;
951− commands::Command::paste_primary_clipboard_before.execute(cxt);
918+ commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
952919 return EventResult::Consumed(None);
953920 }
954921
@@ -963,7 +930,7 @@ impl EditorView {
963930impl Component for EditorView {
964931 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
965932 let mut cxt = commands::Context {
966− editor: &mut cx.editor,
933+ editor: cx.editor,
967934 count: None,
968935 register: None,
969936 callback: None,
@@ -1140,13 +1107,31 @@ impl Component for EditorView {
11401107 disp.push_str(&s);
11411108 }
11421109 }
1110+ let style = cx.editor.theme.get("ui.text");
1111+ let macro_width = if cx.editor.macro_recording.is_some() {
1112+ 3
1113+ } else {
1114+ 0
1115+ };
11431116 surface.set_string(
1144− area.x + area.width.saturating_sub(key_width),
1117+ area.x + area.width.saturating_sub(key_width + macro_width),
11451118 area.y + area.height.saturating_sub(1),
11461119 disp.get(disp.len().saturating_sub(key_width as usize)..)
11471120 .unwrap_or(&disp),
1148− cx.editor.theme.get("ui.text"),
1121+ style,
11491122 );
1123+ if let Some((reg, _)) = cx.editor.macro_recording {
1124+ let disp = format!("[{}]", reg);
1125+ let style = style
1126+ .fg(helix_view::graphics::Color::Yellow)
1127+ .add_modifier(Modifier::BOLD);
1128+ surface.set_string(
1129+ area.x + area.width.saturating_sub(3),
1130+ area.y + area.height.saturating_sub(1),
1131+ &disp,
1132+ style,
1133+ );
1134+ }
11501135 }
11511136
11521137 if let Some(completion) = self.completion.as_mut() {
@@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
11721157 key.modifiers.remove(KeyModifiers::SHIFT)
11731158 }
11741159}
1175−
1176−#[inline]
1177−const fn abs_diff(a: usize, b: usize) -> usize {
1178− if a > b {
1179− a - b
1180− } else {
1181− b - a
1182− }
1183−}
helix-term/src/ui/markdown.rs+1−5
@@ -228,6 +228,7 @@ impl Component for Markdown {
228228 return None;
229229 }
230230 let contents = parse(&self.contents, None, &self.config_loader);
231+ // TODO: account for tab width
231232 let max_text_width = (viewport.0 - padding).min(120);
232233 let mut text_width = 0;
233234 let mut height = padding;
@@ -240,11 +241,6 @@ impl Component for Markdown {
240241 } else if content_width > text_width {
241242 text_width = content_width;
242243 }
243−
244− if height >= viewport.1 {
245− height = viewport.1;
246− break;
247− }
248244 }
249245
250246 Some((text_width + padding, height))
helix-term/src/ui/menu.rs+2−2
@@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
190190 _ => return EventResult::Ignored,
191191 };
192192
193− let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
193+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
194194 // remove the layer
195195 compositor.pop();
196196 })));
@@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
202202 return close_fn;
203203 }
204204 // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
205− shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
205+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
206206 self.move_up();
207207 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
208208 return EventResult::Consumed(None);
helix-term/src/ui/mod.rs+1
@@ -186,6 +186,7 @@ pub mod completers {
186186 &helix_core::config_dir().join("themes"),
187187 ));
188188 names.push("default".into());
189+ names.push("base16_default".into());
189190
190191 let mut names: Vec<_> = names
191192 .into_iter()
helix-term/src/ui/picker.rs+7−7
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
4646}
4747
4848pub enum CachedPreview {
49− Document(Document),
49+ Document(Box<Document>),
5050 Binary,
5151 LargeFile,
5252 NotFound,
@@ -140,7 +140,7 @@ impl<T> FilePicker<T> {
140140 _ => {
141141 // TODO: enable syntax highlighting; blocked by async rendering
142142 Document::open(path, None, Some(&editor.theme), None)
143− .map(CachedPreview::Document)
143+ .map(|doc| CachedPreview::Document(Box::new(doc)))
144144 .unwrap_or(CachedPreview::NotFound)
145145 }
146146 },
@@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
404404 _ => return EventResult::Ignored,
405405 };
406406
407− let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
407+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
408408 // remove the layer
409409 compositor.last_picker = compositor.pop();
410410 })));
411411
412412 match key_event.into() {
413− shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
413+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
414414 self.move_up();
415415 }
416416 key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -421,19 +421,19 @@ impl<T: 'static> Component for Picker<T> {
421421 }
422422 key!(Enter) => {
423423 if let Some(option) = self.selection() {
424− (self.callback_fn)(&mut cx.editor, option, Action::Replace);
424+ (self.callback_fn)(cx.editor, option, Action::Replace);
425425 }
426426 return close_fn;
427427 }
428428 ctrl!('s') => {
429429 if let Some(option) = self.selection() {
430− (self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit);
430+ (self.callback_fn)(cx.editor, option, Action::HorizontalSplit);
431431 }
432432 return close_fn;
433433 }
434434 ctrl!('v') => {
435435 if let Some(option) = self.selection() {
436− (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit);
436+ (self.callback_fn)(cx.editor, option, Action::VerticalSplit);
437437 }
438438 return close_fn;
439439 }
helix-term/src/ui/popup.rs+24−5
@@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
1515 contents: T,
1616 position: Option<Position>,
1717 size: (u16, u16),
18+ child_size: (u16, u16),
1819 scroll: usize,
20+ id: &'static str,
1921}
2022
2123impl<T: Component> Popup<T> {
22− pub fn new(contents: T) -> Self {
24+ pub fn new(id: &'static str, contents: T) -> Self {
2325 Self {
2426 contents,
2527 position: None,
2628 size: (0, 0),
29+ child_size: (0, 0),
2730 scroll: 0,
31+ id,
2832 }
2933 }
3034
@@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
6872 pub fn scroll(&mut self, offset: usize, direction: bool) {
6973 if direction {
7074 self.scroll += offset;
75+
76+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
77+ self.scroll = (self.scroll + offset).min(max_offset as usize);
7178 } else {
7279 self.scroll = self.scroll.saturating_sub(offset);
7380 }
@@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
93100 _ => return EventResult::Ignored,
94101 };
95102
96− let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
103+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
97104 // remove the layer
98105 compositor.pop();
99106 })));
@@ -115,13 +122,21 @@ impl<T: Component> Component for Popup<T> {
115122 // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
116123 }
117124
118− fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
125+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
126+ let max_width = 120.min(viewport.0);
127+ let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
128+
119129 let (width, height) = self
120130 .contents
121− .required_size((120, 26)) // max width, max height
131+ .required_size((max_width, max_height))
122132 .expect("Component needs required_size implemented in order to be embedded in a popup");
123133
124− self.size = (width, height);
134+ self.child_size = (width, height);
135+ self.size = (width.min(max_width), height.min(max_height));
136+
137+ // re-clamp scroll offset
138+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
139+ self.scroll = self.scroll.min(max_offset as usize);
125140
126141 Some(self.size)
127142 }
@@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
143158
144159 self.contents.render(area, surface, cx);
145160 }
161+
162+ fn id(&self) -> Option<&'static str> {
163+ Some(self.id)
164+ }
146165}
helix-term/src/ui/prompt.rs+2−2
@@ -426,7 +426,7 @@ impl Component for Prompt {
426426 _ => return EventResult::Ignored,
427427 };
428428
429− let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
429+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
430430 // remove the layer
431431 compositor.pop();
432432 })));
@@ -505,7 +505,7 @@ impl Component for Prompt {
505505 self.change_completion_selection(CompletionDirection::Forward);
506506 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
507507 }
508− shift!(BackTab) => {
508+ shift!(Tab) => {
509509 self.change_completion_selection(CompletionDirection::Backward);
510510 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
511511 }
helix-tui/src/buffer.rs+1−10
@@ -102,7 +102,7 @@ impl Default for Cell {
102102/// buf.get_mut(5, 0).set_char('x');
103103/// assert_eq!(buf.get(5, 0).symbol, "x");
104104/// ```
105−#[derive(Debug, Clone, PartialEq)]
105+#[derive(Debug, Default, Clone, PartialEq)]
106106pub struct Buffer {
107107 /// The area represented by this buffer
108108 pub area: Rect,
@@ -111,15 +111,6 @@ pub struct Buffer {
111111 pub content: Vec<Cell>,
112112}
113113
114−impl Default for Buffer {
115− fn default() -> Buffer {
116− Buffer {
117− area: Default::default(),
118− content: Vec::new(),
119− }
120− }
121−}
122−
123114impl Buffer {
124115 /// Returns a Buffer with all cells set to the default one
125116 pub fn empty(area: Rect) -> Buffer {
helix-tui/src/text.rs+2−14
@@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> {
195195}
196196
197197/// A string composed of clusters of graphemes, each with their own style.
198−#[derive(Debug, Clone, PartialEq)]
198+#[derive(Debug, Default, Clone, PartialEq)]
199199pub struct Spans<'a>(pub Vec<Span<'a>>);
200200
201−impl<'a> Default for Spans<'a> {
202− fn default() -> Spans<'a> {
203− Spans(Vec::new())
204− }
205−}
206−
207201impl<'a> Spans<'a> {
208202 /// Returns the width of the underlying string.
209203 ///
@@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String {
280274/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
281275/// assert_eq!(6, text.height());
282276/// ```
283−#[derive(Debug, Clone, PartialEq)]
277+#[derive(Debug, Default, Clone, PartialEq)]
284278pub struct Text<'a> {
285279 pub lines: Vec<Spans<'a>>,
286280}
287281
288−impl<'a> Default for Text<'a> {
289− fn default() -> Text<'a> {
290− Text { lines: Vec::new() }
291− }
292−}
293−
294282impl<'a> Text<'a> {
295283 /// Create some text (potentially multiple lines) with no style.
296284 ///
helix-tui/src/widgets/table.rs+1−10
@@ -363,21 +363,12 @@ impl<'a> Table<'a> {
363363 }
364364}
365365
366−#[derive(Debug, Clone)]
366+#[derive(Debug, Default, Clone)]
367367pub struct TableState {
368368 pub offset: usize,
369369 pub selected: Option<usize>,
370370}
371371
372−impl Default for TableState {
373− fn default() -> TableState {
374− TableState {
375− offset: 0,
376− selected: None,
377− }
378− }
379−}
380−
381372impl TableState {
382373 pub fn selected(&self) -> Option<usize> {
383374 self.selected
helix-view/src/document.rs+6
@@ -104,6 +104,7 @@ pub struct Document {
104104
105105 last_saved_revision: usize,
106106 version: i32, // should be usize?
107+ pub(crate) modified_since_accessed: bool,
107108
108109 diagnostics: Vec<Diagnostic>,
109110 language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +128,7 @@ impl fmt::Debug for Document {
127128 // .field("history", &self.history)
128129 .field("last_saved_revision", &self.last_saved_revision)
129130 .field("version", &self.version)
131+ .field("modified_since_accessed", &self.modified_since_accessed)
130132 .field("diagnostics", &self.diagnostics)
131133 // .field("language_server", &self.language_server)
132134 .finish()
@@ -344,6 +346,7 @@ impl Document {
344346 history: Cell::new(History::default()),
345347 savepoint: None,
346348 last_saved_revision: 0,
349+ modified_since_accessed: false,
347350 language_server: None,
348351 }
349352 }
@@ -639,6 +642,9 @@ impl Document {
639642 selection.clone().ensure_invariants(self.text.slice(..)),
640643 );
641644 }
645+
646+ // set modified since accessed
647+ self.modified_since_accessed = true;
642648 }
643649
644650 if !transaction.changes().is_empty() {
helix-view/src/editor.rs+102−85
@@ -2,6 +2,7 @@ use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
33 document::{Mode, SCRATCH_BUFFER_NAME},
44 graphics::{CursorKind, Rect},
5+ input::KeyEvent,
56 theme::{self, Theme},
67 tree::{self, Tree},
78 Document, DocumentId, View, ViewId,
@@ -11,6 +12,7 @@ use futures_util::future;
1112use std::{
1213 collections::{BTreeMap, HashMap},
1314 io::stdin,
15+ num::NonZeroUsize,
1416 path::{Path, PathBuf},
1517 pin::Pin,
1618 sync::Arc,
@@ -18,7 +20,7 @@ use std::{
1820
1921use tokio::time::{sleep, Duration, Instant, Sleep};
2022
21−use anyhow::Error;
23+use anyhow::{bail, Error};
2224
2325pub use helix_core::diagnostic::Severity;
2426pub use helix_core::register::Registers;
@@ -105,6 +107,8 @@ pub struct Config {
105107 pub file_picker: FilePickerConfig,
106108 /// Shape for cursor in each mode
107109 pub cursor_shape: CursorShapeConfig,
110+ /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
111+ pub true_color: bool,
108112}
109113
110114// Cursor shape is read and used on every rendered frame and so needs
@@ -141,7 +145,7 @@ impl Default for CursorShapeConfig {
141145 }
142146}
143147
144−#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
148+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
145149#[serde(rename_all = "kebab-case")]
146150pub enum LineNumber {
147151 /// Show absolute line number
@@ -171,6 +175,7 @@ impl Default for Config {
171175 auto_info: true,
172176 file_picker: FilePickerConfig::default(),
173177 cursor_shape: CursorShapeConfig::default(),
178+ true_color: false,
174179 }
175180 }
176181}
@@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
190195#[derive(Debug)]
191196pub struct Editor {
192197 pub tree: Tree,
193− pub next_document_id: usize,
198+ pub next_document_id: DocumentId,
194199 pub documents: BTreeMap<DocumentId, Document>,
195200 pub count: Option<std::num::NonZeroUsize>,
196201 pub selected_register: Option<char>,
197202 pub registers: Registers,
203+ pub macro_recording: Option<(char, Vec<KeyEvent>)>,
198204 pub theme: Theme,
199205 pub language_servers: helix_lsp::Registry,
200206 pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -223,8 +229,8 @@ pub enum Action {
223229impl Editor {
224230 pub fn new(
225231 mut area: Rect,
226− themes: Arc<theme::Loader>,
227− config_loader: Arc<syntax::Loader>,
232+ theme_loader: Arc<theme::Loader>,
233+ syn_loader: Arc<syntax::Loader>,
228234 config: Config,
229235 ) -> Self {
230236 let language_servers = helix_lsp::Registry::new();
@@ -234,14 +240,15 @@ impl Editor {
234240
235241 Self {
236242 tree: Tree::new(area),
237− next_document_id: 0,
243+ next_document_id: DocumentId::default(),
238244 documents: BTreeMap::new(),
239245 count: None,
240246 selected_register: None,
241− theme: themes.default(),
247+ macro_recording: None,
248+ theme: theme_loader.default(),
242249 language_servers,
243− syn_loader: config_loader,
244− theme_loader: themes,
250+ syn_loader,
251+ theme_loader,
245252 registers: Registers::default(),
246253 clipboard_provider: get_clipboard_provider(),
247254 status_msg: None,
@@ -297,14 +304,51 @@ impl Editor {
297304 self._refresh();
298305 }
299306
300− pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
301− use anyhow::Context;
302− let theme = self
303− .theme_loader
304− .load(theme.as_ref())
305− .with_context(|| format!("failed setting theme `{}`", theme))?;
306− self.set_theme(theme);
307− Ok(())
307+ /// Refreshes the language server for a given document
308+ pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
309+ let doc = self.documents.get_mut(&doc_id)?;
310+ doc.detect_language(Some(&self.theme), &self.syn_loader);
311+ Self::launch_language_server(&mut self.language_servers, doc)
312+ }
313+
314+ /// Launch a language server for a given document
315+ fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
316+ // try to find a language server based on the language name
317+ let language_server = doc.language.as_ref().and_then(|language| {
318+ ls.get(language)
319+ .map_err(|e| {
320+ log::error!(
321+ "Failed to initialize the LSP for `{}` {{ {} }}",
322+ language.scope(),
323+ e
324+ )
325+ })
326+ .ok()
327+ });
328+ if let Some(language_server) = language_server {
329+ // only spawn a new lang server if the servers aren't the same
330+ if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
331+ if let Some(language_server) = doc.language_server() {
332+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
333+ }
334+ let language_id = doc
335+ .language()
336+ .and_then(|s| s.split('.').last()) // source.rust
337+ .map(ToOwned::to_owned)
338+ .unwrap_or_default();
339+
340+ // TODO: this now races with on_init code if the init happens too quickly
341+ tokio::spawn(language_server.text_document_did_open(
342+ doc.url().unwrap(),
343+ doc.version(),
344+ doc.text(),
345+ language_id,
346+ ));
347+
348+ doc.set_language_server(Some(language_server));
349+ }
350+ }
351+ Some(())
308352 }
309353
310354 fn _refresh(&mut self) {
@@ -358,7 +402,8 @@ impl Editor {
358402 .tree
359403 .traverse()
360404 .any(|(_, v)| v.doc == doc.id && v.id != view.id);
361− let view = view_mut!(self);
405+
406+ let (view, doc) = current!(self);
362407 if remove_empty_scratch {
363408 // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
364409 // borrow, invalidating direct access to `doc.id`.
@@ -367,7 +412,16 @@ impl Editor {
367412 } else {
368413 let jump = (view.doc, doc.selection(view.id).clone());
369414 view.jumps.push(jump);
370− view.last_accessed_doc = Some(view.doc);
415+ // Set last accessed doc if it is a different document
416+ if doc.id != id {
417+ view.last_accessed_doc = Some(view.doc);
418+ // Set last modified doc if modified and last modified doc is different
419+ if std::mem::take(&mut doc.modified_since_accessed)
420+ && view.last_modified_docs[0] != Some(id)
421+ {
422+ view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
423+ }
424+ }
371425 }
372426
373427 let view_id = view.id;
@@ -377,23 +431,22 @@ impl Editor {
377431 }
378432 Action::Load => {
379433 let view_id = view!(self).id;
380− if let Some(doc) = self.document_mut(id) {
381− if doc.selections().is_empty() {
382− doc.selections.insert(view_id, Selection::point(0));
383− }
434+ let doc = self.documents.get_mut(&id).unwrap();
435+ if doc.selections().is_empty() {
436+ doc.selections.insert(view_id, Selection::point(0));
384437 }
385438 return;
386439 }
387− Action::HorizontalSplit => {
388− let view = View::new(id);
389− let view_id = self.tree.split(view, Layout::Horizontal);
390− // initialize selection for view
391− let doc = self.documents.get_mut(&id).unwrap();
392− doc.selections.insert(view_id, Selection::point(0));
393− }
394− Action::VerticalSplit => {
440+ Action::HorizontalSplit | Action::VerticalSplit => {
395441 let view = View::new(id);
396− let view_id = self.tree.split(view, Layout::Vertical);
442+ let view_id = self.tree.split(
443+ view,
444+ match action {
445+ Action::HorizontalSplit => Layout::Horizontal,
446+ Action::VerticalSplit => Layout::Vertical,
447+ _ => unreachable!(),
448+ },
449+ );
397450 // initialize selection for view
398451 let doc = self.documents.get_mut(&id).unwrap();
399452 doc.selections.insert(view_id, Selection::point(0));
@@ -403,16 +456,19 @@ impl Editor {
403456 self._refresh();
404457 }
405458
406− fn new_document(&mut self, mut document: Document) -> DocumentId {
407− let id = DocumentId(self.next_document_id);
408− self.next_document_id += 1;
409− document.id = id;
410− self.documents.insert(id, document);
459+ /// Generate an id for a new document and register it.
460+ fn new_document(&mut self, mut doc: Document) -> DocumentId {
461+ let id = self.next_document_id;
462+ // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
463+ self.next_document_id =
464+ DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
465+ doc.id = id;
466+ self.documents.insert(id, doc);
411467 id
412468 }
413469
414− fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
415− let id = self.new_document(document);
470+ fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
471+ let id = self.new_document(doc);
416472 self.switch(id, action);
417473 id
418474 }
@@ -428,54 +484,16 @@ impl Editor {
428484
429485 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
430486 let path = helix_core::path::get_canonicalized_path(&path)?;
431−
432− let id = self
433− .documents()
434− .find(|doc| doc.path() == Some(&path))
435− .map(|doc| doc.id);
487+ let id = self.document_by_path(&path).map(|doc| doc.id);
436488
437489 let id = if let Some(id) = id {
438490 id
439491 } else {
440492 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
441493
442− // try to find a language server based on the language name
443− let language_server = doc.language.as_ref().and_then(|language| {
444− self.language_servers
445− .get(language)
446− .map_err(|e| {
447− log::error!(
448− "Failed to initialize the LSP for `{}` {{ {} }}",
449− language.scope(),
450− e
451− )
452− })
453− .ok()
454− });
455−
456− if let Some(language_server) = language_server {
457− let language_id = doc
458− .language()
459− .and_then(|s| s.split('.').last()) // source.rust
460− .map(ToOwned::to_owned)
461− .unwrap_or_default();
462−
463− // TODO: this now races with on_init code if the init happens too quickly
464− tokio::spawn(language_server.text_document_did_open(
465− doc.url().unwrap(),
466− doc.version(),
467− doc.text(),
468− language_id,
469− ));
470−
471− doc.set_language_server(Some(language_server));
472− }
494+ let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
473495
474− let id = DocumentId(self.next_document_id);
475− self.next_document_id += 1;
476− doc.id = id;
477− self.documents.insert(id, doc);
478− id
496+ self.new_document(doc)
479497 };
480498
481499 self.switch(id, action);
@@ -498,11 +516,11 @@ impl Editor {
498516 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
499517 let doc = match self.documents.get(&doc_id) {
500518 Some(doc) => doc,
501− None => anyhow::bail!("document does not exist"),
519+ None => bail!("document does not exist"),
502520 };
503521
504522 if !force && doc.is_modified() {
505− anyhow::bail!(
523+ bail!(
506524 "buffer {:?} is modified",
507525 doc.relative_path()
508526 .map(|path| path.to_string_lossy().to_string())
@@ -535,7 +553,7 @@ impl Editor {
535553 // If the document we removed was visible in all views, we will have no more views. We don't
536554 // want to close the editor just for a simple buffer close, so we need to create a new view
537555 // containing either an existing document, or a brand new document.
538− if self.tree.views().peekable().peek().is_none() {
556+ if self.tree.views().next().is_none() {
539557 let doc_id = self
540558 .documents
541559 .iter()
@@ -620,8 +638,7 @@ impl Editor {
620638 }
621639
622640 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
623− let view = view!(self);
624− let doc = &self.documents[&view.doc];
641+ let (view, doc) = current_ref!(self);
625642 let cursor = doc
626643 .selection(view.id)
627644 .primary()
helix-view/src/graphics.rs+1−12
@@ -33,7 +33,7 @@ pub struct Margin {
3333
3434/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
3535/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
36−#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
36+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
3737pub struct Rect {
3838 pub x: u16,
3939 pub y: u16,
@@ -41,17 +41,6 @@ pub struct Rect {
4141 pub height: u16,
4242}
4343
44−impl Default for Rect {
45− fn default() -> Rect {
46− Rect {
47− x: 0,
48− y: 0,
49− width: 0,
50− height: 0,
51− }
52− }
53−}
54−
5544impl Rect {
5645 /// Creates a new rect, with width and height limited to keep the area under max u16.
5746 /// If clipped, aspect ratio will be preserved.
helix-view/src/gutter.rsnew file+96
@@ -0,0 +1,96 @@
1+use std::fmt::Write;
2+
3+use crate::{editor::Config, graphics::Style, Document, Theme, View};
4+
5+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
6+pub type Gutter =
7+ for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
8+
9+pub fn diagnostic<'doc>(
10+ doc: &'doc Document,
11+ _view: &View,
12+ theme: &Theme,
13+ _config: &Config,
14+ _is_focused: bool,
15+ _width: usize,
16+) -> GutterFn<'doc> {
17+ let warning = theme.get("warning");
18+ let error = theme.get("error");
19+ let info = theme.get("info");
20+ let hint = theme.get("hint");
21+ let diagnostics = doc.diagnostics();
22+
23+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
24+ use helix_core::diagnostic::Severity;
25+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
26+ let diagnostic = &diagnostics[index];
27+ write!(out, "●").unwrap();
28+ return Some(match diagnostic.severity {
29+ Some(Severity::Error) => error,
30+ Some(Severity::Warning) | None => warning,
31+ Some(Severity::Info) => info,
32+ Some(Severity::Hint) => hint,
33+ });
34+ }
35+ None
36+ })
37+}
38+
39+pub fn line_number<'doc>(
40+ doc: &'doc Document,
41+ view: &View,
42+ theme: &Theme,
43+ config: &Config,
44+ is_focused: bool,
45+ width: usize,
46+) -> GutterFn<'doc> {
47+ let text = doc.text().slice(..);
48+ let last_line = view.last_line(doc);
49+ // Whether to draw the line number for the last line of the
50+ // document or not. We only draw it if it's not an empty line.
51+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
52+
53+ let linenr = theme.get("ui.linenr");
54+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
55+
56+ let current_line = doc
57+ .text()
58+ .char_to_line(doc.selection(view.id).primary().cursor(text));
59+
60+ let config = config.line_number;
61+
62+ Box::new(move |line: usize, selected: bool, out: &mut String| {
63+ if line == last_line && !draw_last {
64+ write!(out, "{:>1$}", '~', width).unwrap();
65+ Some(linenr)
66+ } else {
67+ use crate::editor::LineNumber;
68+ let line = match config {
69+ LineNumber::Absolute => line + 1,
70+ LineNumber::Relative => {
71+ if current_line == line {
72+ line + 1
73+ } else {
74+ abs_diff(current_line, line)
75+ }
76+ }
77+ };
78+ let style = if selected && is_focused {
79+ linenr_select
80+ } else {
81+ linenr
82+ };
83+ write!(out, "{:>1$}", line, width).unwrap();
84+ Some(style)
85+ }
86+ })
87+}
88+
89+#[inline(always)]
90+const fn abs_diff(a: usize, b: usize) -> usize {
91+ if a > b {
92+ a - b
93+ } else {
94+ b - a
95+ }
96+}
helix-view/src/input.rs+34−10
@@ -36,7 +36,6 @@ pub(crate) mod keys {
3636 pub(crate) const PAGEUP: &str = "pageup";
3737 pub(crate) const PAGEDOWN: &str = "pagedown";
3838 pub(crate) const TAB: &str = "tab";
39− pub(crate) const BACKTAB: &str = "backtab";
4039 pub(crate) const DELETE: &str = "del";
4140 pub(crate) const INSERT: &str = "ins";
4241 pub(crate) const NULL: &str = "null";
@@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
8281 KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
8382 KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
8483 KeyCode::Tab => f.write_str(keys::TAB)?,
85− KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
8684 KeyCode::Delete => f.write_str(keys::DELETE)?,
8785 KeyCode::Insert => f.write_str(keys::INSERT)?,
8886 KeyCode::Null => f.write_str(keys::NULL)?,
@@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
116114 KeyCode::PageUp => keys::PAGEUP.len(),
117115 KeyCode::PageDown => keys::PAGEDOWN.len(),
118116 KeyCode::Tab => keys::TAB.len(),
119− KeyCode::BackTab => keys::BACKTAB.len(),
120117 KeyCode::Delete => keys::DELETE.len(),
121118 KeyCode::Insert => keys::INSERT.len(),
122119 KeyCode::Null => keys::NULL.len(),
@@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
166163 keys::PAGEUP => KeyCode::PageUp,
167164 keys::PAGEDOWN => KeyCode::PageDown,
168165 keys::TAB => KeyCode::Tab,
169− keys::BACKTAB => KeyCode::BackTab,
170166 keys::DELETE => KeyCode::Delete,
171167 keys::INSERT => KeyCode::Insert,
172168 keys::NULL => KeyCode::Null,
@@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent {
220216
221217#[cfg(feature = "term")]
222218impl From<crossterm::event::KeyEvent> for KeyEvent {
223− fn from(
224− crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
225− ) -> KeyEvent {
226− KeyEvent {
227− code: code.into(),
228− modifiers: modifiers.into(),
219+ fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
220+ if code == crossterm::event::KeyCode::BackTab {
221+ // special case for BackTab -> Shift-Tab
222+ let mut modifiers: KeyModifiers = modifiers.into();
223+ modifiers.insert(KeyModifiers::SHIFT);
224+ Self {
225+ code: KeyCode::Tab,
226+ modifiers,
227+ }
228+ } else {
229+ Self {
230+ code: code.into(),
231+ modifiers: modifiers.into(),
232+ }
233+ }
234+ }
235+}
236+
237+#[cfg(feature = "term")]
238+impl From<KeyEvent> for crossterm::event::KeyEvent {
239+ fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
240+ if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
241+ // special case for Shift-Tab -> BackTab
242+ let mut modifiers = modifiers;
243+ modifiers.remove(KeyModifiers::SHIFT);
244+ crossterm::event::KeyEvent {
245+ code: crossterm::event::KeyCode::BackTab,
246+ modifiers: modifiers.into(),
247+ }
248+ } else {
249+ crossterm::event::KeyEvent {
250+ code: code.into(),
251+ modifiers: modifiers.into(),
252+ }
229253 }
230254 }
231255}
helix-view/src/keyboard.rs+1−4
@@ -79,8 +79,6 @@ pub enum KeyCode {
7979 PageDown,
8080 /// Tab key.
8181 Tab,
82− /// Shift + Tab key.
83− BackTab,
8482 /// Delete key.
8583 Delete,
8684 /// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
116114 KeyCode::PageUp => CKeyCode::PageUp,
117115 KeyCode::PageDown => CKeyCode::PageDown,
118116 KeyCode::Tab => CKeyCode::Tab,
119− KeyCode::BackTab => CKeyCode::BackTab,
120117 KeyCode::Delete => CKeyCode::Delete,
121118 KeyCode::Insert => CKeyCode::Insert,
122119 KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
144141 CKeyCode::PageUp => KeyCode::PageUp,
145142 CKeyCode::PageDown => KeyCode::PageDown,
146143 CKeyCode::Tab => KeyCode::Tab,
147− CKeyCode::BackTab => KeyCode::BackTab,
144+ CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
148145 CKeyCode::Delete => KeyCode::Delete,
149146 CKeyCode::Insert => KeyCode::Insert,
150147 CKeyCode::F(f_number) => KeyCode::F(f_number),
helix-view/src/lib.rs+13−2
@@ -5,6 +5,7 @@ pub mod clipboard;
55pub mod document;
66pub mod editor;
77pub mod graphics;
8+pub mod gutter;
89pub mod info;
910pub mod input;
1011pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
1213pub mod tree;
1314pub mod view;
1415
15−#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
16−pub struct DocumentId(usize);
16+use std::num::NonZeroUsize;
17+
18+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
19+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
20+pub struct DocumentId(NonZeroUsize);
21+
22+impl Default for DocumentId {
23+ fn default() -> DocumentId {
24+ // Safety: 1 is non-zero
25+ DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
26+ }
27+}
1728
1829slotmap::new_key_type! {
1930 pub struct ViewId;
helix-view/src/theme.rs+45−6
@@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
1515pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
1616 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
1717});
18+pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
19+ toml::from_slice(include_bytes!("../../base16_theme.toml"))
20+ .expect("Failed to parse base 16 default theme")
21+});
1822
1923#[derive(Clone, Debug)]
2024pub struct Loader {
@@ -35,6 +39,9 @@ impl Loader {
3539 if name == "default" {
3640 return Ok(self.default());
3741 }
42+ if name == "base16_default" {
43+ return Ok(self.base16_default());
44+ }
3845 let filename = format!("{}.toml", name);
3946
4047 let user_path = self.user_dir.join(&filename);
@@ -74,12 +81,20 @@ impl Loader {
7481 pub fn default(&self) -> Theme {
7582 DEFAULT_THEME.clone()
7683 }
84+
85+ /// Returns the alternative 16-color default theme
86+ pub fn base16_default(&self) -> Theme {
87+ BASE16_DEFAULT_THEME.clone()
88+ }
7789}
7890
7991#[derive(Clone, Debug)]
8092pub struct Theme {
81− scopes: Vec<String>,
93+ // UI styles are stored in a HashMap
8294 styles: HashMap<String, Style>,
95+ // tree-sitter highlight styles are stored in a Vec to optimize lookups
96+ scopes: Vec<String>,
97+ highlights: Vec<Style>,
8398}
8499
85100impl<'de> Deserialize<'de> for Theme {
@@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
88103 D: Deserializer<'de>,
89104 {
90105 let mut styles = HashMap::new();
106+ let mut scopes = Vec::new();
107+ let mut highlights = Vec::new();
91108
92109 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
93110 // TODO: alert user of parsing failures in editor
@@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
102119 .unwrap_or_default();
103120
104121 styles.reserve(colors.len());
122+ scopes.reserve(colors.len());
123+ highlights.reserve(colors.len());
124+
105125 for (name, style_value) in colors {
106126 let mut style = Style::default();
107127 if let Err(err) = palette.parse_style(&mut style, style_value) {
108128 warn!("{}", err);
109129 }
110− styles.insert(name, style);
130+
131+ // these are used both as UI and as highlights
132+ styles.insert(name.clone(), style);
133+ scopes.push(name);
134+ highlights.push(style);
111135 }
112136 }
113137
114− let scopes = styles.keys().map(ToString::to_string).collect();
115− Ok(Self { scopes, styles })
138+ Ok(Self {
139+ scopes,
140+ styles,
141+ highlights,
142+ })
116143 }
117144}
118145
119146impl Theme {
147+ #[inline]
148+ pub fn highlight(&self, index: usize) -> Style {
149+ self.highlights[index]
150+ }
151+
120152 pub fn get(&self, scope: &str) -> Style {
121− self.try_get(scope)
122− .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
153+ self.try_get(scope).unwrap_or_default()
123154 }
124155
125156 pub fn try_get(&self, scope: &str) -> Option<Style> {
@@ -134,6 +165,14 @@ impl Theme {
134165 pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
135166 self.scopes().iter().position(|s| s == scope)
136167 }
168+
169+ pub fn is_16_color(&self) -> bool {
170+ self.styles.iter().all(|(_, style)| {
171+ [style.fg, style.bg]
172+ .into_iter()
173+ .all(|color| !matches!(color, Some(Color::Rgb(..))))
174+ })
175+ }
137176}
138177
139178struct ThemePalette {
helix-view/src/view.rs+26−4
@@ -1,6 +1,10 @@
11use std::borrow::Cow;
22
3−use crate::{graphics::Rect, Document, DocumentId, ViewId};
3+use crate::{
4+ graphics::Rect,
5+ gutter::{self, Gutter},
6+ Document, DocumentId, ViewId,
7+};
48use helix_core::{
59 graphemes::{grapheme_width, RopeGraphemes},
610 line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
6064 }
6165}
6266
67+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
68+
6369#[derive(Debug)]
6470pub struct View {
6571 pub id: ViewId,
@@ -69,6 +75,11 @@ pub struct View {
6975 pub jumps: JumpList,
7076 /// the last accessed file before the current one
7177 pub last_accessed_doc: Option<DocumentId>,
78+ /// the last modified files before the current one
79+ /// ordered from most frequent to least frequent
80+ // uses two docs because we want to be able to swap between the
81+ // two last modified docs which we need to manually keep track of
82+ pub last_modified_docs: [Option<DocumentId>; 2],
7283}
7384
7485impl View {
@@ -80,13 +91,23 @@ impl View {
8091 area: Rect::default(), // will get calculated upon inserting into tree
8192 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
8293 last_accessed_doc: None,
94+ last_modified_docs: [None, None],
8395 }
8496 }
8597
98+ pub fn gutters(&self) -> &[(Gutter, usize)] {
99+ GUTTERS
100+ }
101+
86102 pub fn inner_area(&self) -> Rect {
87− // TODO: not ideal
88− const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
89− self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
103+ // TODO: cache this
104+ let offset = self
105+ .gutters()
106+ .iter()
107+ .map(|(_, width)| *width as u16)
108+ .sum::<u16>()
109+ + 1; // +1 for some space between gutters and line
110+ self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
90111 }
91112
92113 //
@@ -276,6 +297,7 @@ mod tests {
276297 use super::*;
277298 use helix_core::Rope;
278299 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
300+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
279301
280302 #[test]
281303 fn test_text_pos_at_screen_coords() {
languages.toml+35−1
@@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = " " }
1111[language.config]
1212cargo = { loadOutDirsFromCheck = true }
1313procMacro = { enable = false }
14+diagnostics = { disabled = ["unresolved-proc-macro"] }
1415
1516[[language]]
1617name = "toml"
@@ -249,7 +250,6 @@ language-server = { command = "julia", args = [
249250 using Pkg;
250251 import StaticLint;
251252 env_path = dirname(Pkg.Types.Context().env.project_file);
252−
253253 server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
254254 server.runlinter = true;
255255 run(server);
@@ -396,3 +396,37 @@ shebangs = ["perl"]
396396roots = []
397397comment-token = "#"
398398indent = { tab-width = 2, unit = " " }
399+
400+[[language]]
401+name = "racket"
402+scope = "source.rkt"
403+roots = []
404+file-types = ["rkt"]
405+shebangs = ["racket"]
406+comment-token = ";"
407+language-server = { command = "racket", args = ["-l", "racket-langserver"] }
408+
409+[[language]]
410+name = "wgsl"
411+scope = "source.wgsl"
412+file-types = ["wgsl"]
413+roots = []
414+comment-token = "//"
415+indent = { tab-width = 4, unit = " " }
416+
417+[[language]]
418+name = "llvm"
419+scope = "source.llvm"
420+roots = []
421+file-types = ["ll"]
422+comment-token = ";"
423+indent = { tab-width = 2, unit = " " }
424+
425+[[language]]
426+name = "markdown"
427+scope = "source.md"
428+injection-regex = "md|markdown"
429+file-types = ["md"]
430+roots = []
431+
432+indent = { tab-width = 2, unit = " " }
runtime/queries/haskell/highlights.scm+1−1
@@ -8,7 +8,7 @@
88(constructor) @constructor
99(pragma) @pragma
1010(comment) @comment
11−(signature name: (variable) @fun_type_name)
11+(signature name: (variable) @type)
1212(function name: (variable) @function)
1313(constraint class: (class_name (type)) @class)
1414(class (class_head class: (class_name (type)) @class))
runtime/queries/julia/locals.scm+11−11
@@ -2,24 +2,24 @@
22(import_statement
33 (identifier) @definition.import)
44(variable_declaration
5− (identifier) @definition.var)
5+ (identifier) @local.definition)
66(variable_declaration
77 (tuple_expression
8− (identifier) @definition.var))
8+ (identifier) @local.definition))
99(for_binding
10− (identifier) @definition.var)
10+ (identifier) @local.definition)
1111(for_binding
1212 (tuple_expression
13− (identifier) @definition.var))
13+ (identifier) @local.definition))
1414
1515(assignment_expression
1616 (tuple_expression
17− (identifier) @definition.var))
17+ (identifier) @local.definition))
1818(assignment_expression
1919 (bare_tuple_expression
20− (identifier) @definition.var))
20+ (identifier) @local.definition))
2121(assignment_expression
22− (identifier) @definition.var)
22+ (identifier) @local.definition)
2323
2424(type_parameter_list
2525 (identifier) @definition.type)
@@ -43,11 +43,11 @@
4343 (identifier) @definition.parameter)
4444
4545(function_definition
46− name: (identifier) @definition.function) @scope
46+ name: (identifier) @definition.function) @local.scope
4747(macro_definition
48− name: (identifier) @definition.macro) @scope
48+ name: (identifier) @definition.macro) @local.scope
4949
50−(identifier) @reference
50+(identifier) @local.reference
5151
5252[
5353 (try_statement)
@@ -56,4 +56,4 @@
5656 (let_statement)
5757 (compound_expression)
5858 (for_statement)
59−] @scope
59+] @local.scope
runtime/queries/latex/highlights.scm+14−14
@@ -278,7 +278,7 @@
278278 "\\includeinkscape"
279279 "\\usepgflibrary"
280280 "\\usetikzlibrary"
281−] @include
281+] @keyword.control.import
282282
283283[
284284 "\\part"
@@ -318,60 +318,60 @@
318318["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
319319
320320(chapter
321− text: (brace_group) @text.title)
321+ text: (brace_group) @markup.heading)
322322
323323(part
324− text: (brace_group) @text.title)
324+ text: (brace_group) @markup.heading)
325325
326326(section
327− text: (brace_group) @text.title)
327+ text: (brace_group) @markup.heading)
328328
329329(subsection
330− text: (brace_group) @text.title)
330+ text: (brace_group) @markup.heading)
331331
332332(subsubsection
333− text: (brace_group) @text.title)
333+ text: (brace_group) @markup.heading)
334334
335335(paragraph
336− text: (brace_group) @text.title)
336+ text: (brace_group) @markup.heading)
337337
338338(subparagraph
339− text: (brace_group) @text.title)
339+ text: (brace_group) @markup.heading)
340340
341341((environment
342342 (begin
343343 name: (word) @_frame)
344344 (brace_group
345− child: (text) @text.title))
345+ child: (text) @markup.heading))
346346 (#eq? @_frame "frame"))
347347
348348((generic_command
349349 name:(generic_command_name) @_name
350350 arg: (brace_group
351− (text) @text.title))
351+ (text) @markup.heading))
352352 (#eq? @_name "\\frametitle"))
353353
354354;; Formatting
355355
356356((generic_command
357357 name:(generic_command_name) @_name
358− arg: (_) @text.emphasis)
358+ arg: (_) @markup.italic)
359359 (#eq? @_name "\\emph"))
360360
361361((generic_command
362362 name:(generic_command_name) @_name
363− arg: (_) @text.emphasis)
363+ arg: (_) @markup.italic)
364364 (#match? @_name "^(\\\\textit|\\\\mathit)$"))
365365
366366((generic_command
367367 name:(generic_command_name) @_name
368− arg: (_) @text.strong)
368+ arg: (_) @markup.bold)
369369 (#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
370370
371371((generic_command
372372 name:(generic_command_name) @_name
373373 .
374− arg: (_) @text.uri)
374+ arg: (_) @markup.underline.link)
375375 (#match? @_name "^(\\\\url|\\\\href)$"))
376376
377377(ERROR) @error
runtime/queries/ledger/highlights.scm+1−1
@@ -12,7 +12,7 @@
1212((account) @variable.other.member)
1313((commodity) @text.literal)
1414
15−"include" @include
15+"include" @keyword.local.import
1616
1717[
1818 "account"
runtime/queries/llvm/highlights.scmnew file+14
@@ -0,0 +1,14 @@
1+(type) @type
2+(statement) @keyword.operator
3+(number) @constant.numeric.integer
4+(comment) @comment
5+(string) @string
6+(label) @label
7+(keyword) @keyword
8+"ret" @keyword.control.return
9+(boolean) @constant.builtin.boolean
10+(float) @constant.numeric.float
11+(constant) @constant
12+(identifier) @variable
13+(symbol) @punctuation.delimiter
14+(bracket) @punctuation.bracket
runtime/queries/markdown/highlights.scmnew file+35
@@ -0,0 +1,35 @@
1+[
2+ (atx_heading)
3+ (setext_heading)
4+] @markup.heading
5+
6+(code_fence_content) @none
7+
8+[
9+ (indented_code_block)
10+ (fenced_code_block)
11+] @markup.raw.block
12+
13+(code_span) @markup.raw.inline
14+
15+(emphasis) @markup.italic
16+
17+(strong_emphasis) @markup.bold
18+
19+(link_destination) @markup.underline.link
20+
21+; (link_label) @markup.label ; TODO: rename
22+
23+[
24+ (list_marker_plus)
25+ (list_marker_minus)
26+ (list_marker_star)
27+ (list_marker_dot)
28+ (list_marker_parenthesis)
29+] @punctuation.special
30+
31+[
32+ (backslash_escape)
33+ (hard_line_break)
34+] @string.character.escape
35+
runtime/queries/markdown/injections.scmnew file+8
@@ -0,0 +1,8 @@
1+(fenced_code_block
2+ (info_string) @injection.language
3+ (code_fence_content) @injection.content)
4+
5+((html_block) @injection.content
6+ (#set! injection.language "html"))
7+((html_tag) @injection.content
8+ (#set! injection.language "html"))
runtime/queries/ocaml/highlights.scm+1−1
@@ -90,7 +90,7 @@
9090
9191["exception" "try"] @keyword.control.exception
9292
93−["include" "open"] @include
93+["include" "open"] @keyword.control.import
9494
9595["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
9696
runtime/queries/perl/indents.tomlnew file+17
@@ -0,0 +1,17 @@
1+indent = [
2+ "function",
3+ "identifier",
4+ "method_invocation",
5+ "if_statement",
6+ "unless_statement",
7+ "if_simple_statement",
8+ "unless_simple_statement",
9+ "variable_declaration",
10+ "block",
11+ "list_item",
12+ "word_list_qw"
13+]
14+
15+outdent = [
16+ "}"
17+]
runtime/queries/svelte/highlights.scm+1−1
@@ -25,7 +25,7 @@
2525
2626((attribute
2727 (attribute_name) @_attr
28− (quoted_attribute_value (attribute_value) @markup.undeline.link))
28+ (quoted_attribute_value (attribute_value) @markup.underline.link))
2929 (#match? @_attr "^(href|src)$"))
3030
3131(tag_name) @tag
runtime/queries/wgsl/highlights.scmnew file+102
@@ -0,0 +1,102 @@
1+(const_literal) @constant.numeric
2+
3+(type_declaration) @type
4+
5+(function_declaration
6+ (identifier) @function)
7+
8+(struct_declaration
9+ (identifier) @type)
10+
11+(type_constructor_or_function_call_expression
12+ (type_declaration) @function)
13+
14+(parameter
15+ (variable_identifier_declaration (identifier) @variable.parameter))
16+
17+[
18+ "struct"
19+ "bitcast"
20+ ; "block"
21+ "discard"
22+ "enable"
23+ "fallthrough"
24+ "fn"
25+ "let"
26+ "private"
27+ "read"
28+ "read_write"
29+ "return"
30+ "storage"
31+ "type"
32+ "uniform"
33+ "var"
34+ "workgroup"
35+ "write"
36+ (texel_format)
37+] @keyword ; TODO reserved keywords
38+
39+[
40+ (true)
41+ (false)
42+] @constant.builtin.boolean
43+
44+[ "," "." ":" ";" ] @punctuation.delimiter
45+
46+;; brackets
47+[
48+ "("
49+ ")"
50+ "["
51+ "]"
52+ "{"
53+ "}"
54+] @punctuation.bracket
55+
56+[
57+ "loop"
58+ "for"
59+ "break"
60+ "continue"
61+ "continuing"
62+] @keyword.control.repeat
63+
64+[
65+ "if"
66+ "else"
67+ "elseif"
68+ "switch"
69+ "case"
70+ "default"
71+] @keyword.control.conditional
72+
73+[
74+ "&"
75+ "&&"
76+ "/"
77+ "!"
78+ "="
79+ "=="
80+ "!="
81+ ">"
82+ ">="
83+ ">>"
84+ "<"
85+ "<="
86+ "<<"
87+ "%"
88+ "-"
89+ "+"
90+ "|"
91+ "||"
92+ "*"
93+ "~"
94+ "^"
95+] @operator
96+
97+(attribute
98+ (identifier) @variable.other.member)
99+
100+(comment) @comment
101+
102+(ERROR) @error
runtime/themes/base16_default_dark.toml+19−18
@@ -1,27 +1,28 @@
1−# Author: RayGervais<raygervais@hotmail.ca>
1+# Author: RayGervais <raygervais@hotmail.ca>
22
33"ui.background" = { bg = "base00" }
44"ui.menu" = "base01"
5−"ui.menu.selected" = { fg = "base04", bg = "base01" }
6−"ui.linenr" = {fg = "base01" }
5+"ui.menu.selected" = { fg = "base01", bg = "base04" }
6+"ui.linenr" = { fg = "base03", bg = "base01" }
77"ui.popup" = { bg = "base01" }
88"ui.window" = { bg = "base01" }
9−"ui.liner.selected" = "base02"
10−"ui.selection" = "base02"
11−"comment" = "base03"
12−"ui.statusline" = {fg = "base04", bg = "base01" }
9+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
10+"ui.selection" = { bg = "base02" }
11+"comment" = { fg = "base03", modifiers = ["italic"] }
12+"ui.statusline" = { fg = "base04", bg = "base01" }
1313"ui.help" = { fg = "base04", bg = "base01" }
14−"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
15−"ui.text" = { fg = "base05" }
14+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
15+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
16+"ui.text" = "base05"
1617"operator" = "base05"
17−"ui.text.focus" = { fg = "base05" }
18+"ui.text.focus" = "base05"
1819"variable" = "base08"
1920"constant.numeric" = "base09"
2021"constant" = "base09"
21−"attributes" = "base09"
22+"attributes" = "base09"
2223"type" = "base0A"
2324"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
24−"strings" = "base0B"
25+"string" = "base0B"
2526"variable.other.member" = "base0B"
2627"constant.character.escape" = "base0C"
2728"function" = "base0D"
@@ -30,15 +31,15 @@
3031"keyword" = "base0E"
3132"label" = "base0E"
3233"namespace" = "base0E"
33−"ui.popup" = { bg = "base01" }
34−"ui.window" = { bg = "base00" }
35−"ui.help" = { bg = "base01", fg = "base06" }
34+"ui.help" = { fg = "base06", bg = "base01" }
3635
37−"info" = "base03"
36+"diagnostic" = { modifiers = ["underlined"] }
37+"ui.gutter" = { bg = "base01" }
38+"info" = "base0D"
3839"hint" = "base03"
3940"debug" = "base03"
40−"diagnostic" = "base03"
41−"error" = "base0E"
41+"warning" = "base09"
42+"error" = "base08"
4243
4344[palette]
4445base00 = "#181818" # Default Background
runtime/themes/base16_default_light.tomlnew file+60
@@ -0,0 +1,60 @@
1+# Author: NNB <nnbnh@protonmail.com>
2+
3+"ui.background" = { bg = "base00" }
4+"ui.menu" = "base01"
5+"ui.menu.selected" = { fg = "base01", bg = "base04" }
6+"ui.linenr" = { fg = "base03", bg = "base01" }
7+"ui.popup" = { bg = "base01" }
8+"ui.window" = { bg = "base01" }
9+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
10+"ui.selection" = { bg = "base02" }
11+"comment" = { fg = "base03", modifiers = ["italic"] }
12+"ui.statusline" = { fg = "base04", bg = "base01" }
13+"ui.help" = { fg = "base04", bg = "base01" }
14+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
15+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
16+"ui.text" = "base05"
17+"operator" = "base05"
18+"ui.text.focus" = "base05"
19+"variable" = "base08"
20+"constant.numeric" = "base09"
21+"constant" = "base09"
22+"attributes" = "base09"
23+"type" = "base0A"
24+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
25+"string" = "base0B"
26+"variable.other.member" = "base0B"
27+"constant.character.escape" = "base0C"
28+"function" = "base0D"
29+"constructor" = "base0D"
30+"special" = "base0D"
31+"keyword" = "base0E"
32+"label" = "base0E"
33+"namespace" = "base0E"
34+"ui.help" = { fg = "base06", bg = "base01" }
35+
36+"diagnostic" = { modifiers = ["underlined"] }
37+"ui.gutter" = { bg = "base01" }
38+"info" = "base0D"
39+"hint" = "base03"
40+"debug" = "base03"
41+"warning" = "base09"
42+"error" = "base08"
43+
44+[palette]
45+base00 = "#f8f8f8" # Default Background
46+base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
47+base02 = "#d8d8d8" # Selection Background
48+base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
49+base04 = "#585858" # Dark Foreground (Used for status bars)
50+base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
51+base06 = "#282828" # Light Foreground (Not often used)
52+base07 = "#181818" # Light Background (Not often used)
53+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
54+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
55+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
56+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
57+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
58+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
59+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
60+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
runtime/themes/base16_terminal.tomlnew file+39
@@ -0,0 +1,39 @@
1+# Author: NNB <nnbnh@protonmail.com>
2+
3+"ui.menu" = "black"
4+"ui.menu.selected" = { modifiers = ["reversed"] }
5+"ui.linenr" = { fg = "light-gray", bg = "black" }
6+"ui.popup" = { bg = "black" }
7+"ui.window" = { bg = "black" }
8+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
9+"ui.selection" = { fg = "gray", modifiers = ["reversed"] }
10+"comment" = { fg = "light-gray", modifiers = ["italic"] }
11+"ui.statusline" = { fg = "white", bg = "black" }
12+"ui.statusline.inactive" = { fg = "gray", bg = "black" }
13+"ui.help" = { fg = "white", bg = "black" }
14+"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] }
15+"ui.cursor.primary" = { modifiers = ["reversed"] }
16+"variable" = "light-red"
17+"constant.numeric" = "yellow"
18+"constant" = "yellow"
19+"attributes" = "yellow"
20+"type" = "light-yellow"
21+"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] }
22+"string" = "light-green"
23+"variable.other.member" = "light-green"
24+"constant.character.escape" = "light-cyan"
25+"function" = "light-blue"
26+"constructor" = "light-blue"
27+"special" = "light-blue"
28+"keyword" = "light-magenta"
29+"label" = "light-magenta"
30+"namespace" = "light-magenta"
31+"ui.help" = { fg = "white", bg = "black" }
32+
33+"diagnostic" = { modifiers = ["underlined"] }
34+"ui.gutter" = { bg = "black" }
35+"info" = "light-blue"
36+"hint" = "gray"
37+"debug" = "gray"
38+"warning" = "yellow"
39+"error" = "light-red"
runtime/themes/monokai_pro.tomlnew file+102
@@ -0,0 +1,102 @@
1+# Author : WindSoilder<WindSoilder@outlook.com>
2+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
3+# Credit goes to the original creator: https://monokai.pro
4+
5+"ui.linenr.selected" = { bg = "base3" }
6+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
7+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
8+
9+"info" = "base8"
10+"hint" = "base8"
11+
12+# background color
13+"ui.background" = { bg = "base2" }
14+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
15+
16+# status bars, panels, modals, autocompletion
17+"ui.statusline" = { bg = "base4" }
18+"ui.popup" = { bg = "base3" }
19+"ui.window" = { bg = "base3" }
20+"ui.help" = { bg = "base3" }
21+
22+# active line, highlighting
23+"ui.selection" = { bg = "base4" }
24+"ui.cursor.match" = { bg = "base4" }
25+
26+# comments, nord3 based lighter color
27+"comment" = { fg = "base5", modifiers = ["italic"] }
28+"ui.linenr" = { fg = "base5" }
29+
30+# cursor, variables, constants, attributes, fields
31+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
32+"attribute" = "blue"
33+"variable" = "base8"
34+"constant" = "orange"
35+"variable.builtin" = "red"
36+"constant.builtin" = "red"
37+"namespace" = "base8"
38+
39+# base text, punctuation
40+"ui.text" = { fg = "base8" }
41+"punctuation" = "base6"
42+
43+# classes, types, primiatives
44+"type" = "green"
45+"type.builtin" = { fg = "red"}
46+"label" = "base8"
47+
48+# declaration, methods, routines
49+"constructor" = "blue"
50+"function" = "green"
51+"function.macro" = { fg = "blue" }
52+"function.builtin" = { fg = "cyan" }
53+
54+# operator, tags, units, punctuations
55+"operator" = "red"
56+"variable.other.member" = "base8"
57+
58+# keywords, special
59+"keyword" = { fg = "red" }
60+"keyword.directive" = "blue"
61+"variable.parameter" = "#f59762"
62+
63+# error
64+"error" = "red"
65+
66+# annotations, decorators
67+"special" = "#f59762"
68+"module" = "#f59762"
69+
70+# warnings, escape characters, regex
71+"warning" = "orange"
72+"constant.character.escape" = { fg = "base8" }
73+
74+# strings
75+"string" = "yellow"
76+
77+# integer, floating point
78+"constant.numeric" = "purple"
79+
80+# make diagnostic underlined, to distinguish with selection text.
81+diagnostic = { modifiers = ["underlined"] }
82+
83+[palette]
84+# primary colors
85+"red" = "#ff6188"
86+"orange" = "#fc9867"
87+"yellow" = "#ffd866"
88+"green" = "#a9dc76"
89+"blue" = "#78dce8"
90+"purple" = "#ab9df2"
91+# base colors, sorted from darkest to lightest
92+"base0" = "#19181a"
93+"base1" = "#221f22"
94+"base2" = "#2d2a2e"
95+"base3" = "#403e41"
96+"base4" = "#5b595c"
97+"base5" = "#727072"
98+"base6" = "#939293"
99+"base7" = "#c1c0c0"
100+"base8" = "#fcfcfa"
101+# variants (for when transparency isn't supported)
102+"base8x0c" = "#363337" # using base2 as bg
runtime/themes/monokai_pro_machine.tomlnew file+102
@@ -0,0 +1,102 @@
1+# Author : WindSoilder<WindSoilder@outlook.com>
2+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
3+# Credit goes to the original creator: https://monokai.pro
4+
5+"ui.linenr.selected" = { bg = "base3" }
6+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
7+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
8+
9+"info" = "base8"
10+"hint" = "base8"
11+
12+# background color
13+"ui.background" = { bg = "base2" }
14+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
15+
16+# status bars, panels, modals, autocompletion
17+"ui.statusline" = { bg = "base4" }
18+"ui.popup" = { bg = "base3" }
19+"ui.window" = { bg = "base3" }
20+"ui.help" = { bg = "base3" }
21+
22+# active line, highlighting
23+"ui.selection" = { bg = "base4" }
24+"ui.cursor.match" = { bg = "base4" }
25+
26+# comments, nord3 based lighter color
27+"comment" = { fg = "base5", modifiers = ["italic"] }
28+"ui.linenr" = { fg = "base5" }
29+
30+# cursor, variables, constants, attributes, fields
31+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
32+"attribute" = "blue"
33+"variable" = "base8"
34+"constant" = "orange"
35+"variable.builtin" = "red"
36+"constant.builtin" = "red"
37+"namespace" = "base8"
38+
39+# base text, punctuation
40+"ui.text" = { fg = "base8" }
41+"punctuation" = "base6"
42+
43+# classes, types, primiatives
44+"type" = "green"
45+"type.builtin" = { fg = "red"}
46+"label" = "base8"
47+
48+# declaration, methods, routines
49+"constructor" = "blue"
50+"function" = "green"
51+"function.macro" = { fg = "blue" }
52+"function.builtin" = { fg = "cyan" }
53+
54+# operator, tags, units, punctuations
55+"operator" = "red"
56+"variable.other.member" = "base8"
57+
58+# keywords, special
59+"keyword" = { fg = "red" }
60+"keyword.directive" = "blue"
61+"variable.parameter" = "#f59762"
62+
63+# error
64+"error" = "red"
65+
66+# annotations, decorators
67+"special" = "#f59762"
68+"module" = "#f59762"
69+
70+# warnings, escape characters, regex
71+"warning" = "orange"
72+"constant.character.escape" = { fg = "base8" }
73+
74+# strings
75+"string" = "yellow"
76+
77+# integer, floating point
78+"constant.numeric" = "purple"
79+
80+# make diagnostic underlined, to distinguish with selection text.
81+diagnostic = { modifiers = ["underlined"] }
82+
83+[palette]
84+# primary colors
85+"red" = "#ff6d7e"
86+"orange" = "#ffb270"
87+"yellow" = "#ffed72"
88+"green" = "#a2e57b"
89+"blue" = "#7cd5f1"
90+"purple" = "#baa0f8"
91+# base colors
92+"base0" = "#161b1e"
93+"base1" = "#1d2528"
94+"base2" = "#273136"
95+"base3" = "#3a4449"
96+"base4" = "#545f62"
97+"base5" = "#6b7678"
98+"base6" = "#798384"
99+"base7" = "#b8c4c3"
100+"base8" = "#f2fffc"
101+# variants
102+"base8x0c" = "#303a3e"
runtime/themes/monokai_pro_octagon.tomlnew file+102
@@ -0,0 +1,102 @@
1+# Author : WindSoilder<WindSoilder@outlook.com>
2+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
3+# Credit goes to the original creator: https://monokai.pro
4+
5+"ui.linenr.selected" = { bg = "base3" }
6+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
7+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
8+
9+"info" = "base8"
10+"hint" = "base8"
11+
12+# background color
13+"ui.background" = { bg = "base2" }
14+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
15+
16+# status bars, panels, modals, autocompletion
17+"ui.statusline" = { bg = "base4" }
18+"ui.popup" = { bg = "base3" }
19+"ui.window" = { bg = "base3" }
20+"ui.help" = { bg = "base3" }
21+
22+# active line, highlighting
23+"ui.selection" = { bg = "base4" }
24+"ui.cursor.match" = { bg = "base4" }
25+
26+# comments, nord3 based lighter color
27+"comment" = { fg = "base5", modifiers = ["italic"] }
28+"ui.linenr" = { fg = "base5" }
29+
30+# cursor, variables, constants, attributes, fields
31+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
32+"attribute" = "blue"
33+"variable" = "base8"
34+"constant" = "orange"
35+"variable.builtin" = "red"
36+"constant.builtin" = "red"
37+"namespace" = "base8"
38+
39+# base text, punctuation
40+"ui.text" = { fg = "base8" }
41+"punctuation" = "base6"
42+
43+# classes, types, primiatives
44+"type" = "green"
45+"type.builtin" = { fg = "red"}
46+"label" = "base8"
47+
48+# declaration, methods, routines
49+"constructor" = "blue"
50+"function" = "green"
51+"function.macro" = { fg = "blue" }
52+"function.builtin" = { fg = "cyan" }
53+
54+# operator, tags, units, punctuations
55+"operator" = "red"
56+"variable.other.member" = "base8"
57+
58+# keywords, special
59+"keyword" = { fg = "red" }
60+"keyword.directive" = "blue"
61+"variable.parameter" = "#f59762"
62+
63+# error
64+"error" = "red"
65+
66+# annotations, decorators
67+"special" = "#f59762"
68+"module" = "#f59762"
69+
70+# warnings, escape characters, regex
71+"warning" = "orange"
72+"constant.character.escape" = { fg = "base8" }
73+
74+# strings
75+"string" = "yellow"
76+
77+# integer, floating point
78+"constant.numeric" = "purple"
79+
80+# make diagnostic underlined, to distinguish with selection text.
81+diagnostic = { modifiers = ["underlined"] }
82+
83+[palette]
84+# primary colors
85+"red" = "#ff657a"
86+"orange" = "#ff9b5e"
87+"yellow" = "#ffd76d"
88+"green" = "#bad761"
89+"blue" = "#9cd1bb"
90+"purple" = "#c39ac9"
91+# base colors
92+"base0" = "#161821"
93+"base1" = "#1e1f2b"
94+"base2" = "#282a3a"
95+"base3" = "#3a3d4b"
96+"base4" = "#535763"
97+"base5" = "#696d77"
98+"base6" = "#767b81"
99+"base7" = "#b2b9bd"
100+"base8" = "#eaf2f1"
101+# variants
102+"base8x0c" = "#303342"
runtime/themes/monokai_pro_ristretto.tomlnew file+102
@@ -0,0 +1,102 @@
1+# Author : WindSoilder<WindSoilder@outlook.com>
2+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
3+# Credit goes to the original creator: https://monokai.pro
4+
5+"ui.linenr.selected" = { bg = "base3" }
6+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
7+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
8+
9+"info" = "base8"
10+"hint" = "base8"
11+
12+# background color
13+"ui.background" = { bg = "base2" }
14+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
15+
16+# status bars, panels, modals, autocompletion
17+"ui.statusline" = { bg = "base4" }
18+"ui.popup" = { bg = "base3" }
19+"ui.window" = { bg = "base3" }
20+"ui.help" = { bg = "base3" }
21+
22+# active line, highlighting
23+"ui.selection" = { bg = "base4" }
24+"ui.cursor.match" = { bg = "base4" }
25+
26+# comments, nord3 based lighter color
27+"comment" = { fg = "base5", modifiers = ["italic"] }
28+"ui.linenr" = { fg = "base5" }
29+
30+# cursor, variables, constants, attributes, fields
31+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
32+"attribute" = "blue"
33+"variable" = "base8"
34+"constant" = "orange"
35+"variable.builtin" = "red"
36+"constant.builtin" = "red"
37+"namespace" = "base8"
38+
39+# base text, punctuation
40+"ui.text" = { fg = "base8" }
41+"punctuation" = "base6"
42+
43+# classes, types, primiatives
44+"type" = "green"
45+"type.builtin" = { fg = "red"}
46+"label" = "base8"
47+
48+# declaration, methods, routines
49+"constructor" = "blue"
50+"function" = "green"
51+"function.macro" = { fg = "blue" }
52+"function.builtin" = { fg = "cyan" }
53+
54+# operator, tags, units, punctuations
55+"operator" = "red"
56+"variable.other.member" = "base8"
57+
58+# keywords, special
59+"keyword" = { fg = "red" }
60+"keyword.directive" = "blue"
61+"variable.parameter" = "#f59762"
62+
63+# error
64+"error" = "red"
65+
66+# annotations, decorators
67+"special" = "#f59762"
68+"module" = "#f59762"
69+
70+# warnings, escape characters, regex
71+"warning" = "orange"
72+"constant.character.escape" = { fg = "base8" }
73+
74+# strings
75+"string" = "yellow"
76+
77+# integer, floating point
78+"constant.numeric" = "purple"
79+
80+# make diagnostic underlined, to distinguish with selection text.
81+diagnostic = { modifiers = ["underlined"] }
82+
83+[palette]
84+# primary colors
85+"red" = "#fd6883"
86+"orange" = "#f38d70"
87+"yellow" = "#f9cc6c"
88+"green" = "#adda78"
89+"blue" = "#85dacc"
90+"purple" = "#a8a9eb"
91+# base colors
92+"base0" = "#191515"
93+"base1" = "#211c1c"
94+"base2" = "#2c2525"
95+"base3" = "#403838"
96+"base4" = "#5b5353"
97+"base5" = "#72696a"
98+"base6" = "#8c8384"
99+"base7" = "#c3b7b8"
100+"base8" = "#fff1f3"
101+# variants
102+"base8x0c" = "#352e2e"
runtime/themes/monokai_pro_spectrum.tomlnew file+102
@@ -0,0 +1,102 @@
1+# Author : WindSoilder<WindSoilder@outlook.com>
2+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
3+# Credit goes to the original creator: https://monokai.pro
4+
5+"ui.linenr.selected" = { bg = "base3" }
6+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
7+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
8+
9+"info" = "base8"
10+"hint" = "base8"
11+
12+# background color
13+"ui.background" = { bg = "base2" }
14+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
15+
16+# status bars, panels, modals, autocompletion
17+"ui.statusline" = { bg = "base4" }
18+"ui.popup" = { bg = "base3" }
19+"ui.window" = { bg = "base3" }
20+"ui.help" = { bg = "base3" }
21+
22+# active line, highlighting
23+"ui.selection" = { bg = "base4" }
24+"ui.cursor.match" = { bg = "base4" }
25+
26+# comments, nord3 based lighter color
27+"comment" = { fg = "base5", modifiers = ["italic"] }
28+"ui.linenr" = { fg = "base5" }
29+
30+# cursor, variables, constants, attributes, fields
31+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
32+"attribute" = "blue"
33+"variable" = "base8"
34+"constant" = "orange"
35+"variable.builtin" = "red"
36+"constant.builtin" = "red"
37+"namespace" = "base8"
38+
39+# base text, punctuation
40+"ui.text" = { fg = "base8" }
41+"punctuation" = "base6"
42+
43+# classes, types, primiatives
44+"type" = "green"
45+"type.builtin" = { fg = "red"}
46+"label" = "base8"
47+
48+# declaration, methods, routines
49+"constructor" = "blue"
50+"function" = "green"
51+"function.macro" = { fg = "blue" }
52+"function.builtin" = { fg = "cyan" }
53+
54+# operator, tags, units, punctuations
55+"operator" = "red"
56+"variable.other.member" = "base8"
57+
58+# keywords, special
59+"keyword" = { fg = "red" }
60+"keyword.directive" = "blue"
61+"variable.parameter" = "#f59762"
62+
63+# error
64+"error" = "red"
65+
66+# annotations, decorators
67+"special" = "#f59762"
68+"module" = "#f59762"
69+
70+# warnings, escape characters, regex
71+"warning" = "orange"
72+"constant.character.escape" = { fg = "base8" }
73+
74+# strings
75+"string" = "yellow"
76+
77+# integer, floating point
78+"constant.numeric" = "purple"
79+
80+# make diagnostic underlined, to distinguish with selection text.
81+diagnostic = { modifiers = ["underlined"] }
82+
83+[palette]
84+# primary colors
85+"red" = "#fc618d"
86+"orange" = "#fd9353"
87+"yellow" = "#fce566"
88+"green" = "#7bd88f"
89+"blue" = "#5ad4e6"
90+"purple" = "#948ae3"
91+# base colors
92+"base0" = "#131313"
93+"base1" = "#191919"
94+"base2" = "#222222"
95+"base3" = "#363537"
96+"base4" = "#525053"
97+"base5" = "#69676c"
98+"base6" = "#8b888f"
99+"base7" = "#bab6c0"
100+"base8" = "#f7f1ff"
101+# variants
102+"base8x0c" = "#2b2b2b"
runtime/themes/rose_pine.toml+1
@@ -10,6 +10,7 @@
1010"ui.selection" = "highlight"
1111"comment" = "subtle"
1212"ui.statusline" = {fg = "foam", bg = "surface" }
13+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
1314"ui.help" = { fg = "foam", bg = "surface" }
1415"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
1516"ui.text" = { fg = "text" }
runtime/themes/rose_pine_dawn.tomlnew file+63
@@ -0,0 +1,63 @@
1+# Author: ChrisHa<chunghha@users.noreply.github.com>
2+# Author: RayGervais<raygervais@hotmail.ca>
3+
4+"ui.background" = { bg = "base" }
5+"ui.menu" = "surface"
6+"ui.menu.selected" = { fg = "iris", bg = "surface" }
7+"ui.linenr" = {fg = "subtle" }
8+"ui.popup" = { bg = "overlay" }
9+"ui.window" = { bg = "overlay" }
10+"ui.liner.selected" = "highlightOverlay"
11+"ui.selection" = "highlight"
12+"comment" = "subtle"
13+"ui.statusline" = {fg = "foam", bg = "surface" }
14+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
15+"ui.help" = { fg = "foam", bg = "surface" }
16+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
17+"ui.text" = { fg = "text" }
18+"operator" = "rose"
19+"ui.text.focus" = { fg = "base05" }
20+"variable" = "text"
21+"number" = "iris"
22+"constant" = "gold"
23+"attributes" = "gold"
24+"type" = "foam"
25+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
26+"string" = "gold"
27+"property" = "foam"
28+"escape" = "subtle"
29+"function" = "rose"
30+"function.builtin" = "rose"
31+"function.method" = "foam"
32+"constructor" = "gold"
33+"special" = "gold"
34+"keyword" = "pine"
35+"label" = "iris"
36+"namespace" = "pine"
37+"ui.popup" = { bg = "overlay" }
38+"ui.window" = { bg = "base" }
39+"ui.help" = { bg = "overlay", fg = "foam" }
40+"text" = "text"
41+
42+"info" = "gold"
43+"hint" = "gold"
44+"debug" = "rose"
45+"diagnostic" = "rose"
46+"error" = "love"
47+
48+[palette]
49+base = "#faf4ed"
50+surface = "#fffaf3"
51+overlay = "#f2e9de"
52+inactive = "#9893a5"
53+subtle = "#6e6a86"
54+text = "#575279"
55+love = "#b4637a"
56+gold = "#ea9d34"
57+rose = "#d7827e"
58+pine = "#286983"
59+foam = "#56949f"
60+iris = "#907aa9"
61+highlight = "#eee9e6"
62+highlightInactive = "#f2ede9"
63+highlightOverlay = "#e4dfde"
runtime/themes/solarized_dark.toml+16−14
@@ -58,13 +58,13 @@
5858"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
5959
6060# 主光标/selectio
61−"ui.cursor.primary" = {fg = "base03", bg = "base1"}
62−"ui.selection.primary" = { fg = "base03", bg = "base01" }
63−"ui.cursor.select" = {fg = "base02", bg = "green"}
64−"ui.selection" = { fg = "base02", bg = "yellow" }
61+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
62+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
63+"ui.selection" = { bg = "base0175" }
64+"ui.selection.primary" = { bg = "base015" }
6565
6666# normal模式的光标
67−"ui.cursor" = {fg = "base03", bg = "green"}
67+"ui.cursor" = {fg = "base02", bg = "cyan"}
6868"ui.cursor.insert" = {fg = "base03", bg = "base3"}
6969# 当前光标匹配的标点符号
7070"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,18 +73,20 @@
7373"error" = { fg = "red", modifiers= ["bold", "underlined"] }
7474"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
7575"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
76−"diagnostic" = { mdifiers = ["underlined"] }
76+"diagnostic" = { modifiers = ["underlined"] }
7777
7878[palette]
7979# 深色 越来越深
80−base03 = "#002b36"
81−base02 = "#073642"
82−base01 = "#586e75"
83−base00 = "#657b83"
84−base0 = "#839496"
85−base1 = "#93a1a1"
86−base2 = "#eee8d5"
87−base3 = "#fdf6e3"
80+base03 = "#002b36"
81+base02 = "#073642"
82+base0175 = "#16404b"
83+base015 = "#2c4f59"
84+base01 = "#586e75"
85+base00 = "#657b83"
86+base0 = "#839496"
87+base1 = "#93a1a1"
88+base2 = "#eee8d5"
89+base3 = "#fdf6e3"
8890
8991# 浅色 越來越浅
9092yellow = "#b58900"
runtime/themes/solarized_light.toml+24−22
@@ -58,13 +58,13 @@
5858"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
5959
6060# 主光标/selectio
61−"ui.cursor.primary" = {fg = "base03", bg = "base1"}
62−"ui.selection.primary" = { fg = "base03", bg = "base01" }
63−"ui.cursor.select" = {fg = "base02", bg = "green"}
64−"ui.selection" = { fg = "base02", bg = "yellow" }
61+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
62+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
63+"ui.selection" = { bg = "base0175" }
64+"ui.selection.primary" = { bg = "base015" }
6565
6666# normal模式的光标
67−"ui.cursor" = {fg = "base03", bg = "green"}
67+"ui.cursor" = {fg = "base02", bg = "cyan"}
6868"ui.cursor.insert" = {fg = "base03", bg = "base3"}
6969# 当前光标匹配的标点符号
7070"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,26 +73,28 @@
7373"error" = { fg = "red", modifiers= ["bold", "underlined"] }
7474"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
7575"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
76−"diagnostic" = { mdifiers = ["underlined"] }
76+"diagnostic" = { modifiers = ["underlined"] }
7777
7878[palette]
79−red = '#dc322f'
80−green = '#859900'
81−yellow = '#b58900'
82−blue = '#268bd2'
83−magenta = '#d33682'
84−cyan = '#2aa198'
85−orange = '#cb4b16'
86−violet = '#6c71c4'
79+red = '#dc322f'
80+green = '#859900'
81+yellow = '#b58900'
82+blue = '#268bd2'
83+magenta = '#d33682'
84+cyan = '#2aa198'
85+orange = '#cb4b16'
86+violet = '#6c71c4'
8787
8888# 深色 越来越深
89−base0 = '#657b83'
90−base1 = '#586e75'
91−base2 = '#073642'
92−base3 = '#002b36'
89+base0 = '#657b83'
90+base1 = '#586e75'
91+base2 = '#073642'
92+base3 = '#002b36'
9393
9494## 浅色 越來越浅
95−base00 = '#839496'
96−base01 = '#93a1a1'
97−base02 = '#eee8d5'
98−base03 = '#fdf6e3'
95+base00 = '#839496'
96+base01 = '#93a1a1'
97+base015 = '#c5c8bd'
98+base0175 = '#dddbcc'
99+base02 = '#eee8d5'
100+base03 = '#fdf6e3'
theme.toml+6
@@ -28,6 +28,12 @@ string = "silver"
2828# used for lifetimes
2929label = "honey"
3030
31+"markup.heading" = "lilac"
32+"markup.bold" = { modifiers = ["bold"] }
33+"markup.italic" = { modifiers = ["italic"] }
34+"markup.underline.link" = { fg = "silver", modifiers = ["underlined"] }
35+"markup.raw" = "almond"
36+
3137# TODO: diferentiate doc comment
3238# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
3339
xtask/Cargo.tomlnew file+11
@@ -0,0 +1,11 @@
1+[package]
2+name = "xtask"
3+version = "0.5.0"
4+edition = "2021"
5+
6+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+[dependencies]
9+helix-term = { version = "0.5", path = "../helix-term" }
10+helix-core = { version = "0.5", path = "../helix-core" }
11+toml = "0.5"
xtask/src/main.rsnew file+277
@@ -0,0 +1,277 @@
1+use std::{env, error::Error};
2+
3+type DynError = Box<dyn Error>;
4+
5+pub mod helpers {
6+ use std::{
7+ fmt::Display,
8+ path::{Path, PathBuf},
9+ };
10+
11+ use crate::path;
12+ use helix_core::syntax::Configuration as LangConfig;
13+
14+ #[derive(Copy, Clone)]
15+ pub enum TsFeature {
16+ Highlight,
17+ TextObjects,
18+ AutoIndent,
19+ }
20+
21+ impl TsFeature {
22+ pub fn all() -> &'static [Self] {
23+ &[Self::Highlight, Self::TextObjects, Self::AutoIndent]
24+ }
25+
26+ pub fn runtime_filename(&self) -> &'static str {
27+ match *self {
28+ Self::Highlight => "highlights.scm",
29+ Self::TextObjects => "textobjects.scm",
30+ Self::AutoIndent => "indents.toml",
31+ }
32+ }
33+ }
34+
35+ impl Display for TsFeature {
36+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37+ write!(
38+ f,
39+ "{}",
40+ match *self {
41+ Self::Highlight => "Syntax Highlighting",
42+ Self::TextObjects => "Treesitter Textobjects",
43+ Self::AutoIndent => "Auto Indent",
44+ }
45+ )
46+ }
47+ }
48+
49+ /// Get the list of languages that support a particular tree-sitter
50+ /// based feature.
51+ pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
52+ let queries_dir = path::ts_queries();
53+
54+ find_files(&queries_dir, feat.runtime_filename())
55+ .iter()
56+ .map(|f| {
57+ // .../helix/runtime/queries/python/highlights.scm
58+ let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
59+ let lang = tail.components().next().unwrap(); // python
60+ lang.as_os_str().to_string_lossy().to_string()
61+ })
62+ .collect()
63+ }
64+
65+ /// Get the list of languages that have any form of tree-sitter
66+ /// queries defined in the runtime directory.
67+ pub fn langs_with_ts_queries() -> Vec<String> {
68+ std::fs::read_dir(path::ts_queries())
69+ .unwrap()
70+ .filter_map(|entry| {
71+ let entry = entry.ok()?;
72+ entry
73+ .file_type()
74+ .ok()?
75+ .is_dir()
76+ .then(|| entry.file_name().to_string_lossy().to_string())
77+ })
78+ .collect()
79+ }
80+
81+ // naive implementation, but suffices for our needs
82+ pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
83+ std::fs::read_dir(dir)
84+ .unwrap()
85+ .filter_map(|entry| {
86+ let path = entry.ok()?.path();
87+ if path.is_dir() {
88+ Some(find_files(&path, filename))
89+ } else {
90+ (path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
91+ }
92+ })
93+ .flatten()
94+ .collect()
95+ }
96+
97+ pub fn lang_config() -> LangConfig {
98+ let bytes = std::fs::read(path::lang_config()).unwrap();
99+ toml::from_slice(&bytes).unwrap()
100+ }
101+}
102+
103+pub mod md_gen {
104+ use crate::DynError;
105+
106+ use crate::helpers;
107+ use crate::path;
108+ use std::fs;
109+
110+ use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
111+
112+ pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
113+ pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
114+
115+ fn md_table_heading(cols: &[String]) -> String {
116+ let mut header = String::new();
117+ header += &md_table_row(cols);
118+ header += &md_table_row(&vec!["---".to_string(); cols.len()]);
119+ header
120+ }
121+
122+ fn md_table_row(cols: &[String]) -> String {
123+ "| ".to_owned() + &cols.join(" | ") + " |\n"
124+ }
125+
126+ fn md_mono(s: &str) -> String {
127+ format!("`{}`", s)
128+ }
129+
130+ pub fn typable_commands() -> Result<String, DynError> {
131+ let mut md = String::new();
132+ md.push_str(&md_table_heading(&[
133+ "Name".to_owned(),
134+ "Description".to_owned(),
135+ ]));
136+
137+ let cmdify = |s: &str| format!("`:{}`", s);
138+
139+ for cmd in TYPABLE_COMMAND_LIST {
140+ let names = std::iter::once(&cmd.name)
141+ .chain(cmd.aliases.iter())
142+ .map(|a| cmdify(a))
143+ .collect::<Vec<_>>()
144+ .join(", ");
145+
146+ md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()]));
147+ }
148+
149+ Ok(md)
150+ }
151+
152+ pub fn lang_features() -> Result<String, DynError> {
153+ let mut md = String::new();
154+ let ts_features = helpers::TsFeature::all();
155+
156+ let mut cols = vec!["Language".to_owned()];
157+ cols.append(
158+ &mut ts_features
159+ .iter()
160+ .map(|t| t.to_string())
161+ .collect::<Vec<_>>(),
162+ );
163+ cols.push("Default LSP".to_owned());
164+
165+ md.push_str(&md_table_heading(&cols));
166+ let config = helpers::lang_config();
167+
168+ let mut langs = config
169+ .language
170+ .iter()
171+ .map(|l| l.language_id.clone())
172+ .collect::<Vec<_>>();
173+ langs.sort_unstable();
174+
175+ let mut ts_features_to_langs = Vec::new();
176+ for &feat in ts_features {
177+ ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
178+ }
179+
180+ let mut row = Vec::new();
181+ for lang in langs {
182+ let lc = config
183+ .language
184+ .iter()
185+ .find(|l| l.language_id == lang)
186+ .unwrap(); // lang comes from config
187+ row.push(lc.language_id.clone());
188+
189+ for (_feat, support_list) in &ts_features_to_langs {
190+ row.push(
191+ if support_list.contains(&lang) {
192+ "✓"
193+ } else {
194+ ""
195+ }
196+ .to_owned(),
197+ );
198+ }
199+ row.push(
200+ lc.language_server
201+ .as_ref()
202+ .map(|s| s.command.clone())
203+ .map(|c| md_mono(&c))
204+ .unwrap_or_default(),
205+ );
206+
207+ md.push_str(&md_table_row(&row));
208+ row.clear();
209+ }
210+
211+ Ok(md)
212+ }
213+
214+ pub fn write(filename: &str, data: &str) {
215+ let error = format!("Could not write to {}", filename);
216+ let path = path::book_gen().join(filename);
217+ fs::write(path, data).expect(&error);
218+ }
219+}
220+
221+pub mod path {
222+ use std::path::{Path, PathBuf};
223+
224+ pub fn project_root() -> PathBuf {
225+ Path::new(env!("CARGO_MANIFEST_DIR"))
226+ .parent()
227+ .unwrap()
228+ .to_path_buf()
229+ }
230+
231+ pub fn book_gen() -> PathBuf {
232+ project_root().join("book/src/generated/")
233+ }
234+
235+ pub fn ts_queries() -> PathBuf {
236+ project_root().join("runtime/queries")
237+ }
238+
239+ pub fn lang_config() -> PathBuf {
240+ project_root().join("languages.toml")
241+ }
242+}
243+
244+pub mod tasks {
245+ use crate::md_gen;
246+ use crate::DynError;
247+
248+ pub fn docgen() -> Result<(), DynError> {
249+ use md_gen::*;
250+ write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
251+ write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
252+ Ok(())
253+ }
254+
255+ pub fn print_help() {
256+ println!(
257+ "
258+Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
259+
260+ Tasks:
261+ docgen: Generate files to be included in the mdbook output.
262+"
263+ );
264+ }
265+}
266+
267+fn main() -> Result<(), DynError> {
268+ let task = env::args().nth(1);
269+ match task {
270+ None => tasks::print_help(),
271+ Some(t) => match t.as_str() {
272+ "docgen" => tasks::docgen()?,
273+ invalid => return Err(format!("Invalid task name: {}", invalid).into()),
274+ },
275+ };
276+ Ok(())
277+}