Merge branch 'master' into 'help-command'

pull/997/head
Omnikar 3 years ago
parent 196dda7319
commit 46d9f49cf9
No known key found for this signature in database
GPG Key ID: 7DE6694CDA7885ED

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

@ -137,3 +137,51 @@ jobs:
with: with:
command: clippy command: clippy
args: -- -D warnings args: -- -D warnings
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Cache cargo registry
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Generate docs
uses: actions-rs/cargo@v1
with:
command: xtask
args: docgen
- name: Check uncommitted documentation changes
run: |
git diff
git diff-files --quiet \
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1)

@ -102,7 +102,7 @@ jobs:
fi fi
cp -r runtime dist cp -r runtime dist
- uses: actions/upload-artifact@v2.2.4 - uses: actions/upload-artifact@v2.3.1
with: with:
name: bins-${{ matrix.build }} name: bins-${{ matrix.build }}
path: dist path: dist

22
.gitmodules vendored

@ -142,11 +142,31 @@
path = helix-syntax/languages/tree-sitter-perl path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true shallow = true
[submodule "helix-syntax/languages/tree-sitter-comment"]
path = helix-syntax/languages/tree-sitter-comment
url = https://github.com/stsewd/tree-sitter-comment
shallow = true
[submodule "helix-syntax/languages/tree-sitter-wgsl"] [submodule "helix-syntax/languages/tree-sitter-wgsl"]
path = helix-syntax/languages/tree-sitter-wgsl path = helix-syntax/languages/tree-sitter-wgsl
url = https://github.com/szebniok/tree-sitter-wgsl url = https://github.com/szebniok/tree-sitter-wgsl
shallow = true shallow = true
[submodule "helix-syntax/tree-sitter-llvm"] [submodule "helix-syntax/languages/tree-sitter-llvm"]
path = helix-syntax/languages/tree-sitter-llvm path = helix-syntax/languages/tree-sitter-llvm
url = https://github.com/benwilliamgraham/tree-sitter-llvm url = https://github.com/benwilliamgraham/tree-sitter-llvm
shallow = true 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
[submodule "helix-syntax/languages/tree-sitter-dart"]
path = helix-syntax/languages/tree-sitter-dart
url = https://github.com/UserNobody14/tree-sitter-dart.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-dockerfile"]
path = helix-syntax/languages/tree-sitter-dockerfile
url = https://github.com/camdencheek/tree-sitter-dockerfile.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-fish"]
path = helix-syntax/languages/tree-sitter-fish
url = https://github.com/ram02z/tree-sitter-fish
shallow = true

66
Cargo.lock generated

