Merge branch 'master' into cursor-shape-new
This commit is contained in:
commit
d4fb1d0633
94 changed files with 3870 additions and 925 deletions
2
.cargo/config
Normal file
2
.cargo/config
Normal file
|
@ -0,0 +1,2 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --"
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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>
|
||||
|
||||
|
|
76
.github/workflows/build.yml
vendored
76
.github/workflows/build.yml
vendored
|
@ -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') }}
|
||||
|
@ -108,6 +108,52 @@ jobs:
|
|||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo registry
|
||||
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.7
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
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:
|
||||
|
@ -126,14 +172,16 @@ jobs:
|
|||
path: target
|
||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run cargo fmt
|
||||
- name: Generate docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
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)
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -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
|
||||
|
|
55
Cargo.lock
generated
55
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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.
|
||||
|
|
20
README.md
20
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
|
||||
|
||||
|
|
38
base16_theme.toml
Normal file
38
base16_theme.toml
Normal file
|
@ -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"
|
|
@ -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)
|
||||
|
|
5
book/src/commands.md
Normal file
5
book/src/commands.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Commands
|
||||
|
||||
Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
|
||||
|
||||
{{#include ./generated/typable-cmd.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
|
||||
|
||||
|
|
42
book/src/generated/lang-support.md
Normal file
42
book/src/generated/lang-support.md
Normal file
|
@ -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` |
|
43
book/src/generated/typable-cmd.md
Normal file
43
book/src/generated/typable-cmd.md
Normal file
|
@ -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. |
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>|</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
|
||||
| <code>A-|</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>|</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
|
||||
| <code>Alt-|</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 |
|
||||
|
|
10
book/src/lang-support.md
Normal file
10
book/src/lang-support.md
Normal file
|
@ -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
|
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
37
docs/CONTRIBUTING.md
Normal file
37
docs/CONTRIBUTING.md
Normal file
|
@ -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
|
54
flake.lock
generated
54
flake.lock
generated
|
@ -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": {
|
||||
|
|
|
@ -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"; }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
// TODO: selections should be extended if range, moved if point.
|
||||
|
||||
// TODO: if not cursor but selection, wrap on both sides of selection (surround)
|
||||
doc.get_char(pos - 1)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// TODO: else return (use default handler that inserts close)
|
||||
(pos, pos, Some(Tendril::from_char(close)))
|
||||
(start_head, start_head, Some(Tendril::from_char(close)))
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
/// 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());
|
||||
|
||||
// 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
|
||||
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(end_ranges, selection.primary_index()))
|
||||
}
|
||||
|
||||
#[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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
490
helix-core/src/increment/date_time.rs
Normal file
490
helix-core/src/increment/date_time.rs
Normal file
|
@ -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(®ex).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)
|
||||
}
|
||||
}
|
||||
}
|
8
helix-core/src/increment/mod.rs
Normal file
8
helix-core/src/increment/mod.rs
Normal file
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
164
helix-core/src/shellwords.rs
Normal file
164
helix-core/src/shellwords.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
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))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos_skip() {
|
||||
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)));
|
||||
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]
|
||||
|
|
|
@ -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]));
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
|
|
1
helix-syntax/languages/tree-sitter-markdown
Submodule
1
helix-syntax/languages/tree-sitter-markdown
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2
|
1
helix-syntax/languages/tree-sitter-wgsl
Submodule
1
helix-syntax/languages/tree-sitter-wgsl
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f00ff52251edbd58f4d39c9c3204383253032c11
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 gutter_style = theme.get("ui.gutter");
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
let selected = cursors.contains(&line);
|
||||
|
||||
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
|
||||
},
|
||||
);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 => {
|
||||
Action::HorizontalSplit | Action::VerticalSplit => {
|
||||
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 => {
|
||||
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()
|
||||
});
|
||||
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
|
||||
|
||||
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 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()
|
||||
|
|
|
@ -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.
|
||||
|
|
96
helix-view/src/gutter.rs
Normal file
96
helix-view/src/gutter.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 = " " }
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
((account) @variable.other.member)
|
||||
((commodity) @text.literal)
|
||||
|
||||
"include" @include
|
||||
"include" @keyword.local.import
|
||||
|
||||
[
|
||||
"account"
|
||||
|
|
14
runtime/queries/llvm/highlights.scm
Normal file
14
runtime/queries/llvm/highlights.scm
Normal file
|
@ -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
|
35
runtime/queries/markdown/highlights.scm
Normal file
35
runtime/queries/markdown/highlights.scm
Normal file
|
@ -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
|
||||
|
8
runtime/queries/markdown/injections.scm
Normal file
8
runtime/queries/markdown/injections.scm
Normal file
|
@ -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"))
|
|
@ -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
|
||||
|
||||
|
|
17
runtime/queries/perl/indents.toml
Normal file
17
runtime/queries/perl/indents.toml
Normal file
|
@ -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 = [
|
||||
"}"
|
||||
]
|
|
@ -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
|
||||
|
|
102
runtime/queries/wgsl/highlights.scm
Normal file
102
runtime/queries/wgsl/highlights.scm
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
60
runtime/themes/base16_default_light.toml
Normal file
60
runtime/themes/base16_default_light.toml
Normal file
|
@ -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 ?>
|
39
runtime/themes/base16_terminal.toml
Normal file
39
runtime/themes/base16_terminal.toml
Normal file
|
@ -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"
|
102
runtime/themes/monokai_pro.toml
Normal file
102
runtime/themes/monokai_pro.toml
Normal file
|
@ -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
|
102
runtime/themes/monokai_pro_machine.toml
Normal file
102
runtime/themes/monokai_pro_machine.toml
Normal file
|
@ -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"
|
102
runtime/themes/monokai_pro_octagon.toml
Normal file
102
runtime/themes/monokai_pro_octagon.toml
Normal file
|
@ -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"
|
102
runtime/themes/monokai_pro_ristretto.toml
Normal file
102
runtime/themes/monokai_pro_ristretto.toml
Normal file
|
@ -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"
|
102
runtime/themes/monokai_pro_spectrum.toml
Normal file
102
runtime/themes/monokai_pro_spectrum.toml
Normal file
|
@ -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"
|
|
@ -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" }
|
||||
|
|
63
runtime/themes/rose_pine_dawn.toml
Normal file
63
runtime/themes/rose_pine_dawn.toml
Normal file
|
@ -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"
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
11
xtask/Cargo.toml
Normal file
11
xtask/Cargo.toml
Normal file
|
@ -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"
|
277
xtask/src/main.rs
Normal file
277
xtask/src/main.rs
Normal file
|
@ -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(())
|
||||
}
|
Loading…
Add table
Reference in a new issue