aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGokul Soumya2021-12-18 03:03:15 +0000
committerGokul Soumya2021-12-18 03:03:15 +0000
commitd4fb1d06333315a3c6e9dc7f0ad8055d91551e3a (patch)
treef43da59eec8b897359b84390b1c7d5b69f3bb169
parent016640f4fb6f620df13a2cab15e749d623197a51 (diff)
parent3ef115d4203fab93f7efe4f65d8dd63cc4535b91 (diff)
Merge branch 'master' into cursor-shape-new
-rw-r--r--.cargo/config2
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md2
-rw-r--r--.github/workflows/build.yml66
-rw-r--r--.github/workflows/release.yml2
-rw-r--r--.gitmodules12
-rw-r--r--Cargo.lock55
-rw-r--r--Cargo.toml1
-rw-r--r--README.md20
-rw-r--r--base16_theme.toml38
-rw-r--r--book/src/SUMMARY.md4
-rw-r--r--book/src/commands.md5
-rw-r--r--book/src/configuration.md1
-rw-r--r--book/src/generated/lang-support.md42
-rw-r--r--book/src/generated/typable-cmd.md43
-rw-r--r--book/src/guides/adding_languages.md2
-rw-r--r--book/src/install.md13
-rw-r--r--book/src/keymap.md83
-rw-r--r--book/src/lang-support.md10
-rw-r--r--book/src/remapping.md10
-rw-r--r--book/src/themes.md19
-rw-r--r--book/src/usage.md2
-rw-r--r--docs/CONTRIBUTING.md37
-rw-r--r--flake.lock54
-rw-r--r--flake.nix6
-rw-r--r--helix-core/Cargo.toml2
-rw-r--r--helix-core/src/auto_pairs.rs421
-rw-r--r--helix-core/src/diagnostic.rs4
-rw-r--r--helix-core/src/increment/date_time.rs490
-rw-r--r--helix-core/src/increment/mod.rs8
-rw-r--r--helix-core/src/increment/number.rs (renamed from helix-core/src/numbers.rs)30
-rw-r--r--helix-core/src/lib.rs5
-rw-r--r--helix-core/src/register.rs14
-rw-r--r--helix-core/src/selection.rs6
-rw-r--r--helix-core/src/shellwords.rs164
-rw-r--r--helix-core/src/surround.rs148
-rw-r--r--helix-core/src/syntax.rs7
-rw-r--r--helix-core/src/textobject.rs10
-rw-r--r--helix-core/src/transaction.rs16
-rw-r--r--helix-lsp/src/lib.rs5
m---------helix-syntax/languages/tree-sitter-markdown0
m---------helix-syntax/languages/tree-sitter-wgsl0
-rw-r--r--helix-term/Cargo.toml1
-rw-r--r--helix-term/src/application.rs30
-rw-r--r--helix-term/src/commands.rs760
-rw-r--r--helix-term/src/compositor.rs26
-rw-r--r--helix-term/src/keymap.rs55
-rw-r--r--helix-term/src/lib.rs11
-rw-r--r--helix-term/src/ui/completion.rs6
-rw-r--r--helix-term/src/ui/editor.rs140
-rw-r--r--helix-term/src/ui/markdown.rs6
-rw-r--r--helix-term/src/ui/menu.rs4
-rw-r--r--helix-term/src/ui/mod.rs1
-rw-r--r--helix-term/src/ui/picker.rs14
-rw-r--r--helix-term/src/ui/popup.rs29
-rw-r--r--helix-term/src/ui/prompt.rs4
-rw-r--r--helix-tui/src/buffer.rs11
-rw-r--r--helix-tui/src/text.rs16
-rw-r--r--helix-tui/src/widgets/table.rs11
-rw-r--r--helix-view/src/document.rs6
-rw-r--r--helix-view/src/editor.rs187
-rw-r--r--helix-view/src/graphics.rs13
-rw-r--r--helix-view/src/gutter.rs96
-rw-r--r--helix-view/src/input.rs44
-rw-r--r--helix-view/src/keyboard.rs5
-rw-r--r--helix-view/src/lib.rs15
-rw-r--r--helix-view/src/theme.rs51
-rw-r--r--helix-view/src/view.rs30
-rw-r--r--languages.toml36
-rw-r--r--runtime/queries/haskell/highlights.scm2
-rw-r--r--runtime/queries/julia/locals.scm22
-rw-r--r--runtime/queries/latex/highlights.scm28
-rw-r--r--runtime/queries/ledger/highlights.scm2
-rw-r--r--runtime/queries/llvm/highlights.scm14
-rw-r--r--runtime/queries/markdown/highlights.scm35
-rw-r--r--runtime/queries/markdown/injections.scm8
-rw-r--r--runtime/queries/ocaml/highlights.scm2
-rw-r--r--runtime/queries/perl/indents.toml17
-rw-r--r--runtime/queries/svelte/highlights.scm2
-rw-r--r--runtime/queries/wgsl/highlights.scm102
-rw-r--r--runtime/themes/base16_default_dark.toml37
-rw-r--r--runtime/themes/base16_default_light.toml60
-rw-r--r--runtime/themes/base16_terminal.toml39
-rw-r--r--runtime/themes/monokai_pro.toml102
-rw-r--r--runtime/themes/monokai_pro_machine.toml102
-rw-r--r--runtime/themes/monokai_pro_octagon.toml102
-rw-r--r--runtime/themes/monokai_pro_ristretto.toml102
-rw-r--r--runtime/themes/monokai_pro_spectrum.toml102
-rw-r--r--runtime/themes/rose_pine.toml1
-rw-r--r--runtime/themes/rose_pine_dawn.toml63
-rw-r--r--runtime/themes/solarized_dark.toml30
-rw-r--r--runtime/themes/solarized_light.toml46
-rw-r--r--theme.toml6
-rw-r--r--xtask/Cargo.toml11
-rw-r--r--xtask/src/main.rs277
94 files changed, 3862 insertions, 919 deletions
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 00000000..35049cbc
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --package xtask --"
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9b7c22e7..958407bb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
-- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
+- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d4822f70..7f18da6a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,19 +25,19 @@ jobs:
override: true
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
override: true
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -109,19 +109,19 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -137,3 +137,51 @@ jobs:
with:
command: clippy
args: -- -D warnings
+
+ docs:
+ name: Docs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - name: Install stable toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache cargo registry
+ uses: actions/cache@v2.1.6
+ with:
+ path: ~/.cargo/registry
+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache cargo index
+ uses: actions/cache@v2.1.6
+ with:
+ path: ~/.cargo/git
+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache cargo target dir
+ uses: actions/cache@v2.1.6
+ with:
+ path: target
+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Generate docs
+ uses: actions-rs/cargo@v1
+ with:
+ command: xtask
+ args: docgen
+
+ - name: Check uncommitted documentation changes
+ run: |
+ git diff
+ git diff-files --quiet \
+ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
+ && exit 1)
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b16fa428..1ce3e092 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -102,7 +102,7 @@ jobs:
fi
cp -r runtime dist
- - uses: actions/upload-artifact@v2.2.4
+ - uses: actions/upload-artifact@v2.3.0
with:
name: bins-${{ matrix.build }}
path: dist
diff --git a/.gitmodules b/.gitmodules
index bf596bdc..9c10846d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -142,3 +142,15 @@
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
+[submodule "helix-syntax/languages/tree-sitter-wgsl"]
+ path = helix-syntax/languages/tree-sitter-wgsl
+ url = https://github.com/szebniok/tree-sitter-wgsl
+ shallow = true
+[submodule "helix-syntax/tree-sitter-llvm"]
+ path = helix-syntax/languages/tree-sitter-llvm
+ url = https://github.com/benwilliamgraham/tree-sitter-llvm
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-markdown"]
+ path = helix-syntax/languages/tree-sitter-markdown
+ url = https://github.com/MDeiml/tree-sitter-markdown
+ shallow = true
diff --git a/Cargo.lock b/Cargo.lock
index 8d2b4562..cd6c0496 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.48"
+version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "arc-swap"
@@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
-version = "0.8.29"
+version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
@@ -258,15 +258,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-executor"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
dependencies = [
"futures-core",
"futures-task",
@@ -275,17 +275,16 @@ dependencies = [
[[package]]
name = "futures-task"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
- "autocfg",
"futures-core",
"futures-task",
"pin-project-lite",
@@ -370,6 +369,7 @@ name = "helix-core"
version = "0.5.0"
dependencies = [
"arc-swap",
+ "chrono",
"etcetera",
"helix-syntax",
"log",
@@ -535,9 +535,9 @@ dependencies = [
[[package]]
name = "itoa"
-version = "0.4.8"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "jsonrpc-core"
@@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
-version = "1.0.130"
+version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.130"
+version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
dependencies = [
"proc-macro2",
"quote",
@@ -897,9 +897,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.71"
+version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
+checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [
"itoa",
"ryu",
@@ -919,9 +919,9 @@ dependencies = [
[[package]]
name = "signal-hook"
-version = "0.3.10"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
+checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
dependencies = [
"libc",
"signal-hook-registry",
@@ -1259,3 +1259,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "xtask"
+version = "0.5.0"
+dependencies = [
+ "helix-core",
+ "helix-term",
+ "toml",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 580cccd6..8c3ee671 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"helix-tui",
"helix-syntax",
"helix-lsp",
+ "xtask",
]
# Build helix-syntax in release mode to make the code path faster in development.
diff --git a/README.md b/README.md
index 3f4087b9..71010cc8 100644
--- a/README.md
+++ b/README.md
@@ -44,8 +44,8 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
-config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
-via the `HELIX_RUNTIME` environment variable.
+config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
+This location can be overriden via the `HELIX_RUNTIME` environment variable.
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
@@ -65,21 +65,7 @@ brew install helix
# Contributing
-Contributors are very welcome! **No contribution is too small and all contributions are valued.**
-
-Some suggestions to get started:
-
-- 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.
-- Help with packaging on various distributions needed!
-- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
- * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
- * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
-- If your preferred language is missing, integrating a tree-sitter grammar for
- it and defining syntax highlight queries for it is straight forward and
- doesn't require much knowledge of the internals.
-
-We provide an [architecture.md](./docs/architecture.md) that should give you
-a good overview of the internals.
+Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
# Getting help
diff --git a/base16_theme.toml b/base16_theme.toml
new file mode 100644
index 00000000..5ec74bcc
--- /dev/null
+++ b/base16_theme.toml
@@ -0,0 +1,38 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "gray", bg = "black" }
+"ui.popup" = { modifiers = ["reversed"] }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "black", bg = "blue" }
+"ui.selection.primary" = { fg = "white", bg = "blue" }
+"comment" = { fg = "gray" }
+"ui.statusline" = { fg = "black", bg = "white" }
+"ui.statusline.inactive" = { fg = "gray", bg = "white" }
+"ui.help" = { modifiers = ["reversed"] }
+"ui.cursor" = { modifiers = ["reversed"] }
+"variable" = "red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "yellow"
+"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] }
+"string" = "green"
+"variable.other.member" = "green"
+"constant.character.escape" = "cyan"
+"function" = "blue"
+"constructor" = "blue"
+"special" = "blue"
+"keyword" = "magenta"
+"label" = "magenta"
+"namespace" = "magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "red"
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 8cadb663..a8f165c0 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -2,10 +2,12 @@
- [Installation](./install.md)
- [Usage](./usage.md)
+ - [Keymap](./keymap.md)
+ - [Commands](./commands.md)
+ - [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
- [Languages](./languages.md)
diff --git a/book/src/commands.md b/book/src/commands.md
new file mode 100644
index 00000000..4c4a5c05
--- /dev/null
+++ b/book/src/commands.md
@@ -0,0 +1,5 @@
+# Commands
+
+Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
+
+{{#include ./generated/typable-cmd.md}}
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 2998bcdc..476c2b39 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -41,6 +41,7 @@ hidden = false
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
+| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
### `[editor.cursor-shape]` Section
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
new file mode 100644
index 00000000..80989e63
--- /dev/null
+++ b/book/src/generated/lang-support.md
@@ -0,0 +1,42 @@
+| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
+| --- | --- | --- | --- | --- |
+| bash | ✓ | | | `bash-language-server` |
+| c | ✓ | | | `clangd` |
+| c-sharp | ✓ | | | |
+| cmake | ✓ | | | `cmake-language-server` |
+| cpp | ✓ | | | `clangd` |
+| css | ✓ | | | |
+| elixir | ✓ | | | `elixir-ls` |
+| glsl | ✓ | | ✓ | |
+| go | ✓ | ✓ | ✓ | `gopls` |
+| html | ✓ | | | |
+| java | ✓ | | | |
+| javascript | ✓ | | ✓ | |
+| json | ✓ | | ✓ | |
+| julia | ✓ | | | `julia` |
+| latex | ✓ | | | |
+| ledger | ✓ | | | |
+| llvm | ✓ | | | |
+| lua | ✓ | | ✓ | |
+| markdown | ✓ | | | |
+| mint | | | | `mint` |
+| nix | ✓ | | ✓ | `rnix-lsp` |
+| ocaml | ✓ | | ✓ | |
+| ocaml-interface | ✓ | | | |
+| perl | ✓ | ✓ | ✓ | |
+| php | ✓ | | ✓ | |
+| prolog | | | | `swipl` |
+| protobuf | ✓ | | ✓ | |
+| python | ✓ | ✓ | ✓ | `pylsp` |
+| racket | | | | `racket` |
+| ruby | ✓ | | | `solargraph` |
+| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+| svelte | ✓ | | ✓ | `svelteserver` |
+| toml | ✓ | | | |
+| tsq | ✓ | | | |
+| tsx | ✓ | | | `typescript-language-server` |
+| typescript | ✓ | | ✓ | `typescript-language-server` |
+| vue | ✓ | | | |
+| wgsl | ✓ | | | |
+| yaml | ✓ | | ✓ | |
+| zig | ✓ | | ✓ | `zls` |
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
new file mode 100644
index 00000000..bb21fd6b
--- /dev/null
+++ b/book/src/generated/typable-cmd.md
@@ -0,0 +1,43 @@
+| Name | Description |
+| --- | --- |
+| `:quit`, `:q` | Close the current view. |
+| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
+| `:open`, `:o` | Open a file from disk into the current view. |
+| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
+| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
+| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
+| `:new`, `:n` | Create a new scratch buffer. |
+| `:format`, `:fmt` | Format the file using the LSP formatter. |
+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
+| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
+| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
+| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
+| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
+| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
+| `:write-all`, `:wa` | Write changes from all views to disk. |
+| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
+| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
+| `:quit-all`, `:qa` | Close all views. |
+| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
+| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
+| `:theme` | Change the editor theme. |
+| `:clipboard-yank` | Yank main selection into system clipboard. |
+| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
+| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:clipboard-paste-after` | Paste system clipboard after selections. |
+| `:clipboard-paste-before` | Paste system clipboard before selections. |
+| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
+| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
+| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
+| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
+| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
+| `:change-current-directory`, `:cd` | Change the current working directory. |
+| `:show-directory`, `:pwd` | Show the current working directory. |
+| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
+| `:reload` | Discard changes and reload from the source file. |
+| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
+| `:vsplit`, `:vs` | Open the file in a vertical split. |
+| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
+| `:tutor` | Open the tutorial. |
+| `:goto`, `:g` | Go to line number. |
diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md
index 446eb479..9ad2c285 100644
--- a/book/src/guides/adding_languages.md
+++ b/book/src/guides/adding_languages.md
@@ -2,7 +2,7 @@
## Submodules
-To add a new langauge, you should first add a tree-sitter submodule. To do this,
+To add a new language, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
diff --git a/book/src/install.md b/book/src/install.md
index b9febbcc..1a5a9daa 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -25,9 +25,16 @@ shell for working on Helix.
Releases are available in the `community` repository.
-Packages are also available on AUR:
-- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
-- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
+A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
+
+### Fedora Linux
+
+You can install the COPR package for Helix via
+
+```
+sudo dnf copr enable varlad/helix
+sudo dnf install helix
+```
## Build from source
diff --git a/book/src/keymap.md b/book/src/keymap.md
index fbe77267..f0a2cb30 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -34,6 +34,7 @@
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
+| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
@@ -45,44 +46,48 @@
### Changes
-| Key | Description | Command |
-| ----- | ----------- | ------- |
-| `r` | Replace with a character | `replace` |
-| `R` | Replace with yanked text | `replace_with_yanked` |
-| `~` | Switch case of the selected text | `switch_case` |
-| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
-| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
-| `i` | Insert before selection | `insert_mode` |
-| `a` | Insert after selection (append) | `append_mode` |
-| `I` | Insert at the start of the line | `prepend_to_line` |
-| `A` | Insert at the end of the line | `append_to_line` |
-| `o` | Open new line below selection | `open_below` |
-| `O` | Open new line above selection | `open_above` |
-| `.` | Repeat last change | N/A |
-| `u` | Undo change | `undo` |
-| `U` | Redo change | `redo` |
-| `Alt-u` | Move backward in history | `earlier` |
-| `Alt-U` | Move forward in history | `later` |
-| `y` | Yank selection | `yank` |
-| `p` | Paste after selection | `paste_after` |
-| `P` | Paste before selection | `paste_before` |
-| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
-| `>` | Indent selection | `indent` |
-| `<` | Unindent selection | `unindent` |
-| `=` | Format selection (**LSP**) | `format_selections` |
-| `d` | Delete selection | `delete_selection` |
-| `c` | Change selection (delete and enter insert mode) | `change_selection` |
-| `Ctrl-a` | Increment object (number) under cursor | `increment` |
-| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
+| Key | Description | Command |
+| ----- | ----------- | ------- |
+| `r` | Replace with a character | `replace` |
+| `R` | Replace with yanked text | `replace_with_yanked` |
+| `~` | Switch case of the selected text | `switch_case` |
+| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
+| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
+| `i` | Insert before selection | `insert_mode` |
+| `a` | Insert after selection (append) | `append_mode` |
+| `I` | Insert at the start of the line | `prepend_to_line` |
+| `A` | Insert at the end of the line | `append_to_line` |
+| `o` | Open new line below selection | `open_below` |
+| `O` | Open new line above selection | `open_above` |
+| `.` | Repeat last change | N/A |
+| `u` | Undo change | `undo` |
+| `U` | Redo change | `redo` |
+| `Alt-u` | Move backward in history | `earlier` |
+| `Alt-U` | Move forward in history | `later` |
+| `y` | Yank selection | `yank` |
+| `p` | Paste after selection | `paste_after` |
+| `P` | Paste before selection | `paste_before` |
+| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
+| `>` | Indent selection | `indent` |
+| `<` | Unindent selection | `unindent` |
+| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
+| `d` | Delete selection | `delete_selection` |
+| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
+| `c` | Change selection (delete and enter insert mode) | `change_selection` |
+| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
+| `q` | Start/stop macro recording to the selected register | `record_macro` |
+| `Q` | Play back a recorded macro from the selected register | `play_macro` |
#### Shell
-| Key | Description | Command |
-| ------ | ----------- | ------- |
-| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
-| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
-| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
-| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
+| Key | Description | Command |
+| ------ | ----------- | ------- |
+| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
+| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
+| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
### Selection manipulation
@@ -158,17 +163,19 @@ Jumps to various locations.
| ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
+| `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| `t` | Go to the top of the screen | `goto_window_top` |
-| `m` | Go to the middle of the screen | `goto_window_middle` |
+| `c` | Go to the middle of the screen | `goto_window_center` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
+| `m` | Go to the last modified/alternate file | `goto_last_modified_file` |
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | 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.
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
+| `f` | Go to files in the selection in horizontal splits | `goto_file` |
+| `F` | Go to files in the selection in vertical splits | `goto_file` |
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
@@ -315,7 +324,7 @@ Keys to use within prompt, Remapping currently not supported.
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
-| `delete`, `Ctrl-d` | Delete previous char |
+| `delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
diff --git a/book/src/lang-support.md b/book/src/lang-support.md
new file mode 100644
index 00000000..3920f342
--- /dev/null
+++ b/book/src/lang-support.md
@@ -0,0 +1,10 @@
+# Language Support
+
+For more information like arguments passed to default LSP server,
+extensions assosciated with a filetype, custom LSP settings, filetype
+specific indent settings, etc see the default
+[`languages.toml`][languages.toml] file.
+
+{{#include ./generated/lang-support.md}}
+
+[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
diff --git a/book/src/remapping.md b/book/src/remapping.md
index fffd189b..1cdf9b1f 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -11,6 +11,8 @@ this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
+C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
+C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"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
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
```
+> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`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
| Down | `"down"` |
| Home | `"home"` |
| End | `"end"` |
-| Page | `"pageup"` |
-| Page | `"pagedown"` |
+| Page Up | `"pageup"` |
+| Page Down | `"pagedown"` |
| Tab | `"tab"` |
-| Back | `"backtab"` |
| Delete | `"del"` |
| Insert | `"ins"` |
| Null | `"null"` |
@@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command.
Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
-> 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.
+> 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`.
diff --git a/book/src/themes.md b/book/src/themes.md
index 6b38fb43..40c14781 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -145,11 +145,12 @@ We use a similar set of scopes as
- `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- - (TODO: return?)
+ - `return`
+ - `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
-- `operator` - `||`, `+=`, `>`, `or`
+- `operator` - `||`, `+=`, `>`
- `function`
- `builtin`
@@ -161,6 +162,20 @@ We use a similar set of scopes as
- `namespace`
+- `markup`
+ - `heading`
+ - `list`
+ - `unnumbered`
+ - `numbered`
+ - `bold`
+ - `italic`
+ - `underline`
+ - `link`
+ - `quote`
+ - `raw`
+ - `inline`
+ - `block`
+
#### Interface
These scopes are used for theming the editor interface.
diff --git a/book/src/usage.md b/book/src/usage.md
index 6b7cbc41..cf7d9d48 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
+| `_` | Black hole |
> 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.
+> The black hole register works as a no-op register, meaning no data will be written to / read from it.
## Surround
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 00000000..bdd771aa
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+# Contributing
+
+Contributors are very welcome! **No contribution is too small and all contributions are valued.**
+
+Some suggestions to get started:
+
+- You can look at the [good first issue][good-first-issue] label on the issue tracker.
+- Help with packaging on various distributions needed!
+- To use print debugging to the [Helix log file][log-file], you must:
+ * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
+ * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
+- If your preferred language is missing, integrating a tree-sitter grammar for
+ it and defining syntax highlight queries for it is straight forward and
+ doesn't require much knowledge of the internals.
+
+We provide an [architecture.md][architecture.md] that should give you
+a good overview of the internals.
+
+# Auto generated documentation
+
+Some parts of [the book][docs] are autogenerated from the code itself,
+like the list of `:commands` and supported languages. To generate these
+files, run
+
+```shell
+cargo xtask docgen
+```
+
+inside the project. We use [xtask][xtask] as an ad-hoc task runner and
+thus do not require any dependencies other than `cargo` (You don't have
+to `cargo install` anything either).
+
+[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
+[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
+[architecture.md]: ./architecture.md
+[docs]: https://docs.helix-editor.com/
+[xtask]: https://github.com/matklad/cargo-xtask
diff --git a/flake.lock b/flake.lock
index 2029d580..2c59f993 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"devshell": {
"locked": {
- "lastModified": 1632436039,
- "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
+ "lastModified": 1637575296,
+ "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=",
"owner": "numtide",
"repo": "devshell",
- "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
+ "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"flake-utils": {
"locked": {
- "lastModified": 1623875721,
- "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+ "lastModified": 1637014545,
+ "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+ "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
@@ -30,22 +30,6 @@
"type": "github"
}
},
- "flakeCompat": {
- "flake": false,
- "locked": {
- "lastModified": 1627913399,
- "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
"nixCargoIntegration": {
"inputs": {
"devshell": "devshell",
@@ -57,11 +41,11 @@
]
},
"locked": {
- "lastModified": 1634796585,
- "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
+ "lastModified": 1638425401,
+ "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
- "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
+ "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37",
"type": "github"
},
"original": {
@@ -72,11 +56,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1634782485,
- "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
+ "lastModified": 1638376152,
+ "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
+ "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f",
"type": "github"
},
"original": {
@@ -88,22 +72,22 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1628186154,
- "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
+ "lastModified": 1637453606,
+ "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "06552b72346632b6943c8032e57e702ea12413bf",
+ "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
+ "ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
- "flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
@@ -115,11 +99,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
- "lastModified": 1634869268,
- "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
+ "lastModified": 1638497756,
+ "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "c02c2d86354327317546501af001886fbb53d374",
+ "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 296a68d5..cbf10c97 100644
--- a/flake.nix
+++ b/flake.nix
@@ -9,10 +9,6 @@
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
- flakeCompat = {
- url = "github:edolstra/flake-compat";
- flake = false;
- };
};
outputs = inputs@{ self, nixCargoIntegration, ... }:
@@ -63,7 +59,7 @@
'';
};
shell = common: prev: {
- packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
+ packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ea695d34..0a2a56d9 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -36,5 +36,7 @@ similar = "2.1"
etcetera = "0.3"
+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
+
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..c037afef 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction};
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
-const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+// [TODO] build this dynamically in language config. see #992
+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// 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
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
-// TODO: delete implementation where it erases the whole bracket (|) -> |
+// [TODO]
+// * delete implementation where it erases the whole bracket (|) -> |
+// * do not reduce to cursors; use whole selections, and surround with pair
+// * change to multi character pairs to handle cases like placing the cursor in the
+// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+ debug!("autopairs hook selection: {:#?}", selection);
+
+ let cursors = selection.clone().cursors(doc.slice(..));
+
for &(open, close) in PAIRS {
if open == ch {
if open == close {
- return handle_same(doc, selection, open);
+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
- return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, open, close));
+ return Some(handle_close(doc, &cursors, open, close));
}
}
None
}
-// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
-// for example "&'a mut", or "fn<'a>"
-
-fn next_char(doc: &Rope, pos: usize) -> Option<char> {
- if pos >= doc.len_chars() {
+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
+ if pos == 0 {
return None;
}
- Some(doc.char(pos))
+
+ doc.get_char(pos - 1)
}
-// TODO: selections should be extended if range, moved if point.
-// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open(
doc: &Rope,
selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
close: char,
close_before: &str,
) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
- let head = pos + offs + open.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let next = doc.get_char(start_head);
+ let end_head = start_head + offs + open.len_utf8();
+
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
match next {
Some(ch) if !close_before.contains(ch) => {
- offs += 1;
- // TODO: else return (use default handler that inserts open)
- (pos, pos, Some(Tendril::from_char(open)))
+ offs += open.len_utf8();
+ (start_head, start_head, Some(Tendril::from_char(open)))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let mut pair = Tendril::with_capacity(2);
- pair.push_char(open);
- pair.push_char(close);
-
- offs += 2;
-
- (pos, pos, Some(pair))
+ let pair = Tendril::from_iter([open, close]);
+ offs += open.len_utf8() + close.len_utf8();
+ (start_head, start_head, Some(pair))
}
}
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
+ let next = doc.get_char(start_head);
+ let end_head = start_head + offs + close.len_utf8();
- let head = pos + offs + close.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
if next == Some(close) {
- // return transaction that moves past close
- (pos, pos, None) // no-op
+ // return transaction that moves past close
+ (start_head, start_head, None) // no-op
} else {
offs += close.len_utf8();
+ (start_head, start_head, Some(Tendril::from_char(close)))
+ }
+ });
+
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+}
- // TODO: else return (use default handler that inserts close)
- (pos, pos, Some(Tendril::from_char(close)))
+/// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(
+ doc: &Rope,
+ selection: &Selection,
+ token: char,
+ close_before: &str,
+ open_before: &str,
+) -> Transaction {
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
+
+ let mut offs = 0;
+
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
+ let end_head = start_head + offs + token.len_utf8();
+
+ // if selection, retain anchor, if cursor, move over
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
+
+ let next = doc.get_char(start_head);
+ let prev = prev_char(doc, start_head);
+
+ if next == Some(token) {
+ // return transaction that moves past close
+ (start_head, start_head, None) // no-op
+ } else {
+ let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
+ pair.push_char(token);
+
+ // for equal pairs, don't insert both open and close if either
+ // side has a non-pair char
+ if (next.is_none() || close_before.contains(next.unwrap()))
+ && (prev.is_none() || open_before.contains(prev.unwrap()))
+ {
+ pair.push_char(token);
+ }
+
+ offs += pair.len();
+ (start_head, start_head, Some(pair))
}
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
-// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
- // if not cursor but selection, wrap
- // let next = next char
-
- // if next == bracket {
- // // if start of syntax node, insert token twice (new pair because node is complete)
- // // elseif colsedBracketAt
- // // is_triple == allow triple && next 3 is equal
- // // cursor jump over
- // }
- //} else if allow_triple && followed by triple {
- //}
- //} else if next != word char && prev != bracket && prev != word char {
- // // condition checks for cases like I' where you don't want I'' (or I'm)
- // insert pair ("")
- //}
- None
+#[cfg(test)]
+mod test {
+ use super::*;
+ use smallvec::smallvec;
+
+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open != close)
+ }
+
+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open == close)
+ }
+
+ fn test_hooks(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ ch: char,
+ expected_doc: &Rope,
+ expected_sel: &Selection,
+ ) {
+ let trans = hook(&in_doc, &in_sel, ch).unwrap();
+ let mut actual_doc = in_doc.clone();
+ assert!(trans.apply(&mut actual_doc));
+ assert_eq!(expected_doc, &actual_doc);
+ assert_eq!(expected_sel, trans.selection().unwrap());
+ }
+
+ fn test_hooks_with_pairs<I, F, R>(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ pairs: I,
+ get_expected_doc: F,
+ actual_sel: &Selection,
+ ) where
+ I: IntoIterator<Item = &'static (char, char)>,
+ F: Fn(char, char) -> R,
+ R: Into<Rope>,
+ Rope: From<R>,
+ {
+ pairs.into_iter().for_each(|(open, close)| {
+ test_hooks(
+ in_doc,
+ in_sel,
+ *open,
+ &Rope::from(get_expected_doc(*open, *close)),
+ actual_sel,
+ )
+ });
+ }
+
+ // [] indicates range
+
+ /// [] -> insert ( -> ([])
+ #[test]
+ fn test_insert_blank() {
+ test_hooks_with_pairs(
+ &Rope::new(),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, close| format!("{}{}", open, close),
+ &Selection::single(1, 1),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> insert -> ([])
+ /// [] ([])
+ #[test]
+ fn test_insert_blank_multi_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from("\n\n\n"),
+ &Selection::new(
+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
+ 0,
+ ),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::point(1), Range::point(4), Range::point(7),),
+ 0,
+ ),
+ );
+ }
+
+ // [TODO] broken until it works with selections
+ /// fo[o] -> append ( -> fo[o(])
+ #[ignore]
+ #[test]
+ fn test_append() {
+ test_hooks_with_pairs(
+ &Rope::from("foo"),
+ &Selection::single(2, 4),
+ PAIRS,
+ |open, close| format!("foo{}{}", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// ([]) -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}", open, close));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ &doc,
+ &Selection::point(2),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> insert ) -> ()[]
+ /// ([]) ()[]
+ #[test]
+ fn test_insert_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
+ smallvec!(Range::point(2), Range::point(5), Range::point(8),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::point(2);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert " -> ("[]")
+ #[test]
+ fn test_insert_nested_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::point(2);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// []word -> insert ( -> ([]word
+ #[test]
+ fn test_insert_open_before_non_pair() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::point(1),
+ )
+ }
+
+ // [TODO] broken until it works with selections
+ /// [wor]d -> insert ( -> ([wor]d
+ #[test]
+ #[ignore]
+ fn test_insert_open_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(0, 4),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(1, 5),
+ )
+ }
+
+ /// we want pairs that are *not* the same char to be inserted after
+ /// a non-pair char, for cases like functions, but for pairs that are
+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
+ ///
+ /// word[] -> insert ( -> word([])
+ /// word[] -> insert ' -> word'[]
+ #[test]
+ fn test_insert_open_after_non_pair() {
+ let doc = Rope::from("word");
+ let sel = Selection::single(5, 4);
+ let expected_sel = Selection::point(5);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ |open, close| format!("word{}{}", open, close),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ |open, _| format!("word{}", open),
+ &expected_sel,
+ );
+ }
}
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index ad1ba16a..4fcf51c9 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,7 +1,7 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
@@ -17,7 +17,7 @@ pub struct Range {
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
pub line: usize,
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 00000000..e3cfe107
--- /dev/null
+++ b/helix-core/src/increment/date_time.rs
@@ -0,0 +1,490 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
+use once_cell::sync::Lazy;
+use regex::Regex;
+use ropey::RopeSlice;
+
+use std::borrow::Cow;
+use std::cmp;
+
+use super::Increment;
+use crate::{Range, Tendril};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct DateTimeIncrementor {
+ date_time: NaiveDateTime,
+ range: Range,
+ fmt: &'static str,
+ field: DateField,
+}
+
+impl DateTimeIncrementor {
+ pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
+ let range = if range.is_empty() {
+ if range.anchor < text.len_chars() {
+ // Treat empty range as a cursor range.
+ range.put_cursor(text, range.anchor + 1, true)
+ } else {
+ // The range is empty and at the end of the text.
+ return None;
+ }
+ } else {
+ range
+ };
+
+ FORMATS.iter().find_map(|format| {
+ let from = range.from().saturating_sub(format.max_len);
+ let to = (range.from() + format.max_len).min(text.len_chars());
+
+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
+ let text: Cow<str> = text.slice(from..to).into();
+
+ let captures = format.regex.captures(&text)?;
+ if captures.len() - 1 != format.fields.len() {
+ return None;
+ }
+
+ let date_time = captures.get(0)?;
+ let offset = range.from() - from_in_text;
+ let range = Range::new(date_time.start() + offset, date_time.end() + offset);
+
+ let field = captures
+ .iter()
+ .skip(1)
+ .enumerate()
+ .find_map(|(i, capture)| {
+ let capture = capture?;
+ let capture_range = capture.range();
+
+ if capture_range.contains(&from_in_text)
+ && capture_range.contains(&(to_in_text - 1))
+ {
+ Some(format.fields[i])
+ } else {
+ None
+ }
+ })?;
+
+ let has_date = format.fields.iter().any(|f| f.unit.is_date());
+ let has_time = format.fields.iter().any(|f| f.unit.is_time());
+
+ let date_time = &text[date_time.start()..date_time.end()];
+ let date_time = match (has_date, has_time) {
+ (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
+ (true, false) => {
+ let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
+
+ date.and_hms(0, 0, 0)
+ }
+ (false, true) => {
+ let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
+
+ NaiveDate::from_ymd(0, 1, 1).and_time(time)
+ }
+ (false, false) => return None,
+ };
+
+ Some(DateTimeIncrementor {
+ date_time,
+ range,
+ fmt: format.fmt,
+ field,
+ })
+ })
+ }
+}
+
+impl Increment for DateTimeIncrementor {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
+ let date_time = match self.field.unit {
+ DateUnit::Years => add_years(self.date_time, amount),
+ DateUnit::Months => add_months(self.date_time, amount),
+ DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
+ DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
+ DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
+ DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
+ DateUnit::AmPm => toggle_am_pm(self.date_time),
+ }
+ .unwrap_or(self.date_time);
+
+ (self.range, date_time.format(self.fmt).to_string().into())
+ }
+}
+
+static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
+ vec![
+ Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
+ Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
+ Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12
+ Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12
+ Format::new("%Y-%m-%d"), // 2021-11-24
+ Format::new("%Y/%m/%d"), // 2021/11/24
+ Format::new("%a %b %d %Y"), // Wed Nov 24 2021
+ Format::new("%d-%b-%Y"), // 24-Nov-2021
+ Format::new("%Y %b %d"), // 2021 Nov 24
+ Format::new("%b %d, %Y"), // Nov 24, 2021
+ Format::new("%-I:%M:%S %P"), // 7:21:53 am
+ Format::new("%-I:%M %P"), // 7:21 am
+ Format::new("%-I:%M:%S %p"), // 7:21:53 AM
+ Format::new("%-I:%M %p"), // 7:21 AM
+ Format::new("%H:%M:%S"), // 23:24:23
+ Format::new("%H:%M"), // 23:24
+ ]
+});
+
+#[derive(Debug)]
+struct Format {
+ fmt: &'static str,
+ fields: Vec<DateField>,
+ regex: Regex,
+ max_len: usize,
+}
+
+impl Format {
+ fn new(fmt: &'static str) -> Self {
+ let mut remaining = fmt;
+ let mut fields = Vec::new();
+ let mut regex = String::new();
+ let mut max_len = 0;
+
+ while let Some(i) = remaining.find('%') {
+ let after = &remaining[i + 1..];
+ let mut chars = after.chars();
+ let c = chars.next().unwrap();
+
+ let spec_len = if c == '-' {
+ 1 + chars.next().unwrap().len_utf8()
+ } else {
+ c.len_utf8()
+ };
+
+ let specifier = &after[..spec_len];
+ let field = DateField::from_specifier(specifier).unwrap();
+ fields.push(field);
+ max_len += field.max_len + remaining[..i].len();
+ regex += &remaining[..i];
+ regex += &format!("({})", field.regex);
+ remaining = &after[spec_len..];
+ }
+
+ let regex = Regex::new(&regex).unwrap();
+
+ Self {
+ fmt,
+ fields,
+ regex,
+ max_len,
+ }
+ }
+}
+
+impl PartialEq for Format {
+ fn eq(&self, other: &Self) -> bool {
+ self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
+ }
+}
+
+impl Eq for Format {}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+struct DateField {
+ regex: &'static str,
+ unit: DateUnit,
+ max_len: usize,
+}
+
+impl DateField {
+ fn from_specifier(specifier: &str) -> Option<Self> {
+ match specifier {
+ "Y" => Some(DateField {
+ regex: r"\d{4}",
+ unit: DateUnit::Years,
+ max_len: 5,
+ }),
+ "y" => Some(DateField {
+ regex: r"\d\d",
+ unit: DateUnit::Years,
+ max_len: 2,
+ }),
+ "m" => Some(DateField {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Months,
+ max_len: 2,
+ }),
+ "d" => Some(DateField {
+ regex: r"[0-3]\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "-d" => Some(DateField {
+ regex: r"[1-3]?\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "a" => Some(DateField {
+ regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+ unit: DateUnit::Days,
+ max_len: 3,
+ }),
+ "A" => Some(DateField {
+ regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+ unit: DateUnit::Days,
+ max_len: 9,
+ }),
+ "b" | "h" => Some(DateField {
+ regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+ unit: DateUnit::Months,
+ max_len: 3,
+ }),
+ "B" => Some(DateField {
+ regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+ unit: DateUnit::Months,
+ max_len: 9,
+ }),
+ "H" => Some(DateField {
+ regex: r"[0-2]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "M" => Some(DateField {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Minutes,
+ max_len: 2,
+ }),
+ "S" => Some(DateField {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Seconds,
+ max_len: 2,
+ }),
+ "I" => Some(DateField {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "-I" => Some(DateField {
+ regex: r"1?\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "P" => Some(DateField {
+ regex: r"am|pm",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ "p" => Some(DateField {
+ regex: r"AM|PM",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum DateUnit {
+ Years,
+ Months,
+ Days,
+ Hours,
+ Minutes,
+ Seconds,
+ AmPm,
+}
+
+impl DateUnit {
+ fn is_date(self) -> bool {
+ matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
+ }
+
+ fn is_time(self) -> bool {
+ matches!(
+ self,
+ DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
+ )
+ }
+}
+
+fn ndays_in_month(year: i32, month: u32) -> u32 {
+ // The first day of the next month...
+ let (y, m) = if month == 12 {
+ (year + 1, 1)
+ } else {
+ (year, month + 1)
+ };
+ let d = NaiveDate::from_ymd(y, m, 1);
+
+ // ...is preceded by the last day of the original month.
+ d.pred().day()
+}
+
+fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let month = (date_time.month0() as i64).checked_add(amount)?;
+ let year = date_time.year() + i32::try_from(month / 12).ok()?;
+ let year = if month.is_negative() { year - 1 } else { year };
+
+ // Normalize month
+ let month = month % 12;
+ let month = if month.is_negative() {
+ month + 12
+ } else {
+ month
+ } as u32
+ + 1;
+
+ let day = cmp::min(date_time.day(), ndays_in_month(year, month));
+
+ Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
+}
+
+fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
+ let ndays = ndays_in_month(year, date_time.month());
+
+ if date_time.day() > ndays {
+ let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
+ Some(d.succ().and_time(date_time.time()))
+ } else {
+ date_time.with_year(year)
+ }
+}
+
+fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
+ date_time.checked_add_signed(duration)
+}
+
+fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
+ if date_time.hour() < 12 {
+ add_duration(date_time, Duration::hours(12))
+ } else {
+ add_duration(date_time, Duration::hours(-12))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::Rope;
+
+ #[test]
+ fn test_increment_date_times() {
+ let tests = [
+ // (original, cursor, amount, expected)
+ ("2020-02-28", 0, 1, "2021-02-28"),
+ ("2020-02-29", 0, 1, "2021-03-01"),
+ ("2020-01-31", 5, 1, "2020-02-29"),
+ ("2020-01-20", 5, 1, "2020-02-20"),
+ ("2021-01-01", 5, -1, "2020-12-01"),
+ ("2021-01-31", 5, -2, "2020-11-30"),
+ ("2020-02-28", 8, 1, "2020-02-29"),
+ ("2021-02-28", 8, 1, "2021-03-01"),
+ ("2021-02-28", 0, -1, "2020-02-28"),
+ ("2021-03-01", 0, -1, "2020-03-01"),
+ ("2020-02-29", 5, -1, "2020-01-29"),
+ ("2020-02-20", 5, -1, "2020-01-20"),
+ ("2020-02-29", 8, -1, "2020-02-28"),
+ ("2021-03-01", 8, -1, "2021-02-28"),
+ ("1980/12/21", 8, 100, "1981/03/31"),
+ ("1980/12/21", 8, -100, "1980/09/12"),
+ ("1980/12/21", 8, 1000, "1983/09/17"),
+ ("1980/12/21", 8, -1000, "1978/03/27"),
+ ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
+ ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
+ ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
+ ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
+ ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
+ ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
+ ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
+ ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
+ ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
+ ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
+ ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
+ ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
+ ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
+ ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
+ ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
+ ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
+ ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
+ ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
+ ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
+ ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
+ ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
+ ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
+ ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
+ ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
+ ("24-Nov-2021", 0, 1, "25-Nov-2021"),
+ ("24-Nov-2021", 3, 1, "24-Dec-2021"),
+ ("24-Nov-2021", 7, 1, "24-Nov-2022"),
+ ("2021 Nov 24", 0, 1, "2022 Nov 24"),
+ ("2021 Nov 24", 5, 1, "2021 Dec 24"),
+ ("2021 Nov 24", 9, 1, "2021 Nov 25"),
+ ("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
+ ("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
+ ("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
+ ("7:21:53 am", 0, 1, "8:21:53 am"),
+ ("7:21:53 am", 3, 1, "7:22:53 am"),
+ ("7:21:53 am", 5, 1, "7:21:54 am"),
+ ("7:21:53 am", 8, 1, "7:21:53 pm"),
+ ("7:21:53 AM", 0, 1, "8:21:53 AM"),
+ ("7:21:53 AM", 3, 1, "7:22:53 AM"),
+ ("7:21:53 AM", 5, 1, "7:21:54 AM"),
+ ("7:21:53 AM", 8, 1, "7:21:53 PM"),
+ ("7:21 am", 0, 1, "8:21 am"),
+ ("7:21 am", 3, 1, "7:22 am"),
+ ("7:21 am", 5, 1, "7:21 pm"),
+ ("7:21 AM", 0, 1, "8:21 AM"),
+ ("7:21 AM", 3, 1, "7:22 AM"),
+ ("7:21 AM", 5, 1, "7:21 PM"),
+ ("23:24:23", 1, 1, "00:24:23"),
+ ("23:24:23", 3, 1, "23:25:23"),
+ ("23:24:23", 6, 1, "23:24:24"),
+ ("23:24", 1, 1, "00:24"),
+ ("23:24", 3, 1, "23:25"),
+ ];
+
+ for (original, cursor, amount, expected) in tests {
+ let rope = Rope::from_str(original);
+ let range = Range::new(cursor, cursor + 1);
+ assert_eq!(
+ DateTimeIncrementor::from_range(rope.slice(..), range)
+ .unwrap()
+ .increment(amount)
+ .1,
+ expected.into()
+ );
+ }
+ }
+
+ #[test]
+ fn test_invalid_date_times() {
+ let tests = [
+ "0000-00-00",
+ "1980-2-21",
+ "1980-12-1",
+ "12345",
+ "2020-02-30",
+ "1999-12-32",
+ "19-12-32",
+ "1-2-3",
+ "0000/00/00",
+ "1980/2/21",
+ "1980/12/1",
+ "12345",
+ "2020/02/30",
+ "1999/12/32",
+ "19/12/32",
+ "1/2/3",
+ "123:456:789",
+ "11:61",
+ "2021-55-12 08:12:54",
+ ];
+
+ for invalid in tests {
+ let rope = Rope::from_str(invalid);
+ let range = Range::new(0, 1);
+
+ assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
+ }
+ }
+}
diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs
new file mode 100644
index 00000000..f5945774
--- /dev/null
+++ b/helix-core/src/increment/mod.rs
@@ -0,0 +1,8 @@
+pub mod date_time;
+pub mod number;
+
+use crate::{Range, Tendril};
+
+pub trait Increment {
+ fn increment(&self, amount: i64) -> (Range, Tendril);
+}
diff --git a/helix-core/src/numbers.rs b/helix-core/src/increment/number.rs
index e9f3c898..a19b7e75 100644
--- a/helix-core/src/numbers.rs
+++ b/helix-core/src/increment/number.rs
@@ -2,6 +2,8 @@ use std::borrow::Cow;
use ropey::RopeSlice;
+use super::Increment;
+
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
@@ -9,9 +11,9 @@ use crate::{
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
- pub range: Range,
- pub value: i64,
- pub radix: u32,
+ value: i64,
+ radix: u32,
+ range: Range,
text: RopeSlice<'a>,
}
@@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
text,
})
}
+}
- /// Add `amount` to the number and return the formatted text.
- pub fn incremented_text(&self, amount: i64) -> Tendril {
+impl<'a> Increment for NumberIncrementor<'a> {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
@@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
}
}
- new_text.into()
+ (self.range, new_text.into())
}
}
@@ -366,7 +369,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -392,7 +396,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -419,7 +424,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -464,7 +470,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -491,7 +498,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index de7e95c1..92a59f31 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -5,18 +5,19 @@ pub mod diagnostic;
pub mod diff;
pub mod graphemes;
pub mod history;
+pub mod increment;
pub mod indent;
pub mod line_ending;
pub mod macros;
pub mod match_brackets;
pub mod movement;
-pub mod numbers;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
+pub mod shellwords;
mod state;
pub mod surround;
pub mod syntax;
@@ -158,7 +159,7 @@ mod merge_toml_tests {
";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
- .expect("Couldn't parse built-in langauges config");
+ .expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user);
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index c5444eb7..b9eb497d 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -15,7 +15,11 @@ impl Register {
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
- Self { name, values }
+ if name == '_' {
+ Self::new(name)
+ } else {
+ Self { name, values }
+ }
}
pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
}
pub fn write(&mut self, values: Vec<String>) {
- self.values = values;
+ if self.name != '_' {
+ self.values = values;
+ }
}
pub fn push(&mut self, value: String) {
- self.values.push(value);
+ if self.name != '_' {
+ self.values.push(value);
+ }
}
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index b4d1dffa..116a1c7c 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -308,10 +308,10 @@ impl Range {
}
impl From<(usize, usize)> for Range {
- fn from(tuple: (usize, usize)) -> Self {
+ fn from((anchor, head): (usize, usize)) -> Self {
Self {
- anchor: tuple.0,
- head: tuple.1,
+ anchor,
+ head,
horiz: None,
}
}
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
new file mode 100644
index 00000000..13f6f3e9
--- /dev/null
+++ b/helix-core/src/shellwords.rs
@@ -0,0 +1,164 @@
+use std::borrow::Cow;
+
+/// Get the vec of escaped / quoted / doublequoted filenames from the input str
+pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
+ enum State {
+ Normal,
+ NormalEscaped,
+ Quoted,
+ QuoteEscaped,
+ Dquoted,
+ DquoteEscaped,
+ }
+
+ use State::*;
+
+ let mut state = Normal;
+ let mut args: Vec<Cow<str>> = Vec::new();
+ let mut escaped = String::with_capacity(input.len());
+
+ let mut start = 0;
+ let mut end = 0;
+
+ for (i, c) in input.char_indices() {
+ state = match state {
+ Normal => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ NormalEscaped
+ }
+ '"' => {
+ end = i;
+ Dquoted
+ }
+ '\'' => {
+ end = i;
+ Quoted
+ }
+ c if c.is_ascii_whitespace() => {
+ end = i;
+ Normal
+ }
+ _ => Normal,
+ },
+ NormalEscaped => Normal,
+ Quoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ QuoteEscaped
+ }
+ '\'' => {
+ end = i;
+ Normal
+ }
+ _ => Quoted,
+ },
+ QuoteEscaped => Quoted,
+ Dquoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ DquoteEscaped
+ }
+ '"' => {
+ end = i;
+ Normal
+ }
+ _ => Dquoted,
+ },
+ DquoteEscaped => Dquoted,
+ };
+
+ if i >= input.len() - 1 && end == 0 {
+ end = i + 1;
+ }
+
+ if end > 0 {
+ let esc_trim = escaped.trim();
+ let inp = &input[start..end];
+
+ if !(esc_trim.is_empty() && inp.trim().is_empty()) {
+ if esc_trim.is_empty() {
+ args.push(inp.into());
+ } else {
+ args.push([escaped, inp.into()].concat().into());
+ escaped = "".to_string();
+ }
+ }
+ start = i + 1;
+ end = 0;
+ }
+ }
+ args
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_normal() {
+ let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
+ let result = shellwords(input);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó"),
+ Cow::from("wörds"),
+ Cow::from(r#"three "with escaping\"#),
+ ];
+ // TODO test is_owned and is_borrowed, once they get stabilized.
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_quoted() {
+ let quoted =
+ r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
+ let result = shellwords(quoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("quote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_dquoted() {
+ let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("dquote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_mixed() {
+ let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from("three' \"with escaping\\"),
+ Cow::from("no space before"),
+ Cow::from("and after"),
+ Cow::from("$#%^@"),
+ Cow::from("%^&(%^"),
+ Cow::from(")(*&^%"),
+ Cow::from(r#"a\\b"#),
+ //last ' just changes to quoted but since we dont have anything after it, it should be ignored
+ ];
+ assert_eq!(expected, result);
+ }
+}
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index 32161b70..b53b0a78 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,4 +1,4 @@
-use crate::{search, Selection};
+use crate::{search, Range, Selection};
use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
pub fn find_nth_pairs_pos(
text: RopeSlice,
ch: char,
- pos: usize,
+ range: Range,
n: usize,
) -> Option<(usize, usize)> {
- let (open, close) = get_pair(ch);
-
- if text.len_chars() < 2 || pos >= text.len_chars() {
+ if text.len_chars() < 2 || range.to() >= text.len_chars() {
return None;
}
+ let (open, close) = get_pair(ch);
+ let pos = range.cursor(text);
+
if open == close {
if Some(open) == text.get_char(pos) {
- // Special case: cursor is directly on a matching char.
- match pos {
- 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
- _ if (pos + 1) == text.len_chars() => {
- Some((search::find_nth_prev(text, open, pos, n)?, pos))
- }
- // We return no match because there's no way to know which
- // side of the char we should be searching on.
- _ => None,
- }
- } else {
- Some((
- search::find_nth_prev(text, open, pos, n)?,
- search::find_nth_next(text, close, pos, n)?,
- ))
+ // Cursor is directly on match char. We return no match
+ // because there's no way to know which side of the char
+ // we should be searching on.
+ return None;
}
+ Some((
+ search::find_nth_prev(text, open, pos, n)?,
+ search::find_nth_next(text, close, pos, n)?,
+ ))
} else {
Some((
find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
) -> Option<Vec<usize>> {
let mut change_pos = Vec::new();
- for range in selection {
- let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
+ for &range in selection {
+ let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
@@ -178,67 +172,91 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
- #[test]
- fn test_find_nth_pairs_pos() {
- let doc = Rope::from("some (text) here");
+ fn check_find_nth_pair_pos(
+ text: &str,
+ cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
+ ) {
+ let doc = Rope::from(text);
let slice = doc.slice(..);
- // cursor on [t]ext
- assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
- // cursor on so[m]e
- assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
- // cursor on bracket itself
- assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
+ for (cursor_pos, ch, n, expected_range) in cases {
+ let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
+ assert_eq!(
+ range, expected_range,
+ "Expected {:?}, got {:?}",
+ expected_range, range
+ );
+ }
}
#[test]
- fn test_find_nth_pairs_pos_skip() {
- let doc = Rope::from("(so (many (good) text) here)");
- let slice = doc.slice(..);
+ fn test_find_nth_pairs_pos() {
+ check_find_nth_pair_pos(
+ "some (text) here",
+ vec![
+ // cursor on [t]ext
+ (6, '(', 1, Some((5, 10))),
+ (6, ')', 1, Some((5, 10))),
+ // cursor on so[m]e
+ (2, '(', 1, None),
+ // cursor on bracket itself
+ (5, '(', 1, Some((5, 10))),
+ (10, '(', 1, Some((5, 10))),
+ ],
+ );
+ }
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
+ #[test]
+ fn test_find_nth_pairs_pos_skip() {
+ check_find_nth_pair_pos(
+ "(so (many (good) text) here)",
+ vec![
+ // cursor on go[o]d
+ (13, '(', 1, Some((10, 15))),
+ (13, '(', 2, Some((4, 21))),
+ (13, '(', 3, Some((0, 27))),
+ ],
+ );
}
#[test]
fn test_find_nth_pairs_pos_same() {
- let doc = Rope::from("'so 'many 'good' text' here'");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
- // cursor on the quotes
- assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
- // this is the best we can do since opening and closing pairs are same
- assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
+ check_find_nth_pair_pos(
+ "'so 'many 'good' text' here'",
+ vec![
+ // cursor on go[o]d
+ (13, '\'', 1, Some((10, 15))),
+ (13, '\'', 2, Some((4, 21))),
+ (13, '\'', 3, Some((0, 27))),
+ // cursor on the quotes
+ (10, '\'', 1, None),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_step() {
- let doc = Rope::from("((so)((many) good (text))(here))");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+ check_find_nth_pair_pos(
+ "((so)((many) good (text))(here))",
+ vec![
+ // cursor on go[o]d
+ (15, '(', 1, Some((5, 24))),
+ (15, '(', 2, Some((0, 31))),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_mixed() {
- let doc = Rope::from("(so [many {good} text] here)");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
+ check_find_nth_pair_pos(
+ "(so [many {good} text] here)",
+ vec![
+ // cursor on go[o]d
+ (13, '{', 1, Some((10, 15))),
+ (13, '[', 1, Some((4, 21))),
+ (13, '(', 1, Some((0, 27))),
+ ],
+ )
}
#[test]
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 142265a8..ef35fc75 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -50,7 +50,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
- pub language_id: String,
+ pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)]
@@ -310,8 +310,9 @@ impl Loader {
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
- static SHEBANG_REGEX: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
+ static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
+ Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
+ });
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 24f063d4..21ceec04 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -114,7 +114,7 @@ pub fn textobject_surround(
ch: char,
count: usize,
) -> Range {
- surround::find_nth_pairs_pos(slice, ch, range.head, count)
+ surround::find_nth_pairs_pos(slice, ch, range, count)
.map(|(anchor, head)| match textobject {
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
#[test]
fn test_textobject_word() {
- // (text, [(cursor position, textobject, final range), ...])
+ // (text, [(char position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
@@ -269,7 +269,9 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range) = case;
- let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
+ // cursor is a single width selection
+ let range = Range::new(pos, pos + 1);
+ let result = textobject_word(slice, range, objtype, 1, false);
assert_eq!(
result,
expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
#[test]
fn test_textobject_surround() {
- // (text, [(cursor position, textobject, final range, count), ...])
+ // (text, [(cursor position, textobject, final range, surround char, count), ...])
let tests = &[
(
"simple (single) surround pairs",
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbe..d8d389f3 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -22,7 +22,7 @@ pub enum Assoc {
}
// ChangeSpec = Change | ChangeSet | Vec<Change>
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ChangeSet {
pub(crate) changes: Vec<Operation>,
/// The required document length. Will refuse to apply changes unless it matches.
@@ -30,16 +30,6 @@ pub struct ChangeSet {
len_after: usize,
}
-impl Default for ChangeSet {
- fn default() -> Self {
- Self {
- changes: Vec::new(),
- len: 0,
- len_after: 0,
- }
- }
-}
-
impl ChangeSet {
pub fn with_capacity(capacity: usize) -> Self {
Self {
@@ -330,7 +320,7 @@ impl ChangeSet {
/// `true` when the set is empty.
#[inline]
pub fn is_empty(&self) -> bool {
- self.changes.is_empty()
+ self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
}
/// Map a position through the changes.
@@ -419,7 +409,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 7fa65928..15cae582 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -337,7 +337,10 @@ impl Registry {
})
.await;
- value.expect("failed to initialize capabilities");
+ if let Err(e) = value {
+ log::error!("failed to initialize language server: {}", e);
+ return;
+ }
// next up, notify<initialized>
_client
diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown
new file mode 160000
+Subproject ad8c32917a16dfbb387d1da567bf0c3fb6fffde
diff --git a/helix-syntax/languages/tree-sitter-wgsl b/helix-syntax/languages/tree-sitter-wgsl
new file mode 160000
+Subproject f00ff52251edbd58f4d39c9c3204383253032c1
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index a0079feb..623c5bb9 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
+default-run = "hx"
[package.metadata.nix]
build = true
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index a795a56e..3e0b6d59 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -76,17 +76,27 @@ impl Application {
None => Ok(def_lang_conf),
};
- let theme = if let Some(theme) = &config.theme {
- match theme_loader.load(theme) {
- Ok(theme) => theme,
- Err(e) => {
- log::warn!("failed to load theme `{}` - {}", theme, e);
+ let true_color = config.editor.true_color || crate::true_color();
+ let theme = config
+ .theme
+ .as_ref()
+ .and_then(|theme| {
+ theme_loader
+ .load(theme)
+ .map_err(|e| {
+ log::warn!("failed to load theme `{}` - {}", theme, e);
+ e
+ })
+ .ok()
+ .filter(|theme| (true_color || theme.is_16_color()))
+ })
+ .unwrap_or_else(|| {
+ if true_color {
theme_loader.default()
+ } else {
+ theme_loader.base16_default()
}
- }
- } else {
- theme_loader.default()
- };
+ });
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into())
@@ -265,7 +275,7 @@ impl Application {
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
- if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
+ if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
return;
}
let editor_view = self
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index a7179c30..cd566720 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1,16 +1,16 @@
use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
history::UndoKind,
+ increment::date_time::DateTimeIncrementor,
+ increment::{number::NumberIncrementor, Increment},
indent,
indent::IndentStyle,
line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
match_brackets,
movement::{self, Direction},
- numbers::NumberIncrementor,
object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
- register::Register,
- search, selection, surround, textobject,
+ search, selection, shellwords, surround, textobject,
unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction,
@@ -25,7 +25,7 @@ use helix_view::{
Document, DocumentId, Editor, ViewId,
};
-use anyhow::{anyhow, bail, Context as _};
+use anyhow::{anyhow, bail, ensure, Context as _};
use helix_lsp::{
block_on, lsp,
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -41,7 +41,7 @@ use crate::{
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, StreamExt};
-use std::num::NonZeroUsize;
+use std::{collections::HashSet, num::NonZeroUsize};
use std::{fmt, future::Future};
use std::{
@@ -70,7 +70,7 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) {
- self.callback = Some(Box::new(|compositor: &mut Compositor| {
+ self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component)
}));
}
@@ -135,47 +135,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
view.offset.row = line.saturating_sub(relative);
}
-/// A command is composed of a static name, and a function that takes the current state plus a count,
-/// and does a side-effect on the state (usually by creating and applying a transaction).
-#[derive(Copy, Clone)]
-pub struct Command {
- name: &'static str,
- fun: fn(cx: &mut Context),
- doc: &'static str,
-}
-
-macro_rules! commands {
+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
+/// Both of these types of commands can be mapped with keybindings in the config.toml.
+#[derive(Clone)]
+pub enum MappableCommand {
+ Typable {
+ name: String,
+ args: Vec<String>,
+ doc: String,
+ },
+ Static {
+ name: &'static str,
+ fun: fn(cx: &mut Context),
+ doc: &'static str,
+ },
+}
+
+macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => {
$(
#[allow(non_upper_case_globals)]
- pub const $name: Self = Self {
+ pub const $name: Self = Self::Static {
name: stringify!($name),
fun: $name,
doc: $doc
};
)*
- pub const COMMAND_LIST: &'static [Self] = &[
+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )*
];
}
}
-impl Command {
+impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
- (self.fun)(cx);
+ match &self {
+ MappableCommand::Typable { name, args, doc: _ } => {
+ let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
+ let mut cx = compositor::Context {
+ editor: cx.editor,
+ jobs: cx.jobs,
+ scroll: None,
+ };
+ if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ }
+ }
+ MappableCommand::Static { fun, .. } => (fun)(cx),
+ }
}
- pub fn name(&self) -> &'static str {
- self.name
+ pub fn name(&self) -> &str {
+ match &self {
+ MappableCommand::Typable { name, .. } => name,
+ MappableCommand::Static { name, .. } => name,
+ }
}
- pub fn doc(&self) -> &'static str {
- self.doc
+ pub fn doc(&self) -> &str {
+ match &self {
+ MappableCommand::Typable { doc, .. } => doc,
+ MappableCommand::Static { doc, .. } => doc,
+ }
}
#[rustfmt::skip]
- commands!(
+ static_commands!(
no_op, "Do nothing",
move_char_left, "Move left",
move_char_right, "Move right",
@@ -232,7 +261,9 @@ impl Command {
extend_line, "Select current line, if already selected, extend to next line",
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
delete_selection, "Delete selection",
+ delete_selection_noyank, "Delete selection, without yanking",
change_selection, "Change selection (delete and enter insert mode)",
+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
collapse_selection, "Collapse selection onto a single cursor",
flip_selections, "Flip selection cursor and anchor",
insert_mode, "Insert before selection",
@@ -258,11 +289,15 @@ impl Command {
goto_implementation, "Goto implementation",
goto_file_start, "Goto file start/line",
goto_file_end, "Goto file end",
+ goto_file, "Goto files in selection",
+ goto_file_hsplit, "Goto files in selection (hsplit)",
+ goto_file_vsplit, "Goto files in selection (vsplit)",
goto_reference, "Goto references",
goto_window_top, "Goto window top",
- goto_window_middle, "Goto window middle",
+ goto_window_center, "Goto window center",
goto_window_bottom, "Goto window bottom",
goto_last_accessed_file, "Goto last accessed file",
+ goto_last_modified_file, "Goto last modified file",
goto_last_modification, "Goto last modification",
goto_line, "Goto line",
goto_last_line, "Goto last line",
@@ -327,6 +362,7 @@ impl Command {
expand_selection, "Expand selection to parent syntax node",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
+ save_selection, "Save the current selection to the jumplist",
jump_view_right, "Jump to the split to the right",
jump_view_left, "Jump to the split to the left",
jump_view_up, "Jump to the split above",
@@ -359,36 +395,56 @@ impl Command {
rename_symbol, "Rename symbol",
increment, "Increment",
decrement, "Decrement",
+ record_macro, "Record macro",
+ play_macro, "Play macro",
);
}
-impl fmt::Debug for Command {
+impl fmt::Debug for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.debug_tuple("Command").field(name).finish()
+ f.debug_tuple("MappableCommand")
+ .field(&self.name())
+ .finish()
}
}
-impl fmt::Display for Command {
+impl fmt::Display for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.write_str(name)
+ f.write_str(self.name())
}
}
-impl std::str::FromStr for Command {
+impl std::str::FromStr for MappableCommand {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- Command::COMMAND_LIST
- .iter()
- .copied()
- .find(|cmd| cmd.name == s)
- .ok_or_else(|| anyhow!("No command named '{}'", s))
+ if let Some(suffix) = s.strip_prefix(':') {
+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
+ let name = typable_command
+ .next()
+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
+ let args = typable_command
+ .map(|s| s.to_owned())
+ .collect::<Vec<String>>();
+ cmd::TYPABLE_COMMAND_MAP
+ .get(name)
+ .map(|cmd| MappableCommand::Typable {
+ name: cmd.name.to_owned(),
+ doc: format!(":{} {:?}", cmd.name, args),
+ args,
+ })
+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
+ } else {
+ MappableCommand::STATIC_COMMAND_LIST
+ .iter()
+ .cloned()
+ .find(|cmd| cmd.name() == s)
+ .ok_or_else(|| anyhow!("No command named '{}'", s))
+ }
}
}
-impl<'de> Deserialize<'de> for Command {
+impl<'de> Deserialize<'de> for MappableCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -398,9 +454,27 @@ impl<'de> Deserialize<'de> for Command {
}
}
-impl PartialEq for Command {
+impl PartialEq for MappableCommand {
fn eq(&self, other: &Self) -> bool {
- self.name() == other.name()
+ match (self, other) {
+ (
+ MappableCommand::Typable {
+ name: first_name, ..
+ },
+ MappableCommand::Typable {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ (
+ MappableCommand::Static {
+ name: first_name, ..
+ },
+ MappableCommand::Static {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ _ => false,
+ }
}
}
@@ -599,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) {
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);
- let pos = line_end_char_index(&text, line);
- range.put_cursor(text, pos, true)
+ let line_end_pos = line_end_char_index(&text, line);
+ let pos = range.cursor(text);
+
+ let mut new_range = range.put_cursor(text, line_end_pos, true);
+ // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
+ if pos != line_end_pos {
+ new_range.head = line_end_pos;
+ }
+ new_range
});
delete_selection_insert_mode(doc, view, &selection);
}
@@ -729,10 +810,12 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) ->
}
fn goto_window(cx: &mut Context, align: Align) {
+ let count = cx.count() - 1;
let (view, doc) = current!(cx.editor);
let height = view.inner_area().height as usize;
+ // respect user given count if any
// - 1 so we have at least one gap in the middle.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type
@@ -741,10 +824,11 @@ fn goto_window(cx: &mut Context, align: Align) {
let last_line = view.last_line(doc);
let line = match align {
- Align::Top => (view.offset.row + scrolloff),
- Align::Center => (view.offset.row + (height / 2)),
- Align::Bottom => last_line.saturating_sub(scrolloff),
+ Align::Top => (view.offset.row + scrolloff + count),
+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
}
+ .max(view.offset.row + scrolloff)
.min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
@@ -756,7 +840,7 @@ fn goto_window_top(cx: &mut Context) {
goto_window(cx, Align::Top)
}
-fn goto_window_middle(cx: &mut Context) {
+fn goto_window_center(cx: &mut Context) {
goto_window(cx, Align::Center)
}
@@ -834,6 +918,49 @@ fn goto_file_end(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+fn goto_file(cx: &mut Context) {
+ goto_file_impl(cx, Action::Replace);
+}
+
+fn goto_file_hsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::HorizontalSplit);
+}
+
+fn goto_file_vsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::VerticalSplit);
+}
+
+fn goto_file_impl(cx: &mut Context, action: Action) {
+ let (view, doc) = current_ref!(cx.editor);
+ let text = doc.text();
+ let selections = doc.selection(view.id);
+ let mut paths: Vec<_> = selections
+ .iter()
+ .map(|r| text.slice(r.from()..r.to()).to_string())
+ .collect();
+ let primary = selections.primary();
+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
+ let current_word = movement::move_next_long_word_start(
+ text.slice(..),
+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
+ 1,
+ );
+ paths.clear();
+ paths.push(
+ text.slice(current_word.from()..current_word.to())
+ .to_string(),
+ );
+ }
+ for sel in paths {
+ let p = sel.trim();
+ if !p.is_empty() {
+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
+ cx.editor.set_error(format!("Open file failed: {:?}", e));
+ }
+ }
+ }
+}
+
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
where
F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1693,19 +1820,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
);
}
-fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
+enum Operation {
+ Delete,
+ Change,
+}
+
+fn delete_selection_impl(cx: &mut Context, op: Operation) {
+ let (view, doc) = current!(cx.editor);
+
let text = doc.text().slice(..);
- let selection = doc.selection(view_id);
+ let selection = doc.selection(view.id);
- // first yank the selection
- let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
- reg.write(values);
+ if cx.register != Some('_') {
+ // first yank the selection
+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
+ let reg_name = cx.register.unwrap_or('"');
+ let registers = &mut cx.editor.registers;
+ let reg = registers.get_mut(reg_name);
+ reg.write(values);
+ };
// then delete
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
- doc.apply(&transaction, view_id);
+ doc.apply(&transaction, view.id);
+
+ match op {
+ Operation::Delete => {
+ doc.append_changes_to_history(view.id);
+ // exit select mode, if currently in select mode
+ exit_select_mode(cx);
+ }
+ Operation::Change => {
+ enter_insert_mode(doc);
+ }
+ }
}
#[inline]
@@ -1720,25 +1870,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
}
fn delete_selection(cx: &mut Context) {
- let reg_name = cx.register.unwrap_or('"');
- let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
- let reg = registers.get_mut(reg_name);
- delete_selection_impl(reg, doc, view.id);
-
- doc.append_changes_to_history(view.id);
+ delete_selection_impl(cx, Operation::Delete);
+}
- // exit select mode, if currently in select mode
- exit_select_mode(cx);
+fn delete_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Delete);
}
fn change_selection(cx: &mut Context) {
- let reg_name = cx.register.unwrap_or('"');
- let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
- let reg = registers.get_mut(reg_name);
- delete_selection_impl(reg, doc, view.id);
- enter_insert_mode(doc);
+ delete_selection_impl(cx, Operation::Change);
+}
+
+fn change_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Change);
}
fn collapse_selection(cx: &mut Context) {
@@ -1806,7 +1952,7 @@ fn append_mode(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-mod cmd {
+pub mod cmd {
use super::*;
use std::collections::HashMap;
@@ -1819,13 +1965,13 @@ mod cmd {
pub aliases: &'static [&'static str],
pub doc: &'static str,
// params, flags, helper, completer
- pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
pub completer: Option<Completer>,
}
fn quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
// last view and we have unsaved changes
@@ -1840,7 +1986,7 @@ mod cmd {
fn force_quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.close(view!(cx.editor).id);
@@ -1850,17 +1996,19 @@ mod cmd {
fn open(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let path = args.get(0).context("wrong argument count")?;
- let _ = cx.editor.open(path.into(), Action::Replace)?;
+ ensure!(!args.is_empty(), "wrong argument count");
+ for arg in args {
+ let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?;
+ }
Ok(())
}
fn buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1871,7 +2019,7 @@ mod cmd {
fn force_buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1880,15 +2028,12 @@ mod cmd {
Ok(())
}
- fn write_impl<P: AsRef<Path>>(
- cx: &mut compositor::Context,
- path: Option<P>,
- ) -> anyhow::Result<()> {
+ fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
let jobs = &mut cx.jobs;
let (_, doc) = current!(cx.editor);
- if let Some(path) = path {
- doc.set_path(Some(path.as_ref()))
+ if let Some(ref path) = path {
+ doc.set_path(Some(path.as_ref().as_ref()))
.context("invalid filepath")?;
}
if doc.path().is_none() {
@@ -1907,12 +2052,17 @@ mod cmd {
});
let future = doc.format_and_save(fmt);
cx.jobs.add(Job::new(future).wait_before_exiting());
+
+ if path.is_some() {
+ let id = doc.id();
+ let _ = cx.editor.refresh_language_server(id);
+ }
Ok(())
}
fn write(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())
@@ -1920,7 +2070,7 @@ mod cmd {
fn new_file(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.new_file(Action::Replace);
@@ -1930,7 +2080,7 @@ mod cmd {
fn format(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor);
@@ -1945,7 +2095,7 @@ mod cmd {
}
fn set_indent_style(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use IndentStyle::*;
@@ -1965,7 +2115,7 @@ mod cmd {
// Attempt to parse argument as an indent style.
let style = match args.get(0) {
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
- Some(&"0") => Some(Tabs),
+ Some(Cow::Borrowed("0")) => Some(Tabs),
Some(arg) => arg
.parse::<u8>()
.ok()
@@ -1984,7 +2134,7 @@ mod cmd {
/// Sets or reports the current document's line ending setting.
fn set_line_ending(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use LineEnding::*;
@@ -2028,7 +2178,7 @@ mod cmd {
fn earlier(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2044,7 +2194,7 @@ mod cmd {
fn later(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2059,7 +2209,7 @@ mod cmd {
fn write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2068,7 +2218,7 @@ mod cmd {
fn force_write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2099,7 +2249,7 @@ mod cmd {
fn write_all_impl(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
quit: bool,
force: bool,
@@ -2135,7 +2285,7 @@ mod cmd {
fn write_all(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, false, false)
@@ -2143,7 +2293,7 @@ mod cmd {
fn write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, false)
@@ -2151,7 +2301,7 @@ mod cmd {
fn force_write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, true)
@@ -2159,7 +2309,7 @@ mod cmd {
fn quit_all_impl(
editor: &mut Editor,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
force: bool,
) -> anyhow::Result<()> {
@@ -2178,23 +2328,23 @@ mod cmd {
fn quit_all(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, false)
+ quit_all_impl(cx.editor, args, event, false)
}
fn force_quit_all(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, true)
+ quit_all_impl(cx.editor, args, event, true)
}
fn cquit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let exit_code = args
@@ -2213,85 +2363,91 @@ mod cmd {
fn theme(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let theme = args.first().context("theme not provided")?;
- cx.editor.set_theme_from_name(theme)
+ let theme = args.first().context("Theme not provided")?;
+ let theme = cx
+ .editor
+ .theme_loader
+ .load(theme)
+ .with_context(|| format!("Failed setting theme {}", theme))?;
+ let true_color = cx.editor.config.true_color || crate::true_color();
+ if !(true_color || theme.is_16_color()) {
+ bail!("Unsupported theme: theme requires true color support");
+ }
+ cx.editor.set_theme(theme);
+ Ok(())
}
fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
}
fn yank_joined_to_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
}
fn yank_main_selection_to_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
}
fn yank_joined_to_primary_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
}
fn paste_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_primary_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn paste_primary_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn replace_selections_with_clipboard_impl(
@@ -2318,7 +2474,7 @@ mod cmd {
fn replace_selections_with_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2326,7 +2482,7 @@ mod cmd {
fn replace_selections_with_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2334,7 +2490,7 @@ mod cmd {
fn show_clipboard_provider(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor
@@ -2344,12 +2500,13 @@ mod cmd {
fn change_current_directory(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let dir = helix_core::path::expand_tilde(
args.first()
.context("target directory not provided")?
+ .as_ref()
.as_ref(),
);
@@ -2367,7 +2524,7 @@ mod cmd {
fn show_current_directory(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2379,7 +2536,7 @@ mod cmd {
/// Sets the [`Document`]'s encoding..
fn set_encoding(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor);
@@ -2395,7 +2552,7 @@ mod cmd {
/// Reload the [`Document`] from its source file.
fn reload(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2404,7 +2561,7 @@ mod cmd {
fn tree_sitter_scopes(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2418,15 +2575,18 @@ mod cmd {
fn vsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::VerticalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::VerticalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
+ }
}
Ok(())
@@ -2434,15 +2594,18 @@ mod cmd {
fn hsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::HorizontalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::HorizontalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
+ }
}
Ok(())
@@ -2450,7 +2613,7 @@ mod cmd {
fn tutor(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2460,6 +2623,24 @@ mod cmd {
Ok(())
}
+ pub(super) fn goto_line_number(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ ensure!(!args.is_empty(), "Line number required");
+
+ let line = args[0].parse::<usize>()?;
+
+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
+
+ let (view, doc) = current!(cx.editor);
+
+ view.ensure_cursor_in_view(doc, line);
+
+ Ok(())
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2513,7 +2694,7 @@ mod cmd {
TypableCommand {
name: "format",
aliases: &["fmt"],
- doc: "Format the file using a formatter.",
+ doc: "Format the file using the LSP formatter.",
fun: format,
completer: None,
},
@@ -2604,7 +2785,7 @@ mod cmd {
TypableCommand {
name: "theme",
aliases: &[],
- doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
+ doc: "Change the editor theme.",
fun: theme,
completer: Some(completers::theme),
},
@@ -2688,7 +2869,7 @@ mod cmd {
TypableCommand {
name: "change-current-directory",
aliases: &["cd"],
- doc: "Change the current working directory (:cd <dir>).",
+ doc: "Change the current working directory.",
fun: change_current_directory,
completer: Some(completers::directory),
},
@@ -2741,17 +2922,25 @@ mod cmd {
fun: tutor,
completer: None,
},
+ TypableCommand {
+ name: "goto",
+ aliases: &["g"],
+ doc: "Go to line number.",
+ fun: goto_line_number,
+ completer: None,
+ }
];
- pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
- TYPABLE_COMMAND_LIST
- .iter()
- .flat_map(|cmd| {
- std::iter::once((cmd.name, cmd))
- .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
- })
- .collect()
- });
+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+ Lazy::new(|| {
+ TYPABLE_COMMAND_LIST
+ .iter()
+ .flat_map(|cmd| {
+ std::iter::once((cmd.name, cmd))
+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+ })
+ .collect()
+ });
}
fn command_mode(cx: &mut Context) {
@@ -2777,7 +2966,7 @@ fn command_mode(cx: &mut Context) {
if let Some(cmd::TypableCommand {
completer: Some(completer),
..
- }) = cmd::COMMANDS.get(parts[0])
+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{
completer(part)
.into_iter()
@@ -2803,8 +2992,18 @@ fn command_mode(cx: &mut Context) {
return;
}
- if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
- if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
+ // If command is numeric, interpret as line number and go there.
+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
+ if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ return;
+ }
+
+ // Handle typable commands
+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
+ let args = shellwords::shellwords(input);
+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
} else {
@@ -2816,7 +3015,7 @@ fn command_mode(cx: &mut Context) {
prompt.doc_fn = Box::new(|input: &str| {
let part = input.split(' ').next().unwrap_or_default();
- if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
return Some(doc);
}
@@ -3254,7 +3453,7 @@ fn apply_workspace_edit(
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker
- cx.callback = Some(Box::new(|compositor: &mut Compositor| {
+ cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker);
}
@@ -3436,10 +3635,14 @@ fn push_jump(editor: &mut Editor) {
}
fn goto_line(cx: &mut Context) {
- if let Some(count) = cx.count {
- push_jump(cx.editor);
+ goto_line_impl(cx.editor, cx.count)
+}
- let (view, doc) = current!(cx.editor);
+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
+ if let Some(count) = count {
+ push_jump(editor);
+
+ let (view, doc) = current!(editor);
let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
// If the last line is blank, don't jump to it.
doc.text().len_lines().saturating_sub(2)
@@ -3498,6 +3701,20 @@ fn goto_last_modification(cx: &mut Context) {
}
}
+fn goto_last_modified_file(cx: &mut Context) {
+ let view = view!(cx.editor);
+ let alternate_file = view
+ .last_modified_docs
+ .into_iter()
+ .flatten()
+ .find(|&id| id != view.doc);
+ if let Some(alt) = alternate_file {
+ cx.editor.switch(alt, Action::Replace);
+ } else {
+ cx.editor.set_error("no last modified buffer".to_owned())
+ }
+}
+
fn select_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -3982,8 +4199,9 @@ pub mod insert {
// The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+ let cursors = selection.clone().cursors(doc.slice(..));
let t = Tendril::from_char(ch);
- let transaction = Transaction::insert(doc, selection, t);
+ let transaction = Transaction::insert(doc, &cursors, t);
Some(transaction)
}
@@ -3998,11 +4216,11 @@ pub mod insert {
};
let text = doc.text();
- let selection = doc.selection(view.id).clone().cursors(text.slice(..));
+ let selection = doc.selection(view.id);
// run through insert hooks, stopping on the first one that returns Some(t)
for hook in hooks {
- if let Some(transaction) = hook(text, &selection, c) {
+ if let Some(transaction) = hook(text, selection, c) {
doc.apply(&transaction, view.id);
break;
}
@@ -4317,11 +4535,8 @@ fn yank_joined_to_clipboard_impl(
fn yank_joined_to_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Clipboard,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
exit_select_mode(cx);
}
@@ -4346,20 +4561,17 @@ fn yank_main_selection_to_clipboard_impl(
}
fn yank_main_selection_to_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
}
fn yank_joined_to_primary_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Selection,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
}
fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
exit_select_mode(cx);
}
@@ -4374,11 +4586,12 @@ fn paste_impl(
doc: &mut Document,
view: &View,
action: Paste,
+ count: usize,
) -> Option<Transaction> {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(value.repeat(count)))
.unwrap(),
);
@@ -4393,7 +4606,7 @@ fn paste_impl(
let mut values = values
.iter()
.map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
- .map(|value| Tendril::from(value.as_ref()))
+ .map(|value| Tendril::from(value.as_ref().repeat(count)))
.chain(repeat);
let text = doc.text();
@@ -4413,7 +4626,7 @@ fn paste_impl(
// paste append
(Paste::After, false) => range.to(),
};
- (pos, pos, Some(values.next().unwrap()))
+ (pos, pos, values.next())
});
Some(transaction)
@@ -4423,13 +4636,14 @@ fn paste_clipboard_impl(
editor: &mut Editor,
action: Paste,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
match editor
.clipboard_provider
.get_contents(clipboard_type)
- .map(|contents| paste_impl(&[contents], doc, view, action))
+ .map(|contents| paste_impl(&[contents], doc, view, action, count))
{
Ok(Some(transaction)) => {
doc.apply(&transaction, view.id);
@@ -4442,22 +4656,43 @@ fn paste_clipboard_impl(
}
fn paste_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn replace_with_yanked(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -4467,12 +4702,12 @@ fn replace_with_yanked(cx: &mut Context) {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from_slice(&value.repeat(count)))
.unwrap(),
);
let mut values = values
.iter()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from_slice(&value.repeat(count)))
.chain(repeat);
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4492,6 +4727,7 @@ fn replace_with_yanked(cx: &mut Context) {
fn replace_selections_with_clipboard_impl(
editor: &mut Editor,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
@@ -4499,7 +4735,11 @@ fn replace_selections_with_clipboard_impl(
Ok(contents) => {
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- (range.from(), range.to(), Some(contents.as_str().into()))
+ (
+ range.from(),
+ range.to(),
+ Some(contents.repeat(count).as_str().into()),
+ )
});
doc.apply(&transaction, view.id);
@@ -4511,21 +4751,22 @@ fn replace_selections_with_clipboard_impl(
}
fn replace_selections_with_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
}
fn replace_selections_with_primary_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
}
fn paste_after(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::After))
+ .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
{
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -4533,13 +4774,14 @@ fn paste_after(cx: &mut Context) {
}
fn paste_before(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::Before))
+ .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
{
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -4935,8 +5177,12 @@ fn hover(cx: &mut Context) {
// skip if contents empty
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
- let popup = Popup::new(contents);
- compositor.push(Box::new(popup));
+ let popup = Popup::new("documentation", contents);
+ if let Some(doc_popup) = compositor.find_id("documentation") {
+ *doc_popup = popup;
+ } else {
+ compositor.push(Box::new(popup));
+ }
}
},
);
@@ -5030,7 +5276,7 @@ fn expand_selection(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
};
- motion(&mut cx.editor);
+ motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}
@@ -5086,6 +5332,12 @@ fn jump_backward(cx: &mut Context) {
};
}
+fn save_selection(cx: &mut Context) {
+ push_jump(cx.editor);
+ cx.editor
+ .set_status("Selection saved to jumplist".to_owned());
+}
+
fn rotate_view(cx: &mut Context) {
cx.editor.focus_next()
}
@@ -5262,7 +5514,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
});
doc.set_selection(view.id, selection);
};
- textobject(&mut cx.editor);
+ textobject(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(textobject)));
}
})
@@ -5428,9 +5680,7 @@ fn shell_impl(
) -> anyhow::Result<(Tendril, bool)> {
use std::io::Write;
use std::process::{Command, Stdio};
- if shell.is_empty() {
- bail!("No shell set");
- }
+ ensure!(!shell.is_empty(), "No shell set");
let mut process = match Command::new(&shell[0])
.args(&shell[1..])
@@ -5594,7 +5844,7 @@ fn rename_symbol(cx: &mut Context) {
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
let edits = block_on(task).unwrap_or_default();
log::debug!("Edits from LSP: {:?}", edits);
- apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
+ apply_workspace_edit(cx.editor, offset_encoding, &edits);
},
);
cx.push_layer(Box::new(prompt));
@@ -5614,16 +5864,45 @@ fn decrement(cx: &mut Context) {
fn increment_impl(cx: &mut Context, amount: i64) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
- let text = doc.text();
+ let text = doc.text().slice(..);
+
+ let changes: Vec<_> = selection
+ .ranges()
+ .iter()
+ .filter_map(|range| {
+ let incrementor: Box<dyn Increment> =
+ if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else {
+ return None;
+ };
+
+ let (range, new_text) = incrementor.increment(amount);
+
+ Some((range.from(), range.to(), Some(new_text)))
+ })
+ .collect();
- let changes = selection.ranges().iter().filter_map(|range| {
- let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
- let new_text = incrementor.incremented_text(amount);
- Some((
- incrementor.range.from(),
- incrementor.range.to(),
- Some(new_text),
- ))
+ // Overlapping changes in a transaction will panic, so we need to find and remove them.
+ // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
+ // incrementing will give overlapping changes, with each change incrementing a different part of
+ // the date. Since these conflict with each other we remove these changes from the transaction
+ // so nothing happens.
+ let mut overlapping_indexes = HashSet::new();
+ for (i, changes) in changes.windows(2).enumerate() {
+ if changes[0].1 > changes[1].0 {
+ overlapping_indexes.insert(i);
+ overlapping_indexes.insert(i + 1);
+ }
+ }
+ let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
+ if overlapping_indexes.contains(&i) {
+ None
+ } else {
+ Some(change)
+ }
});
if changes.clone().count() > 0 {
@@ -5634,3 +5913,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
doc.append_changes_to_history(view.id);
}
}
+
+fn record_macro(cx: &mut Context) {
+ if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
+ // Remove the keypress which ends the recording
+ keys.pop();
+ let s = keys
+ .into_iter()
+ .map(|key| format!("{}", key))
+ .collect::<Vec<_>>()
+ .join(" ");
+ cx.editor.registers.get_mut(reg).write(vec![s]);
+ cx.editor
+ .set_status(format!("Recorded to register {}", reg));
+ } else {
+ let reg = cx.register.take().unwrap_or('@');
+ cx.editor.macro_recording = Some((reg, Vec::new()));
+ cx.editor
+ .set_status(format!("Recording to register {}", reg));
+ }
+}
+
+fn play_macro(cx: &mut Context) {
+ let reg = cx.register.unwrap_or('@');
+ let keys = match cx
+ .editor
+ .registers
+ .get(reg)
+ .and_then(|reg| reg.read().get(0))
+ .context("Register empty")
+ .and_then(|s| {
+ s.split_whitespace()
+ .map(str::parse::<KeyEvent>)
+ .collect::<Result<Vec<_>, _>>()
+ .context("Failed to parse macro")
+ }) {
+ Ok(keys) => keys,
+ Err(e) => {
+ cx.editor.set_error(format!("{}", e));
+ return;
+ }
+ };
+ let count = cx.count();
+
+ cx.callback = Some(Box::new(
+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
+ for _ in 0..count {
+ for &key in keys.iter() {
+ compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
+ }
+ }
+ },
+ ));
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3a644750..321f56a5 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
-pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
// --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer.
@@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
/// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds.
+ ///
+ /// The returned size might be larger than the viewport if the child is too big to fit.
+ /// In this case the parent can use the values to calculate scroll.
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
- // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
- // that way render can use it
None
}
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
+
+ fn id(&self) -> Option<&'static str> {
+ None
+ }
}
use anyhow::Error;
@@ -126,12 +131,17 @@ impl Compositor {
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
+ // If it is a key event and a macro is being recorded, push the key event to the recording.
+ if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
+ keys.push(key.into());
+ }
+
// propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
- callback(self);
+ callback(self, cx);
return true;
}
EventResult::Consumed(None) => return true,
@@ -184,6 +194,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut())
}
+
+ pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
+ let type_name = std::any::type_name::<T>();
+ self.layers
+ .iter_mut()
+ .find(|component| component.type_name() == type_name && component.id() == Some(id))
+ .and_then(|component| component.as_any_mut().downcast_mut())
+ }
}
// View casting, taken straight from Cursive
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 7f6d0c6b..257d5f29 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,4 +1,4 @@
-pub use crate::commands::Command;
+pub use crate::commands::MappableCommand;
use crate::config::Config;
use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
- $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
@@ -120,7 +120,7 @@ macro_rules! keymap {
_key,
keymap!(@trie $value)
);
- debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
+ assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
- Leaf(Command),
- Sequence(Vec<Command>),
+ Leaf(MappableCommand),
+ Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@@ -304,9 +304,9 @@ impl KeyTrie {
pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
- Matched(Command),
+ Matched(MappableCommand),
/// Matched a sequence of commands to execute.
- MatchedSequence(Vec<Command>),
+ MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -386,10 +386,10 @@ impl Keymap {
};
let trie = match trie_node.search(&[*first]) {
- Some(&KeyTrie::Leaf(cmd)) => {
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+ Some(KeyTrie::Leaf(ref cmd)) => {
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
}
- Some(&KeyTrie::Sequence(ref cmds)) => {
+ Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
@@ -408,9 +408,9 @@ impl Keymap {
}
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
- Some(&KeyTrie::Leaf(cmd)) => {
+ Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear();
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
}
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear();
@@ -512,6 +512,7 @@ impl Default for Keymaps {
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_last_line,
+ "f" => goto_file,
"h" => goto_line_start,
"l" => goto_line_end,
"s" => goto_first_nonwhitespace,
@@ -520,9 +521,10 @@ impl Default for Keymaps {
"r" => goto_reference,
"i" => goto_implementation,
"t" => goto_window_top,
- "m" => goto_window_middle,
+ "c" => goto_window_center,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
+ "m" => goto_last_modified_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
@@ -537,9 +539,9 @@ impl Default for Keymaps {
"O" => open_above,
"d" => delete_selection,
- // TODO: also delete without yanking
+ "A-d" => delete_selection_noyank,
"c" => change_selection,
- // TODO: also change delete without yanking
+ "A-c" => change_selection_noyank,
"C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line,
@@ -591,6 +593,9 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,
+ "q" => record_macro,
+ "Q" => play_macro,
+
">" => indent,
"<" => unindent,
"=" => format_selections,
@@ -622,6 +627,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@@ -637,7 +644,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward,
- // "C-s" => save_selection,
+ "C-s" => save_selection,
"space" => { "Space"
"f" => file_picker,
@@ -650,6 +657,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@@ -827,36 +836,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')).kind,
- KeymapResultKind::Matched(Command::normal_mode),
+ KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')).kind,
- KeymapResultKind::Matched(Command::insert_mode),
+ KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')).kind,
- KeymapResultKind::Matched(Command::jump_backward),
+ KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_line_end),
+ &KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
- &KeyTrie::Leaf(Command::delete_char_forward),
+ &KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_last_line),
+ &KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
@@ -890,7 +899,7 @@ mod tests {
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
- &KeyTrie::Leaf(Command::vsplit),
+ &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs
index f5e3a8cd..58cb139c 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -9,3 +9,14 @@ pub mod config;
pub mod job;
pub mod keymap;
pub mod ui;
+
+#[cfg(not(windows))]
+fn true_color() -> bool {
+ std::env::var("COLORTERM")
+ .map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
+ .unwrap_or(false)
+}
+#[cfg(windows)]
+fn true_color() -> bool {
+ true
+}
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29..a55201ff 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -168,7 +168,7 @@ impl Completion {
}
};
});
- let popup = Popup::new(menu);
+ let popup = Popup::new("completion", menu);
let mut completion = Self {
popup,
start_offset,
@@ -328,8 +328,8 @@ impl Component for Completion {
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
- width = rel_width;
- height = rel_height;
+ width = rel_width.min(width);
+ height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index e8f8fd9b..7d57e581 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -17,7 +17,6 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
- editor::LineNumber,
graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
@@ -32,7 +31,7 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
- last_insert: (commands::Command, Vec<KeyEvent>),
+ last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
autoinfo: Option<Info>,
@@ -49,7 +48,7 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
- last_insert: (commands::Command::normal_mode, Vec::new()),
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
autoinfo: None,
@@ -310,17 +309,16 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
- let style = spans.iter().fold(text_style, |acc, span| {
- let style = theme.get(theme.scopes()[span.0].as_str());
- acc.patch(style)
- });
-
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -351,6 +349,10 @@ impl EditorView {
};
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -417,22 +419,6 @@ impl EditorView {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
- let linenr = theme.get("ui.linenr");
- let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
-
- let warning = theme.get("warning");
- let error = theme.get("error");
- let info = theme.get("info");
- let hint = theme.get("hint");
-
- // Whether to draw the line number for the last line of the
- // document or not. We only draw it if it's not an empty line.
- let draw_last = text.line_to_byte(last_line) < text.len_bytes();
-
- let current_line = doc
- .text()
- .char_to_line(doc.selection(view.id).primary().cursor(text));
-
// it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
@@ -442,51 +428,31 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
- for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
- use helix_core::diagnostic::Severity;
- if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
- surface.set_stringn(
- viewport.x,
- viewport.y + i as u16,
- "●",
- 1,
- match diagnostic.severity {
- Some(Severity::Error) => error,
- Some(Severity::Warning) | None => warning,
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- },
- );
- }
+ let mut offset = 0;
- let selected = cursors.contains(&line);
+ let gutter_style = theme.get("ui.gutter");
- let text = if line == last_line && !draw_last {
- " ~".into()
- } else {
- let line = match config.line_number {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
- };
- format!("{:>5}", line)
- };
- surface.set_stringn(
- viewport.x + 1,
- viewport.y + i as u16,
- text,
- 5,
- if selected && is_focused {
- linenr_select
- } else {
- linenr
- },
- );
+ // avoid lots of small allocations by reusing a text buffer for each line
+ let mut text = String::with_capacity(8);
+
+ for (constructor, width) in view.gutters() {
+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
+ text.reserve(*width); // ensure there's enough space for the gutter
+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
+ let selected = cursors.contains(&line);
+
+ if let Some(style) = gutter(line, selected, &mut text) {
+ surface.set_stringn(
+ viewport.x + offset,
+ viewport.y + i as u16,
+ &text,
+ *width,
+ gutter_style.patch(style),
+ );
+ }
+ text.clear();
+ }
+ offset += *width as u16;
}
}
@@ -916,7 +882,7 @@ impl EditorView {
return EventResult::Ignored;
}
- commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
+ commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None)
}
@@ -934,7 +900,8 @@ impl EditorView {
}
if modifiers == crossterm::event::KeyModifiers::ALT {
- commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
+ commands::MappableCommand::replace_selections_with_primary_clipboard
+ .execute(cxt);
return EventResult::Consumed(None);
}
@@ -948,7 +915,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
- commands::Command::paste_primary_clipboard_before.execute(cxt);
+ commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@@ -963,7 +930,7 @@ impl EditorView {
impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context {
- editor: &mut cx.editor,
+ editor: cx.editor,
count: None,
register: None,
callback: None,
@@ -1140,13 +1107,31 @@ impl Component for EditorView {
disp.push_str(&s);
}
}
+ let style = cx.editor.theme.get("ui.text");
+ let macro_width = if cx.editor.macro_recording.is_some() {
+ 3
+ } else {
+ 0
+ };
surface.set_string(
- area.x + area.width.saturating_sub(key_width),
+ area.x + area.width.saturating_sub(key_width + macro_width),
area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp),
- cx.editor.theme.get("ui.text"),
+ style,
);
+ if let Some((reg, _)) = cx.editor.macro_recording {
+ let disp = format!("[{}]", reg);
+ let style = style
+ .fg(helix_view::graphics::Color::Yellow)
+ .add_modifier(Modifier::BOLD);
+ surface.set_string(
+ area.x + area.width.saturating_sub(3),
+ area.y + area.height.saturating_sub(1),
+ &disp,
+ style,
+ );
+ }
}
if let Some(completion) = self.completion.as_mut() {
@@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}
-
-#[inline]
-const fn abs_diff(a: usize, b: usize) -> usize {
- if a > b {
- a - b
- } else {
- b - a
- }
-}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 649703b5..46657fb9 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -228,6 +228,7 @@ impl Component for Markdown {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
+ // TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
@@ -240,11 +241,6 @@ impl Component for Markdown {
} else if content_width > text_width {
text_width = content_width;
}
-
- if height >= viewport.1 {
- height = viewport.1;
- break;
- }
}
Some((text_width + padding, height))
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index e891c149..69053db3 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index cdf42311..f57e2e2b 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -186,6 +186,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
+ names.push("base16_default".into());
let mut names: Vec<_> = names
.into_iter()
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 6b1c5832..1ef94df0 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
}
pub enum CachedPreview {
- Document(Document),
+ Document(Box<Document>),
Binary,
LargeFile,
NotFound,
@@ -140,7 +140,7 @@ impl<T> FilePicker<T> {
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, Some(&editor.theme), None)
- .map(CachedPreview::Document)
+ .map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
@@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -421,19 +421,19 @@ impl<T: 'static> Component for Picker<T> {
}
key!(Enter) => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::Replace);
+ (self.callback_fn)(cx.editor, option, Action::Replace);
}
return close_fn;
}
ctrl!('s') => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit);
+ (self.callback_fn)(cx.editor, option, Action::HorizontalSplit);
}
return close_fn;
}
ctrl!('v') => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit);
+ (self.callback_fn)(cx.editor, option, Action::VerticalSplit);
}
return close_fn;
}
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a1..bf7510a2 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
size: (u16, u16),
+ child_size: (u16, u16),
scroll: usize,
+ id: &'static str,
}
impl<T: Component> Popup<T> {
- pub fn new(contents: T) -> Self {
+ pub fn new(id: &'static str, contents: T) -> Self {
Self {
contents,
position: None,
size: (0, 0),
+ child_size: (0, 0),
scroll: 0,
+ id,
}
}
@@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
+
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
@@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -115,13 +122,21 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
- fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 120.min(viewport.0);
+ let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
let (width, height) = self
.contents
- .required_size((120, 26)) // max width, max height
+ .required_size((max_width, max_height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
- self.size = (width, height);
+ self.child_size = (width, height);
+ self.size = (width.min(max_width), height.min(max_height));
+
+ // re-clamp scroll offset
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
}
@@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
self.contents.render(area, surface, cx);
}
+
+ fn id(&self) -> Option<&'static str> {
+ Some(self.id)
+ }
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index e90b0772..07e1b33c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- shift!(BackTab) => {
+ shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index f480bc2f..c49a0200 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -102,7 +102,7 @@ impl Default for Cell {
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -111,15 +111,6 @@ pub struct Buffer {
pub content: Vec<Cell>,
}
-impl Default for Buffer {
- fn default() -> Buffer {
- Buffer {
- area: Default::default(),
- content: Vec::new(),
- }
- }
-}
-
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
index b8e52479..8a974ddb 100644
--- a/helix-tui/src/text.rs
+++ b/helix-tui/src/text.rs
@@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> {
}
/// A string composed of clusters of graphemes, each with their own style.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
-impl<'a> Default for Spans<'a> {
- fn default() -> Spans<'a> {
- Spans(Vec::new())
- }
-}
-
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
@@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String {
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
-impl<'a> Default for Text<'a> {
- fn default() -> Text<'a> {
- Text { lines: Vec::new() }
- }
-}
-
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs
index d7caa0b0..6aee5988 100644
--- a/helix-tui/src/widgets/table.rs
+++ b/helix-tui/src/widgets/table.rs
@@ -363,21 +363,12 @@ impl<'a> Table<'a> {
}
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Default, Clone)]
pub struct TableState {
pub offset: usize,
pub selected: Option<usize>,
}
-impl Default for TableState {
- fn default() -> TableState {
- TableState {
- offset: 0,
- selected: None,
- }
- }
-}
-
impl TableState {
pub fn selected(&self) -> Option<usize> {
self.selected
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 01975452..a0315bed 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -104,6 +104,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
+ pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +128,7 @@ impl fmt::Debug for Document {
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
+ .field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
@@ -344,6 +346,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
+ modified_since_accessed: false,
language_server: None,
}
}
@@ -639,6 +642,9 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
+
+ // set modified since accessed
+ self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index a121a836..fff4792d 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect},
+ input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
@@ -11,6 +12,7 @@ use futures_util::future;
use std::{
collections::{BTreeMap, HashMap},
io::stdin,
+ num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@@ -18,7 +20,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use anyhow::Error;
+use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@@ -105,6 +107,8 @@ pub struct Config {
pub file_picker: FilePickerConfig,
/// Shape for cursor in each mode
pub cursor_shape: CursorShapeConfig,
+ /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
+ pub true_color: bool,
}
// Cursor shape is read and used on every rendered frame and so needs
@@ -141,7 +145,7 @@ impl Default for CursorShapeConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
@@ -171,6 +175,7 @@ impl Default for Config {
auto_info: true,
file_picker: FilePickerConfig::default(),
cursor_shape: CursorShapeConfig::default(),
+ true_color: false,
}
}
}
@@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
- pub next_document_id: usize,
+ pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
+ pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -223,8 +229,8 @@ pub enum Action {
impl Editor {
pub fn new(
mut area: Rect,
- themes: Arc<theme::Loader>,
- config_loader: Arc<syntax::Loader>,
+ theme_loader: Arc<theme::Loader>,
+ syn_loader: Arc<syntax::Loader>,
config: Config,
) -> Self {
let language_servers = helix_lsp::Registry::new();
@@ -234,14 +240,15 @@ impl Editor {
Self {
tree: Tree::new(area),
- next_document_id: 0,
+ next_document_id: DocumentId::default(),
documents: BTreeMap::new(),
count: None,
selected_register: None,
- theme: themes.default(),
+ macro_recording: None,
+ theme: theme_loader.default(),
language_servers,
- syn_loader: config_loader,
- theme_loader: themes,
+ syn_loader,
+ theme_loader,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@@ -297,14 +304,51 @@ impl Editor {
self._refresh();
}
- pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
- use anyhow::Context;
- let theme = self
- .theme_loader
- .load(theme.as_ref())
- .with_context(|| format!("failed setting theme `{}`", theme))?;
- self.set_theme(theme);
- Ok(())
+ /// Refreshes the language server for a given document
+ pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
+ let doc = self.documents.get_mut(&doc_id)?;
+ doc.detect_language(Some(&self.theme), &self.syn_loader);
+ Self::launch_language_server(&mut self.language_servers, doc)
+ }
+
+ /// Launch a language server for a given document
+ fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
+ // try to find a language server based on the language name
+ let language_server = doc.language.as_ref().and_then(|language| {
+ ls.get(language)
+ .map_err(|e| {
+ log::error!(
+ "Failed to initialize the LSP for `{}` {{ {} }}",
+ language.scope(),
+ e
+ )
+ })
+ .ok()
+ });
+ if let Some(language_server) = language_server {
+ // only spawn a new lang server if the servers aren't the same
+ if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
+ if let Some(language_server) = doc.language_server() {
+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
+ }
+ let language_id = doc
+ .language()
+ .and_then(|s| s.split('.').last()) // source.rust
+ .map(ToOwned::to_owned)
+ .unwrap_or_default();
+
+ // TODO: this now races with on_init code if the init happens too quickly
+ tokio::spawn(language_server.text_document_did_open(
+ doc.url().unwrap(),
+ doc.version(),
+ doc.text(),
+ language_id,
+ ));
+
+ doc.set_language_server(Some(language_server));
+ }
+ }
+ Some(())
}
fn _refresh(&mut self) {
@@ -358,7 +402,8 @@ impl Editor {
.tree
.traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
- let view = view_mut!(self);
+
+ let (view, doc) = current!(self);
if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`.
@@ -367,7 +412,16 @@ impl Editor {
} else {
let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump);
- view.last_accessed_doc = Some(view.doc);
+ // Set last accessed doc if it is a different document
+ if doc.id != id {
+ view.last_accessed_doc = Some(view.doc);
+ // Set last modified doc if modified and last modified doc is different
+ if std::mem::take(&mut doc.modified_since_accessed)
+ && view.last_modified_docs[0] != Some(id)
+ {
+ view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+ }
+ }
}
let view_id = view.id;
@@ -377,23 +431,22 @@ impl Editor {
}
Action::Load => {
let view_id = view!(self).id;
- if let Some(doc) = self.document_mut(id) {
- if doc.selections().is_empty() {
- doc.selections.insert(view_id, Selection::point(0));
- }
+ let doc = self.documents.get_mut(&id).unwrap();
+ if doc.selections().is_empty() {
+ doc.selections.insert(view_id, Selection::point(0));
}
return;
}
- Action::HorizontalSplit => {
- let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Horizontal);
- // initialize selection for view
- let doc = self.documents.get_mut(&id).unwrap();
- doc.selections.insert(view_id, Selection::point(0));
- }
- Action::VerticalSplit => {
+ Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Vertical);
+ let view_id = self.tree.split(
+ view,
+ match action {
+ Action::HorizontalSplit => Layout::Horizontal,
+ Action::VerticalSplit => Layout::Vertical,
+ _ => unreachable!(),
+ },
+ );
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
@@ -403,16 +456,19 @@ impl Editor {
self._refresh();
}
- fn new_document(&mut self, mut document: Document) -> DocumentId {
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- document.id = id;
- self.documents.insert(id, document);
+ /// Generate an id for a new document and register it.
+ fn new_document(&mut self, mut doc: Document) -> DocumentId {
+ let id = self.next_document_id;
+ // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
+ self.next_document_id =
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
+ doc.id = id;
+ self.documents.insert(id, doc);
id
}
- fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
- let id = self.new_document(document);
+ fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
+ let id = self.new_document(doc);
self.switch(id, action);
id
}
@@ -428,54 +484,16 @@ impl Editor {
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
-
- let id = self
- .documents()
- .find(|doc| doc.path() == Some(&path))
- .map(|doc| doc.id);
+ let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
- // try to find a language server based on the language name
- let language_server = doc.language.as_ref().and_then(|language| {
- self.language_servers
- .get(language)
- .map_err(|e| {
- log::error!(
- "Failed to initialize the LSP for `{}` {{ {} }}",
- language.scope(),
- e
- )
- })
- .ok()
- });
-
- if let Some(language_server) = language_server {
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
-
- // TODO: this now races with on_init code if the init happens too quickly
- tokio::spawn(language_server.text_document_did_open(
- doc.url().unwrap(),
- doc.version(),
- doc.text(),
- language_id,
- ));
-
- doc.set_language_server(Some(language_server));
- }
+ let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- doc.id = id;
- self.documents.insert(id, doc);
- id
+ self.new_document(doc)
};
self.switch(id, action);
@@ -498,11 +516,11 @@ impl Editor {
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) {
Some(doc) => doc,
- None => anyhow::bail!("document does not exist"),
+ None => bail!("document does not exist"),
};
if !force && doc.is_modified() {
- anyhow::bail!(
+ bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
@@ -535,7 +553,7 @@ impl Editor {
// If the document we removed was visible in all views, we will have no more views. We don't
// want to close the editor just for a simple buffer close, so we need to create a new view
// containing either an existing document, or a brand new document.
- if self.tree.views().peekable().peek().is_none() {
+ if self.tree.views().next().is_none() {
let doc_id = self
.documents
.iter()
@@ -620,8 +638,7 @@ impl Editor {
}
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
- let view = view!(self);
- let doc = &self.documents[&view.doc];
+ let (view, doc) = current_ref!(self);
let cursor = doc
.selection(view.id)
.primary()
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index acdaa696..892aa646 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -33,7 +33,7 @@ pub struct Margin {
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -41,17 +41,6 @@ pub struct Rect {
pub height: u16,
}
-impl Default for Rect {
- fn default() -> Rect {
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- }
- }
-}
-
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
new file mode 100644
index 00000000..af016c56
--- /dev/null
+++ b/helix-view/src/gutter.rs
@@ -0,0 +1,96 @@
+use std::fmt::Write;
+
+use crate::{editor::Config, graphics::Style, Document, Theme, View};
+
+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
+pub type Gutter =
+ for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
+
+pub fn diagnostic<'doc>(
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _config: &Config,
+ _is_focused: bool,
+ _width: usize,
+) -> GutterFn<'doc> {
+ let warning = theme.get("warning");
+ let error = theme.get("error");
+ let info = theme.get("info");
+ let hint = theme.get("hint");
+ let diagnostics = doc.diagnostics();
+
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ use helix_core::diagnostic::Severity;
+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
+ let diagnostic = &diagnostics[index];
+ write!(out, "●").unwrap();
+ return Some(match diagnostic.severity {
+ Some(Severity::Error) => error,
+ Some(Severity::Warning) | None => warning,
+ Some(Severity::Info) => info,
+ Some(Severity::Hint) => hint,
+ });
+ }
+ None
+ })
+}
+
+pub fn line_number<'doc>(
+ doc: &'doc Document,
+ view: &View,
+ theme: &Theme,
+ config: &Config,
+ is_focused: bool,
+ width: usize,
+) -> GutterFn<'doc> {
+ let text = doc.text().slice(..);
+ let last_line = view.last_line(doc);
+ // Whether to draw the line number for the last line of the
+ // document or not. We only draw it if it's not an empty line.
+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+
+ let linenr = theme.get("ui.linenr");
+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
+
+ let current_line = doc
+ .text()
+ .char_to_line(doc.selection(view.id).primary().cursor(text));
+
+ let config = config.line_number;
+
+ Box::new(move |line: usize, selected: bool, out: &mut String| {
+ if line == last_line && !draw_last {
+ write!(out, "{:>1$}", '~', width).unwrap();
+ Some(linenr)
+ } else {
+ use crate::editor::LineNumber;
+ let line = match config {
+ LineNumber::Absolute => line + 1,
+ LineNumber::Relative => {
+ if current_line == line {
+ line + 1
+ } else {
+ abs_diff(current_line, line)
+ }
+ }
+ };
+ let style = if selected && is_focused {
+ linenr_select
+ } else {
+ linenr
+ };
+ write!(out, "{:>1$}", line, width).unwrap();
+ Some(style)
+ }
+ })
+}
+
+#[inline(always)]
+const fn abs_diff(a: usize, b: usize) -> usize {
+ if a > b {
+ a - b
+ } else {
+ b - a
+ }
+}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204cc..92caa517 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -36,7 +36,6 @@ pub(crate) mod keys {
pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab";
- pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null";
@@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?,
- KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?,
@@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(),
- KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(),
@@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab,
- keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null,
@@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
- fn from(
- crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
- ) -> KeyEvent {
- KeyEvent {
- code: code.into(),
- modifiers: modifiers.into(),
+ fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
+ if code == crossterm::event::KeyCode::BackTab {
+ // special case for BackTab -> Shift-Tab
+ let mut modifiers: KeyModifiers = modifiers.into();
+ modifiers.insert(KeyModifiers::SHIFT);
+ Self {
+ code: KeyCode::Tab,
+ modifiers,
+ }
+ } else {
+ Self {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
+ }
+ }
+}
+
+#[cfg(feature = "term")]
+impl From<KeyEvent> for crossterm::event::KeyEvent {
+ fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
+ if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
+ // special case for Shift-Tab -> BackTab
+ let mut modifiers = modifiers;
+ modifiers.remove(KeyModifiers::SHIFT);
+ crossterm::event::KeyEvent {
+ code: crossterm::event::KeyCode::BackTab,
+ modifiers: modifiers.into(),
+ }
+ } else {
+ crossterm::event::KeyEvent {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
}
}
}
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 810aa063..f1717209 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -79,8 +79,6 @@ pub enum KeyCode {
PageDown,
/// Tab key.
Tab,
- /// Shift + Tab key.
- BackTab,
/// Delete key.
Delete,
/// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
- KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
- CKeyCode::BackTab => KeyCode::BackTab,
+ CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 3e779356..a56c914d 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document;
pub mod editor;
pub mod graphics;
+pub mod gutter;
pub mod info;
pub mod input;
pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
pub mod tree;
pub mod view;
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
-pub struct DocumentId(usize);
+use std::num::NonZeroUsize;
+
+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct DocumentId(NonZeroUsize);
+
+impl Default for DocumentId {
+ fn default() -> DocumentId {
+ // Safety: 1 is non-zero
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
+ }
+}
slotmap::new_key_type! {
pub struct ViewId;
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 757316bd..4a2ecbba 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
+pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
+ toml::from_slice(include_bytes!("../../base16_theme.toml"))
+ .expect("Failed to parse base 16 default theme")
+});
#[derive(Clone, Debug)]
pub struct Loader {
@@ -35,6 +39,9 @@ impl Loader {
if name == "default" {
return Ok(self.default());
}
+ if name == "base16_default" {
+ return Ok(self.base16_default());
+ }
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
@@ -74,12 +81,20 @@ impl Loader {
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
+
+ /// Returns the alternative 16-color default theme
+ pub fn base16_default(&self) -> Theme {
+ BASE16_DEFAULT_THEME.clone()
+ }
}
#[derive(Clone, Debug)]
pub struct Theme {
- scopes: Vec<String>,
+ // UI styles are stored in a HashMap
styles: HashMap<String, Style>,
+ // tree-sitter highlight styles are stored in a Vec to optimize lookups
+ scopes: Vec<String>,
+ highlights: Vec<Style>,
}
impl<'de> Deserialize<'de> for Theme {
@@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let mut styles = HashMap::new();
+ let mut scopes = Vec::new();
+ let mut highlights = Vec::new();
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
// TODO: alert user of parsing failures in editor
@@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
.unwrap_or_default();
styles.reserve(colors.len());
+ scopes.reserve(colors.len());
+ highlights.reserve(colors.len());
+
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
- styles.insert(name, style);
+
+ // these are used both as UI and as highlights
+ styles.insert(name.clone(), style);
+ scopes.push(name);
+ highlights.push(style);
}
}
- let scopes = styles.keys().map(ToString::to_string).collect();
- Ok(Self { scopes, styles })
+ Ok(Self {
+ scopes,
+ styles,
+ highlights,
+ })
}
}
impl Theme {
+ #[inline]
+ pub fn highlight(&self, index: usize) -> Style {
+ self.highlights[index]
+ }
+
pub fn get(&self, scope: &str) -> Style {
- self.try_get(scope)
- .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
+ self.try_get(scope).unwrap_or_default()
}
pub fn try_get(&self, scope: &str) -> Option<Style> {
@@ -134,6 +165,14 @@ impl Theme {
pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope)
}
+
+ pub fn is_16_color(&self) -> bool {
+ self.styles.iter().all(|(_, style)| {
+ [style.fg, style.bg]
+ .into_iter()
+ .all(|color| !matches!(color, Some(Color::Rgb(..))))
+ })
+ }
}
struct ThemePalette {
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index a77f1562..94d67acd 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,6 +1,10 @@
use std::borrow::Cow;
-use crate::{graphics::Rect, Document, DocumentId, ViewId};
+use crate::{
+ graphics::Rect,
+ gutter::{self, Gutter},
+ Document, DocumentId, ViewId,
+};
use helix_core::{
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
}
}
+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
+
#[derive(Debug)]
pub struct View {
pub id: ViewId,
@@ -69,6 +75,11 @@ pub struct View {
pub jumps: JumpList,
/// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>,
+ /// the last modified files before the current one
+ /// ordered from most frequent to least frequent
+ // uses two docs because we want to be able to swap between the
+ // two last modified docs which we need to manually keep track of
+ pub last_modified_docs: [Option<DocumentId>; 2],
}
impl View {
@@ -80,13 +91,23 @@ impl View {
area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None,
+ last_modified_docs: [None, None],
}
}
+ pub fn gutters(&self) -> &[(Gutter, usize)] {
+ GUTTERS
+ }
+
pub fn inner_area(&self) -> Rect {
- // TODO: not ideal
- const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
- self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
+ // TODO: cache this
+ let offset = self
+ .gutters()
+ .iter()
+ .map(|(_, width)| *width as u16)
+ .sum::<u16>()
+ + 1; // +1 for some space between gutters and line
+ self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
}
//
@@ -276,6 +297,7 @@ mod tests {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test]
fn test_text_pos_at_screen_coords() {
diff --git a/languages.toml b/languages.toml
index 2ce9321e..abb94acf 100644
--- a/languages.toml
+++ b/languages.toml
@@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = " " }
[language.config]
cargo = { loadOutDirsFromCheck = true }
procMacro = { enable = false }
+diagnostics = { disabled = ["unresolved-proc-macro"] }
[[language]]
name = "toml"
@@ -249,7 +250,6 @@ language-server = { command = "julia", args = [
using Pkg;
import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file);
-
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true;
run(server);
@@ -396,3 +396,37 @@ shebangs = ["perl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "racket"
+scope = "source.rkt"
+roots = []
+file-types = ["rkt"]
+shebangs = ["racket"]
+comment-token = ";"
+language-server = { command = "racket", args = ["-l", "racket-langserver"] }
+
+[[language]]
+name = "wgsl"
+scope = "source.wgsl"
+file-types = ["wgsl"]
+roots = []
+comment-token = "//"
+indent = { tab-width = 4, unit = " " }
+
+[[language]]
+name = "llvm"
+scope = "source.llvm"
+roots = []
+file-types = ["ll"]
+comment-token = ";"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "markdown"
+scope = "source.md"
+injection-regex = "md|markdown"
+file-types = ["md"]
+roots = []
+
+indent = { tab-width = 2, unit = " " }
diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm
index 72187876..8006cb07 100644
--- a/runtime/queries/haskell/highlights.scm
+++ b/runtime/queries/haskell/highlights.scm
@@ -8,7 +8,7 @@
(constructor) @constructor
(pragma) @pragma
(comment) @comment
-(signature name: (variable) @fun_type_name)
+(signature name: (variable) @type)
(function name: (variable) @function)
(constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class))
diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm
index f8b34f71..d5ac794e 100644
--- a/runtime/queries/julia/locals.scm
+++ b/runtime/queries/julia/locals.scm
@@ -2,24 +2,24 @@
(import_statement
(identifier) @definition.import)
(variable_declaration
- (identifier) @definition.var)
+ (identifier) @local.definition)
(variable_declaration
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(for_binding
- (identifier) @definition.var)
+ (identifier) @local.definition)
(for_binding
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
(bare_tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
- (identifier) @definition.var)
+ (identifier) @local.definition)
(type_parameter_list
(identifier) @definition.type)
@@ -43,11 +43,11 @@
(identifier) @definition.parameter)
(function_definition
- name: (identifier) @definition.function) @scope
+ name: (identifier) @definition.function) @local.scope
(macro_definition
- name: (identifier) @definition.macro) @scope
+ name: (identifier) @definition.macro) @local.scope
-(identifier) @reference
+(identifier) @local.reference
[
(try_statement)
@@ -56,4 +56,4 @@
(let_statement)
(compound_expression)
(for_statement)
-] @scope
+] @local.scope
diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm
index f045c82d..2e308f77 100644
--- a/runtime/queries/latex/highlights.scm
+++ b/runtime/queries/latex/highlights.scm
@@ -278,7 +278,7 @@
"\\includeinkscape"
"\\usepgflibrary"
"\\usetikzlibrary"
-] @include
+] @keyword.control.import
[
"\\part"
@@ -318,60 +318,60 @@
["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
(chapter
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(part
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(section
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subsection
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subsubsection
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(paragraph
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subparagraph
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
((environment
(begin
name: (word) @_frame)
(brace_group
- child: (text) @text.title))
+ child: (text) @markup.heading))
(#eq? @_frame "frame"))
((generic_command
name:(generic_command_name) @_name
arg: (brace_group
- (text) @text.title))
+ (text) @markup.heading))
(#eq? @_name "\\frametitle"))
;; Formatting
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.emphasis)
+ arg: (_) @markup.italic)
(#eq? @_name "\\emph"))
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.emphasis)
+ arg: (_) @markup.italic)
(#match? @_name "^(\\\\textit|\\\\mathit)$"))
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.strong)
+ arg: (_) @markup.bold)
(#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
((generic_command
name:(generic_command_name) @_name
.
- arg: (_) @text.uri)
+ arg: (_) @markup.underline.link)
(#match? @_name "^(\\\\url|\\\\href)$"))
(ERROR) @error
diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm
index bdf5f2db..02a9ea9a 100644
--- a/runtime/queries/ledger/highlights.scm
+++ b/runtime/queries/ledger/highlights.scm
@@ -12,7 +12,7 @@
((account) @variable.other.member)
((commodity) @text.literal)
-"include" @include
+"include" @keyword.local.import
[
"account"
diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm
new file mode 100644
index 00000000..73afe85e
--- /dev/null
+++ b/runtime/queries/llvm/highlights.scm
@@ -0,0 +1,14 @@
+(type) @type
+(statement) @keyword.operator
+(number) @constant.numeric.integer
+(comment) @comment
+(string) @string
+(label) @label
+(keyword) @keyword
+"ret" @keyword.control.return
+(boolean) @constant.builtin.boolean
+(float) @constant.numeric.float
+(constant) @constant
+(identifier) @variable
+(symbol) @punctuation.delimiter
+(bracket) @punctuation.bracket
diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm
new file mode 100644
index 00000000..a102719b
--- /dev/null
+++ b/runtime/queries/markdown/highlights.scm
@@ -0,0 +1,35 @@
+[
+ (atx_heading)
+ (setext_heading)
+] @markup.heading
+
+(code_fence_content) @none
+
+[
+ (indented_code_block)
+ (fenced_code_block)
+] @markup.raw.block
+
+(code_span) @markup.raw.inline
+
+(emphasis) @markup.italic
+
+(strong_emphasis) @markup.bold
+
+(link_destination) @markup.underline.link
+
+; (link_label) @markup.label ; TODO: rename
+
+[
+ (list_marker_plus)
+ (list_marker_minus)
+ (list_marker_star)
+ (list_marker_dot)
+ (list_marker_parenthesis)
+] @punctuation.special
+
+[
+ (backslash_escape)
+ (hard_line_break)
+] @string.character.escape
+
diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm
new file mode 100644
index 00000000..ff3c5fe6
--- /dev/null
+++ b/runtime/queries/markdown/injections.scm
@@ -0,0 +1,8 @@
+(fenced_code_block
+ (info_string) @injection.language
+ (code_fence_content) @injection.content)
+
+((html_block) @injection.content
+ (#set! injection.language "html"))
+((html_tag) @injection.content
+ (#set! injection.language "html"))
diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm
index 15f46cc1..a08b1267 100644
--- a/runtime/queries/ocaml/highlights.scm
+++ b/runtime/queries/ocaml/highlights.scm
@@ -90,7 +90,7 @@
["exception" "try"] @keyword.control.exception
-["include" "open"] @include
+["include" "open"] @keyword.control.import
["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml
new file mode 100644
index 00000000..365e0663
--- /dev/null
+++ b/runtime/queries/perl/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+ "function",
+ "identifier",
+ "method_invocation",
+ "if_statement",
+ "unless_statement",
+ "if_simple_statement",
+ "unless_simple_statement",
+ "variable_declaration",
+ "block",
+ "list_item",
+ "word_list_qw"
+]
+
+outdent = [
+ "}"
+]
diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm
index 4fcdfd66..f9eef6b5 100644
--- a/runtime/queries/svelte/highlights.scm
+++ b/runtime/queries/svelte/highlights.scm
@@ -25,7 +25,7 @@
((attribute
(attribute_name) @_attr
- (quoted_attribute_value (attribute_value) @markup.undeline.link))
+ (quoted_attribute_value (attribute_value) @markup.underline.link))
(#match? @_attr "^(href|src)$"))
(tag_name) @tag
diff --git a/runtime/queries/wgsl/highlights.scm b/runtime/queries/wgsl/highlights.scm
new file mode 100644
index 00000000..7fbc87d8
--- /dev/null
+++ b/runtime/queries/wgsl/highlights.scm
@@ -0,0 +1,102 @@
+(const_literal) @constant.numeric
+
+(type_declaration) @type
+
+(function_declaration
+ (identifier) @function)
+
+(struct_declaration
+ (identifier) @type)
+
+(type_constructor_or_function_call_expression
+ (type_declaration) @function)
+
+(parameter
+ (variable_identifier_declaration (identifier) @variable.parameter))
+
+[
+ "struct"
+ "bitcast"
+ ; "block"
+ "discard"
+ "enable"
+ "fallthrough"
+ "fn"
+ "let"
+ "private"
+ "read"
+ "read_write"
+ "return"
+ "storage"
+ "type"
+ "uniform"
+ "var"
+ "workgroup"
+ "write"
+ (texel_format)
+] @keyword ; TODO reserved keywords
+
+[
+ (true)
+ (false)
+] @constant.builtin.boolean
+
+[ "," "." ":" ";" ] @punctuation.delimiter
+
+;; brackets
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "loop"
+ "for"
+ "break"
+ "continue"
+ "continuing"
+] @keyword.control.repeat
+
+[
+ "if"
+ "else"
+ "elseif"
+ "switch"
+ "case"
+ "default"
+] @keyword.control.conditional
+
+[
+ "&"
+ "&&"
+ "/"
+ "!"
+ "="
+ "=="
+ "!="
+ ">"
+ ">="
+ ">>"
+ "<"
+ "<="
+ "<<"
+ "%"
+ "-"
+ "+"
+ "|"
+ "||"
+ "*"
+ "~"
+ "^"
+] @operator
+
+(attribute
+ (identifier) @variable.other.member)
+
+(comment) @comment
+
+(ERROR) @error
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
index d65995c0..fd4b0fea 100644
--- a/runtime/themes/base16_default_dark.toml
+++ b/runtime/themes/base16_default_dark.toml
@@ -1,27 +1,28 @@
-# Author: RayGervais<raygervais@hotmail.ca>
+# Author: RayGervais <raygervais@hotmail.ca>
"ui.background" = { bg = "base00" }
"ui.menu" = "base01"
-"ui.menu.selected" = { fg = "base04", bg = "base01" }
-"ui.linenr" = {fg = "base01" }
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" }
-"ui.liner.selected" = "base02"
-"ui.selection" = "base02"
-"comment" = "base03"
-"ui.statusline" = {fg = "base04", bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
"ui.help" = { fg = "base04", bg = "base01" }
-"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
-"ui.text" = { fg = "base05" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
"operator" = "base05"
-"ui.text.focus" = { fg = "base05" }
+"ui.text.focus" = "base05"
"variable" = "base08"
"constant.numeric" = "base09"
"constant" = "base09"
-"attributes" = "base09"
+"attributes" = "base09"
"type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
-"strings" = "base0B"
+"string" = "base0B"
"variable.other.member" = "base0B"
"constant.character.escape" = "base0C"
"function" = "base0D"
@@ -30,15 +31,15 @@
"keyword" = "base0E"
"label" = "base0E"
"namespace" = "base0E"
-"ui.popup" = { bg = "base01" }
-"ui.window" = { bg = "base00" }
-"ui.help" = { bg = "base01", fg = "base06" }
+"ui.help" = { fg = "base06", bg = "base01" }
-"info" = "base03"
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
"hint" = "base03"
"debug" = "base03"
-"diagnostic" = "base03"
-"error" = "base0E"
+"warning" = "base09"
+"error" = "base08"
[palette]
base00 = "#181818" # Default Background
diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml
new file mode 100644
index 00000000..596990da
--- /dev/null
+++ b/runtime/themes/base16_default_light.toml
@@ -0,0 +1,60 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.background" = { bg = "base00" }
+"ui.menu" = "base01"
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
+"ui.popup" = { bg = "base01" }
+"ui.window" = { bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
+"ui.help" = { fg = "base04", bg = "base01" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
+"operator" = "base05"
+"ui.text.focus" = "base05"
+"variable" = "base08"
+"constant.numeric" = "base09"
+"constant" = "base09"
+"attributes" = "base09"
+"type" = "base0A"
+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
+"string" = "base0B"
+"variable.other.member" = "base0B"
+"constant.character.escape" = "base0C"
+"function" = "base0D"
+"constructor" = "base0D"
+"special" = "base0D"
+"keyword" = "base0E"
+"label" = "base0E"
+"namespace" = "base0E"
+"ui.help" = { fg = "base06", bg = "base01" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
+"hint" = "base03"
+"debug" = "base03"
+"warning" = "base09"
+"error" = "base08"
+
+[palette]
+base00 = "#f8f8f8" # Default Background
+base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
+base02 = "#d8d8d8" # Selection Background
+base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
+base04 = "#585858" # Dark Foreground (Used for status bars)
+base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
+base06 = "#282828" # Light Foreground (Not often used)
+base07 = "#181818" # Light Background (Not often used)
+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml
new file mode 100644
index 00000000..123ceaea
--- /dev/null
+++ b/runtime/themes/base16_terminal.toml
@@ -0,0 +1,39 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "light-gray", bg = "black" }
+"ui.popup" = { bg = "black" }
+"ui.window" = { bg = "black" }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "gray", modifiers = ["reversed"] }
+"comment" = { fg = "light-gray", modifiers = ["italic"] }
+"ui.statusline" = { fg = "white", bg = "black" }
+"ui.statusline.inactive" = { fg = "gray", bg = "black" }
+"ui.help" = { fg = "white", bg = "black" }
+"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] }
+"ui.cursor.primary" = { modifiers = ["reversed"] }
+"variable" = "light-red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "light-yellow"
+"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] }
+"string" = "light-green"
+"variable.other.member" = "light-green"
+"constant.character.escape" = "light-cyan"
+"function" = "light-blue"
+"constructor" = "light-blue"
+"special" = "light-blue"
+"keyword" = "light-magenta"
+"label" = "light-magenta"
+"namespace" = "light-magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "light-blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "light-red"
diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml
new file mode 100644
index 00000000..bf8a4a84
--- /dev/null
+++ b/runtime/themes/monokai_pro.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff6188"
+"orange" = "#fc9867"
+"yellow" = "#ffd866"
+"green" = "#a9dc76"
+"blue" = "#78dce8"
+"purple" = "#ab9df2"
+# base colors, sorted from darkest to lightest
+"base0" = "#19181a"
+"base1" = "#221f22"
+"base2" = "#2d2a2e"
+"base3" = "#403e41"
+"base4" = "#5b595c"
+"base5" = "#727072"
+"base6" = "#939293"
+"base7" = "#c1c0c0"
+"base8" = "#fcfcfa"
+# variants (for when transparency isn't supported)
+"base8x0c" = "#363337" # using base2 as bg
diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml
new file mode 100644
index 00000000..d8a701f1
--- /dev/null
+++ b/runtime/themes/monokai_pro_machine.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff6d7e"
+"orange" = "#ffb270"
+"yellow" = "#ffed72"
+"green" = "#a2e57b"
+"blue" = "#7cd5f1"
+"purple" = "#baa0f8"
+# base colors
+"base0" = "#161b1e"
+"base1" = "#1d2528"
+"base2" = "#273136"
+"base3" = "#3a4449"
+"base4" = "#545f62"
+"base5" = "#6b7678"
+"base6" = "#798384"
+"base7" = "#b8c4c3"
+"base8" = "#f2fffc"
+# variants
+"base8x0c" = "#303a3e"
diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml
new file mode 100644
index 00000000..74459472
--- /dev/null
+++ b/runtime/themes/monokai_pro_octagon.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#ff657a"
+"orange" = "#ff9b5e"
+"yellow" = "#ffd76d"
+"green" = "#bad761"
+"blue" = "#9cd1bb"
+"purple" = "#c39ac9"
+# base colors
+"base0" = "#161821"
+"base1" = "#1e1f2b"
+"base2" = "#282a3a"
+"base3" = "#3a3d4b"
+"base4" = "#535763"
+"base5" = "#696d77"
+"base6" = "#767b81"
+"base7" = "#b2b9bd"
+"base8" = "#eaf2f1"
+# variants
+"base8x0c" = "#303342"
diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml
new file mode 100644
index 00000000..a9cf4b34
--- /dev/null
+++ b/runtime/themes/monokai_pro_ristretto.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#fd6883"
+"orange" = "#f38d70"
+"yellow" = "#f9cc6c"
+"green" = "#adda78"
+"blue" = "#85dacc"
+"purple" = "#a8a9eb"
+# base colors
+"base0" = "#191515"
+"base1" = "#211c1c"
+"base2" = "#2c2525"
+"base3" = "#403838"
+"base4" = "#5b5353"
+"base5" = "#72696a"
+"base6" = "#8c8384"
+"base7" = "#c3b7b8"
+"base8" = "#fff1f3"
+# variants
+"base8x0c" = "#352e2e"
diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml
new file mode 100644
index 00000000..232adfbd
--- /dev/null
+++ b/runtime/themes/monokai_pro_spectrum.toml
@@ -0,0 +1,102 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+[palette]
+# primary colors
+"red" = "#fc618d"
+"orange" = "#fd9353"
+"yellow" = "#fce566"
+"green" = "#7bd88f"
+"blue" = "#5ad4e6"
+"purple" = "#948ae3"
+# base colors
+"base0" = "#131313"
+"base1" = "#191919"
+"base2" = "#222222"
+"base3" = "#363537"
+"base4" = "#525053"
+"base5" = "#69676c"
+"base6" = "#8b888f"
+"base7" = "#bab6c0"
+"base8" = "#f7f1ff"
+# variants
+"base8x0c" = "#2b2b2b"
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
index 53777008..256b33c8 100644
--- a/runtime/themes/rose_pine.toml
+++ b/runtime/themes/rose_pine.toml
@@ -10,6 +10,7 @@
"ui.selection" = "highlight"
"comment" = "subtle"
"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
"ui.help" = { fg = "foam", bg = "surface" }
"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
"ui.text" = { fg = "text" }
diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml
new file mode 100644
index 00000000..43ba24ed
--- /dev/null
+++ b/runtime/themes/rose_pine_dawn.toml
@@ -0,0 +1,63 @@
+# Author: ChrisHa<chunghha@users.noreply.github.com>
+# Author: RayGervais<raygervais@hotmail.ca>
+
+"ui.background" = { bg = "base" }
+"ui.menu" = "surface"
+"ui.menu.selected" = { fg = "iris", bg = "surface" }
+"ui.linenr" = {fg = "subtle" }
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "overlay" }
+"ui.liner.selected" = "highlightOverlay"
+"ui.selection" = "highlight"
+"comment" = "subtle"
+"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
+"ui.help" = { fg = "foam", bg = "surface" }
+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
+"ui.text" = { fg = "text" }
+"operator" = "rose"
+"ui.text.focus" = { fg = "base05" }
+"variable" = "text"
+"number" = "iris"
+"constant" = "gold"
+"attributes" = "gold"
+"type" = "foam"
+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
+"string" = "gold"
+"property" = "foam"
+"escape" = "subtle"
+"function" = "rose"
+"function.builtin" = "rose"
+"function.method" = "foam"
+"constructor" = "gold"
+"special" = "gold"
+"keyword" = "pine"
+"label" = "iris"
+"namespace" = "pine"
+"ui.popup" = { bg = "overlay" }
+"ui.window" = { bg = "base" }
+"ui.help" = { bg = "overlay", fg = "foam" }
+"text" = "text"
+
+"info" = "gold"
+"hint" = "gold"
+"debug" = "rose"
+"diagnostic" = "rose"
+"error" = "love"
+
+[palette]
+base = "#faf4ed"
+surface = "#fffaf3"
+overlay = "#f2e9de"
+inactive = "#9893a5"
+subtle = "#6e6a86"
+text = "#575279"
+love = "#b4637a"
+gold = "#ea9d34"
+rose = "#d7827e"
+pine = "#286983"
+foam = "#56949f"
+iris = "#907aa9"
+highlight = "#eee9e6"
+highlightInactive = "#f2ede9"
+highlightOverlay = "#e4dfde"
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index 984c86ee..979fdaac 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -58,13 +58,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,18 +73,20 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
[palette]
# 深色 越来越深
-base03 = "#002b36"
-base02 = "#073642"
-base01 = "#586e75"
-base00 = "#657b83"
-base0 = "#839496"
-base1 = "#93a1a1"
-base2 = "#eee8d5"
-base3 = "#fdf6e3"
+base03 = "#002b36"
+base02 = "#073642"
+base0175 = "#16404b"
+base015 = "#2c4f59"
+base01 = "#586e75"
+base00 = "#657b83"
+base0 = "#839496"
+base1 = "#93a1a1"
+base2 = "#eee8d5"
+base3 = "#fdf6e3"
# 浅色 越來越浅
yellow = "#b58900"
diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml
index 0ab1b962..ded90cc4 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -58,13 +58,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,26 +73,28 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
[palette]
-red = '#dc322f'
-green = '#859900'
-yellow = '#b58900'
-blue = '#268bd2'
-magenta = '#d33682'
-cyan = '#2aa198'
-orange = '#cb4b16'
-violet = '#6c71c4'
+red = '#dc322f'
+green = '#859900'
+yellow = '#b58900'
+blue = '#268bd2'
+magenta = '#d33682'
+cyan = '#2aa198'
+orange = '#cb4b16'
+violet = '#6c71c4'
# 深色 越来越深
-base0 = '#657b83'
-base1 = '#586e75'
-base2 = '#073642'
-base3 = '#002b36'
+base0 = '#657b83'
+base1 = '#586e75'
+base2 = '#073642'
+base3 = '#002b36'
## 浅色 越來越浅
-base00 = '#839496'
-base01 = '#93a1a1'
-base02 = '#eee8d5'
-base03 = '#fdf6e3'
+base00 = '#839496'
+base01 = '#93a1a1'
+base015 = '#c5c8bd'
+base0175 = '#dddbcc'
+base02 = '#eee8d5'
+base03 = '#fdf6e3'
diff --git a/theme.toml b/theme.toml
index 8c0d1f6c..0a79861e 100644
--- a/theme.toml
+++ b/theme.toml
@@ -28,6 +28,12 @@ string = "silver"
# used for lifetimes
label = "honey"
+"markup.heading" = "lilac"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.underline.link" = { fg = "silver", modifiers = ["underlined"] }
+"markup.raw" = "almond"
+
# TODO: diferentiate doc comment
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
new file mode 100644
index 00000000..fe5d55d4
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "xtask"
+version = "0.5.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+helix-term = { version = "0.5", path = "../helix-term" }
+helix-core = { version = "0.5", path = "../helix-core" }
+toml = "0.5"
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 00000000..7256653a
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,277 @@
+use std::{env, error::Error};
+
+type DynError = Box<dyn Error>;
+
+pub mod helpers {
+ use std::{
+ fmt::Display,
+ path::{Path, PathBuf},
+ };
+
+ use crate::path;
+ use helix_core::syntax::Configuration as LangConfig;
+
+ #[derive(Copy, Clone)]
+ pub enum TsFeature {
+ Highlight,
+ TextObjects,
+ AutoIndent,
+ }
+
+ impl TsFeature {
+ pub fn all() -> &'static [Self] {
+ &[Self::Highlight, Self::TextObjects, Self::AutoIndent]
+ }
+
+ pub fn runtime_filename(&self) -> &'static str {
+ match *self {
+ Self::Highlight => "highlights.scm",
+ Self::TextObjects => "textobjects.scm",
+ Self::AutoIndent => "indents.toml",
+ }
+ }
+ }
+
+ impl Display for TsFeature {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match *self {
+ Self::Highlight => "Syntax Highlighting",
+ Self::TextObjects => "Treesitter Textobjects",
+ Self::AutoIndent => "Auto Indent",
+ }
+ )
+ }
+ }
+
+ /// Get the list of languages that support a particular tree-sitter
+ /// based feature.
+ pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
+ let queries_dir = path::ts_queries();
+
+ find_files(&queries_dir, feat.runtime_filename())
+ .iter()
+ .map(|f| {
+ // .../helix/runtime/queries/python/highlights.scm
+ let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
+ let lang = tail.components().next().unwrap(); // python
+ lang.as_os_str().to_string_lossy().to_string()
+ })
+ .collect()
+ }
+
+ /// Get the list of languages that have any form of tree-sitter
+ /// queries defined in the runtime directory.
+ pub fn langs_with_ts_queries() -> Vec<String> {
+ std::fs::read_dir(path::ts_queries())
+ .unwrap()
+ .filter_map(|entry| {
+ let entry = entry.ok()?;
+ entry
+ .file_type()
+ .ok()?
+ .is_dir()
+ .then(|| entry.file_name().to_string_lossy().to_string())
+ })
+ .collect()
+ }
+
+ // naive implementation, but suffices for our needs
+ pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
+ std::fs::read_dir(dir)
+ .unwrap()
+ .filter_map(|entry| {
+ let path = entry.ok()?.path();
+ if path.is_dir() {
+ Some(find_files(&path, filename))
+ } else {
+ (path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
+ }
+ })
+ .flatten()
+ .collect()
+ }
+
+ pub fn lang_config() -> LangConfig {
+ let bytes = std::fs::read(path::lang_config()).unwrap();
+ toml::from_slice(&bytes).unwrap()
+ }
+}
+
+pub mod md_gen {
+ use crate::DynError;
+
+ use crate::helpers;
+ use crate::path;
+ use std::fs;
+
+ use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
+
+ pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
+ pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
+
+ fn md_table_heading(cols: &[String]) -> String {
+ let mut header = String::new();
+ header += &md_table_row(cols);
+ header += &md_table_row(&vec!["---".to_string(); cols.len()]);
+ header
+ }
+
+ fn md_table_row(cols: &[String]) -> String {
+ "| ".to_owned() + &cols.join(" | ") + " |\n"
+ }
+
+ fn md_mono(s: &str) -> String {
+ format!("`{}`", s)
+ }
+
+ pub fn typable_commands() -> Result<String, DynError> {
+ let mut md = String::new();
+ md.push_str(&md_table_heading(&[
+ "Name".to_owned(),
+ "Description".to_owned(),
+ ]));
+
+ let cmdify = |s: &str| format!("`:{}`", s);
+
+ for cmd in TYPABLE_COMMAND_LIST {
+ let names = std::iter::once(&cmd.name)
+ .chain(cmd.aliases.iter())
+ .map(|a| cmdify(a))
+ .collect::<Vec<_>>()
+ .join(", ");
+
+ md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()]));
+ }
+
+ Ok(md)
+ }
+
+ pub fn lang_features() -> Result<String, DynError> {
+ let mut md = String::new();
+ let ts_features = helpers::TsFeature::all();
+
+ let mut cols = vec!["Language".to_owned()];
+ cols.append(
+ &mut ts_features
+ .iter()
+ .map(|t| t.to_string())
+ .collect::<Vec<_>>(),
+ );
+ cols.push("Default LSP".to_owned());
+
+ md.push_str(&md_table_heading(&cols));
+ let config = helpers::lang_config();
+
+ let mut langs = config
+ .language
+ .iter()
+ .map(|l| l.language_id.clone())
+ .collect::<Vec<_>>();
+ langs.sort_unstable();
+
+ let mut ts_features_to_langs = Vec::new();
+ for &feat in ts_features {
+ ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
+ }
+
+ let mut row = Vec::new();
+ for lang in langs {
+ let lc = config
+ .language
+ .iter()
+ .find(|l| l.language_id == lang)
+ .unwrap(); // lang comes from config
+ row.push(lc.language_id.clone());
+
+ for (_feat, support_list) in &ts_features_to_langs {
+ row.push(
+ if support_list.contains(&lang) {
+ "✓"
+ } else {
+ ""
+ }
+ .to_owned(),
+ );
+ }
+ row.push(
+ lc.language_server
+ .as_ref()
+ .map(|s| s.command.clone())
+ .map(|c| md_mono(&c))
+ .unwrap_or_default(),
+ );
+
+ md.push_str(&md_table_row(&row));
+ row.clear();
+ }
+
+ Ok(md)
+ }
+
+ pub fn write(filename: &str, data: &str) {
+ let error = format!("Could not write to {}", filename);
+ let path = path::book_gen().join(filename);
+ fs::write(path, data).expect(&error);
+ }
+}
+
+pub mod path {
+ use std::path::{Path, PathBuf};
+
+ pub fn project_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .unwrap()
+ .to_path_buf()
+ }
+
+ pub fn book_gen() -> PathBuf {
+ project_root().join("book/src/generated/")
+ }
+
+ pub fn ts_queries() -> PathBuf {
+ project_root().join("runtime/queries")
+ }
+
+ pub fn lang_config() -> PathBuf {
+ project_root().join("languages.toml")
+ }
+}
+
+pub mod tasks {
+ use crate::md_gen;
+ use crate::DynError;
+
+ pub fn docgen() -> Result<(), DynError> {
+ use md_gen::*;
+ write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
+ write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
+ Ok(())
+ }
+
+ pub fn print_help() {
+ println!(
+ "
+Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
+
+ Tasks:
+ docgen: Generate files to be included in the mdbook output.
+"
+ );
+ }
+}
+
+fn main() -> Result<(), DynError> {
+ let task = env::args().nth(1);
+ match task {
+ None => tasks::print_help(),
+ Some(t) => match t.as_str() {
+ "docgen" => tasks::docgen()?,
+ invalid => return Err(format!("Invalid task name: {}", invalid).into()),
+ },
+ };
+ Ok(())
+}