@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.29" version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -258,15 +258,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -275,15 +275,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -535,9 +535,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]] [[package]]
name = "jsonrpc-core" name = "jsonrpc-core"
@ -690,9 +690,9 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@ -700,9 +700,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.130" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.130" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -897,9 +897,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.72" version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -919,9 +919,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.10" version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
dependencies = [ dependencies = [
"libc", "libc",
"signal-hook-registry", "signal-hook-registry",
@ -1069,11 +1069,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
dependencies = [ dependencies = [
"autocfg",
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
@ -1089,9 +1088,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1259,3 +1258,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xtask"
version = "0.5.0"
dependencies = [
"helix-core",
"helix-term",
"toml",
]

@ -6,6 +6,7 @@ members = [
"helix-tui", "helix-tui",
"helix-syntax", "helix-syntax",
"helix-lsp", "helix-lsp",
"xtask",
] ]
# Build helix-syntax in release mode to make the code path faster in development. # Build helix-syntax in release mode to make the code path faster in development.

@ -44,8 +44,8 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`. 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 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 config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
via the `HELIX_RUNTIME` environment variable. 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 Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir. that sets the variable to the install dir.
@ -65,21 +65,7 @@ brew install helix
# Contributing # Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.** Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
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.
# Getting help # Getting help

@ -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) - [Installation](./install.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Keymap](./keymap.md)
- [Commands](./commands.md)
- [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md) - [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md) - [Configuration](./configuration.md)
- [Themes](./themes.md) - [Themes](./themes.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)
- [Languages](./languages.md) - [Languages](./languages.md)

@ -0,0 +1,5 @@
# Commands
Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
{{#include ./generated/typable-cmd.md}}

@ -23,6 +23,7 @@ To override global configuration parameters, create a `config.toml` file located
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `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` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` | | `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.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default. `[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.

@ -0,0 +1,47 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| bash | ✓ | | | `bash-language-server` |
| c | ✓ | | | `clangd` |
| c-sharp | ✓ | | | |
| cmake | ✓ | | | `cmake-language-server` |
| comment | ✓ | | | |
| cpp | ✓ | | | `clangd` |
| css | ✓ | | | |
| dart | ✓ | | ✓ | `dart` |
| dockerfile | ✓ | | | `docker-langserver` |
| elixir | ✓ | | | `elixir-ls` |
| fish | ✓ | ✓ | ✓ | |
| 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` |
| scala | ✓ | | ✓ | `metals` |
| svelte | ✓ | | ✓ | `svelteserver` |
| toml | ✓ | | | |
| tsq | ✓ | | | |
| tsx | ✓ | | | `typescript-language-server` |
| typescript | ✓ | | ✓ | `typescript-language-server` |
| vue | ✓ | | | |
| wgsl | ✓ | | | |
| yaml | ✓ | | ✓ | |
| zig | ✓ | | ✓ | `zls` |

@ -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. |

@ -42,7 +42,16 @@ These are the available keys and descriptions for the file.
## Queries ## Queries
For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries. For a language to have syntax-highlighting and indentation among
other things, you have to add queries. Add a directory for your
language with the path `runtime/queries/<name>/`. The tree-sitter
[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
gives more info on how to write queries.
> NOTE: When evaluating queries, the first matching query takes
precedence, which is different from other editors like neovim where
the last matching query supercedes the ones before it. See
[this issue][neovim-query-precedence] for an example.
## Common Issues ## Common Issues
@ -58,3 +67,4 @@ For a language to have syntax-highlighting and indentation among other things, y
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection [treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml [languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090

@ -27,6 +27,15 @@ Releases are available in the `community` repository.
A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which 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 ## Build from source
``` ```

@ -34,6 +34,7 @@
| `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `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` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A | | `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A |
@ -69,13 +70,15 @@
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | | `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` | | `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` | | `<` | Unindent selection | `unindent` |
| `=` | Format selection (**LSP**) | `format_selections` | | `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` | | `d` | Delete selection | `delete_selection` |
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | | `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` |
| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | | `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
| `Ctrl-a` | Increment object (number) under cursor | `increment` | | `Ctrl-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | | `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 #### Shell
@ -158,7 +161,7 @@ Jumps to various locations.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` | | `g` | Go to line number `<n>` else start of file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` | | `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selection | `goto_file` | | `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` | | `h` | Go to the start of the line | `goto_line_start` |

@ -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 ```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select' # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal] [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 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 w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line "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 "A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert 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 Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows: `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"` | | Down | `"down"` |
| Home | `"home"` | | Home | `"home"` |
| End | `"end"` | | End | `"end"` |
| Page | `"pageup"` | | Page Up | `"pageup"` |
| Page | `"pagedown"` | | Page Down | `"pagedown"` |
| Tab | `"tab"` | | Tab | `"tab"` |
| Back | `"backtab"` |
| Delete | `"del"` | | Delete | `"del"` |
| Insert | `"ins"` | | Insert | `"ins"` |
| Null | `"null"` | | 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. 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 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`.

@ -105,6 +105,7 @@ We use a similar set of scopes as
- `type` - Types - `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`) - `builtin` - Primitive types provided by the language (`int`, `usize`)
- `constructor`
- `constant` (TODO: constant.other.placeholder for %v) - `constant` (TODO: constant.other.placeholder for %v)
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
@ -162,6 +163,21 @@ We use a similar set of scopes as
- `namespace` - `namespace`
- `markup`
- `heading`
- `list`
- `unnumbered`
- `numbered`
- `bold`
- `italic`
- `link`
- `url`
- `label`
- `quote`
- `raw`
- `inline`
- `block`
#### Interface #### Interface
These scopes are used for theming the editor interface. These scopes are used for theming the editor interface.

@ -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

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"devshell": { "devshell": {
"locked": { "locked": {
"lastModified": 1637575296, "lastModified": 1639692811,
"narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=", "narHash": "sha256-wOOBH0fVsfNqw/5ZWRoKspyesoXBgiwEOUBH4c7JKEo=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618", "rev": "d3a1f5bec3632b33346865b1c165bf2420bb2f52",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -41,11 +41,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1638425401, "lastModified": 1639807801,
"narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=", "narHash": "sha256-y32tMq1LTRVbMW3QN5i98iOQjQt2QSsif3ayUkD1o3g=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37", "rev": "b5bbaa4f5239e6f0619846f9a5380f07baa853d3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -56,11 +56,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1638376152, "lastModified": 1639699734,
"narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=", "narHash": "sha256-tlX6WebGmiHb2Hmniff+ltYp+7dRfdsBxw9YczLsP60=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f", "rev": "03ec468b14067729a285c2c7cfa7b9434a04816c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -99,11 +99,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1638497756, "lastModified": 1639880499,
"narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=", "narHash": "sha256-/BibDmFwgWuuTUkNVO6YlvuTSWM9dpBvlZoTAPs7ORI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a", "rev": "c6c83589ae048af20d93d01eb07a4176012093d0",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -23,7 +23,7 @@ unicode-width = "0.1"
unicode-general-category = "0.4" unicode-general-category = "0.4"
# slab = "0.4.2" # slab = "0.4.2"
tree-sitter = "0.20" tree-sitter = "0.20"
once_cell = "1.8" once_cell = "1.9"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"

@ -1,7 +1,8 @@
//! When typing the opening character of one of the possible pairs defined below, //! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character. //! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction}; use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use log::debug;
use smallvec::SmallVec; use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/ // 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: // insert hook:
// Fn(doc, selection, char) => Option<Transaction> // Fn(doc, selection, char) => Option<Transaction>
@ -25,14 +28,19 @@ 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 // 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 (|) -> |
// * 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] #[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
for &(open, close) in PAIRS { for &(open, close) in PAIRS {
if open == ch { if open == ch {
if open == close { if open == close {
return handle_same(doc, selection, open); return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else { } else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
} }
@ -47,18 +55,44 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction>
None None
} }
// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close ' fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
// for example "&'a mut", or "fn<'a>" if pos == 0 {
fn next_char(doc: &Rope, pos: usize) -> Option<char> {
if pos >= doc.len_chars() {
return None; return None;
} }
Some(doc.char(pos))
doc.get_char(pos - 1)
}
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(
start_range: &Range,
offset: usize,
typed_char: char,
len_inserted: usize,
) -> Range {
let end_head = start_range.head + offset + typed_char.len_utf8();
let end_anchor = match (start_range.len(), start_range.direction()) {
// if we have a zero width cursor, it shifts to the same number
(0, _) => end_head,
// if we are inserting for a regular one-width cursor, the anchor
// moves with the head
(1, Direction::Forward) => end_head - 1,
(1, Direction::Backward) => end_head + 1,
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
(_, Direction::Forward) => start_range.anchor + offset,
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
(_, Direction::Backward) => start_range.anchor + offset + len_inserted,
};
Range::new(end_anchor, end_head)
} }
// TODO: selections should be extended if range, moved if point.
// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open( fn handle_open(
doc: &Rope, doc: &Rope,
selection: &Selection, selection: &Selection,
@ -66,98 +100,510 @@ fn handle_open(
close: char, close: char,
close_before: &str, close_before: &str,
) -> Transaction { ) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len()); let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let pos = range.head; let cursor = start_range.cursor(doc.slice(..));
let next = next_char(doc, pos); let next_char = doc.get_char(cursor);
let len_inserted;
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,
));
match next { let change = match next_char {
Some(ch) if !close_before.contains(ch) => { Some(ch) if !close_before.contains(ch) => {
offs += 1; len_inserted = open.len_utf8();
// TODO: else return (use default handler that inserts open) (cursor, cursor, Some(Tendril::from_char(open)))
(pos, pos, Some(Tendril::from_char(open)))
} }
// None | Some(ch) if close_before.contains(ch) => {} // None | Some(ch) if close_before.contains(ch) => {}
_ => { _ => {
// insert open & close // insert open & close
let mut pair = Tendril::with_capacity(2); let pair = Tendril::from_iter([open, close]);
pair.push_char(open); len_inserted = open.len_utf8() + close.len_utf8();
pair.push_char(close); (cursor, cursor, Some(pair))
}
};
offs += 2; let next_range = get_next_range(start_range, offs, open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
(pos, pos, Some(pair)) change
}
}
}); });
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 { 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 mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let pos = range.head; let cursor = start_range.cursor(doc.slice(..));
let next = next_char(doc, pos); let next_char = doc.get_char(cursor);
let mut len_inserted = 0;
let head = pos + offs + close.len_utf8(); let change = if next_char == Some(close) {
// if selection, retain anchor, if cursor, move over // return transaction that moves past close
ranges.push(Range::new( (cursor, cursor, None) // no-op
if range.is_empty() { } else {
head len_inserted += close.len_utf8();
} else { (cursor, cursor, Some(Tendril::from_char(close)))
range.anchor + offs };
},
head,
));
if next == Some(close) { let next_range = get_next_range(start_range, offs, close, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
change
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
t
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(
doc: &Rope,
selection: &Selection,
token: char,
close_before: &str,
open_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let mut len_inserted = 0;
let next_char = doc.get_char(cursor);
let prev_char = prev_char(doc, cursor);
let change = if next_char == Some(token) {
// return transaction that moves past close // return transaction that moves past close
(pos, pos, None) // no-op (cursor, cursor, None) // no-op
} else { } else {
offs += close.len_utf8(); let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
pair.push_char(token);
// TODO: else return (use default handler that inserts close) // for equal pairs, don't insert both open and close if either
(pos, pos, Some(Tendril::from_char(close))) // side has a non-pair char
} if (next_char.is_none() || close_before.contains(next_char.unwrap()))
&& (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
{
pair.push_char(token);
}
len_inserted += pair.len();
(cursor, cursor, Some(pair))
};
let next_range = get_next_range(start_range, offs, token, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
change
}); });
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
} }
// handle cases where open and close is the same, or in triples ("""docstring""") #[cfg(test)]
fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> { mod test {
// if not cursor but selection, wrap use super::*;
// let next = next char use smallvec::smallvec;
// if next == bracket { fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
// // if start of syntax node, insert token twice (new pair because node is complete) PAIRS.iter().filter(|(open, close)| open != close)
// // elseif colsedBracketAt }
// // is_triple == allow triple && next 3 is equal
// // cursor jump over fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
// } PAIRS.iter().filter(|(open, close)| open == close)
//} else if allow_triple && followed by triple { }
//}
//} else if next != word char && prev != bracket && prev != word char { fn test_hooks(
// // condition checks for cases like I' where you don't want I'' (or I'm) in_doc: &Rope,
// insert pair ("") in_sel: &Selection,
//} ch: char,
None 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(2, 1),
);
}
/// [] -> append ( -> ([])
#[test]
fn test_append_blank() {
test_hooks_with_pairs(
// this is what happens when you have a totally blank document and then append
&Rope::from("\n\n"),
&Selection::single(0, 2),
PAIRS,
|open, close| format!("\n{}{}\n", open, close),
&Selection::single(0, 3),
);
}
/// [] ([])
/// [] -> 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::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0,
),
);
}
/// fo[o] -> append ( -> fo[o(])
#[test]
fn test_append() {
test_hooks_with_pairs(
&Rope::from("foo\n"),
&Selection::single(2, 4),
differing_pairs(),
|open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5),
);
}
/// fo[o] fo[o(])
/// fo[o] -> append ( -> fo[o(])
/// fo[o] fo[o(])
#[test]
fn test_append_multi() {
test_hooks_with_pairs(
&Rope::from("foo\nfoo\nfoo\n"),
&Selection::new(
smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
0,
),
differing_pairs(),
|open, close| {
format!(
"foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
open = open,
close = close
)
},
&Selection::new(
smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
0,
),
);
}
/// ([]) -> 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::single(3, 2),
);
}
}
/// [(]) -> append ) -> [()]
#[test]
fn test_append_close_inside_pair() {
for (open, close) in PAIRS {
let doc = Rope::from(format!("{}{}\n", open, close));
test_hooks(
&doc,
&Selection::single(0, 2),
*close,
&doc,
&Selection::single(0, 3),
);
}
}
/// ([]) ()[]
/// ([]) -> 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),),
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);
}
}
/// [(]) [()]
/// [(]) -> append ) -> [()]
/// [(]) [()]
#[test]
fn test_append_close_inside_pair_multi_cursor() {
let sel = Selection::new(
smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
0,
);
let expected_sel = Selection::new(
smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
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::single(3, 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);
}
}
/// [word(]) -> append ( -> [word((]))
#[test]
fn test_append_open_inside_pair() {
let sel = Selection::single(0, 6);
let expected_sel = Selection::single(0, 7);
for (open, close) in differing_pairs() {
let doc = Rope::from(format!("word{}{}", open, close));
let expected_doc = Rope::from(format!(
"word{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::single(3, 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);
}
}
}
/// [(]) -> append " -> [("]")
#[test]
fn test_append_nested_open_inside_pair() {
let sel = Selection::single(0, 2);
let expected_sel = Selection::single(0, 3);
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::single(2, 1),
)
}
/// [wor]d -> insert ( -> ([wor]d
#[test]
fn test_insert_open_with_selection() {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(3, 0),
PAIRS,
|open, _| format!("{}word", open),
&Selection::single(4, 1),
)
}
/// [wor]d -> append ) -> [wor)]d
#[test]
fn test_append_close_inside_non_pair_with_selection() {
let sel = Selection::single(0, 4);
let expected_sel = Selection::single(0, 5);
for (_, close) in PAIRS {
let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close));
test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
}
}
/// foo[ wor]d -> insert ( -> foo([) wor]d
#[test]
fn test_insert_open_trailing_word_with_selection() {
test_hooks_with_pairs(
&Rope::from("foo word"),
&Selection::single(7, 3),
differing_pairs(),
|open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4),
)
}
/// 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::single(6, 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,
);
}
/// appending with only a cursor should stay a cursor
///
/// [] -> append to end "foo -> "foo[]"
#[test]
fn test_append_single_cursor() {
test_hooks_with_pairs(
&Rope::from("\n"),
&Selection::single(0, 1),
PAIRS,
|open, close| format!("{}{}\n", open, close),
&Selection::single(1, 2),
);
}
} }

@ -17,6 +17,7 @@ mod position;
pub mod register; pub mod register;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
pub mod shellwords;
mod state; mod state;
pub mod surround; pub mod surround;
pub mod syntax; pub mod syntax;

@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[
('\"', '\"'), ('\"', '\"'),
]; ];
// limit matching pairs to only ( ) { } [ ] < > // limit matching pairs to only ( ) { } [ ] < > ' ' " "
// Returns the position of the matching bracket under cursor. // Returns the position of the matching bracket under cursor.
// //

@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::array::{self, IntoIter};
use ropey::Rope; use ropey::Rope;
use super::*; use super::*;
@ -360,7 +358,7 @@ mod test {
((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line ((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
]; ];
for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) { for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move); range = move_horizontally(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into()) assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
} }
@ -374,7 +372,7 @@ mod test {
let mut range = Range::point(position); let mut range = Range::point(position);
let moves_and_expected_coordinates = IntoIter::new([ let moves_and_expected_coordinates = [
((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n... ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n... ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n... ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
@ -384,7 +382,7 @@ mod test {
((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n... ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
]); ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates { for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move); range = move_horizontally(slice, range, direction, amount, Movement::Move);
@ -402,11 +400,11 @@ mod test {
let mut range = Range::point(position); let mut range = Range::point(position);
let original_anchor = range.anchor; let original_anchor = range.anchor;
let moves = IntoIter::new([ let moves = [
(Direction::Forward, 1usize), (Direction::Forward, 1usize),
(Direction::Forward, 5usize), (Direction::Forward, 5usize),
(Direction::Backward, 3usize), (Direction::Backward, 3usize),
]); ];
for (direction, amount) in moves { for (direction, amount) in moves {
range = move_horizontally(slice, range, direction, amount, Movement::Extend); range = move_horizontally(slice, range, direction, amount, Movement::Extend);
@ -420,7 +418,7 @@ mod test {
let slice = text.slice(..); let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into(), true); let position = pos_at_coords(slice, (0, 0).into(), true);
let mut range = Range::point(position); let mut range = Range::point(position);
let moves_and_expected_coordinates = IntoIter::new([ let moves_and_expected_coordinates = [
((Direction::Forward, 1usize), (1, 0)), ((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)), ((Direction::Forward, 2usize), (3, 0)),
((Direction::Forward, 1usize), (4, 0)), ((Direction::Forward, 1usize), (4, 0)),
@ -430,7 +428,7 @@ mod test {
((Direction::Backward, 0usize), (4, 0)), ((Direction::Backward, 0usize), (4, 0)),
((Direction::Forward, 5), (5, 0)), ((Direction::Forward, 5), (5, 0)),
((Direction::Forward, 999usize), (5, 0)), ((Direction::Forward, 999usize), (5, 0)),
]); ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates { for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_vertically(slice, range, direction, amount, Movement::Move); range = move_vertically(slice, range, direction, amount, Movement::Move);
@ -450,7 +448,7 @@ mod test {
H, H,
V, V,
} }
let moves_and_expected_coordinates = IntoIter::new([ let moves_and_expected_coordinates = [
// Places cursor at the end of line // Places cursor at the end of line
((Axis::H, Direction::Forward, 8usize), (0, 8)), ((Axis::H, Direction::Forward, 8usize), (0, 8)),
// First descent preserves column as the target line is wider // First descent preserves column as the target line is wider
@ -463,7 +461,7 @@ mod test {
((Axis::V, Direction::Backward, 999usize), (0, 8)), ((Axis::V, Direction::Backward, 999usize), (0, 8)),
((Axis::V, Direction::Forward, 4usize), (4, 8)), ((Axis::V, Direction::Forward, 4usize), (4, 8)),
((Axis::V, Direction::Forward, 999usize), (5, 0)), ((Axis::V, Direction::Forward, 999usize), (5, 0)),
]); ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis { range = match axis {
@ -489,7 +487,7 @@ mod test {
H, H,
V, V,
} }
let moves_and_expected_coordinates = IntoIter::new([ let moves_and_expected_coordinates = [
// Places cursor at the fourth kana. // Places cursor at the fourth kana.
((Axis::H, Direction::Forward, 4), (0, 4)), ((Axis::H, Direction::Forward, 4), (0, 4)),
// Descent places cursor at the 4th character. // Descent places cursor at the 4th character.
@ -498,7 +496,7 @@ mod test {
((Axis::H, Direction::Backward, 1usize), (1, 3)), ((Axis::H, Direction::Backward, 1usize), (1, 3)),
// Jumping back up 1 line. // Jumping back up 1 line.
((Axis::V, Direction::Backward, 1usize), (0, 3)), ((Axis::V, Direction::Backward, 1usize), (0, 3)),
]); ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis { range = match axis {
@ -530,7 +528,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_next_words() { fn test_behaviour_when_moving_to_start_of_next_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic forward motion stops at the first space", ("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]), vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor", (" Starting from a boundary advances the anchor",
@ -604,7 +602,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 0), Range::new(0, 6)), (1, Range::new(0, 0), Range::new(0, 6)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -616,7 +614,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_next_long_words() { fn test_behaviour_when_moving_to_start_of_next_long_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic forward motion stops at the first space", ("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]), vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor", (" Starting from a boundary advances the anchor",
@ -688,7 +686,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 0), Range::new(0, 8)), (1, Range::new(0, 0), Range::new(0, 8)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -700,7 +698,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_previous_words() { fn test_behaviour_when_moving_to_start_of_previous_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic backward motion from the middle of a word", ("Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))]), vec![(1, Range::new(3, 3), Range::new(4, 0))]),
@ -773,7 +771,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 6), Range::new(6, 0)), (1, Range::new(0, 6), Range::new(6, 0)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -785,7 +783,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_previous_long_words() { fn test_behaviour_when_moving_to_start_of_previous_long_words() {
let tests = array::IntoIter::new([ let tests = [
( (
"Basic backward motion from the middle of a word", "Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))], vec![(1, Range::new(3, 3), Range::new(4, 0))],
@ -870,7 +868,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 8), Range::new(8, 0)), (1, Range::new(0, 8), Range::new(8, 0)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -882,7 +880,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_end_of_next_words() { fn test_behaviour_when_moving_to_end_of_next_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic forward motion from the start of a word to the end of it", ("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]), vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next", ("Basic forward motion from the end of a word to the end of the next",
@ -954,7 +952,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 0), Range::new(0, 5)), (1, Range::new(0, 0), Range::new(0, 5)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -966,7 +964,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_end_of_previous_words() { fn test_behaviour_when_moving_to_end_of_previous_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic backward motion from the middle of a word", ("Basic backward motion from the middle of a word",
vec![(1, Range::new(9, 9), Range::new(10, 5))]), vec![(1, Range::new(9, 9), Range::new(10, 5))]),
("Starting from after boundary retreats the anchor", ("Starting from after boundary retreats the anchor",
@ -1036,7 +1034,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 10), Range::new(10, 4)), (1, Range::new(0, 10), Range::new(10, 4)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {
@ -1048,7 +1046,7 @@ mod test {
#[test] #[test]
fn test_behaviour_when_moving_to_end_of_next_long_words() { fn test_behaviour_when_moving_to_end_of_next_long_words() {
let tests = array::IntoIter::new([ let tests = [
("Basic forward motion from the start of a word to the end of it", ("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]), vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next", ("Basic forward motion from the end of a word to the end of the next",
@ -1118,7 +1116,7 @@ mod test {
vec![ vec![
(1, Range::new(0, 0), Range::new(0, 7)), (1, Range::new(0, 0), Range::new(0, 7)),
]), ]),
]); ];
for (sample, scenario) in tests { for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() { for (count, begin, expected_end) in scenario.into_iter() {

@ -7,6 +7,7 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary, prev_grapheme_boundary,
}, },
movement::Direction,
Assoc, ChangeSet, RopeSlice, Assoc, ChangeSet, RopeSlice,
}; };
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@ -82,6 +83,13 @@ impl Range {
std::cmp::max(self.anchor, self.head) std::cmp::max(self.anchor, self.head)
} }
/// Total length of the range.
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.to() - self.from()
}
/// The (inclusive) range of lines that the range overlaps. /// The (inclusive) range of lines that the range overlaps.
#[inline] #[inline]
#[must_use] #[must_use]
@ -102,6 +110,18 @@ impl Range {
self.anchor == self.head self.anchor == self.head
} }
/// `Direction::Backward` when head < anchor.
/// `Direction::Backward` otherwise.
#[inline]
#[must_use]
pub fn direction(&self) -> Direction {
if self.head < self.anchor {
Direction::Backward
} else {
Direction::Forward
}
}
/// Check two ranges for overlap. /// Check two ranges for overlap.
#[must_use] #[must_use]
pub fn overlaps(&self, other: &Self) -> bool { pub fn overlaps(&self, other: &Self) -> bool {

@ -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);
}
}

@ -50,7 +50,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[serde(rename = "name")]
pub language_id: String, pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)] #[serde(default)]

@ -409,7 +409,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into /// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction. /// a single transaction.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction { pub struct Transaction {
changes: ChangeSet, changes: ChangeSet,
selection: Option<Selection>, selection: Option<Selection>,

@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio = { version = "1.15", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1.8" tokio-stream = "0.1.8"

@ -202,7 +202,7 @@ impl Client {
Ok(result) => Output::Success(Success { Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2), jsonrpc: Some(Version::V2),
id, id,
result, result: serde_json::to_value(result)?,
}), }),
Err(error) => Output::Failure(Failure { Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2), jsonrpc: Some(Version::V2),
@ -800,4 +800,16 @@ impl Client {
let response = self.request::<lsp::request::Rename>(params).await?; let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default()) Ok(response.unwrap_or_default())
} }
pub fn command(&self, command: lsp::Command) -> impl Future<Output = Result<Value>> {
let params = lsp::ExecuteCommandParams {
command: command.command,
arguments: command.arguments.unwrap_or_default(),
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
};
self.call::<lsp::request::ExecuteCommand>(params)
}
} }

@ -203,6 +203,7 @@ pub mod util {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum MethodCall { pub enum MethodCall {
WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams), WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
} }
impl MethodCall { impl MethodCall {
@ -215,6 +216,12 @@ impl MethodCall {
.expect("Failed to parse WorkDoneCreate params"); .expect("Failed to parse WorkDoneCreate params");
Self::WorkDoneProgressCreate(params) Self::WorkDoneProgressCreate(params)
} }
lsp::request::ApplyWorkspaceEdit::METHOD => {
let params: lsp::ApplyWorkspaceEditParams = params
.parse()
.expect("Failed to parse ApplyWorkspaceEdit params");
Self::ApplyWorkspaceEdit(params)
}
_ => { _ => {
log::warn!("unhandled lsp request: {}", method); log::warn!("unhandled lsp request: {}", method);
return None; return None;

@ -0,0 +1,13 @@
helix-syntax
============
Syntax highlighting for helix, (shallow) submodules resides here.
Differences from nvim-treesitter
--------------------------------
As the syntax are commonly ported from
<https://github.com/nvim-treesitter/nvim-treesitter>.
Note that we do not support the custom `#any-of` predicate which is
supported by neovim so one needs to change it to `#match` with regex.

@ -0,0 +1 @@
Subproject commit 5dd3c62f1bbe378b220fe16b317b85247898639e

@ -0,0 +1 @@
Subproject commit 6a25376685d1d47968c2cef06d4db8d84a70025e

@ -0,0 +1 @@
Subproject commit 7af32bc04a66ab196f5b9f92ac471f29372ae2ce

@ -0,0 +1 @@
Subproject commit 04e54ab6585dfd4fee6ddfe5849af56f101b6d4f

@ -0,0 +1 @@
Subproject commit d4f61bed8ecb632addcd5e088c4f4cb9c1bf1c5b

@ -0,0 +1 @@
Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2

@ -1 +1 @@
Subproject commit fb23ed9a99da012d86b7a5059b9d8928607cce29 Subproject commit 0a3dd53a7fc4b352a538397d054380aaa28be54c

@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix" repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com" homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"] include = ["src/**/*", "README.md"]
default-run = "hx"
[package.metadata.nix] [package.metadata.nix]
build = true build = true
@ -26,7 +27,7 @@ helix-view = { version = "0.5", path = "../helix-view" }
helix-lsp = { version = "0.5", path = "../helix-lsp" } helix-lsp = { version = "0.5", path = "../helix-lsp" }
anyhow = "1" anyhow = "1"
once_cell = "1.8" once_cell = "1.9"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1" num_cpus = "1"

@ -1,8 +1,12 @@
use helix_core::{merge_toml_values, syntax}; use helix_core::{merge_toml_values, syntax};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{theme, Editor}; use helix_view::{theme, Editor};
use serde_json::json;
use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; use crate::{
args::Args, commands::apply_workspace_edit, compositor::Compositor, config::Config, job::Jobs,
ui,
};
use log::{error, warn}; use log::{error, warn};
@ -76,17 +80,27 @@ impl Application {
None => Ok(def_lang_conf), None => Ok(def_lang_conf),
}; };
let theme = if let Some(theme) = &config.theme { let true_color = config.editor.true_color || crate::true_color();
match theme_loader.load(theme) { let theme = config
Ok(theme) => theme, .theme
Err(e) => { .as_ref()
log::warn!("failed to load theme `{}` - {}", theme, e); .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() theme_loader.default()
} else {
theme_loader.base16_default()
} }
} });
} else {
theme_loader.default()
};
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into()) .and_then(|conf| conf.try_into())
@ -520,14 +534,6 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall { Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, .. method, params, id, ..
}) => { }) => {
let language_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let call = match MethodCall::parse(&method, params) { let call = match MethodCall::parse(&method, params) {
Some(call) => call, Some(call) => call,
None => { None => {
@ -557,8 +563,42 @@ impl Application {
if spinner.is_stopped() { if spinner.is_stopped() {
spinner.start(); spinner.start();
} }
let language_server =
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
} }
MethodCall::ApplyWorkspaceEdit(params) => {
apply_workspace_edit(
&mut self.editor,
helix_lsp::OffsetEncoding::Utf8,
&params.edit,
);
let language_server =
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
tokio::spawn(language_server.reply(
id,
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: true,
failure_reason: None,
failed_change: None,
})),
));
}
} }
} }
e => unreachable!("{:?}", e), e => unreachable!("{:?}", e),

@ -10,7 +10,7 @@ use helix_core::{
movement::{self, Direction}, movement::{self, Direction},
object, pos_at_coords, object, pos_at_coords,
regex::{self, Regex, RegexBuilder}, regex::{self, Regex, RegexBuilder},
search, selection, surround, textobject, search, selection, shellwords, surround, textobject,
unicode::width::UnicodeWidthChar, unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction, Transaction,
@ -70,7 +70,7 @@ pub struct Context<'a> {
impl<'a> Context<'a> { impl<'a> Context<'a> {
/// Push a new component onto the compositor. /// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) { pub fn push_layer(&mut self, component: Box<dyn Component>) {
self.callback = Some(Box::new(|compositor: &mut Compositor| { self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component) compositor.push(component)
})); }));
} }
@ -173,14 +173,14 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) { pub fn execute(&self, cx: &mut Context) {
match &self { match &self {
MappableCommand::Typable { name, args, doc: _ } => { MappableCommand::Typable { name, args, doc: _ } => {
let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect(); let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) { if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
let mut cx = compositor::Context { let mut cx = compositor::Context {
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
scroll: None, scroll: None,
}; };
if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) { if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
} }
} }
@ -287,11 +287,11 @@ impl MappableCommand {
add_newline_below, "Add newline below", add_newline_below, "Add newline below",
goto_type_definition, "Goto type definition", goto_type_definition, "Goto type definition",
goto_implementation, "Goto implementation", goto_implementation, "Goto implementation",
goto_file_start, "Goto file start/line", goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end", goto_file_end, "Goto file end",
goto_file, "Goto files in the selection", goto_file, "Goto files in selection",
goto_file_hsplit, "Goto files in the selection in horizontal splits", goto_file_hsplit, "Goto files in selection (hsplit)",
goto_file_vsplit, "Goto files in the selection in vertical splits", goto_file_vsplit, "Goto files in selection (vsplit)",
goto_reference, "Goto references", goto_reference, "Goto references",
goto_window_top, "Goto window top", goto_window_top, "Goto window top",
goto_window_center, "Goto window center", goto_window_center, "Goto window center",
@ -362,6 +362,7 @@ impl MappableCommand {
expand_selection, "Expand selection to parent syntax node", expand_selection, "Expand selection to parent syntax node",
jump_forward, "Jump forward on jumplist", jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist", jump_backward, "Jump backward on jumplist",
save_selection, "Save the current selection to the jumplist",
jump_view_right, "Jump to the split to the right", jump_view_right, "Jump to the split to the right",
jump_view_left, "Jump to the split to the left", jump_view_left, "Jump to the split to the left",
jump_view_up, "Jump to the split above", jump_view_up, "Jump to the split above",
@ -394,6 +395,8 @@ impl MappableCommand {
rename_symbol, "Rename symbol", rename_symbol, "Rename symbol",
increment, "Increment", increment, "Increment",
decrement, "Decrement", decrement, "Decrement",
record_macro, "Record macro",
play_macro, "Play macro",
); );
} }
@ -670,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) {
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text); let line = range.cursor_line(text);
let pos = line_end_char_index(&text, line); let line_end_pos = line_end_char_index(&text, line);
range.put_cursor(text, pos, true) let pos = range.cursor(text);
let mut new_range = range.put_cursor(text, line_end_pos, true);
// don't want to remove the line separator itself if the cursor doesn't reach the end of line.
if pos != line_end_pos {
new_range.head = line_end_pos;
}
new_range
}); });
delete_selection_insert_mode(doc, view, &selection); delete_selection_insert_mode(doc, view, &selection);
} }
@ -1928,7 +1938,7 @@ fn append_mode(cx: &mut Context) {
if !last_range.is_empty() && last_range.head == end { if !last_range.is_empty() && last_range.head == end {
let transaction = Transaction::change( let transaction = Transaction::change(
doc.text(), doc.text(),
std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]), [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
); );
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }
@ -1942,7 +1952,7 @@ fn append_mode(cx: &mut Context) {
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
} }
mod cmd { pub mod cmd {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
@ -1955,13 +1965,13 @@ mod cmd {
pub aliases: &'static [&'static str], pub aliases: &'static [&'static str],
pub doc: &'static str, pub doc: &'static str,
// params, flags, helper, completer // params, flags, helper, completer
pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>, pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
pub completer: Option<Completer>, pub completer: Option<Completer>,
} }
fn quit( fn quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// last view and we have unsaved changes // last view and we have unsaved changes
@ -1976,7 +1986,7 @@ mod cmd {
fn force_quit( fn force_quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.editor.close(view!(cx.editor).id); cx.editor.close(view!(cx.editor).id);
@ -1986,17 +1996,19 @@ mod cmd {
fn open( fn open(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let path = args.get(0).context("wrong argument count")?; ensure!(!args.is_empty(), "wrong argument count");
let _ = cx.editor.open(path.into(), Action::Replace)?; for arg in args {
let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?;
}
Ok(()) Ok(())
} }
fn buffer_close( fn buffer_close(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let view = view!(cx.editor); let view = view!(cx.editor);
@ -2007,7 +2019,7 @@ mod cmd {
fn force_buffer_close( fn force_buffer_close(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let view = view!(cx.editor); let view = view!(cx.editor);
@ -2016,15 +2028,12 @@ mod cmd {
Ok(()) Ok(())
} }
fn write_impl<P: AsRef<Path>>( fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
cx: &mut compositor::Context,
path: Option<P>,
) -> anyhow::Result<()> {
let jobs = &mut cx.jobs; let jobs = &mut cx.jobs;
let (_, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
if let Some(ref path) = path { if let Some(ref path) = path {
doc.set_path(Some(path.as_ref())) doc.set_path(Some(path.as_ref().as_ref()))
.context("invalid filepath")?; .context("invalid filepath")?;
} }
if doc.path().is_none() { if doc.path().is_none() {
@ -2053,7 +2062,7 @@ mod cmd {
fn write( fn write(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_impl(cx, args.first()) write_impl(cx, args.first())
@ -2061,7 +2070,7 @@ mod cmd {
fn new_file( fn new_file(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.editor.new_file(Action::Replace); cx.editor.new_file(Action::Replace);
@ -2071,7 +2080,7 @@ mod cmd {
fn format( fn format(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
@ -2086,7 +2095,7 @@ mod cmd {
} }
fn set_indent_style( fn set_indent_style(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use IndentStyle::*; use IndentStyle::*;
@ -2106,7 +2115,7 @@ mod cmd {
// Attempt to parse argument as an indent style. // Attempt to parse argument as an indent style.
let style = match args.get(0) { let style = match args.get(0) {
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs), Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
Some(&"0") => Some(Tabs), Some(Cow::Borrowed("0")) => Some(Tabs),
Some(arg) => arg Some(arg) => arg
.parse::<u8>() .parse::<u8>()
.ok() .ok()
@ -2125,7 +2134,7 @@ mod cmd {
/// Sets or reports the current document's line ending setting. /// Sets or reports the current document's line ending setting.
fn set_line_ending( fn set_line_ending(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use LineEnding::*; use LineEnding::*;
@ -2169,7 +2178,7 @@ mod cmd {
fn earlier( fn earlier(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?; let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@ -2185,7 +2194,7 @@ mod cmd {
fn later( fn later(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?; let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@ -2200,7 +2209,7 @@ mod cmd {
fn write_quit( fn write_quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_impl(cx, args.first())?; write_impl(cx, args.first())?;
@ -2209,7 +2218,7 @@ mod cmd {
fn force_write_quit( fn force_write_quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_impl(cx, args.first())?; write_impl(cx, args.first())?;
@ -2240,7 +2249,7 @@ mod cmd {
fn write_all_impl( fn write_all_impl(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
quit: bool, quit: bool,
force: bool, force: bool,
@ -2276,7 +2285,7 @@ mod cmd {
fn write_all( fn write_all(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_all_impl(cx, args, event, false, false) write_all_impl(cx, args, event, false, false)
@ -2284,7 +2293,7 @@ mod cmd {
fn write_all_quit( fn write_all_quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, false) write_all_impl(cx, args, event, true, false)
@ -2292,7 +2301,7 @@ mod cmd {
fn force_write_all_quit( fn force_write_all_quit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, true) write_all_impl(cx, args, event, true, true)
@ -2300,7 +2309,7 @@ mod cmd {
fn quit_all_impl( fn quit_all_impl(
editor: &mut Editor, editor: &mut Editor,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
force: bool, force: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -2319,7 +2328,7 @@ mod cmd {
fn quit_all( fn quit_all(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
quit_all_impl(cx.editor, args, event, false) quit_all_impl(cx.editor, args, event, false)
@ -2327,7 +2336,7 @@ mod cmd {
fn force_quit_all( fn force_quit_all(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
event: PromptEvent, event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
quit_all_impl(cx.editor, args, event, true) quit_all_impl(cx.editor, args, event, true)
@ -2335,7 +2344,7 @@ mod cmd {
fn cquit( fn cquit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let exit_code = args let exit_code = args
@ -2354,16 +2363,26 @@ mod cmd {
fn theme( fn theme(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let theme = args.first().context("theme not provided")?; let theme = args.first().context("Theme not provided")?;
cx.editor.set_theme_from_name(theme) let theme = cx
.editor
.theme_loader
.load(theme)
.with_context(|| format!("Failed setting theme {}", theme))?;
let true_color = cx.editor.config.true_color || crate::true_color();
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
}
cx.editor.set_theme(theme);
Ok(())
} }
fn yank_main_selection_to_clipboard( fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
@ -2371,20 +2390,18 @@ mod cmd {
fn yank_joined_to_clipboard( fn yank_joined_to_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
let separator = args let default_sep = Cow::Borrowed(doc.line_ending.as_str());
.first() let separator = args.first().unwrap_or(&default_sep);
.copied()
.unwrap_or_else(|| doc.line_ending.as_str());
yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard) yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
} }
fn yank_main_selection_to_primary_clipboard( fn yank_main_selection_to_primary_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection) yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
@ -2392,47 +2409,45 @@ mod cmd {
fn yank_joined_to_primary_clipboard( fn yank_joined_to_primary_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
let separator = args let default_sep = Cow::Borrowed(doc.line_ending.as_str());
.first() let separator = args.first().unwrap_or(&default_sep);
.copied()
.unwrap_or_else(|| doc.line_ending.as_str());
yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection) yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
} }
fn paste_clipboard_after( fn paste_clipboard_after(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
} }
fn paste_clipboard_before( fn paste_clipboard_before(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
} }
fn paste_primary_clipboard_after( fn paste_primary_clipboard_after(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
} }
fn paste_primary_clipboard_before( fn paste_primary_clipboard_before(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
} }
fn replace_selections_with_clipboard_impl( fn replace_selections_with_clipboard_impl(
@ -2459,7 +2474,7 @@ mod cmd {
fn replace_selections_with_clipboard( fn replace_selections_with_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@ -2467,7 +2482,7 @@ mod cmd {
fn replace_selections_with_primary_clipboard( fn replace_selections_with_primary_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@ -2475,7 +2490,7 @@ mod cmd {
fn show_clipboard_provider( fn show_clipboard_provider(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.editor cx.editor
@ -2485,12 +2500,13 @@ mod cmd {
fn change_current_directory( fn change_current_directory(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let dir = helix_core::path::expand_tilde( let dir = helix_core::path::expand_tilde(
args.first() args.first()
.context("target directory not provided")? .context("target directory not provided")?
.as_ref()
.as_ref(), .as_ref(),
); );
@ -2508,7 +2524,7 @@ mod cmd {
fn show_current_directory( fn show_current_directory(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?; let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@ -2520,7 +2536,7 @@ mod cmd {
/// Sets the [`Document`]'s encoding.. /// Sets the [`Document`]'s encoding..
fn set_encoding( fn set_encoding(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (_, doc) = current!(cx.editor); let (_, doc) = current!(cx.editor);
@ -2536,7 +2552,7 @@ mod cmd {
/// Reload the [`Document`] from its source file. /// Reload the [`Document`] from its source file.
fn reload( fn reload(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -2545,7 +2561,7 @@ mod cmd {
fn tree_sitter_scopes( fn tree_sitter_scopes(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -2559,15 +2575,18 @@ mod cmd {
fn vsplit( fn vsplit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let id = view!(cx.editor).doc; let id = view!(cx.editor).doc;
if let Some(path) = args.get(0) { if args.is_empty() {
cx.editor.open(path.into(), Action::VerticalSplit)?;
} else {
cx.editor.switch(id, Action::VerticalSplit); cx.editor.switch(id, Action::VerticalSplit);
} else {
for arg in args {
cx.editor
.open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
}
} }
Ok(()) Ok(())
@ -2575,15 +2594,18 @@ mod cmd {
fn hsplit( fn hsplit(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let id = view!(cx.editor).doc; let id = view!(cx.editor).doc;
if let Some(path) = args.get(0) { if args.is_empty() {
cx.editor.open(path.into(), Action::HorizontalSplit)?;
} else {
cx.editor.switch(id, Action::HorizontalSplit); cx.editor.switch(id, Action::HorizontalSplit);
} else {
for arg in args {
cx.editor
.open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
}
} }
Ok(()) Ok(())
@ -2591,7 +2613,7 @@ mod cmd {
fn tutor( fn tutor(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[&str], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let path = helix_core::runtime_dir().join("tutor.txt"); let path = helix_core::runtime_dir().join("tutor.txt");
@ -2603,7 +2625,7 @@ mod cmd {
pub(super) fn goto_line_number( pub(super) fn goto_line_number(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[&str], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
ensure!(!args.is_empty(), "Line number required"); ensure!(!args.is_empty(), "Line number required");
@ -2709,7 +2731,7 @@ mod cmd {
TypableCommand { TypableCommand {
name: "format", name: "format",
aliases: &["fmt"], aliases: &["fmt"],
doc: "Format the file using a formatter.", doc: "Format the file using the LSP formatter.",
fun: format, fun: format,
completer: None, completer: None,
}, },
@ -2800,7 +2822,7 @@ mod cmd {
TypableCommand { TypableCommand {
name: "theme", name: "theme",
aliases: &[], aliases: &[],
doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)", doc: "Change the editor theme.",
fun: theme, fun: theme,
completer: Some(completers::theme), completer: Some(completers::theme),
}, },
@ -2884,7 +2906,7 @@ mod cmd {
TypableCommand { TypableCommand {
name: "change-current-directory", name: "change-current-directory",
aliases: &["cd"], aliases: &["cd"],
doc: "Change the current working directory (:cd <dir>).", doc: "Change the current working directory.",
fun: change_current_directory, fun: change_current_directory,
completer: Some(completers::directory), completer: Some(completers::directory),
}, },
@ -3016,7 +3038,7 @@ fn command_mode(cx: &mut Context) {
// If command is numeric, interpret as line number and go there. // If command is numeric, interpret as line number and go there.
if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() { if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) { if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
} }
return; return;
@ -3024,7 +3046,8 @@ fn command_mode(cx: &mut Context) {
// Handle typable commands // Handle typable commands
if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { let args = shellwords::shellwords(input);
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
} }
} else { } else {
@ -3298,12 +3321,19 @@ pub fn code_action(cx: &mut Context) {
move |editor, code_action, _action| match code_action { move |editor, code_action, _action| match code_action {
lsp::CodeActionOrCommand::Command(command) => { lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command); log::debug!("code action command: {:?}", command);
editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); execute_lsp_command(editor, command.clone());
} }
lsp::CodeActionOrCommand::CodeAction(code_action) => { lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action); log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit { if let Some(ref workspace_edit) = code_action.edit {
apply_workspace_edit(editor, offset_encoding, workspace_edit) log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone());
} }
} }
}, },
@ -3314,6 +3344,26 @@ pub fn code_action(cx: &mut Context) {
) )
} }
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
let (_view, doc) = current!(editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
let command_future = language_server.command(cmd);
tokio::spawn(async move {
let res = command_future.await;
if let Err(e) = res {
log::error!("execute LSP command: {}", e);
}
});
}
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp; use lsp::ResourceOp;
use std::fs; use std::fs;
@ -3367,7 +3417,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
} }
} }
fn apply_workspace_edit( pub fn apply_workspace_edit(
editor: &mut Editor, editor: &mut Editor,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit, workspace_edit: &lsp::WorkspaceEdit,
@ -3474,7 +3524,7 @@ fn apply_workspace_edit(
fn last_picker(cx: &mut Context) { fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker // TODO: last picker does not seem to work well with buffer_picker
cx.callback = Some(Box::new(|compositor: &mut Compositor| { cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(picker) = compositor.last_picker.take() { if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker); compositor.push(picker);
} }
@ -3631,6 +3681,7 @@ fn normal_mode(cx: &mut Context) {
doc.mode = Mode::Normal; doc.mode = Mode::Normal;
try_restore_indent(doc, view.id);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
// if leaving append mode, move cursor back by 1 // if leaving append mode, move cursor back by 1
@ -3648,6 +3699,36 @@ fn normal_mode(cx: &mut Context) {
} }
} }
fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
let doc_changes = doc.changes().changes();
let text = doc.text().slice(..);
let pos = doc.selection(view_id).primary().cursor(text);
let mut can_restore_indent = false;
// Removes trailing whitespace if insert mode is exited after starting a blank new line.
use helix_core::chars::char_is_whitespace;
use helix_core::Operation;
if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
doc_changes
{
if move_pos + inserted_str.len32() as usize == pos
&& inserted_str.starts_with('\n')
&& inserted_str.chars().skip(1).all(char_is_whitespace)
{
can_restore_indent = true;
}
}
if can_restore_indent {
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
let line_start_pos = text.line_to_char(range.cursor_line(text));
(line_start_pos, pos, None)
});
doc.apply(&transaction, view_id);
}
}
// Store a jump on the jumplist. // Store a jump on the jumplist.
fn push_jump(editor: &mut Editor) { fn push_jump(editor: &mut Editor) {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
@ -4220,8 +4301,9 @@ pub mod insert {
// The default insert hook: simply insert the character // The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
let cursors = selection.clone().cursors(doc.slice(..));
let t = Tendril::from_char(ch); let t = Tendril::from_char(ch);
let transaction = Transaction::insert(doc, selection, t); let transaction = Transaction::insert(doc, &cursors, t);
Some(transaction) Some(transaction)
} }
@ -4236,11 +4318,11 @@ pub mod insert {
}; };
let text = doc.text(); let text = doc.text();
let selection = doc.selection(view.id).clone().cursors(text.slice(..)); let selection = doc.selection(view.id);
// run through insert hooks, stopping on the first one that returns Some(t) // run through insert hooks, stopping on the first one that returns Some(t)
for hook in hooks { for hook in hooks {
if let Some(transaction) = hook(text, &selection, c) { if let Some(transaction) = hook(text, selection, c) {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
break; break;
} }
@ -4606,11 +4688,12 @@ fn paste_impl(
doc: &mut Document, doc: &mut Document,
view: &View, view: &View,
action: Paste, action: Paste,
count: usize,
) -> Option<Transaction> { ) -> Option<Transaction> {
let repeat = std::iter::repeat( let repeat = std::iter::repeat(
values values
.last() .last()
.map(|value| Tendril::from_slice(value)) .map(|value| Tendril::from(value.repeat(count)))
.unwrap(), .unwrap(),
); );
@ -4625,7 +4708,7 @@ fn paste_impl(
let mut values = values let mut values = values
.iter() .iter()
.map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
.map(|value| Tendril::from(value.as_ref())) .map(|value| Tendril::from(value.as_ref().repeat(count)))
.chain(repeat); .chain(repeat);
let text = doc.text(); let text = doc.text();
@ -4645,7 +4728,7 @@ fn paste_impl(
// paste append // paste append
(Paste::After, false) => range.to(), (Paste::After, false) => range.to(),
}; };
(pos, pos, Some(values.next().unwrap())) (pos, pos, values.next())
}); });
Some(transaction) Some(transaction)
@ -4655,13 +4738,14 @@ fn paste_clipboard_impl(
editor: &mut Editor, editor: &mut Editor,
action: Paste, action: Paste,
clipboard_type: ClipboardType, clipboard_type: ClipboardType,
count: usize,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
match editor match editor
.clipboard_provider .clipboard_provider
.get_contents(clipboard_type) .get_contents(clipboard_type)
.map(|contents| paste_impl(&[contents], doc, view, action)) .map(|contents| paste_impl(&[contents], doc, view, action, count))
{ {
Ok(Some(transaction)) => { Ok(Some(transaction)) => {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
@ -4674,22 +4758,43 @@ fn paste_clipboard_impl(
} }
fn paste_clipboard_after(cx: &mut Context) { fn paste_clipboard_after(cx: &mut Context) {
let _ = paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard); let _ = paste_clipboard_impl(
cx.editor,
Paste::After,
ClipboardType::Clipboard,
cx.count(),
);
} }
fn paste_clipboard_before(cx: &mut Context) { fn paste_clipboard_before(cx: &mut Context) {
let _ = paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard); let _ = paste_clipboard_impl(
cx.editor,
Paste::Before,
ClipboardType::Clipboard,
cx.count(),
);
} }
fn paste_primary_clipboard_after(cx: &mut Context) { fn paste_primary_clipboard_after(cx: &mut Context) {
let _ = paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection); let _ = paste_clipboard_impl(
cx.editor,
Paste::After,
ClipboardType::Selection,
cx.count(),
);
} }
fn paste_primary_clipboard_before(cx: &mut Context) { fn paste_primary_clipboard_before(cx: &mut Context) {
let _ = paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection); let _ = paste_clipboard_impl(
cx.editor,
Paste::Before,
ClipboardType::Selection,
cx.count(),
);
} }
fn replace_with_yanked(cx: &mut Context) { fn replace_with_yanked(cx: &mut Context) {
let count = cx.count();
let reg_name = cx.register.unwrap_or('"'); let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers; let registers = &mut cx.editor.registers;
@ -4699,12 +4804,12 @@ fn replace_with_yanked(cx: &mut Context) {
let repeat = std::iter::repeat( let repeat = std::iter::repeat(
values values
.last() .last()
.map(|value| Tendril::from_slice(value)) .map(|value| Tendril::from_slice(&value.repeat(count)))
.unwrap(), .unwrap(),
); );
let mut values = values let mut values = values
.iter() .iter()
.map(|value| Tendril::from_slice(value)) .map(|value| Tendril::from_slice(&value.repeat(count)))
.chain(repeat); .chain(repeat);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@ -4724,6 +4829,7 @@ fn replace_with_yanked(cx: &mut Context) {
fn replace_selections_with_clipboard_impl( fn replace_selections_with_clipboard_impl(
editor: &mut Editor, editor: &mut Editor,
clipboard_type: ClipboardType, clipboard_type: ClipboardType,
count: usize,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
@ -4731,7 +4837,11 @@ fn replace_selections_with_clipboard_impl(
Ok(contents) => { Ok(contents) => {
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), Some(contents.as_str().into())) (
range.from(),
range.to(),
Some(contents.repeat(count).as_str().into()),
)
}); });
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
@ -4743,21 +4853,22 @@ fn replace_selections_with_clipboard_impl(
} }
fn replace_selections_with_clipboard(cx: &mut Context) { fn replace_selections_with_clipboard(cx: &mut Context) {
let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard); let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
} }
fn replace_selections_with_primary_clipboard(cx: &mut Context) { fn replace_selections_with_primary_clipboard(cx: &mut Context) {
let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection); let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
} }
fn paste_after(cx: &mut Context) { fn paste_after(cx: &mut Context) {
let count = cx.count();
let reg_name = cx.register.unwrap_or('"'); let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers; let registers = &mut cx.editor.registers;
if let Some(transaction) = registers if let Some(transaction) = registers
.read(reg_name) .read(reg_name)
.and_then(|values| paste_impl(values, doc, view, Paste::After)) .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
{ {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
@ -4765,13 +4876,14 @@ fn paste_after(cx: &mut Context) {
} }
fn paste_before(cx: &mut Context) { fn paste_before(cx: &mut Context) {
let count = cx.count();
let reg_name = cx.register.unwrap_or('"'); let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers; let registers = &mut cx.editor.registers;
if let Some(transaction) = registers if let Some(transaction) = registers
.read(reg_name) .read(reg_name)
.and_then(|values| paste_impl(values, doc, view, Paste::Before)) .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
{ {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
@ -5167,8 +5279,12 @@ fn hover(cx: &mut Context) {
// skip if contents empty // skip if contents empty
let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new(contents); let popup = Popup::new("documentation", contents);
compositor.push(Box::new(popup)); if let Some(doc_popup) = compositor.find_id("documentation") {
*doc_popup = popup;
} else {
compositor.push(Box::new(popup));
}
} }
}, },
); );
@ -5318,6 +5434,12 @@ fn jump_backward(cx: &mut Context) {
}; };
} }
fn save_selection(cx: &mut Context) {
push_jump(cx.editor);
cx.editor
.set_status("Selection saved to jumplist".to_owned());
}
fn rotate_view(cx: &mut Context) { fn rotate_view(cx: &mut Context) {
cx.editor.focus_next() cx.editor.focus_next()
} }
@ -5893,3 +6015,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
} }
} }
fn record_macro(cx: &mut Context) {
if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
// Remove the keypress which ends the recording
keys.pop();
let s = keys
.into_iter()
.map(|key| format!("{}", key))
.collect::<Vec<_>>()
.join(" ");
cx.editor.registers.get_mut(reg).write(vec![s]);
cx.editor
.set_status(format!("Recorded to register {}", reg));
} else {
let reg = cx.register.take().unwrap_or('@');
cx.editor.macro_recording = Some((reg, Vec::new()));
cx.editor
.set_status(format!("Recording to register {}", reg));
}
}
fn play_macro(cx: &mut Context) {
let reg = cx.register.unwrap_or('@');
let keys = match cx
.editor
.registers
.get(reg)
.and_then(|reg| reg.read().get(0))
.context("Register empty")
.and_then(|s| {
s.split_whitespace()
.map(str::parse::<KeyEvent>)
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse macro")
}) {
Ok(keys) => keys,
Err(e) => {
cx.editor.set_error(format!("{}", e));
return;
}
};
let count = cx.count();
cx.callback = Some(Box::new(
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
for _ in 0..count {
for &key in keys.iter() {
compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
}
}
},
));
}

@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event; use crossterm::event::Event;
use tui::buffer::Buffer as Surface; 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(), // --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer. // .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. /// 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. /// 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)> { 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 None
} }
fn type_name(&self) -> &'static str { fn type_name(&self) -> &'static str {
std::any::type_name::<Self>() std::any::type_name::<Self>()
} }
fn id(&self) -> Option<&'static str> {
None
}
} }
use anyhow::Error; use anyhow::Error;
@ -126,12 +131,17 @@ impl Compositor {
} }
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { 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 // propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling) // run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() { for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) { match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => { EventResult::Consumed(Some(callback)) => {
callback(self); callback(self, cx);
return true; return true;
} }
EventResult::Consumed(None) => return true, EventResult::Consumed(None) => return true,
@ -184,6 +194,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name) .find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut()) .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 // View casting, taken straight from Cursive

@ -593,6 +593,9 @@ impl Default for Keymaps {
// paste_all // paste_all
"P" => paste_before, "P" => paste_before,
"q" => record_macro,
"Q" => play_macro,
">" => indent, ">" => indent,
"<" => unindent, "<" => unindent,
"=" => format_selections, "=" => format_selections,
@ -641,7 +644,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i> "tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward, "C-o" => jump_backward,
// "C-s" => save_selection, "C-s" => save_selection,
"space" => { "Space" "space" => { "Space"
"f" => file_picker, "f" => file_picker,

@ -9,3 +9,14 @@ pub mod config;
pub mod job; pub mod job;
pub mod keymap; pub mod keymap;
pub mod ui; 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 { let mut completion = Self {
popup, popup,
start_offset, start_offset,
@ -328,8 +328,8 @@ impl Component for Completion {
let y = popup_y; let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width; width = rel_width.min(width);
height = rel_height; height = rel_height.min(height);
} }
Rect::new(x, y, width, height) Rect::new(x, y, width, height)
} else { } else {

@ -425,6 +425,8 @@ impl EditorView {
let mut offset = 0; let mut offset = 0;
let gutter_style = theme.get("ui.gutter");
// avoid lots of small allocations by reusing a text buffer for each line // avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8); let mut text = String::with_capacity(8);
@ -440,7 +442,7 @@ impl EditorView {
viewport.y + i as u16, viewport.y + i as u16,
&text, &text,
*width, *width,
style, gutter_style.patch(style),
); );
} }
text.clear(); text.clear();
@ -1100,13 +1102,31 @@ impl Component for EditorView {
disp.push_str(&s); 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( 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), area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..) disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp), .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() { if let Some(completion) = self.completion.as_mut() {

@ -241,11 +241,6 @@ impl Component for Markdown {
} else if content_width > text_width { } else if content_width > text_width {
text_width = content_width; text_width = content_width;
} }
if height >= viewport.1 {
height = viewport.1;
break;
}
} }
Some((text_width + padding, height)) Some((text_width + padding, height))

@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));
@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn; return close_fn;
} }
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) // 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.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None); return EventResult::Consumed(None);

@ -186,6 +186,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"), &helix_core::config_dir().join("themes"),
)); ));
names.push("default".into()); names.push("default".into());
names.push("base16_default".into());
let mut names: Vec<_> = names let mut names: Vec<_> = names
.into_iter() .into_iter()

@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.last_picker = compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));
match key_event.into() { match key_event.into() {
shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => { shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up(); self.move_up();
} }
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {

@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
contents: T, contents: T,
position: Option<Position>, position: Option<Position>,
size: (u16, u16), size: (u16, u16),
child_size: (u16, u16),
scroll: usize, scroll: usize,
id: &'static str,
} }
impl<T: Component> Popup<T> { impl<T: Component> Popup<T> {
pub fn new(contents: T) -> Self { pub fn new(id: &'static str, contents: T) -> Self {
Self { Self {
contents, contents,
position: None, position: None,
size: (0, 0), size: (0, 0),
child_size: (0, 0),
scroll: 0, scroll: 0,
id,
} }
} }
@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) { pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction { if direction {
self.scroll += offset; 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 { } else {
self.scroll = self.scroll.saturating_sub(offset); self.scroll = self.scroll.saturating_sub(offset);
} }
@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); 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. // 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 let (width, height) = self
.contents .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"); .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) Some(self.size)
} }
@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
self.contents.render(area, surface, cx); self.contents.render(area, surface, cx);
} }
fn id(&self) -> Option<&'static str> {
Some(self.id)
}
} }

@ -127,7 +127,7 @@ impl Prompt {
let mut char_position = char_indices let mut char_position = char_indices
.iter() .iter()
.position(|(idx, _)| *idx == self.cursor) .position(|(idx, _)| *idx == self.cursor)
.unwrap_or_else(|| char_indices.len()); .unwrap_or(char_indices.len());
for _ in 0..rep { for _ in 0..rep {
// Skip any non-whitespace characters // Skip any non-whitespace characters
@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));
@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward); self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }
shift!(BackTab) => { shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward); self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }

@ -21,7 +21,7 @@ helix-lsp = { version = "0.5", path = "../helix-lsp"}
crossterm = { version = "0.22", optional = true } crossterm = { version = "0.22", optional = true }
# Conversion traits # Conversion traits
once_cell = "1.8" once_cell = "1.9"
url = "2" url = "2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }

@ -889,6 +889,10 @@ impl Document {
self.indent_style.as_str() self.indent_style.as_str()
} }
pub fn changes(&self) -> &ChangeSet {
&self.changes
}
#[inline] #[inline]
/// File path on disk. /// File path on disk.
pub fn path(&self) -> Option<&PathBuf> { pub fn path(&self) -> Option<&PathBuf> {

@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::SCRATCH_BUFFER_NAME, document::SCRATCH_BUFFER_NAME,
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
input::KeyEvent,
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
@ -19,7 +20,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep}; use tokio::time::{sleep, Duration, Instant, Sleep};
use anyhow::{bail, Context, Error}; use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
@ -104,6 +105,8 @@ pub struct Config {
/// Whether to display infoboxes. Defaults to true. /// Whether to display infoboxes. Defaults to true.
pub auto_info: bool, pub auto_info: bool,
pub file_picker: FilePickerConfig, pub file_picker: FilePickerConfig,
/// 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,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
@ -136,6 +139,7 @@ impl Default for Config {
completion_trigger_len: 2, completion_trigger_len: 2,
auto_info: true, auto_info: true,
file_picker: FilePickerConfig::default(), file_picker: FilePickerConfig::default(),
true_color: false,
} }
} }
} }
@ -160,6 +164,7 @@ pub struct Editor {
pub count: Option<std::num::NonZeroUsize>, pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme, pub theme: Theme,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>, pub clipboard_provider: Box<dyn ClipboardProvider>,
@ -203,6 +208,7 @@ impl Editor {
documents: BTreeMap::new(), documents: BTreeMap::new(),
count: None, count: None,
selected_register: None, selected_register: None,
macro_recording: None,
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers, language_servers,
syn_loader, syn_loader,
@ -262,15 +268,6 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
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 /// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?; let doc = self.documents.get_mut(&doc_id)?;

@ -22,7 +22,8 @@ pub fn diagnostic<'doc>(
Box::new(move |line: usize, _selected: bool, out: &mut String| { Box::new(move |line: usize, _selected: bool, out: &mut String| {
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) { if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
let diagnostic = &diagnostics[index];
write!(out, "●").unwrap(); write!(out, "●").unwrap();
return Some(match diagnostic.severity { return Some(match diagnostic.severity {
Some(Severity::Error) => error, Some(Severity::Error) => error,

@ -36,7 +36,6 @@ pub(crate) mod keys {
pub(crate) const PAGEUP: &str = "pageup"; pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown"; pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab"; pub(crate) const TAB: &str = "tab";
pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del"; pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins"; pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null"; pub(crate) const NULL: &str = "null";
@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?, KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?, KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?, KeyCode::Tab => f.write_str(keys::TAB)?,
KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?, KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?, KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?, KeyCode::Null => f.write_str(keys::NULL)?,
@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(), KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(), KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(), KeyCode::Tab => keys::TAB.len(),
KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(), KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(), KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(), KeyCode::Null => keys::NULL.len(),
@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp, keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown, keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab, keys::TAB => KeyCode::Tab,
keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete, keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert, keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null, keys::NULL => KeyCode::Null,
@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")] #[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent { impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from( fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent, if code == crossterm::event::KeyCode::BackTab {
) -> KeyEvent { // special case for BackTab -> Shift-Tab
KeyEvent { let mut modifiers: KeyModifiers = modifiers.into();
code: code.into(), modifiers.insert(KeyModifiers::SHIFT);
modifiers: modifiers.into(), 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, PageDown,
/// Tab key. /// Tab key.
Tab, Tab,
/// Shift + Tab key.
BackTab,
/// Delete key. /// Delete key.
Delete, Delete,
/// Insert key. /// Insert key.
@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::PageUp => CKeyCode::PageUp, KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown, KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab, KeyCode::Tab => CKeyCode::Tab,
KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete, KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert, KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number), KeyCode::F(f_number) => CKeyCode::F(f_number),
@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::PageUp => KeyCode::PageUp, CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown, CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab, CKeyCode::Tab => KeyCode::Tab,
CKeyCode::BackTab => KeyCode::BackTab, CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete, CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert, CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number), CKeyCode::F(f_number) => KeyCode::F(f_number),

@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") 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)] #[derive(Clone, Debug)]
pub struct Loader { pub struct Loader {
@ -35,6 +39,9 @@ impl Loader {
if name == "default" { if name == "default" {
return Ok(self.default()); return Ok(self.default());
} }
if name == "base16_default" {
return Ok(self.base16_default());
}
let filename = format!("{}.toml", name); let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename); let user_path = self.user_dir.join(&filename);
@ -74,6 +81,11 @@ impl Loader {
pub fn default(&self) -> Theme { pub fn default(&self) -> Theme {
DEFAULT_THEME.clone() DEFAULT_THEME.clone()
} }
/// Returns the alternative 16-color default theme
pub fn base16_default(&self) -> Theme {
BASE16_DEFAULT_THEME.clone()
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -138,8 +150,7 @@ impl Theme {
} }
pub fn get(&self, scope: &str) -> Style { pub fn get(&self, scope: &str) -> Style {
self.try_get(scope) self.try_get(scope).unwrap_or_default()
.unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
} }
pub fn try_get(&self, scope: &str) -> Option<Style> { pub fn try_get(&self, scope: &str) -> Option<Style> {
@ -154,6 +165,14 @@ impl Theme {
pub fn find_scope_index(&self, scope: &str) -> Option<usize> { pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope) 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 { struct ThemePalette {

@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = " " }
[language.config] [language.config]
cargo = { loadOutDirsFromCheck = true } cargo = { loadOutDirsFromCheck = true }
procMacro = { enable = false } procMacro = { enable = false }
diagnostics = { disabled = ["unresolved-proc-macro"] }
[[language]] [[language]]
name = "toml" name = "toml"
@ -44,6 +45,17 @@ comment-token = "#"
language-server = { command = "elixir-ls" } language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]]
name = "fish"
scope = "source.fish"
injection-regex = "fish"
file-types = ["fish"]
shebangs = ["fish"]
roots = []
comment-token = "#"
indent = { tab-width = 4, unit = " " }
[[language]] [[language]]
name = "mint" name = "mint"
scope = "source.mint" scope = "source.mint"
@ -249,7 +261,6 @@ language-server = { command = "julia", args = [
using Pkg; using Pkg;
import StaticLint; import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file); env_path = dirname(Pkg.Types.Context().env.project_file);
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true; server.runlinter = true;
run(server); run(server);
@ -406,6 +417,13 @@ shebangs = ["racket"]
comment-token = ";" comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] } language-server = { command = "racket", args = ["-l", "racket-langserver"] }
[[language]]
name = "comment"
scope = "scope.comment"
roots = []
file-types = []
injection-regex = "comment"
[[language]] [[language]]
name = "wgsl" name = "wgsl"
scope = "source.wgsl" scope = "source.wgsl"
@ -421,3 +439,42 @@ roots = []
file-types = ["ll"] file-types = ["ll"]
comment-token = ";" comment-token = ";"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]]
name = "markdown"
scope = "source.md"
injection-regex = "md|markdown"
file-types = ["md"]
roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "dart"
scope = "source.dart"
file-types = ["dart"]
roots = ["pubspec.yaml"]
auto-format = true
comment-token = "//"
language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "scala"
scope = "source.scala"
roots = ["build.sbt"]
file-types = ["scala", "sbt"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "metals" }
[[language]]
name = "dockerfile"
scope = "source.dockerfile"
injection-regex = "docker|dockerfile"
roots = ["Dockerfile"]
file-types = ["Dockerfile", "dockerfile"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "docker-langserver", args = ["--stdio"] }

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,4 @@
((line_comment) @injection.content
(#set! injection.language "comment"))
((bracket_comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,30 @@
[
"("
")"
] @punctuation.bracket
":" @punctuation.delimiter
((tag (name) @warning)
(#match? @warning "^(TODO|HACK|WARNING)$"))
("text" @warning
(#match? @warning "^(TODO|HACK|WARNING)$"))
((tag (name) @error)
(match? @error "^(FIXME|XXX|BUG)$"))
("text" @error
(match? @error "^(FIXME|XXX|BUG)$"))
(tag
(name) @ui.text
(user)? @constant)
; Issue number (#123)
("text" @constant.numeric
(#match? @constant.numeric "^#[0-9]+$"))
; User mention (@user)
("text" @tag
(#match? @tag "^[@][a-zA-Z0-9_-]+$"))

@ -0,0 +1,237 @@
(dotted_identifier_list) @string
; Methods
; --------------------
(super) @function.builtin
(function_expression_body (identifier) @function.method)
((identifier)(selector (argument_part)) @function.method)
; Annotations
; --------------------
(annotation
name: (identifier) @attribute)
(marker_annotation
name: (identifier) @attribute)
; Types
; --------------------
(class_definition
name: (identifier) @type)
(constructor_signature
name: (identifier) @function.method)
(function_signature
name: (identifier) @function.method)
(getter_signature
(identifier) @function.builtin)
(setter_signature
name: (identifier) @function.builtin)
(enum_declaration
name: (identifier) @type)
(enum_constant
name: (identifier) @type.builtin)
(void_type) @type.builtin
((scoped_identifier
scope: (identifier) @type)
(#match? @type "^[a-zA-Z]"))
((scoped_identifier
scope: (identifier) @type
name: (identifier) @type)
(#match? @type "^[a-zA-Z]"))
; the DisabledDrawerButtons in : const DisabledDrawerButtons(history: true),
(type_identifier) @type.builtin
; Variables
; --------------------
; the "File" in var file = File();
((identifier) @namespace
(#match? @namespace "^_?[A-Z].*[a-z]")) ; catch Classes or IClasses not CLASSES
("Function" @type.builtin)
(inferred_type) @type.builtin
; properties
(unconditional_assignable_selector
(identifier) @variable.other.member)
(conditional_assignable_selector
(identifier) @variable.other.member)
; assignments
; --------------------
; the "strings" in : strings = "some string"
(assignment_expression
left: (assignable_expression) @variable)
(this) @variable.builtin
; Parameters
; --------------------
(formal_parameter
name: (identifier) @variable)
(named_argument
(label (identifier) @variable))
; Literals
; --------------------
[
(hex_integer_literal)
(decimal_integer_literal)
(decimal_floating_point_literal)
;(octal_integer_literal)
;(hex_floating_point_literal)
] @constant.numeric.integer
(symbol_literal) @string.special.symbol
(string_literal) @string
[
(const_builtin)
(final_builtin)
] @variable.builtin
[
(true)
(false)
] @constant.builtin.boolean
(null_literal) @constant.builtin
(comment) @comment.line
(documentation_comment) @comment.block.documentation
; Tokens
; --------------------
(template_substitution
"$" @punctuation.special
"{" @punctuation.special
"}" @punctuation.special
) @embedded
(template_substitution
"$" @punctuation.special
(identifier_dollar_escaped) @variable
) @embedded
(escape_sequence) @constant.character.escape
; Punctuation
;---------------------
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
";"
"."
","
":"
] @punctuation.delimiter
; Operators
;---------------------
[
"@"
"?"
"=>"
".."
"=="
"&&"
"%"
"<"
">"
"="
">="
"<="
"||"
(multiplicative_operator)
(increment_operator)
(is_operator)
(prefix_operator)
(equality_operator)
(additive_operator)
] @operator
; Keywords
; --------------------
["import" "library" "export"] @keyword.control.import
["do" "while" "continue" "for"] @keyword.control.repeat
["return" "yield"] @keyword.control.return
["as" "in" "is"] @keyword.operator
[
"?."
"??"
"if"
"else"
"switch"
"default"
"late"
] @keyword.control.conditional
[
"try"
"throw"
"catch"
"finally"
(break_statement)
] @keyword.control.exception
; Reserved words (cannot be used as identifiers)
[
(case_builtin)
"abstract"
"async"
"async*"
"await"
"class"
"covariant"
"deferred"
"dynamic"
"enum"
"extends"
"extension"
"external"
"factory"
"Function"
"get"
"implements"
"interface"
"mixin"
"new"
"on"
"operator"
"part"
"required"
"set"
"show"
"static"
"super"
"sync*"
"typedef"
"with"
] @keyword
; when used as an identifier:
((identifier) @variable.builtin
(#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$"))
; Error
(ERROR) @error

@ -0,0 +1,20 @@
indent = [
"class_body",
"function_body",
"function_expression_body",
"declaration",
"initializers",
"switch_block",
"if_statement",
"formal_parameter_list",
"formal_parameter",
"list_literal",
"return_statement",
"arguments"
]
outdent = [
"}",
"]",
")"
]

@ -0,0 +1,20 @@
; Scopes
;-------
[
(block)
(try_statement)
(catch_clause)
(finally_clause)
] @local.scope
; Definitions
;------------
(class_definition
body: (_) @local.definition)
; References
;------------
(identifier) @local.reference

@ -0,0 +1,51 @@
[
"FROM"
"AS"
"RUN"
"CMD"
"LABEL"
"EXPOSE"
"ENV"
"ADD"
"COPY"
"ENTRYPOINT"
"VOLUME"
"USER"
"WORKDIR"
"ARG"
"ONBUILD"
"STOPSIGNAL"
"HEALTHCHECK"
"SHELL"
"MAINTAINER"
"CROSS_BUILD"
] @keyword
[
":"
"@"
] @operator
(comment) @comment
(image_spec
(image_tag
":" @punctuation.special)
(image_digest
"@" @punctuation.special))
(double_quoted_string) @string
(expansion
[
"$"
"{"
"}"
] @punctuation.special
) @none
((variable) @constant
(#match? @constant "^[A-Z][A-Z_0-9]*$"))

@ -0,0 +1,6 @@
((comment) @injection.content
(#set! injection.language "comment"))
([(shell_command) (shell_fragment)] @injection.content
(#set! injection.language "bash"))

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,156 @@
;; Operators
[
"&&"
"||"
"|"
"&"
"="
"!="
".."
"!"
(direction)
(stream_redirect)
(test_option)
] @operator
[
"not"
"and"
"or"
] @keyword.operator
;; Conditionals
(if_statement
[
"if"
"end"
] @keyword.control.conditional)
(switch_statement
[
"switch"
"end"
] @keyword.control.conditional)
(case_clause
[
"case"
] @keyword.control.conditional)
(else_clause
[
"else"
] @keyword.control.conditional)
(else_if_clause
[
"else"
"if"
] @keyword.control.conditional)
;; Loops/Blocks
(while_statement
[
"while"
"end"
] @keyword.control.repeat)
(for_statement
[
"for"
"end"
] @keyword.control.repeat)
(begin_statement
[
"begin"
"end"
] @keyword.control.repeat)
;; Keywords
[
"in"
(break)
(continue)
] @keyword
"return" @keyword.control.return
;; Punctuation
[
"["
"]"
"{"
"}"
"("
")"
] @punctuation.bracket
"," @punctuation.delimiter
;; Commands
(command
argument: [
(word) @variable.parameter (#match? @variable.parameter "^-")
]
)
; non-bultin command names
(command name: (word) @function)
; derived from builtin -n (fish 3.2.2)
(command
name: [
(word) @function.builtin
(#match? @function.builtin "^(\.|:|_|alias|argparse|bg|bind|block|breakpoint|builtin|cd|command|commandline|complete|contains|count|disown|echo|emit|eval|exec|exit|fg|functions|history|isatty|jobs|math|printf|pwd|random|read|realpath|set|set_color|source|status|string|test|time|type|ulimit|wait)$")
]
)
(test_command "test" @function.builtin)
;; Functions
(function_definition ["function" "end"] @keyword.function)
(function_definition
name: [
(word) (concatenation)
]
@function)
(function_definition
option: [
(word)
(concatenation (word))
] @variable.parameter (#match? @variable.parameter "^-")
)
;; Strings
[(double_quote_string) (single_quote_string)] @string
(escape_sequence) @constant.character.escape
;; Variables
(variable_name) @variable
(variable_expansion) @constant
;; Nodes
(integer) @constant.numeric.integer
(float) @constant.numeric.float
(comment) @comment
(test_option) @string
((word) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(true|false)$"))
;; Error
(ERROR) @error

@ -0,0 +1,12 @@
indent = [
"function_definition",
"while_statement",
"for_statement",
"if_statement",
"begin_statement",
"switch_statement",
]
outdent = [
"end"
]

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1 @@
(function_definition) @function.around

@ -1,3 +1,4 @@
(preproc_arg) @glsl ; inherits: c
(comment) @comment ((preproc_arg) @injection.content
(#set! injection.language "glsl"))

@ -8,7 +8,7 @@
(constructor) @constructor (constructor) @constructor
(pragma) @pragma (pragma) @pragma
(comment) @comment (comment) @comment
(signature name: (variable) @fun_type_name) (signature name: (variable) @type)
(function name: (variable) @function) (function name: (variable) @function)
(constraint class: (class_name (type)) @class) (constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class)) (class (class_head class: (class_name (type)) @class))

@ -1,5 +1,7 @@
; TODO: re-add when markdown is added. ; TODO: re-add when markdown is added.
; ((triple_string) @markdown ; ((triple_string) @injection.content
; (#offset! @markdown 0 3 0 -3)) ; (#offset! @injection.content 0 3 0 -3)
; (#set! injection.language "markdown"))
(comment) @comment ((comment) @injection.content
(#set! injection.language "comment"))

@ -2,24 +2,24 @@
(import_statement (import_statement
(identifier) @definition.import) (identifier) @definition.import)
(variable_declaration (variable_declaration
(identifier) @definition.var) (identifier) @local.definition)
(variable_declaration (variable_declaration
(tuple_expression (tuple_expression
(identifier) @definition.var)) (identifier) @local.definition))
(for_binding (for_binding
(identifier) @definition.var) (identifier) @local.definition)
(for_binding (for_binding
(tuple_expression (tuple_expression
(identifier) @definition.var)) (identifier) @local.definition))
(assignment_expression (assignment_expression
(tuple_expression (tuple_expression
(identifier) @definition.var)) (identifier) @local.definition))
(assignment_expression (assignment_expression
(bare_tuple_expression (bare_tuple_expression
(identifier) @definition.var)) (identifier) @local.definition))
(assignment_expression (assignment_expression
(identifier) @definition.var) (identifier) @local.definition)
(type_parameter_list (type_parameter_list
(identifier) @definition.type) (identifier) @definition.type)
@ -43,11 +43,11 @@
(identifier) @definition.parameter) (identifier) @definition.parameter)
(function_definition (function_definition
name: (identifier) @definition.function) @scope name: (identifier) @definition.function) @local.scope
(macro_definition (macro_definition
name: (identifier) @definition.macro) @scope name: (identifier) @definition.macro) @local.scope
(identifier) @reference (identifier) @local.reference
[ [
(try_statement) (try_statement)
@ -56,4 +56,4 @@
(let_statement) (let_statement)
(compound_expression) (compound_expression)
(for_statement) (for_statement)
] @scope ] @local.scope

@ -278,7 +278,7 @@
"\\includeinkscape" "\\includeinkscape"
"\\usepgflibrary" "\\usepgflibrary"
"\\usetikzlibrary" "\\usetikzlibrary"
] @include ] @keyword.control.import
[ [
"\\part" "\\part"
@ -318,60 +318,60 @@
["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
(chapter (chapter
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(part (part
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(section (section
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(subsection (subsection
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(subsubsection (subsubsection
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(paragraph (paragraph
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
(subparagraph (subparagraph
text: (brace_group) @text.title) text: (brace_group) @markup.heading)
((environment ((environment
(begin (begin
name: (word) @_frame) name: (word) @_frame)
(brace_group (brace_group
child: (text) @text.title)) child: (text) @markup.heading))
(#eq? @_frame "frame")) (#eq? @_frame "frame"))
((generic_command ((generic_command
name:(generic_command_name) @_name name:(generic_command_name) @_name
arg: (brace_group arg: (brace_group
(text) @text.title)) (text) @markup.heading))
(#eq? @_name "\\frametitle")) (#eq? @_name "\\frametitle"))
;; Formatting ;; Formatting
((generic_command ((generic_command
name:(generic_command_name) @_name name:(generic_command_name) @_name
arg: (_) @text.emphasis) arg: (_) @markup.italic)
(#eq? @_name "\\emph")) (#eq? @_name "\\emph"))
((generic_command ((generic_command
name:(generic_command_name) @_name name:(generic_command_name) @_name
arg: (_) @text.emphasis) arg: (_) @markup.italic)
(#match? @_name "^(\\\\textit|\\\\mathit)$")) (#match? @_name "^(\\\\textit|\\\\mathit)$"))
((generic_command ((generic_command
name:(generic_command_name) @_name name:(generic_command_name) @_name
arg: (_) @text.strong) arg: (_) @markup.bold)
(#match? @_name "^(\\\\textbf|\\\\mathbf)$")) (#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
((generic_command ((generic_command
name:(generic_command_name) @_name name:(generic_command_name) @_name
. .
arg: (_) @text.uri) arg: (_) @markup.link.url)
(#match? @_name "^(\\\\url|\\\\href)$")) (#match? @_name "^(\\\\url|\\\\href)$"))
(ERROR) @error (ERROR) @error

@ -12,7 +12,7 @@
((account) @variable.other.member) ((account) @variable.other.member)
((commodity) @text.literal) ((commodity) @text.literal)
"include" @include "include" @keyword.local.import
[ [
"account" "account"

@ -1,2 +1,2 @@
(comment) @comment ([(comment) (note)] @injection.content
(note) @comment (#set! injection.language "comment"))

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,36 @@
[
(atx_heading)
(setext_heading)
] @markup.heading
(code_fence_content) @none
[
(indented_code_block)
(fenced_code_block)
] @markup.raw.block
(block_quote) @markup.quote
(code_span) @markup.raw.inline
(emphasis) @markup.italic
(strong_emphasis) @markup.bold
(link_destination) @markup.link.url
(link_label) @markup.link.label
[
(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

@ -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 ["exception" "try"] @keyword.control.exception
["include" "open"] @include ["include" "open"] @keyword.control.import
["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat ["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat

@ -8,6 +8,6 @@ indent = [
"match_case", "match_case",
] ]
oudent = [ outdent = [
"}", "}",
] ]

@ -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 = [
"}"
]

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -127,11 +127,16 @@
"await" "await"
] @keyword.control ] @keyword.control
"use" @keyword.control.import
(mod_item "mod" @keyword.control.import !body)
(use_as_clause "as" @keyword.control.import)
(type_cast_expression "as" @keyword.operator)
[ [
(crate) (crate)
(super) (super)
"as" "as"
"use"
"pub" "pub"
"mod" "mod"
"extern" "extern"
@ -242,10 +247,9 @@
; --- ; ---
; Macros ; Macros
; --- ; ---
(meta_item (meta_item
(identifier) @attribute) (identifier) @function.macro)
(attribute_item) @attribute
(inner_attribute_item) @attribute (inner_attribute_item) @attribute
(macro_definition (macro_definition
@ -259,7 +263,7 @@
"!" @function.macro) "!" @function.macro)
(metavariable) @variable.parameter (metavariable) @variable.parameter
(fragment_specifier) @variable.parameter (fragment_specifier) @type

@ -1,3 +1,6 @@
([(line_comment) (block_comment)] @injection.content
(#set! injection.language "comment"))
((macro_invocation ((macro_invocation
(token_tree) @injection.content) (token_tree) @injection.content)
(#set! injection.language "rust") (#set! injection.language "rust")

@ -0,0 +1,203 @@
; CREDITS @stumash (stuart.mashaal@gmail.com)
;; variables
((identifier) @variable.builtin
(#match? @variable.builtin "^this$"))
(interpolation) @none
; Assume other uppercase names constants.
; NOTE: In order to distinguish constants we highlight
; all the identifiers that are uppercased. But this solution
; is not suitable for all occurrences e.g. it will highlight
; an uppercased method as a constant if used with no params.
; Introducing highlighting for those specific cases, is probably
; best way to resolve the issue.
((identifier) @constant (#match? @constant "^[A-Z]"))
;; types
(type_identifier) @type
(class_definition
name: (identifier) @type)
(object_definition
name: (identifier) @type)
(trait_definition
name: (identifier) @type)
(type_definition
name: (type_identifier) @type)
; method definition
(class_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
(object_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
(trait_definition
body: (template_body
(function_definition
name: (identifier) @function.method)))
; imports
(import_declaration
path: (identifier) @namespace)
((stable_identifier (identifier) @namespace))
((import_declaration
path: (identifier) @type) (#match? @type "^[A-Z]"))
((stable_identifier (identifier) @type) (#match? @type "^[A-Z]"))
((import_selectors (identifier) @type) (#match? @type "^[A-Z]"))
; method invocation
(call_expression
function: (identifier) @function)
(call_expression
function: (field_expression
field: (identifier) @function.method))
((call_expression
function: (identifier) @variable.other.member)
(#match? @variable.other.member "^[A-Z]"))
(generic_function
function: (identifier) @function)
(
(identifier) @function.builtin
(#match? @function.builtin "^super$")
)
; function definitions
(function_definition
name: (identifier) @function)
(parameter
name: (identifier) @variable.parameter)
; expressions
(field_expression field: (identifier) @variable.other.member)
(field_expression value: (identifier) @type
(#match? @type "^[A-Z]"))
(infix_expression operator: (identifier) @operator)
(infix_expression operator: (operator_identifier) @operator)
(infix_type operator: (operator_identifier) @operator)
(infix_type operator: (operator_identifier) @operator)
; literals
(boolean_literal) @constant.builtin.boolean
(integer_literal) @constant.numeric.integer
(floating_point_literal) @constant.numeric.float
(symbol_literal) @string.special.symbol
[
(string)
(character_literal)
(interpolated_string_expression)
] @string
(interpolation "$" @punctuation.special)
;; keywords
[
"abstract"
"case"
"class"
"extends"
"final"
"finally"
;; `forSome` existential types not implemented yet
"implicit"
"lazy"
;; `macro` not implemented yet
"object"
"override"
"package"
"private"
"protected"
"sealed"
"trait"
"type"
"val"
"var"
"with"
] @keyword
(null_literal) @constant.builtin
(wildcard) @keyword
;; special keywords
"new" @keyword.operator
[
"else"
"if"
"match"
"try"
"catch"
"throw"
] @keyword.control.conditional
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
"."
","
] @punctuation.delimiter
[
"do"
"for"
"while"
"yield"
] @keyword.control.repeat
"def" @keyword.function
[
"=>"
"<-"
"@"
] @keyword.operator
"import" @keyword.control.import
"return" @keyword.control.return
(comment) @comment
;; `case` is a conditional keyword in case_block
(case_block
(case_clause ("case") @keyword.control.conditional))
(identifier) @variable

@ -0,0 +1,23 @@
indent = [
"block",
"arguments",
"parameter",
"class_definition",
"trait_definition",
"object_definition",
"function_definition",
"val_definition",
"import_declaration",
"while_expression",
"do_while_expression",
"for_expression",
"try_expression",
"match_expression"
]
outdent = [
"}",
"]",
")"
]

@ -20,12 +20,12 @@
((element (start_tag (tag_name) @_tag) (text) @markup.inline) ((element (start_tag (tag_name) @_tag) (text) @markup.inline)
(#match? @_tag "^(code|kbd)$")) (#match? @_tag "^(code|kbd)$"))
((element (start_tag (tag_name) @_tag) (text) @markup.underline.link) ((element (start_tag (tag_name) @_tag) (text) @markup.link.url)
(#eq? @_tag "a")) (#eq? @_tag "a"))
((attribute ((attribute
(attribute_name) @_attr (attribute_name) @_attr
(quoted_attribute_value (attribute_value) @markup.undeline.link)) (quoted_attribute_value (attribute_value) @markup.link.url))
(#match? @_attr "^(href|src)$")) (#match? @_attr "^(href|src)$"))
(tag_name) @tag (tag_name) @tag

@ -26,5 +26,5 @@
(#set! injection.language "typescript") (#set! injection.language "typescript")
) )
(comment) @comment ((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -1,9 +1,19 @@
(block_mapping_pair key: (_) @variable.other.member) (block_mapping_pair
(flow_mapping (_ key: (_) @variable.other.member)) key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))
(block_mapping_pair
key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))
(flow_mapping
(_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)))
(flow_mapping
(_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member))))
(boolean_scalar) @constant.builtin.boolean (boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin (null_scalar) @constant.builtin
(double_quote_scalar) @string (double_quote_scalar) @string
(single_quote_scalar) @string (single_quote_scalar) @string
(block_scalar) @string
(string_scalar) @string
(escape_sequence) @constant.character.escape (escape_sequence) @constant.character.escape
(integer_scalar) @constant.numeric.integer (integer_scalar) @constant.numeric.integer
(float_scalar) @constant.numeric.float (float_scalar) @constant.numeric.float
@ -30,4 +40,4 @@
"}" "}"
] @punctuation.bracket ] @punctuation.bracket
["*" "&"] @punctuation.special ["*" "&" "---" "..."] @punctuation.special

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save