Merge branch 'master' into colored-indent-guides

pull/6/head
SoraTenshi 2 years ago
commit c14320f26e

@ -14,10 +14,10 @@ jobs:
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
uses: cachix/install-nix-action@v18
- name: Authenticate with Cachix
uses: cachix/cachix-action@v10
uses: cachix/cachix-action@v11
with:
name: helix
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

76
Cargo.lock generated

@ -2,6 +2,17 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -22,9 +33,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.65"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
name = "arc-swap"
@ -277,15 +288,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
@ -294,15 +305,15 @@ dependencies = [
[[package]]
name = "futures-task"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
[[package]]
name = "futures-util"
version = "0.3.24"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures-core",
"futures-task",
@ -383,11 +394,21 @@ dependencies = [
"memmap2",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "helix-core"
version = "0.6.0"
dependencies = [
"arc-swap",
"bitflags",
"chrono",
"encoding_rs",
"etcetera",
@ -507,6 +528,7 @@ dependencies = [
"helix-core",
"helix-view",
"serde",
"termini",
"unicode-segmentation",
]
@ -917,18 +939,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.145"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.145"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
dependencies = [
"proc-macro2",
"quote",
@ -937,9 +959,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
dependencies = [
"itoa",
"ryu",
@ -1099,11 +1121,20 @@ dependencies = [
"winapi",
]
[[package]]
name = "termini"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c0f7ecb9c2a380d2686a747e4fc574043712326e8d39fbd220ab3bd29768a12"
dependencies = [
"dirs-next",
]
[[package]]
name = "textwrap"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
@ -1196,9 +1227,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af"
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
dependencies = [
"futures-core",
"pin-project-lite",
@ -1253,10 +1284,11 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"regex",
]

@ -41,20 +41,41 @@ cd helix
cargo install --path helix-term
```
This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
Helix 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, or `%AppData%/helix/runtime` on Windows).
| OS | Command |
| -------------------- | -------------------------------------------- |
| Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux/macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
| OS | Command |
| -------------------- | ------------------------------------------------ |
| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires
elevated priviliges - i.e. PowerShell or Cmd must be run as administrator.
**PowerShell:**
```powershell
New-Item -ItemType SymbolicLink -Target "runtime" -Path "$Env:AppData\helix\runtime"
```
**Cmd:**
```cmd
cd %appdata%\helix
mklink /D runtime "<helix-repo>\runtime"
```
The runtime location can be overridden via the `HELIX_RUNTIME` environment variable.
> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term`,
> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`.
This location can be overridden via the `HELIX_RUNTIME` environment variable.
If you plan on keeping the repo locally, an alternative to copying/symlinking
runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime`
(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory).
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
@ -62,12 +83,32 @@ that sets the variable to the install dir.
> NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
In order to use LSP features like auto-complete, you will need to
[install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
for a language.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Adding Helix to your desktop environment
If installing from source, to use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## MacOS
Helix can be installed on MacOS through homebrew:

@ -45,9 +45,11 @@ on unix operating systems.
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "line-numbers"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
@ -251,5 +253,6 @@ Example:
render = true
character = "╎"
rainbow = "normal"
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1
```

@ -15,6 +15,7 @@
| cpp | ✓ | ✓ | ✓ | `clangd` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | | ✓ | `dart` |
| devicetree | ✓ | | | |
| diff | ✓ | | | |
@ -66,7 +67,7 @@
| llvm | ✓ | ✓ | ✓ | |
| llvm-mir | ✓ | ✓ | ✓ | |
| llvm-mir-yaml | ✓ | | ✓ | |
| lua | ✓ | | ✓ | `lua-language-server` |
| lua | ✓ | | ✓ | `lua-language-server` |
| make | ✓ | | | |
| markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | |
@ -86,7 +87,8 @@
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| python | ✓ | ✓ | | `pylsp` |
| purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
| r | ✓ | | | `R` |
| racket | | | | `racket` |
| regex | ✓ | | | |
@ -124,4 +126,4 @@
| wgsl | ✓ | | | `wgsl_analyzer` |
| xit | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server` |
| zig | ✓ | | ✓ | `zls` |
| zig | ✓ | | ✓ | `zls` |

@ -28,7 +28,7 @@
| `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme. |
| `:theme` | Change the editor theme (show current theme if no name specified). |
| `: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. |
@ -44,6 +44,7 @@
| `: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. |
| `:update` | Write changes only if the file has been modified. |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
@ -65,7 +66,7 @@
| `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. |
| `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output after each selection. |
| `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. |
| `:run-shell-command`, `:sh` | Run a shell command |

@ -46,6 +46,20 @@ capture on the same line, the indent level isn't changed at all.
- `@outdent` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent` apply.
- `@extend`:
Extend the range of this node to the end of the line and to lines that
are indented more than the line that this node starts on. This is useful
for languages like Python, where for the purpose of indentation some nodes
(like functions or classes) should also contain indented lines that follow them.
- `@extend.prevent-once`:
Prevents the first extension of an ancestor of this node. For example, in Python
a return expression always ends the block that it is in. Note that this only stops the
extension of the next `@extend` capture. If multiple ancestors are captured,
only the extension of the innermost one is prevented. All other ancestors are unaffected
(regardless of whether the innermost ancestor would actually have been extended).
## Predicates
In some cases, an S-expression cannot express exactly what pattern should be matched.

@ -50,6 +50,23 @@ sudo dnf install helix
sudo xbps-install helix
```
## Windows
Helix can be installed using [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/).
**Scoop:**
```
scoop install helix
```
**Chocolatey:**
```
choco install helix
```
## Build from source
```
@ -58,26 +75,67 @@ cd helix
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` and build tree-sitter grammars in `./runtime/grammars`.
Helix also needs it's 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 overridden
via the `HELIX_RUNTIME` environment variable.
| OS | command |
|-------------------|-----------|
|windows(cmd.exe) |`xcopy /e /i runtime %AppData%/helix/runtime` |
|windows(powershell)|`xcopy /e /i runtime $Env:AppData\helix\runtime` |
|linux/macos |`ln -s $PWD/runtime ~/.config/helix/runtime`|
| OS | Command |
| -------------------- | ------------------------------------------------ |
| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires
elevated priviliges - i.e. PowerShell or Cmd must be run as administrator.
**PowerShell:**
```powershell
New-Item -ItemType SymbolicLink -Target "runtime" -Path "$Env:AppData\helix\runtime"
```
**Cmd:**
```cmd
cd %appdata%\helix
mklink /D runtime "<helix-repo>\runtime"
```
The runtime location can be overridden via the `HELIX_RUNTIME` environment variable.
> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term`,
> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`.
If you plan on keeping the repo locally, an alternative to copying/symlinking
runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime`
(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory).
To use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## Finishing up the installation
## Finishing up the installation
To make sure everything is set up as expected you should finally run the helix healthcheck via
To make sure everything is set up as expected you should finally run the helix healthcheck via
```
hx --health
```
For more information on the information displayed in the healthcheck results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck).
For more information on the information displayed in the health check results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck).
### Building tree-sitter grammars

@ -129,7 +129,7 @@
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` |
| `J` | Join lines inside selection | `join_selections` |
| `A-J` | Join lines inside selection and select space | `join_selections_space` |
| `Alt-J` | Join lines inside selection and select space | `join_selections_space` |
| `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -167,10 +167,13 @@ These sub-modes are accessible from normal mode and typically switch back to nor
#### View mode
Accessed by typing `z` in [normal mode](#normal-mode).
View mode is intended for scrolling and manipulating the view without changing
the selection. The "sticky" variant of this mode is persistent; use the Escape
key to return to normal mode after usage (useful when you're simply looking
over text and not actively editing it).
the selection. The "sticky" variant of this mode (accessed by typing `Z` in
normal mode) is persistent; use the Escape key to return to normal mode after
usage (useful when you're simply looking over text and not actively editing
it).
| Key | Description | Command |
@ -188,6 +191,8 @@ over text and not actively editing it).
#### Goto mode
Accessed by typing `g` in [normal mode](#normal-mode).
Jumps to various locations.
| Key | Description | Command |
@ -213,9 +218,10 @@ Jumps to various locations.
#### Match mode
Enter this mode using `m` from normal mode. See the relevant section
in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
and [textobject](./usage.md#textobject) usage.
Accessed by typing `m` in [normal mode](#normal-mode).
See the relevant section in [Usage](./usage.md) for an explanation about
[surround](./usage.md#surround) and [textobject](./usage.md#textobjects) usage.
| Key | Description | Command |
| ----- | ----------- | ------- |
@ -230,6 +236,8 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
#### Window mode
Accessed by typing `Ctrl-w` in [normal mode](#normal-mode).
This layer is similar to Vim keybindings as Kakoune does not support window.
| Key | Description | Command |
@ -252,8 +260,9 @@ This layer is similar to Vim keybindings as Kakoune does not support window.
#### Space mode
This layer is a kludge of mappings, mostly pickers.
Accessed by typing `Space` in [normal mode](#normal-mode).
This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- |
@ -264,8 +273,8 @@ This layer is a kludge of mappings, mostly pickers.
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `g` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` |
| `G` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker`
| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` |
| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` |
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
@ -278,7 +287,7 @@ This layer is a kludge of mappings, mostly pickers.
| `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` |
> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
> TIP: Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
##### Popup
@ -311,8 +320,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `]t` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
| `]Space` | Add newline below | `add_newline_below` |
## Insert mode
@ -391,7 +400,6 @@ Keys to use within picker. Remapping currently not supported.
| `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
@ -415,8 +423,8 @@ Keys to use within prompt, Remapping currently not supported.
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word |
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete next char |
| `Backspace`, `Ctrl-h` | Delete previous char |
| `Delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |

@ -50,7 +50,7 @@ These configuration keys are available:
| `name` | The name of the language |
| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. Extensions and full file names are supported. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
| `shebangs` | The interpreters from the shebang line, for example `["sh", "bash"]` |
| `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| `auto-format` | Whether to autoformat this language when saving |
@ -63,6 +63,32 @@ These configuration keys are available:
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `max-line-length` | Maximum line length. Used for the `:reflow` command |
### File-type detection and the `file-types` key
Helix determines which language configuration to use with the `file-types` key
from the above section. `file-types` is a list of strings or tables, for
example:
```toml
file-types = ["Makefile", "toml", { suffix = ".git/config" }]
```
When determining a language configuration to use, Helix searches the file-types
with the following priorities:
1. Exact match: if the filename of a file is an exact match of a string in a
`file-types` list, that language wins. In the example above, `"Makefile"`
will match against `Makefile` files.
2. Extension: if there are no exact matches, any `file-types` string that
matches the file extension of a given file wins. In the example above, the
`"toml"` matches files like `Cargo.toml` or `languages.toml`.
3. Suffix: if there are still no matches, any values in `suffix` tables
are checked against the full path of the given file. In the example above,
the `{ suffix = ".git/config" }` would match against any `config` files
in `.git` directories. Note: `/` is used as the directory separator but is
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.
### Language Server configuration
The `language-server` field takes the following keys:

@ -11,11 +11,11 @@ this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
C-s = ":w" # Maps the Ctrl-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps the Ctrl-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
"C-S-esc" = "extend_line" # Maps Ctrl-Shift-Escape to extend_line
g = { a = "code_action" } # Maps `ga` to show possible code actions
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
@ -25,7 +25,7 @@ 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
Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
| Key name | Representation |

@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix
Each line in the theme file is specified as below:
```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }
```
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
@ -77,17 +77,35 @@ The following values may be used as modifiers.
Less common modifiers might not be supported by your terminal emulator.
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
> Note: The `underlined` modifier is deprecated and only available for backwards compatibility.
> Its behavior is equivalent to setting `underline.style="line"`.
### Underline Style
One of the following values may be used as a value for `underline.style`.
Some styles might not be supported by your terminal emulator.
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
| `line` |
| `curl` |
| `dashed` |
| `dot` |
| `double_line` |
### Rainbow
@ -236,51 +254,53 @@ These scopes are used for theming the editor interface.
- `hover` - for hover popup ui
| Key | Notes |
| --- | --- |
| `ui.background` | |
| `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.popup` | Documentation popups (e.g space-k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Border lines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section])|
| `ui.virtual.whitespace` | Visible white-space characters |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor |
| `ui.cursorline.secondary` | The lines of any other cursors |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
| Key | Notes |
| --- | --- |
| `ui.background` | |
| `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.popup` | Documentation popups (e.g Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Border lines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `ui.virtual.whitespace` | Visible whitespace characters |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
You can check compliance to spec with

@ -53,7 +53,7 @@ Multiple characters are currently not supported, but planned.
## Syntax-tree Motions
`A-p`, `A-o`, `A-i`, and `A-n` (or `Alt` and arrow keys) move the primary
`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) move the primary
selection according to the selection's place in the syntax tree. Let's walk
through an example to get familiar with them. Many languages have a syntax like
so for function calls:
@ -100,13 +100,13 @@ in the tree above.
func([arg1], arg2, arg3)
```
Using `A-n` would select the next sibling in the syntax tree: `arg2`.
Using `Alt-n` would select the next sibling in the syntax tree: `arg2`.
```
func(arg1, [arg2], arg3)
```
While `A-o` would expand the selection to the parent node. In the tree above we
While `Alt-o` would expand the selection to the parent node. In the tree above we
can see that we would select the `arguments` node.
```
@ -114,10 +114,10 @@ func[(arg1, arg2, arg3)]
```
There is also some nuanced behavior that prevents you from getting stuck on a
node with no sibling. If we have a selection on `arg1`, `A-p` would bring us
node with no sibling. If we have a selection on `arg1`, `Alt-p` would bring us
to the previous child node. Since `arg1` doesn't have a sibling to its left,
though, we climb the syntax tree and then take the previous selection. So `A-p`
will move the selection over to the "func" `identifier`.
though, we climb the syntax tree and then take the previous selection. So
`Alt-p` will move the selection over to the "func" `identifier`.
```
[func](arg1, arg2, arg3)

@ -35,7 +35,8 @@ to `cargo install` anything either).
Integration tests for helix-term can be run with `cargo integration-test`. Code
contributors are strongly encouraged to write integration tests for their code.
Existing tests can be used as examples. Helpers can be found in
[helpers.rs][helpers.rs]
[helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL`
environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`.
## Minimum Stable Rust Version (MSRV) Policy

@ -29,6 +29,7 @@ tree-sitter = "0.20"
once_cell = "1.15"
arc-swap = "1"
regex = "1"
bitflags = "1.3"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
@ -42,7 +43,7 @@ encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.4"
textwrap = "0.15.1"
textwrap = "0.16.0"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

@ -7,7 +7,6 @@ use std::collections::HashMap;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
@ -147,13 +146,7 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
}
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(
doc: &Rope,
start_range: &Range,
offset: usize,
typed_char: char,
len_inserted: usize,
) -> Range {
fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
// When the character under the cursor changes due to complete pair
// insertion, we must look backward a grapheme and then add the length
// of the insertion to put the resulting cursor in the right place, e.g.
@ -173,8 +166,8 @@ fn get_next_range(
// inserting at the very end of the document after the last newline
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
return Range::new(
start_range.anchor + offset + typed_char.len_utf8(),
start_range.head + offset + typed_char.len_utf8(),
start_range.anchor + offset + 1,
start_range.head + offset + 1,
);
}
@ -204,21 +197,18 @@ fn get_next_range(
// trivial case: only inserted a single-char opener, just move the selection
if len_inserted == 1 {
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
start_range.anchor + offset + typed_char.len_utf8()
start_range.anchor + offset + 1
} else {
start_range.anchor + offset
};
return Range::new(
end_anchor,
start_range.head + offset + typed_char.len_utf8(),
);
return Range::new(end_anchor, start_range.head + offset + 1);
}
// If the head = 0, then we must be in insert mode with a backward
// cursor, which implies the head will just move
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
start_range.head + offset + typed_char.len_utf8()
start_range.head + offset + 1
} else {
// We must have a forward cursor, which means we must move to the
// other end of the grapheme to get to where the new characters
@ -244,8 +234,7 @@ fn get_next_range(
(_, Direction::Forward) => {
if single_grapheme {
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
+ typed_char.len_utf8()
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + 1
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
@ -259,7 +248,9 @@ fn get_next_range(
// if we're backward, then the head is at the first char
// of the typed char, so we need to add the length of
// the closing char
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
+ len_inserted
+ offset
} else {
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
@ -280,9 +271,12 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let next_char = doc.get_char(cursor);
let len_inserted;
// Since auto pairs are currently limited to single chars, we're either
// inserting exactly one or two chars. When arbitrary length pairs are
// added, these will need to be changed.
let change = match next_char {
Some(_) if !pair.should_close(doc, start_range) => {
len_inserted = pair.open.len_utf8();
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.open);
(cursor, cursor, Some(tendril))
@ -290,12 +284,12 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
_ => {
// insert open & close
let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
len_inserted = 2;
(cursor, cursor, Some(pair_str))
}
};
let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@ -309,7 +303,6 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
@ -321,13 +314,13 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
len_inserted += pair.close.len_utf8();
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.close);
(cursor, cursor, Some(tendril))
};
let next_range = get_next_range(doc, start_range, offs, pair.close, len_inserted);
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@ -363,11 +356,11 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
pair_str.push(pair.close);
}
len_inserted += pair_str.len();
len_inserted += pair_str.chars().count();
(cursor, cursor, Some(pair_str))
};
let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@ -378,551 +371,3 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
log::debug!("auto pair transaction: {:#?}", t);
t
}
#[cfg(test)]
mod test {
use super::*;
use smallvec::smallvec;
const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)
}
fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open == close)
}
fn test_hooks(
in_doc: &Rope,
in_sel: &Selection,
ch: char,
pairs: &[(char, char)],
expected_doc: &Rope,
expected_sel: &Selection,
) {
let pairs = AutoPairs::new(pairs.iter());
let trans = hook(in_doc, in_sel, ch, &pairs).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,
test_pairs: I,
pairs: &[(char, char)],
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>,
{
test_pairs.into_iter().for_each(|(open, close)| {
test_hooks(
in_doc,
in_sel,
*open,
pairs,
&Rope::from(get_expected_doc(*open, *close)),
actual_sel,
)
});
}
// [] indicates range
/// [] -> insert ( -> ([])
#[test]
fn test_insert_blank() {
test_hooks_with_pairs(
&Rope::from(LINE_END),
&Selection::single(1, 0),
DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("{}{}{}", open, close, LINE_END),
&Selection::single(2, 1),
);
let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
test_hooks_with_pairs(
&empty_doc,
&Selection::single(empty_doc.len_chars(), LINE_END.len()),
DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| {
format!(
"{line_end}{open}{close}{line_end}",
open = open,
close = close,
line_end = LINE_END
)
},
&Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
);
}
#[test]
fn test_insert_before_multi_code_point_graphemes() {
for (_, close) in differing_pairs() {
test_hooks(
&Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
&Selection::single(13, 6),
*close,
DEFAULT_PAIRS,
&Rope::from(format!("hello {}👨‍👩‍👧‍👦 goodbye{}", close, LINE_END)),
&Selection::single(14, 7),
);
}
}
#[test]
fn test_insert_at_end_of_document() {
test_hooks_with_pairs(
&Rope::from(LINE_END),
&Selection::single(LINE_END.len(), LINE_END.len()),
DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
);
test_hooks_with_pairs(
&Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
);
}
/// [] -> 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(format!("{line_end}{line_end}", line_end = LINE_END)),
// before inserting the pair, the cursor covers all of both empty lines
&Selection::single(0, LINE_END.len() * 2),
DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| {
format!(
"{line_end}{open}{close}{line_end}",
line_end = LINE_END,
open = open,
close = close
)
},
// after inserting pair, the cursor covers the first new line and the open char
&Selection::single(0, LINE_END.len() + 2),
);
}
/// [] ([])
/// [] -> 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,
),
DEFAULT_PAIRS,
DEFAULT_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(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5),
);
}
/// foo[] -> append to end of line ( -> foo([])
#[test]
fn test_append_single_cursor() {
test_hooks_with_pairs(
&Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3, 3 + LINE_END.len()),
differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", open, close, LINE_END),
&Selection::single(4, 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(),
DEFAULT_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 DEFAULT_PAIRS {
let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks(
&doc,
&Selection::single(2, 1),
*close,
DEFAULT_PAIRS,
&doc,
&Selection::single(2 + LINE_END.len(), 2),
);
}
}
/// [(]) -> append ) -> [()]
#[test]
fn test_append_close_inside_pair() {
for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks(
&doc,
&Selection::single(0, 2),
*close,
DEFAULT_PAIRS,
&doc,
&Selection::single(0, 2 + LINE_END.len()),
);
}
}
/// ([]) ()[]
/// ([]) -> 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 DEFAULT_PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &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 DEFAULT_PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &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,
DEFAULT_PAIRS,
&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,
DEFAULT_PAIRS,
&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,
DEFAULT_PAIRS,
&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,
DEFAULT_PAIRS,
&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),
DEFAULT_PAIRS,
DEFAULT_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),
DEFAULT_PAIRS,
DEFAULT_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 DEFAULT_PAIRS {
let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close));
test_hooks(
&doc,
&sel,
*close,
DEFAULT_PAIRS,
&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(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4),
)
}
/// foo([) wor]d -> insert ) -> foo()[ wor]d
#[test]
fn test_insert_close_inside_pair_trailing_word_with_selection() {
for (open, close) in differing_pairs() {
test_hooks(
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 4),
*close,
DEFAULT_PAIRS,
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 5),
)
}
}
/// we want pairs that are *not* the same char to be inserted after
/// a non-pair char, for cases like functions, but for pairs that are
/// the same char, we want to *not* insert a pair to handle cases like "I'm"
///
/// word[] -> insert ( -> word([])
/// word[] -> insert ' -> word'[]
#[test]
fn test_insert_open_after_non_pair() {
let doc = Rope::from(format!("word{}", LINE_END));
let sel = Selection::single(5, 4);
let expected_sel = Selection::single(6, 5);
test_hooks_with_pairs(
&doc,
&sel,
differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("word{}{}{}", open, close, LINE_END),
&expected_sel,
);
test_hooks_with_pairs(
&doc,
&sel,
matching_pairs(),
DEFAULT_PAIRS,
|open, _| format!("word{}{}", open, LINE_END),
&expected_sel,
);
}
#[test]
fn test_configured_pairs() {
let test_pairs = &[('`', ':'), ('+', '-')];
test_hooks_with_pairs(
&Rope::from(LINE_END),
&Selection::single(1, 0),
test_pairs,
test_pairs,
|open, close| format!("{}{}{}", open, close, LINE_END),
&Selection::single(2, 1),
);
let doc = Rope::from(format!("foo`: word{}", LINE_END));
test_hooks(
&doc,
&Selection::single(9, 4),
':',
test_pairs,
&doc,
&Selection::single(9, 5),
)
}
}

@ -192,13 +192,15 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
/// Computes for node and all ancestors whether they are the first node on their line.
/// The first entry in the return value represents the root node, the last one the node itself
fn get_first_in_line(mut node: Node, byte_pos: usize, new_line: bool) -> Vec<bool> {
fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
let mut first_in_line = Vec::new();
loop {
if let Some(prev) = node.prev_sibling() {
// If we insert a new line, the first node at/after the cursor is considered to be the first in its line
let first = prev.end_position().row != node.start_position().row
|| (new_line && node.start_byte() >= byte_pos && prev.start_byte() < byte_pos);
|| new_line_byte_pos.map_or(false, |byte_pos| {
node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
});
first_in_line.push(Some(first));
} else {
// Nodes that have no previous siblings are first in their line if and only if their parent is
@ -298,8 +300,21 @@ enum IndentScope {
Tail,
}
/// Execute the indent query.
/// Returns for each node (identified by its id) a list of indent captures for that node.
/// A capture from the indent query which does not define an indent but extends
/// the range of a node. This is used before the indent is calculated.
enum ExtendCapture {
Extend,
PreventOnce,
}
/// The result of running a tree-sitter indent query. This stores for
/// each node (identified by its ID) the relevant captures (already filtered
/// by predicates).
struct IndentQueryResult {
indent_captures: HashMap<usize, Vec<IndentCapture>>,
extend_captures: HashMap<usize, Vec<ExtendCapture>>,
}
fn query_indents(
query: &Query,
syntax: &Syntax,
@ -309,8 +324,9 @@ fn query_indents(
// Position of the (optional) newly inserted line break.
// Given as (line, byte_pos)
new_line_break: Option<(usize, usize)>,
) -> HashMap<usize, Vec<IndentCapture>> {
) -> IndentQueryResult {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
cursor.set_byte_range(range);
// Iterate over all captures from the query
for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
@ -374,10 +390,24 @@ fn query_indents(
continue;
}
for capture in m.captures {
let capture_type = query.capture_names()[capture.index as usize].as_str();
let capture_type = match capture_type {
let capture_name = query.capture_names()[capture.index as usize].as_str();
let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent,
"outdent" => IndentCaptureType::Outdent,
"extend" => {
extend_captures
.entry(capture.node.id())
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::Extend);
continue;
}
"extend.prevent-once" => {
extend_captures
.entry(capture.node.id())
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::PreventOnce);
continue;
}
_ => {
// Ignore any unknown captures (these may be needed for predicates such as #match?)
continue;
@ -420,7 +450,72 @@ fn query_indents(
.push(indent_capture);
}
}
indent_captures
IndentQueryResult {
indent_captures,
extend_captures,
}
}
/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
/// Any ancestor of deepest_preceding which is also a descendant of node may be "extended". In that case, node will be updated,
/// so that the indent computation starts with the correct syntax node.
fn extend_nodes<'a>(
node: &mut Node<'a>,
deepest_preceding: Option<Node<'a>>,
extend_captures: &HashMap<usize, Vec<ExtendCapture>>,
text: RopeSlice,
line: usize,
tab_width: usize,
) {
if let Some(mut deepest_preceding) = deepest_preceding {
let mut stop_extend = false;
while deepest_preceding != *node {
let mut extend_node = false;
// This will be set to true if this node is captured, regardless of whether
// it actually will be extended (e.g. because the cursor isn't indented
// more than the node).
let mut node_captured = false;
if let Some(captures) = extend_captures.get(&deepest_preceding.id()) {
for capture in captures {
match capture {
ExtendCapture::PreventOnce => {
stop_extend = true;
}
ExtendCapture::Extend => {
node_captured = true;
// We extend the node if
// - the cursor is on the same line as the end of the node OR
// - the line that the cursor is on is more indented than the
// first line of the node
if deepest_preceding.end_position().row == line {
extend_node = true;
} else {
let cursor_indent =
indent_level_for_line(text.line(line), tab_width);
let node_indent = indent_level_for_line(
text.line(deepest_preceding.start_position().row),
tab_width,
);
if cursor_indent > node_indent {
extend_node = true;
}
}
}
}
}
}
// If we encountered some `StopExtend` capture before, we don't
// extend the node even if we otherwise would
if node_captured && stop_extend {
stop_extend = false;
} else if extend_node && !stop_extend {
*node = deepest_preceding;
break;
}
// This parent always exists since node is an ancestor of deepest_preceding
deepest_preceding = deepest_preceding.parent().unwrap();
}
}
}
/// Use the syntax tree to determine the indentation for a given position.
@ -459,40 +554,73 @@ fn query_indents(
/// },
/// );
/// ```
#[allow(clippy::too_many_arguments)]
pub fn treesitter_indent_for_pos(
query: &Query,
syntax: &Syntax,
indent_style: &IndentStyle,
tab_width: usize,
text: RopeSlice,
line: usize,
pos: usize,
new_line: bool,
) -> Option<String> {
let byte_pos = text.char_to_byte(pos);
// The innermost tree-sitter node which is considered for the indent
// computation. It may change if some predeceding node is extended
let mut node = syntax
.tree()
.root_node()
.descendant_for_byte_range(byte_pos, byte_pos)?;
let mut first_in_line = get_first_in_line(node, byte_pos, new_line);
let new_line_break = if new_line {
Some((line, byte_pos))
} else {
None
let (query_result, deepest_preceding) = {
// The query range should intersect with all nodes directly preceding
// the position of the indent query in case one of them is extended.
let mut deepest_preceding = None; // The deepest node preceding the indent query position
let mut tree_cursor = node.walk();
for child in node.children(&mut tree_cursor) {
if child.byte_range().end <= byte_pos {
deepest_preceding = Some(child);
}
}
deepest_preceding = deepest_preceding.map(|mut prec| {
// Get the deepest directly preceding node
while prec.child_count() > 0 {
prec = prec.child(prec.child_count() - 1).unwrap();
}
prec
});
let query_range = deepest_preceding
.map(|prec| prec.byte_range().end - 1..byte_pos + 1)
.unwrap_or(byte_pos..byte_pos + 1);
crate::syntax::PARSER.with(|ts_parser| {
let mut ts_parser = ts_parser.borrow_mut();
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
let query_result = query_indents(
query,
syntax,
&mut cursor,
text,
query_range,
new_line.then(|| (line, byte_pos)),
);
ts_parser.cursors.push(cursor);
(query_result, deepest_preceding)
})
};
let query_result = crate::syntax::PARSER.with(|ts_parser| {
let mut ts_parser = ts_parser.borrow_mut();
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
let query_result = query_indents(
query,
syntax,
&mut cursor,
text,
byte_pos..byte_pos + 1,
new_line_break,
);
ts_parser.cursors.push(cursor);
query_result
});
let indent_captures = query_result.indent_captures;
let extend_captures = query_result.extend_captures;
// Check for extend captures, potentially changing the node that the indent calculation starts with
extend_nodes(
&mut node,
deepest_preceding,
&extend_captures,
text,
line,
tab_width,
);
let mut first_in_line = get_first_in_line(node, new_line.then(|| byte_pos));
let mut result = Indentation::default();
// We always keep track of all the indent changes on one line, in order to only indent once
@ -504,7 +632,7 @@ pub fn treesitter_indent_for_pos(
// one entry for each ancestor of the node (which is what we iterate over)
let is_first = *first_in_line.last().unwrap();
// Apply all indent definitions for this node
if let Some(definitions) = query_result.get(&node.id()) {
if let Some(definitions) = indent_captures.get(&node.id()) {
for definition in definitions {
match definition.scope {
IndentScope::All => {
@ -550,7 +678,13 @@ pub fn treesitter_indent_for_pos(
node = parent;
first_in_line.pop();
} else {
result.add_line(&indent_for_line_below);
// Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for.
if (node.start_position().row < line)
|| (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
{
result.add_line(&indent_for_line_below);
}
result.add_line(&indent_for_line);
break;
}
@ -579,6 +713,7 @@ pub fn indent_for_newline(
query,
syntax,
indent_style,
tab_width,
text,
line_before,
line_before_end_pos,

@ -3,8 +3,9 @@ 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,
OnWhitespace,
Unquoted,
UnquotedEscaped,
Quoted,
QuoteEscaped,
Dquoted,
@ -13,7 +14,7 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
use State::*;
let mut state = Normal;
let mut state = Unquoted;
let mut args: Vec<Cow<str>> = Vec::new();
let mut escaped = String::with_capacity(input.len());
@ -22,31 +23,47 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
for (i, c) in input.char_indices() {
state = match state {
Normal => match c {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
NormalEscaped
UnquotedEscaped
} else {
Normal
OnWhitespace
}
}
'"' => {
c if c.is_ascii_whitespace() => {
end = i;
Dquoted
OnWhitespace
}
'\'' => {
end = i;
Quoted
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
UnquotedEscaped
} else {
Unquoted
}
}
c if c.is_ascii_whitespace() => {
end = i;
Normal
OnWhitespace
}
_ => Normal,
_ => Unquoted,
},
NormalEscaped => Normal,
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
@ -59,7 +76,7 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
}
'\'' => {
end = i;
Normal
OnWhitespace
}
_ => Quoted,
},
@ -76,7 +93,7 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
}
'"' => {
end = i;
Normal
OnWhitespace
}
_ => Dquoted,
},
@ -195,4 +212,18 @@ mod test {
];
assert_eq!(expected, result);
}
#[test]
fn test_lists() {
let input =
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
let result = shellwords(input);
let expected = vec![
Cow::from(":set"),
Cow::from("statusline.center"),
Cow::from(r#"["file-type","file-encoding"]"#),
Cow::from(r#"["list", "in", "qoutes"]"#),
];
assert_eq!(expected, result);
}
}

@ -8,13 +8,15 @@ use crate::{
};
use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags;
use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, HashSet, VecDeque},
collections::{HashMap, VecDeque},
fmt,
mem::replace,
path::Path,
str::FromStr,
sync::Arc,
@ -59,17 +61,23 @@ pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
}
impl Default for Configuration {
fn default() -> Self {
crate::config::default_syntax_loader()
}
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>,
pub max_line_length: Option<usize>,
@ -117,6 +125,78 @@ pub struct LanguageConfiguration {
pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum FileType {
/// The extension of the file, either the `Path::extension` or the full
/// filename if the file does not have an extension.
Extension(String),
/// The suffix of a file. This is compared to a given file's absolute
/// path, so it can be used to detect files based on their directories.
Suffix(String),
}
impl Serialize for FileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
match self {
FileType::Extension(extension) => serializer.serialize_str(extension),
FileType::Suffix(suffix) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for FileType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct FileTypeVisitor;
impl<'de> serde::de::Visitor<'de> for FileTypeVisitor {
type Value = FileType;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or table")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(FileType::Extension(value.to_string()))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix(
suffix.replace('/', &std::path::MAIN_SEPARATOR.to_string()),
)),
Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}",
key
))),
None => Err(serde::de::Error::custom(
"expected a `suffix` key in the `file-types` entry",
)),
}
}
}
deserializer.deserialize_any(FileTypeVisitor)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
@ -353,20 +433,24 @@ pub fn read_query(language: &str, filename: &str) -> String {
impl LanguageConfiguration {
fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
let language = self.language_id.to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
let highlights_query = read_query(&self.language_id, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = read_query(&language, "locals.scm");
let injections_query = read_query(&self.language_id, "injections.scm");
let locals_query = read_query(&self.language_id, "locals.scm");
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id))
.map_err(|e| log::info!("{}", e))
.map_err(|err| {
log::error!(
"Failed to load tree-sitter parser for language {:?}: {}",
self.language_id,
err
)
})
.ok()?;
let config = HighlightConfiguration::new(
language,
@ -418,14 +502,20 @@ impl LanguageConfiguration {
}
fn load_query(&self, kind: &str) -> Option<Query> {
let lang_name = self.language_id.to_ascii_lowercase();
let query_text = read_query(&lang_name, kind);
let query_text = read_query(&self.language_id, kind);
if query_text.is_empty() {
return None;
}
let lang = self.highlight_config.get()?.as_ref()?.language;
Query::new(lang, &query_text)
.map_err(|e| log::error!("Failed to parse {} queries for {}: {}", kind, lang_name, e))
.map_err(|e| {
log::error!(
"Failed to parse {} queries for {}: {}",
kind,
self.language_id,
e
)
})
.ok()
}
}
@ -436,7 +526,8 @@ impl LanguageConfiguration {
pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>,
scopes: ArcSwap<Vec<String>>,
@ -446,7 +537,8 @@ impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
scopes: ArcSwap::from_pointee(Vec::new()),
};
@ -457,9 +549,14 @@ impl Loader {
for file_type in &config.file_types {
// entry().or_insert(Vec::new).push(language_id);
loader
.language_config_ids_by_file_type
.insert(file_type.clone(), language_id);
match file_type {
FileType::Extension(extension) => loader
.language_config_ids_by_extension
.insert(extension.clone(), language_id),
FileType::Suffix(suffix) => loader
.language_config_ids_by_suffix
.insert(suffix.clone(), language_id),
};
}
for shebang in &config.shebangs {
loader
@ -479,11 +576,22 @@ impl Loader {
let configuration_id = path
.file_name()
.and_then(|n| n.to_str())
.and_then(|file_name| self.language_config_ids_by_file_type.get(file_name))
.and_then(|file_name| self.language_config_ids_by_extension.get(file_name))
.or_else(|| {
path.extension()
.and_then(|extension| extension.to_str())
.and_then(|extension| self.language_config_ids_by_file_type.get(extension))
.and_then(|extension| self.language_config_ids_by_extension.get(extension))
})
.or_else(|| {
self.language_config_ids_by_suffix
.iter()
.find_map(|(file_type, id)| {
if path.to_str()?.ends_with(file_type) {
Some(id)
} else {
None
}
})
});
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
@ -594,6 +702,7 @@ impl Syntax {
tree: None,
config,
depth: 0,
flags: LayerUpdateFlags::empty(),
ranges: vec![Range {
start_byte: 0,
end_byte: usize::MAX,
@ -656,9 +765,10 @@ impl Syntax {
}
}
for layer in &mut self.layers.values_mut() {
for layer in self.layers.values_mut() {
// The root layer always covers the whole range (0..usize::MAX)
if layer.depth == 0 {
layer.flags = LayerUpdateFlags::MODIFIED;
continue;
}
@ -689,6 +799,8 @@ impl Syntax {
edit.new_end_position,
point_sub(range.end_point, edit.old_end_position),
);
layer.flags |= LayerUpdateFlags::MOVED;
}
// if the edit starts in the space before and extends into the range
else if edit.start_byte < range.start_byte {
@ -703,11 +815,13 @@ impl Syntax {
edit.new_end_position,
point_sub(range.end_point, edit.old_end_position),
);
layer.flags = LayerUpdateFlags::MODIFIED;
}
// If the edit is an insertion at the start of the tree, shift
else if edit.start_byte == range.start_byte && is_pure_insertion {
range.start_byte = edit.new_end_byte;
range.start_point = edit.new_end_position;
layer.flags |= LayerUpdateFlags::MOVED;
} else {
range.end_byte = range
.end_byte
@ -717,6 +831,7 @@ impl Syntax {
edit.new_end_position,
point_sub(range.end_point, edit.old_end_position),
);
layer.flags = LayerUpdateFlags::MODIFIED;
}
}
}
@ -731,27 +846,33 @@ impl Syntax {
let source_slice = source.slice(..);
let mut touched = HashSet::new();
// TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit
while let Some(layer_id) = queue.pop_front() {
// Mark the layer as touched
touched.insert(layer_id);
let layer = &mut self.layers[layer_id];
// Mark the layer as touched
layer.flags |= LayerUpdateFlags::TOUCHED;
// If a tree already exists, notify it of changes.
if let Some(tree) = &mut layer.tree {
for edit in edits.iter().rev() {
// Apply the edits in reverse.
// If we applied them in order then edit 1 would disrupt the positioning of edit 2.
tree.edit(edit);
if layer
.flags
.intersects(LayerUpdateFlags::MODIFIED | LayerUpdateFlags::MOVED)
{
for edit in edits.iter().rev() {
// Apply the edits in reverse.
// If we applied them in order then edit 1 would disrupt the positioning of edit 2.
tree.edit(edit);
}
}
}
// Re-parse the tree.
layer.parse(&mut ts_parser.parser, source)?;
if layer.flags.contains(LayerUpdateFlags::MODIFIED) {
// Re-parse the tree.
layer.parse(&mut ts_parser.parser, source)?;
}
} else {
// always parse if this layer has never been parsed before
layer.parse(&mut ts_parser.parser, source)?;
}
// Switch to an immutable borrow.
let layer = &self.layers[layer_id];
@ -855,6 +976,8 @@ impl Syntax {
config,
depth,
ranges,
// set the modified flag to ensure the layer is parsed
flags: LayerUpdateFlags::empty(),
})
});
@ -868,8 +991,11 @@ impl Syntax {
// Return the cursor back in the pool.
ts_parser.cursors.push(cursor);
// Remove all untouched layers
self.layers.retain(|id, _| touched.contains(&id));
// Reset all `LayerUpdateFlags` and remove all untouched layers
self.layers.retain(|_, layer| {
replace(&mut layer.flags, LayerUpdateFlags::empty())
.contains(LayerUpdateFlags::TOUCHED)
});
Ok(())
})
@ -968,6 +1094,16 @@ impl Syntax {
// TODO: Folding
}
bitflags! {
/// Flags that track the status of a layer
/// in the `Sytaxn::update` function
struct LayerUpdateFlags : u32{
const MODIFIED = 0b001;
const MOVED = 0b010;
const TOUCHED = 0b100;
}
}
#[derive(Debug)]
pub struct LanguageLayer {
// mode
@ -975,7 +1111,8 @@ pub struct LanguageLayer {
pub config: Arc<HighlightConfiguration>,
pub(crate) tree: Option<Tree>,
pub ranges: Vec<Range>,
pub depth: usize,
pub depth: u32,
flags: LayerUpdateFlags,
}
impl LanguageLayer {
@ -1191,7 +1328,7 @@ struct HighlightIter<'a> {
layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize,
next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>,
last_highlight_range: Option<(usize, usize, u32)>,
}
// Adapter to convert rope chunks to bytes
@ -1224,7 +1361,7 @@ struct HighlightIterLayer<'a> {
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
depth: usize,
depth: u32,
ranges: &'a [Range],
}
@ -1993,6 +2130,57 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
}
}
pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
pretty_print_tree_impl(fmt, node, true, None, 0)
}
fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
node: Node,
is_root: bool,
field_name: Option<&str>,
depth: usize,
) -> fmt::Result {
fn is_visible(node: Node) -> bool {
node.is_missing()
|| (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}
if is_visible(node) {
let indentation_columns = depth * 2;
write!(fmt, "{:indentation_columns$}", "")?;
if let Some(field_name) = field_name {
write!(fmt, "{}: ", field_name)?;
}
write!(fmt, "({}", node.kind())?;
} else if is_root {
write!(fmt, "(\"{}\")", node.kind())?;
}
for child_idx in 0..node.child_count() {
if let Some(child) = node.child(child_idx) {
if is_visible(child) {
fmt.write_char('\n')?;
}
pretty_print_tree_impl(
fmt,
child,
false,
node.field_name_for_child(child_idx as u32),
depth + 1,
)?;
}
}
if is_visible(node) {
write!(fmt, ")")?;
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
@ -2013,7 +2201,7 @@ mod test {
);
let loader = Loader::new(Configuration { language: vec![] });
let language = get_language("Rust").unwrap();
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
let textobject = TextObjectQuery { query };
@ -2073,7 +2261,7 @@ mod test {
let loader = Loader::new(Configuration { language: vec![] });
let language = get_language("Rust").unwrap();
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
language,
&std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm")
@ -2164,6 +2352,63 @@ mod test {
);
}
#[track_caller]
fn assert_pretty_print(source: &str, expected: &str, start: usize, end: usize) {
let source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] });
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let root = syntax
.tree()
.root_node()
.descendant_for_byte_range(start, end)
.unwrap();
let mut output = String::new();
pretty_print_tree(&mut output, root).unwrap();
assert_eq!(expected, output);
}
#[test]
fn test_pretty_print() {
let source = r#"/// Hello"#;
assert_pretty_print(source, "(line_comment)", 0, source.len());
// A large tree should be indented with fields:
let source = r#"fn main() {
println!("Hello, World!");
}"#;
assert_pretty_print(
source,
concat!(
"(function_item\n",
" name: (identifier)\n",
" parameters: (parameters)\n",
" body: (block\n",
" (expression_statement\n",
" (macro_invocation\n",
" macro: (identifier)\n",
" (token_tree\n",
" (string_literal))))))",
),
0,
source.len(),
);
// Selecting a token should print just that token:
let source = r#"fn main() {}"#;
assert_pretty_print(source, r#"("fn")"#, 0, 1);
// Error nodes are printed as errors:
let source = r#"}{"#;
assert_pretty_print(source, "(ERROR)", 0, source.len());
}
#[test]
fn test_load_runtime_file() {
// Test to make sure we can load some data from the runtime directory.

@ -34,7 +34,7 @@ pub fn print(s: &str) -> (String, Selection) {
let mut left = String::with_capacity(s.len());
'outer: while let Some(c) = iter.next() {
let start = left.len();
let start = left.chars().count();
if c != '#' {
left.push(c);
@ -63,6 +63,7 @@ pub fn print(s: &str) -> (String, Selection) {
left.push(c);
continue;
}
if !head_at_beg {
let prev = left.pop().unwrap();
if prev != '|' {
@ -71,15 +72,18 @@ pub fn print(s: &str) -> (String, Selection) {
continue;
}
}
iter.next(); // skip "#"
if is_primary {
primary_idx = Some(ranges.len());
}
let (anchor, head) = match head_at_beg {
true => (left.len(), start),
false => (start, left.len()),
true => (left.chars().count(), start),
false => (start, left.chars().count()),
};
ranges.push(Range::new(anchor, head));
continue 'outer;
}
@ -95,6 +99,7 @@ pub fn print(s: &str) -> (String, Selection) {
Some(i) => i,
None => panic!("missing primary `#[|]#` {:?}", s),
};
let selection = Selection::new(ranges, primary);
(left, selection)
}
@ -141,3 +146,119 @@ pub fn plain(s: &str, selection: Selection) -> String {
}
out
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn print_single() {
assert_eq!(
(String::from("hello"), Selection::single(1, 0)),
print("#[|h]#ello")
);
assert_eq!(
(String::from("hello"), Selection::single(0, 1)),
print("#[h|]#ello")
);
assert_eq!(
(String::from("hello"), Selection::single(4, 0)),
print("#[|hell]#o")
);
assert_eq!(
(String::from("hello"), Selection::single(0, 4)),
print("#[hell|]#o")
);
assert_eq!(
(String::from("hello"), Selection::single(5, 0)),
print("#[|hello]#")
);
assert_eq!(
(String::from("hello"), Selection::single(0, 5)),
print("#[hello|]#")
);
}
#[test]
fn print_multi() {
assert_eq!(
(
String::from("hello"),
Selection::new(
SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]),
0
)
),
print("#[|h]#ell#(|o)#")
);
assert_eq!(
(
String::from("hello"),
Selection::new(
SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]),
0
)
),
print("#[h|]#ell#(o|)#")
);
assert_eq!(
(
String::from("hello"),
Selection::new(
SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]),
0
)
),
print("#[|he]#l#(|lo)#")
);
assert_eq!(
(
String::from("hello\r\nhello\r\nhello\r\n"),
Selection::new(
SmallVec::from_slice(&[
Range::new(7, 5),
Range::new(21, 19),
Range::new(14, 12)
]),
0
)
),
print("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#")
);
}
#[test]
fn print_multi_byte_code_point() {
assert_eq!(
(String::from("„“"), Selection::single(1, 0)),
print("#[|„]#“")
);
assert_eq!(
(String::from("„“"), Selection::single(2, 1)),
print("„#[|“]#")
);
assert_eq!(
(String::from("„“"), Selection::single(0, 1)),
print("#[„|]#“")
);
assert_eq!(
(String::from("„“"), Selection::single(1, 2)),
print("„#[“|]#")
);
assert_eq!(
(String::from("they said „hello“"), Selection::single(11, 10)),
print("they said #[|„]#hello“")
);
}
#[test]
fn print_multi_code_point_grapheme() {
assert_eq!(
(
String::from("hello 👨‍👩‍👧‍👦 goodbye"),
Selection::single(13, 6)
),
print("hello #[|👨‍👩‍👧‍👦]# goodbye")
);
}
}

@ -50,6 +50,7 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
indent_query,
&syntax,
&IndentStyle::Spaces(4),
4,
text,
i,
text.line_to_char(i) + pos,

@ -726,7 +726,7 @@ pub mod events {
#[serde(tag = "event", content = "body")]
// seq is omitted as unused and is not sent by some implementations
pub enum Event {
Initialized,
Initialized(Option<DebuggerCapabilities>),
Stopped(Stopped),
Continued(Continued),
Exited(Exited),

@ -67,7 +67,6 @@ pub fn get_language(name: &str) -> Result<Language> {
#[cfg(not(target_arch = "wasm32"))]
pub fn get_language(name: &str) -> Result<Language> {
use libloading::{Library, Symbol};
let name = name.to_ascii_lowercase();
let mut library_path = crate::runtime_dir().join("grammars").join(&name);
library_path.set_extension(DYLIB_EXTENSION);

@ -23,5 +23,5 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.21", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.10"
tokio-stream = "0.1.11"
which = "4.2"

@ -1,12 +1,19 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
diagnostic::{DiagnosticTag, NumberOrString},
path::get_relative_path,
pos_at_coords, syntax, Selection,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{align_view, editor::ConfigEvent, theme, tree::Layout, Align, Editor};
use helix_view::{
align_view,
document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent},
theme,
tree::Layout,
Align, Editor,
};
use serde_json::json;
use crate::{
@ -19,7 +26,7 @@ use crate::{
ui::{self, overlay::overlayed},
};
use log::{error, warn};
use log::{debug, error, warn};
use std::{
io::{stdin, stdout, Write},
sync::Arc,
@ -30,8 +37,8 @@ use anyhow::{Context, Error};
use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as CrosstermEvent,
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent,
},
execute, terminal,
tty::IsTty,
@ -95,6 +102,7 @@ fn restore_term() -> Result<(), Error> {
execute!(
stdout,
DisableBracketedPaste,
DisableFocusChange,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?;
@ -102,7 +110,11 @@ fn restore_term() -> Result<(), Error> {
}
impl Application {
pub fn new(args: Args, config: Config) -> Result<Self, Error> {
pub fn new(
args: Args,
config: Config,
syn_loader_conf: syntax::Configuration,
) -> Result<Self, Error> {
#[cfg(feature = "integration")]
setup_integration_logging();
@ -129,14 +141,6 @@ impl Application {
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
default_syntax_loader()
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new().context("build compositor")?;
@ -245,6 +249,10 @@ impl Application {
Ok(app)
}
#[cfg(feature = "integration")]
fn render(&mut self) {}
#[cfg(not(feature = "integration"))]
fn render(&mut self) {
let compositor = &mut self.compositor;
@ -275,9 +283,6 @@ impl Application {
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
#[cfg(feature = "integration")]
let mut idle_handled = false;
loop {
if self.editor.should_close() {
return false;
@ -294,26 +299,6 @@ impl Application {
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
}
Some((id, call)) = self.editor.language_servers.incoming.next() => {
self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render();
self.last_render = Instant::now();
}
}
Some(payload) = self.editor.debugger_events.next() => {
let needs_render = self.editor.handle_debugger_message(payload).await;
if needs_render {
self.render();
}
}
Some(config_event) = self.editor.config_events.1.recv() => {
self.handle_config_events(config_event);
self.render();
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
@ -322,26 +307,22 @@ impl Application {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
}
_ = &mut self.editor.idle_timer => {
// idle timeout
self.editor.clear_idle_timer();
self.handle_idle_timeout();
event = self.editor.wait_event() => {
let _idle_handled = self.handle_editor_event(event).await;
#[cfg(feature = "integration")]
{
idle_handled = true;
if _idle_handled {
return true;
}
}
}
}
// for integration tests only, reset the idle timer after every
// event to make a signal when test events are done processing
// event to signal when test events are done processing
#[cfg(feature = "integration")]
{
if idle_handled {
return true;
}
self.editor.reset_idle_timer();
}
}
@ -435,22 +416,122 @@ impl Application {
}
pub fn handle_idle_timeout(&mut self) {
use crate::compositor::EventResult;
let editor_view = self
.compositor
.find::<ui::EditorView>()
.expect("expected at least one EditorView");
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) {
let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
if should_render {
self.render();
}
}
pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult) {
let doc_save_event = match doc_save_event {
Ok(event) => event,
Err(err) => {
self.editor.set_error(err.to_string());
return;
}
};
let doc = match self.editor.document_mut(doc_save_event.doc_id) {
None => {
warn!(
"received document saved event for non-existent doc id: {}",
doc_save_event.doc_id
);
return;
}
Some(doc) => doc,
};
debug!(
"document {:?} saved with revision {}",
doc.path(),
doc_save_event.revision
);
doc.set_last_saved_revision(doc_save_event.revision);
let lines = doc_save_event.text.len_lines();
let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) {
if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
log::error!(
"error setting path for doc '{:?}': {}",
doc.path(),
err.to_string(),
);
self.editor.set_error(err.to_string());
return;
}
let loader = self.editor.syn_loader.clone();
// borrowing the same doc again to get around the borrow checker
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
let _ = self.editor.refresh_language_server(id);
}
// TODO: fix being overwritten by lsp
self.editor.set_status(format!(
"'{}' written, {}L {}B",
get_relative_path(&doc_save_event.path).to_string_lossy(),
lines,
bytes
));
}
#[inline(always)]
pub async fn handle_editor_event(&mut self, event: EditorEvent) -> bool {
log::debug!("received editor event: {:?}", event);
match event {
EditorEvent::DocumentSaved(event) => {
self.handle_document_write(event);
self.render();
}
EditorEvent::ConfigEvent(event) => {
self.handle_config_events(event);
self.render();
}
EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render();
self.last_render = Instant::now();
}
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
if needs_render {
self.render();
}
}
EditorEvent::IdleTimer => {
self.editor.clear_idle_timer();
self.handle_idle_timeout();
#[cfg(feature = "integration")]
{
return true;
}
}
}
false
}
pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
@ -516,14 +597,14 @@ impl Application {
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
let language_id =
doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
let url = match doc.url() {
Some(url) => url,
None => continue, // skip documents with no path
};
let language_id =
doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
url,
doc.version(),
@ -845,7 +926,12 @@ impl Application {
async fn claim_term(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, terminal::EnterAlternateScreen, EnableBracketedPaste)?;
execute!(
stdout,
terminal::EnterAlternateScreen,
EnableBracketedPaste,
EnableFocusChange
)?;
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
if self.config.load().editor.mouse {
execute!(stdout, EnableMouseCapture)?;
@ -871,11 +957,10 @@ impl Application {
self.event_loop(input_stream).await;
let err = self.close().await.err();
let close_errs = self.close().await;
restore_term()?;
if let Some(err) = err {
for err in close_errs {
self.editor.exit_code = 1;
eprintln!("Error: {}", err);
}
@ -883,13 +968,33 @@ impl Application {
Ok(self.editor.exit_code)
}
pub async fn close(&mut self) -> anyhow::Result<()> {
self.jobs.finish().await?;
pub async fn close(&mut self) -> Vec<anyhow::Error> {
// [NOTE] we intentionally do not return early for errors because we
// want to try to run as much cleanup as we can, regardless of
// errors along the way
let mut errs = Vec::new();
if let Err(err) = self
.jobs
.finish(&mut self.editor, Some(&mut self.compositor))
.await
{
log::error!("Error executing job: {}", err);
errs.push(err);
};
if let Err(err) = self.editor.flush_writes().await {
log::error!("Error writing: {}", err);
errs.push(err);
}
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
errs.push(anyhow::format_err!(
"Timed out waiting for language servers to shutdown"
));
}
Ok(())
errs
}
}

@ -32,8 +32,14 @@ impl Args {
"--version" => args.display_version = true,
"--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
"--vsplit" => args.split = Some(Layout::Vertical),
"--hsplit" => args.split = Some(Layout::Horizontal),
"--vsplit" => match args.split {
Some(_) => anyhow::bail!("can only set a split once of a specific type"),
None => args.split = Some(Layout::Vertical),
},
"--hsplit" => match args.split {
Some(_) => anyhow::bail!("can only set a split once of a specific type"),
None => args.split = Some(Layout::Horizontal),
},
"--health" => {
args.health = true;
args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));

@ -27,6 +27,7 @@ use helix_core::{
SmallVec, Tendril, Transaction,
};
use helix_view::{
apply_transaction,
clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
@ -46,13 +47,14 @@ use movement::Movement;
use crate::{
args,
compositor::{self, Component, Compositor},
job::Callback,
keymap::ReverseKeymap,
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, StreamExt};
use std::{collections::HashMap, fmt, future::Future};
use crate::job::{self, Jobs};
use futures_util::StreamExt;
use std::{collections::HashMap, fmt, fmt::Write, future::Future};
use std::{collections::HashSet, num::NonZeroUsize};
use std::{
@ -106,10 +108,11 @@ impl<'a> Context<'a> {
let callback = Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
});
},
));
Ok(call)
});
self.jobs.callback(callback);
@ -859,7 +862,7 @@ fn align_selections(cx: &mut Context) {
changes.sort_unstable_by_key(|(from, _, _)| *from);
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn goto_window(cx: &mut Context, align: Align) {
@ -1289,7 +1292,7 @@ fn replace(cx: &mut Context) {
}
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
})
}
@ -1306,7 +1309,7 @@ where
(range.from(), range.to(), Some(text))
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn switch_case(cx: &mut Context) {
@ -1864,10 +1867,15 @@ fn global_search(cx: &mut Context) {
.hidden(file_picker_config.hidden)
.parents(file_picker_config.parents)
.ignore(file_picker_config.ignore)
.follow_links(file_picker_config.follow_symlinks)
.git_ignore(file_picker_config.git_ignore)
.git_global(file_picker_config.git_global)
.git_exclude(file_picker_config.git_exclude)
.max_depth(file_picker_config.max_depth)
// We always want to ignore the .git directory, otherwise if
// `ignore` is turned off above, we end up with a lot of noise
// in our picker.
.filter_entry(|entry| entry.file_name() != ".git")
.build_parallel()
.run(|| {
let mut searcher = searcher.clone();
@ -1919,8 +1927,8 @@ fn global_search(cx: &mut Context) {
let show_picker = async move {
let all_matches: Vec<FileResult> =
UnboundedReceiverStream::new(all_matches_rx).collect().await;
let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
if all_matches.is_empty() {
editor.set_status("No matches found");
return;
@ -1956,7 +1964,8 @@ fn global_search(cx: &mut Context) {
},
);
compositor.push(Box::new(overlayed(picker)));
});
},
));
Ok(call)
};
cx.jobs.callback(show_picker);
@ -2112,7 +2121,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
match op {
Operation::Delete => {
@ -2126,14 +2135,11 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
}
#[inline]
fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) {
let view_id = view.id;
// then delete
fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: &Selection) {
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
doc.apply(&transaction, view_id);
apply_transaction(&transaction, doc, view);
}
fn delete_selection(cx: &mut Context) {
@ -2224,12 +2230,12 @@ fn append_mode(cx: &mut Context) {
.iter()
.last()
.expect("selection should always have at least one range");
if !last_range.is_empty() && last_range.head == end {
if !last_range.is_empty() && last_range.to() == end {
let transaction = Transaction::change(
doc.text(),
[(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
let selection = doc.selection(view.id).clone().transform(|range| {
@ -2410,29 +2416,28 @@ impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap;
fn label(&self, keymap: &Self::Data) -> Spans {
// formats key bindings, multiple bindings are comma separated,
// individual key presses are joined with `+`
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings
.iter()
.map(|bind| {
bind.iter()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.join("+")
})
.collect::<Vec<String>>()
.join(", ")
bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() {
acc.push_str(", ");
}
bind.iter().fold(false, |needs_plus, key| {
write!(&mut acc, "{}{}", if needs_plus { "+" } else { "" }, key)
.expect("Writing to a string can only fail on an Out-Of-Memory error");
true
});
acc
})
};
match self {
MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
None => doc.as_str().into(),
None => format!("{} [{}]", doc, name).into(),
},
MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
None => (*doc).into(),
None => format!("{} [{}]", doc, name).into(),
},
}
}
@ -2472,12 +2477,12 @@ pub fn command_palette(cx: &mut Context) {
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker
cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
cx.callback = Some(Box::new(|compositor, cx| {
if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker);
} else {
cx.editor.set_error("no last picker")
}
// XXX: figure out how to show error when no last picker lifetime
// cx.editor.set_error("no last picker")
}));
}
@ -2501,13 +2506,6 @@ fn insert_at_line_end(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
/// example because we just applied the same changes while saving.
enum Modified {
SetUnmodified,
LeaveModified,
}
// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
// it applies them, but only if the doc hasn't changed.
//
@ -2516,34 +2514,44 @@ enum Modified {
async fn make_format_callback(
doc_id: DocumentId,
doc_version: i32,
modified: Modified,
view_id: ViewId,
format: impl Future<Output = Result<Transaction, FormatterError>> + Send + 'static,
write: Option<(Option<PathBuf>, bool)>,
) -> anyhow::Result<job::Callback> {
let format = format.await?;
let call: job::Callback = Box::new(move |editor, _compositor| {
if !editor.documents.contains_key(&doc_id) {
let format = format.await;
let call: job::Callback = Callback::Editor(Box::new(move |editor| {
if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) {
return;
}
let scrolloff = editor.config().scrolloff;
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor);
if doc.version() == doc_version {
doc.apply(&format, view.id);
doc.append_changes_to_history(view.id);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
if let Modified::SetUnmodified = modified {
doc.reset_modified();
let view = view_mut!(editor, view_id);
if let Ok(format) = format {
if doc.version() == doc_version {
apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
} else {
log::info!("discarded formatting changes because the document changed");
}
} else {
log::info!("discarded formatting changes because the document changed");
}
});
if let Some((path, force)) = write {
let id = doc.id();
if let Err(err) = editor.save(id, path, force) {
editor.set_error(format!("Error saving: {}", err));
}
}
}));
Ok(call)
}
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
pub enum Open {
Below,
Above,
@ -2616,7 +2624,7 @@ fn open(cx: &mut Context, open: Open) {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
// o inserts a new line after each line with a selection
@ -2637,7 +2645,7 @@ fn normal_mode(cx: &mut Context) {
cx.editor.mode = Mode::Normal;
let (view, doc) = current!(cx.editor);
try_restore_indent(doc, view.id);
try_restore_indent(doc, view);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
@ -2654,7 +2662,7 @@ fn normal_mode(cx: &mut Context) {
}
}
fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
fn try_restore_indent(doc: &mut Document, view: &mut View) {
use helix_core::chars::char_is_whitespace;
use helix_core::Operation;
@ -2673,18 +2681,18 @@ fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
let doc_changes = doc.changes().changes();
let text = doc.text().slice(..);
let range = doc.selection(view_id).primary();
let range = doc.selection(view.id).primary();
let pos = range.cursor(text);
let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
// Removes tailing whitespaces.
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
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);
apply_transaction(&transaction, doc, view);
}
}
@ -2882,7 +2890,7 @@ pub mod insert {
/// Exclude the cursor in range.
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
if range.to() == cursor.to() {
if range.to() == cursor.to() && text.len_chars() != cursor.to() {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, cursor.to()),
@ -2998,7 +3006,7 @@ pub mod insert {
let (view, doc) = current!(cx.editor);
if let Some(t) = transaction {
doc.apply(&t, view.id);
apply_transaction(&t, doc, view);
}
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@ -3014,13 +3022,13 @@ pub mod insert {
// TODO: round out to nearest indentation level (for example a line with 3 spaces should
// indent by one to reach 4 spaces).
let indent = Tendril::from(doc.indent_unit());
let indent = Tendril::from(doc.indent_style.as_str());
let transaction = Transaction::insert(
doc.text(),
&doc.selection(view.id).clone().cursors(doc.text().slice(..)),
indent,
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
pub fn insert_newline(cx: &mut Context) {
@ -3107,14 +3115,14 @@ pub mod insert {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
pub fn delete_char_backward(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let indent_unit = doc.indent_unit();
let indent_unit = doc.indent_style.as_str();
let tab_size = doc.tab_width();
let auto_pairs = doc.auto_pairs(cx.editor);
@ -3201,7 +3209,7 @@ pub mod insert {
}
});
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
@ -3219,7 +3227,7 @@ pub mod insert {
None,
)
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
@ -3230,8 +3238,8 @@ pub mod insert {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
let cursor = Range::point(range.cursor(text));
let next = movement::move_prev_word_start(text, cursor, count);
let anchor = movement::move_prev_word_start(text, range, count).from();
let next = Range::new(anchor, range.cursor(text));
exclude_cursor(text, next, range)
});
delete_selection_insert_mode(doc, view, &selection);
@ -3244,10 +3252,11 @@ pub mod insert {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc
.selection(view.id)
.clone()
.transform(|range| movement::move_next_word_start(text, range, count));
let selection = doc.selection(view.id).clone().transform(|range| {
let head = movement::move_next_word_end(text, range, count).to();
Range::new(range.cursor(text), head)
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
@ -3260,7 +3269,7 @@ fn undo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.undo(view.id) {
if !doc.undo(view) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3271,7 +3280,7 @@ fn redo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.redo(view.id) {
if !doc.redo(view) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3283,7 +3292,7 @@ fn earlier(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.earlier(view.id, UndoKind::Steps(1)) {
if !doc.earlier(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3295,7 +3304,7 @@ fn later(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.later(view.id, UndoKind::Steps(1)) {
if !doc.later(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3347,9 +3356,15 @@ fn yank_joined_to_clipboard_impl(
.map(Cow::into_owned)
.collect();
let clipboard_text = match clipboard_type {
ClipboardType::Clipboard => "system clipboard",
ClipboardType::Selection => "primary clipboard",
};
let msg = format!(
"joined and yanked {} selection(s) to system clipboard",
"joined and yanked {} selection(s) to {}",
values.len(),
clipboard_text,
);
let joined = values.join(separator);
@ -3378,6 +3393,11 @@ fn yank_main_selection_to_clipboard_impl(
let (view, doc) = current!(editor);
let text = doc.text().slice(..);
let message_text = match clipboard_type {
ClipboardType::Clipboard => "yanked main selection to system clipboard",
ClipboardType::Selection => "yanked main selection to primary clipboard",
};
let value = doc.selection(view.id).primary().fragment(text);
if let Err(e) = editor
@ -3387,7 +3407,7 @@ fn yank_main_selection_to_clipboard_impl(
bail!("Couldn't set system clipboard content: {}", e);
}
editor.set_status("yanked main selection to system clipboard");
editor.set_status(message_text);
Ok(())
}
@ -3413,7 +3433,7 @@ enum Paste {
Cursor,
}
fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste, count: usize) {
fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Paste, count: usize) {
let repeat = std::iter::repeat(
values
.last()
@ -3456,7 +3476,7 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste,
};
(pos, pos, values.next())
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
@ -3548,7 +3568,7 @@ fn replace_with_yanked(cx: &mut Context) {
}
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
}
}
@ -3571,7 +3591,7 @@ fn replace_selections_with_clipboard_impl(
)
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
}
@ -3628,7 +3648,7 @@ fn indent(cx: &mut Context) {
let lines = get_lines(doc, view.id);
// Indent by one level
let indent = Tendril::from(doc.indent_unit().repeat(count));
let indent = Tendril::from(doc.indent_style.as_str().repeat(count));
let transaction = Transaction::change(
doc.text(),
@ -3641,7 +3661,7 @@ fn indent(cx: &mut Context) {
Some((pos, pos, Some(indent.clone())))
}),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn unindent(cx: &mut Context) {
@ -3680,7 +3700,7 @@ fn unindent(cx: &mut Context) {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn format_selections(cx: &mut Context) {
@ -3727,7 +3747,7 @@ fn format_selections(cx: &mut Context) {
// language_server.offset_encoding(),
// );
// doc.apply(&transaction, view.id);
// apply_transaction(&transaction, doc, view);
}
}
@ -3782,7 +3802,7 @@ fn join_selections_inner(cx: &mut Context, select_space: bool) {
Transaction::change(doc.text(), changes.into_iter())
};
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@ -3935,7 +3955,7 @@ fn toggle_comments(cx: &mut Context) {
.map(|tc| tc.as_ref());
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
}
@ -3991,7 +4011,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
.map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
fn rotate_selection_contents_forward(cx: &mut Context) {
@ -4457,13 +4477,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
(" ", "... or any character acting as a pair"),
];
cx.editor.autoinfo = Some(Info::new(
title,
help_text
.into_iter()
.map(|(col1, col2)| (col1.to_string(), col2.to_string()))
.collect(),
));
cx.editor.autoinfo = Some(Info::new(title, &help_text));
}
fn surround_add(cx: &mut Context) {
@ -4487,7 +4501,7 @@ fn surround_add(cx: &mut Context) {
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
})
}
@ -4526,7 +4540,7 @@ fn surround_replace(cx: &mut Context) {
(pos, pos + 1, Some(t))
}),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
});
})
}
@ -4553,7 +4567,7 @@ fn surround_delete(cx: &mut Context) {
let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
})
}
@ -4651,6 +4665,8 @@ fn shell_impl(
if input.is_some() || cfg!(windows) {
process.stdin(Stdio::piped());
} else {
process.stdin(Stdio::null());
}
let mut process = match process.spawn() {
@ -4728,7 +4744,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
if behavior != &ShellBehavior::Ignore {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
}
@ -4791,7 +4807,7 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
});
let transaction = Transaction::change(text, changes);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
/// Increment object under cursor by count.
@ -4884,7 +4900,7 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let transaction = Transaction::change(doc.text(), changes);
let transaction = transaction.with_selection(selection.clone());
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
}

@ -118,11 +118,14 @@ fn dap_callback<T, F>(
let callback = Box::pin(async move {
let json = call.await?;
let response = serde_json::from_value(json)?;
let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
});
let call: Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
callback(editor, compositor, response)
},
));
Ok(call)
});
jobs.callback(callback);
}
@ -274,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) {
let completions = template.completion.clone();
let name = template.name.clone();
let callback = Box::pin(async move {
let call: Callback = Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, name, Vec::new());
compositor.push(Box::new(prompt));
});
let call: Callback =
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, name, Vec::new());
compositor.push(Box::new(prompt));
}));
Ok(call)
});
cx.jobs.callback(callback);
@ -332,10 +336,11 @@ fn debug_parameter_prompt(
let config_name = config_name.clone();
let params = params.clone();
let callback = Box::pin(async move {
let call: Callback = Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, config_name, params);
compositor.push(Box::new(prompt));
});
let call: Callback =
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, config_name, params);
compositor.push(Box::new(prompt));
}));
Ok(call)
});
cx.jobs.callback(callback);
@ -582,7 +587,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
None => return,
};
let callback = Box::pin(async move {
let call: Callback = Box::new(move |editor, compositor| {
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
let mut prompt = Prompt::new(
"condition:".into(),
None,
@ -610,7 +615,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
prompt.insert_str(&condition, editor)
}
compositor.push(Box::new(prompt));
});
}));
Ok(call)
});
cx.jobs.callback(callback);
@ -624,7 +629,7 @@ pub fn dap_edit_log(cx: &mut Context) {
None => return,
};
let callback = Box::pin(async move {
let call: Callback = Box::new(move |editor, compositor| {
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
let mut prompt = Prompt::new(
"log-message:".into(),
None,
@ -651,7 +656,7 @@ pub fn dap_edit_log(cx: &mut Context) {
prompt.insert_str(&log_message, editor);
}
compositor.push(Box::new(prompt));
});
}));
Ok(call)
});
cx.jobs.callback(callback);

@ -1,6 +1,6 @@
use helix_lsp::{
block_on,
lsp::{self, DiagnosticSeverity, NumberOrString},
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
@ -9,7 +9,7 @@ use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, Selection};
use helix_view::{editor::Action, theme::Style};
use helix_view::{apply_transaction, editor::Action, theme::Style};
use crate::{
compositor::{self, Compositor},
@ -18,7 +18,9 @@ use crate::{
},
};
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
use std::{
borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc,
};
/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
@ -43,23 +45,32 @@ impl ui::menu::Item for lsp::Location {
type Data = PathBuf;
fn label(&self, cwdir: &Self::Data) -> Spans {
let file: Cow<'_, str> = (self.uri.scheme() == "file")
.then(|| {
self.uri
.to_file_path()
.map(|path| {
// strip root prefix
path.strip_prefix(&cwdir)
.map(|path| path.to_path_buf())
.unwrap_or(path)
})
.map(|path| Cow::from(path.to_string_lossy().into_owned()))
.ok()
})
.flatten()
.unwrap_or_else(|| self.uri.as_str().into());
let line = self.range.start.line;
format!("{}:{}", file, line).into()
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String::with_capacity(self.uri.as_str().len());
if self.uri.scheme() == "file" {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
let mut write_path_to_res = || -> Option<()> {
let path = self.uri.to_file_path().ok()?;
res.push_str(&path.strip_prefix(&cwdir).unwrap_or(&path).to_string_lossy());
Some(())
};
write_path_to_res();
} else {
// Never allocates since we declared the string with this capacity already.
res.push_str(self.uri.as_str());
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", self.range.start.line)
.expect("Will only failed if allocating fail");
res.into()
}
}
@ -73,10 +84,8 @@ impl ui::menu::Item for lsp::SymbolInformation {
} else {
match self.location.uri.to_file_path() {
Ok(path) => {
let relative_path = helix_core::path::get_relative_path(path.as_path())
.to_string_lossy()
.into_owned();
format!("{} ({})", &self.name, relative_path).into()
let get_relative_path = path::get_relative_path(path.as_path());
format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
}
Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
}
@ -115,24 +124,21 @@ impl ui::menu::Item for PickerDiagnostic {
// remove background as it is distracting in the picker list
style.bg = None;
let code = self
let code: Cow<'_, str> = self
.diag
.code
.as_ref()
.map(|c| match c {
NumberOrString::Number(n) => n.to_string(),
NumberOrString::String(s) => s.to_string(),
NumberOrString::Number(n) => n.to_string().into(),
NumberOrString::String(s) => s.as_str().into(),
})
.map(|code| format!(" ({})", code))
.unwrap_or_default();
let path = match format {
DiagnosticsFormat::HideSourcePath => String::new(),
DiagnosticsFormat::ShowSourcePath => {
let path = path::get_truncated_path(self.url.path())
.to_string_lossy()
.into_owned();
format!("{}: ", path)
let path = path::get_truncated_path(self.url.path());
format!("{}: ", path.to_string_lossy())
}
};
@ -211,7 +217,6 @@ fn sym_picker(
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
cx.editor.set_error(err);
return;
}
@ -421,6 +426,63 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
}
}
/// Determines the category of the `CodeAction` using the `CodeAction::kind` field.
/// Returns a number that represent these categories.
/// Categories with a lower number should be displayed first.
///
///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker)
/// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings.
///
/// The order used here is modeled after the [vscode sourcecode](https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts>)
fn action_category(action: &CodeActionOrCommand) -> u32 {
if let CodeActionOrCommand::CodeAction(CodeAction {
kind: Some(kind), ..
}) = action
{
let mut components = kind.as_str().split('.');
match components.next() {
Some("quickfix") => 0,
Some("refactor") => match components.next() {
Some("extract") => 1,
Some("inline") => 2,
Some("rewrite") => 3,
Some("move") => 4,
Some("surround") => 5,
_ => 7,
},
Some("source") => 6,
_ => 7,
}
} else {
7
}
}
fn action_prefered(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
is_preferred: Some(true),
..
})
)
}
fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
diagnostics: Some(diagnostics),
..
}) if !diagnostics.is_empty()
)
}
pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@ -452,15 +514,60 @@ pub fn code_action(cx: &mut Context) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let actions = match response {
let mut actions = match response {
Some(a) => a,
None => return,
};
// remove disabled code actions
actions.retain(|action| {
matches!(
action,
CodeActionOrCommand::Command(_)
| CodeActionOrCommand::CodeAction(CodeAction { disabled: None, .. })
)
});
if actions.is_empty() {
editor.set_status("No code actions available");
return;
}
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because langauge servers are usually tested against it.
// VScode sorts the codeaction two times:
//
// First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction
// that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate
// submenus that only contain a certain category (see `action_category`) of actions.
//
// Below this done in in a single sorting step
actions.sort_by(|action1, action2| {
// sort actions by category
let order = action_category(action1).cmp(&action_category(action2));
if order != Ordering::Equal {
return order;
}
// within the categories sort by relevancy.
// Modeled after the `codeActionsComparator` function in vscode:
// https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeAction.ts
// if one code action fixes a diagnostic but the other one doesn't show it first
let order = action_fixes_diagnostics(action1)
.cmp(&action_fixes_diagnostics(action2))
.reverse();
if order != Ordering::Equal {
return order;
}
// if one of the codeactions is marked as prefered show it first
// otherwise keep the original LSP sorting
action_prefered(action1)
.cmp(&action_prefered(action2))
.reverse()
});
let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
@ -596,9 +703,7 @@ pub fn apply_workspace_edit(
}
};
let doc = editor
.document_mut(doc_id)
.expect("Document for document_changes not found");
let doc = doc_mut!(editor, &doc_id);
// Need to determine a view for apply/append_changes_to_history
let selections = doc.selections();
@ -619,7 +724,7 @@ pub fn apply_workspace_edit(
text_edits,
offset_encoding,
);
doc.apply(&transaction, view_id);
apply_transaction(&transaction, doc, view_mut!(editor, view_id));
doc.append_changes_to_history(view_id);
};

@ -1,8 +1,13 @@
use std::ops::Deref;
use crate::job::Job;
use super::*;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use helix_view::{
apply_transaction,
editor::{Action, CloseError, ConfigEvent},
};
use ui::completers::{self, Completer};
#[derive(Clone)]
@ -16,6 +21,8 @@ pub struct TypableCommand {
}
fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
log::debug!("quitting...");
if event != PromptEvent::Validate {
return Ok(());
}
@ -27,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
buffers_remaining_impl(cx.editor)?
}
cx.block_try_flush_writes()?;
cx.editor.close(view!(cx.editor).id);
Ok(())
@ -43,6 +51,7 @@ fn force_quit(
ensure!(args.is_empty(), ":quit! takes no arguments");
cx.block_try_flush_writes()?;
cx.editor.close(view!(cx.editor).id);
Ok(())
@ -67,14 +76,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
}
fn buffer_close_by_ids_impl(
editor: &mut Editor,
cx: &mut compositor::Context,
doc_ids: &[DocumentId],
force: bool,
) -> anyhow::Result<()> {
cx.block_try_flush_writes()?;
let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
.iter()
.filter_map(|&doc_id| {
if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) {
if let Err(CloseError::BufferModified(name)) = cx.editor.close_document(doc_id, force) {
Some((doc_id, name))
} else {
None
@ -83,11 +94,11 @@ fn buffer_close_by_ids_impl(
.unzip();
if let Some(first) = modified_ids.first() {
let current = doc!(editor);
let current = doc!(cx.editor);
// If the current document is unmodified, and there are modified
// documents, switch focus to the first modified doc.
if !modified_ids.contains(&current.id()) {
editor.switch(*first, Action::Replace);
cx.editor.switch(*first, Action::Replace);
}
bail!(
"{} unsaved buffer(s) remaining: {:?}",
@ -146,7 +157,7 @@ fn buffer_close(
}
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx.editor, &document_ids, false)
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close(
@ -159,7 +170,7 @@ fn force_buffer_close(
}
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx.editor, &document_ids, true)
buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
@ -181,7 +192,7 @@ fn buffer_close_others(
}
let document_ids = buffer_gather_others_impl(cx.editor);
buffer_close_by_ids_impl(cx.editor, &document_ids, false)
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close_others(
@ -194,7 +205,7 @@ fn force_buffer_close_others(
}
let document_ids = buffer_gather_others_impl(cx.editor);
buffer_close_by_ids_impl(cx.editor, &document_ids, true)
buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
@ -211,7 +222,7 @@ fn buffer_close_all(
}
let document_ids = buffer_gather_all_impl(cx.editor);
buffer_close_by_ids_impl(cx.editor, &document_ids, false)
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close_all(
@ -224,7 +235,7 @@ fn force_buffer_close_all(
}
let document_ids = buffer_gather_all_impl(cx.editor);
buffer_close_by_ids_impl(cx.editor, &document_ids, true)
buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_next(
@ -258,39 +269,30 @@ fn write_impl(
path: Option<&Cow<str>>,
force: bool,
) -> anyhow::Result<()> {
let auto_format = cx.editor.config().auto_format;
let editor_auto_fmt = cx.editor.config().auto_format;
let jobs = &mut cx.jobs;
let doc = doc_mut!(cx.editor);
let (view, doc) = current!(cx.editor);
let path = path.map(AsRef::as_ref);
if let Some(ref path) = path {
doc.set_path(Some(path.as_ref().as_ref()))
.context("invalid filepath")?;
}
if doc.path().is_none() {
bail!("cannot write a buffer without a filename");
}
let fmt = if auto_format {
let fmt = if editor_auto_fmt {
doc.auto_format().map(|fmt| {
let shared = fmt.shared();
let callback = make_format_callback(
doc.id(),
doc.version(),
Modified::SetUnmodified,
shared.clone(),
view.id,
fmt,
Some((path.map(Into::into), force)),
);
jobs.callback(callback);
shared
jobs.add(Job::with_callback(callback).wait_before_exiting());
})
} else {
None
};
let future = doc.format_and_save(fmt, force);
cx.jobs.add(Job::new(future).wait_before_exiting());
if path.is_some() {
if fmt.is_none() {
let id = doc.id();
doc.detect_language(cx.editor.syn_loader.clone());
let _ = cx.editor.refresh_language_server(id);
cx.editor.save(id, path, force)?;
}
Ok(())
@ -343,10 +345,9 @@ fn format(
return Ok(());
}
let doc = doc!(cx.editor);
let (view, doc) = current!(cx.editor);
if let Some(format) = doc.format() {
let callback =
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
let callback = make_format_callback(doc.id(), doc.version(), view.id, format, None);
cx.jobs.callback(callback);
}
@ -462,7 +463,7 @@ fn set_line_ending(
}
}),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
@ -480,7 +481,7 @@ fn earlier(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
let success = doc.earlier(view.id, uk);
let success = doc.earlier(view, uk);
if !success {
cx.editor.set_status("Already at oldest change");
}
@ -499,7 +500,7 @@ fn later(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
let success = doc.later(view.id, uk);
let success = doc.later(view, uk);
if !success {
cx.editor.set_status("Already at newest change");
}
@ -517,7 +518,7 @@ fn write_quit(
}
write_impl(cx, args.first(), false)?;
helix_lsp::block_on(cx.jobs.finish())?;
cx.block_try_flush_writes()?;
quit(cx, &[], event)
}
@ -531,6 +532,7 @@ fn force_write_quit(
}
write_impl(cx, args.first(), true)?;
cx.block_try_flush_writes()?;
force_quit(cx, &[], event)
}
@ -559,110 +561,128 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()>
Ok(())
}
fn write_all_impl(
pub fn write_all_impl(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
quit: bool,
force: bool,
write_scratch: bool,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let mut errors = String::new();
let mut errors: Vec<&'static str> = Vec::new();
let auto_format = cx.editor.config().auto_format;
let jobs = &mut cx.jobs;
let current_view = view!(cx.editor);
// save all documents
for doc in &mut cx.editor.documents.values_mut() {
if doc.path().is_none() {
errors.push_str("cannot write a buffer without a filename\n");
continue;
}
let saves: Vec<_> = cx
.editor
.documents
.values_mut()
.filter_map(|doc| {
if !doc.is_modified() {
return None;
}
if doc.path().is_none() {
if write_scratch {
errors.push("cannot write a buffer without a filename\n");
}
return None;
}
if !doc.is_modified() {
continue;
}
// Look for a view to apply the formatting change to. If the document
// is in the current view, just use that. Otherwise, since we don't
// have any other metric available for better selection, just pick
// the first view arbitrarily so that we still commit the document
// state for undos. If somehow we have a document that has not been
// initialized with any view, initialize it with the current view.
let target_view = if doc.selections().contains_key(&current_view.id) {
current_view.id
} else if let Some(view) = doc.selections().keys().next() {
*view
} else {
doc.ensure_view_init(current_view.id);
current_view.id
};
let fmt = if auto_format {
doc.auto_format().map(|fmt| {
let callback = make_format_callback(
doc.id(),
doc.version(),
target_view,
fmt,
Some((None, force)),
);
jobs.add(Job::with_callback(callback).wait_before_exiting());
})
} else {
None
};
if fmt.is_none() {
return Some(doc.id());
}
let fmt = if auto_format {
doc.auto_format().map(|fmt| {
let shared = fmt.shared();
let callback = make_format_callback(
doc.id(),
doc.version(),
Modified::SetUnmodified,
shared.clone(),
);
jobs.callback(callback);
shared
})
} else {
None
};
let future = doc.format_and_save(fmt, force);
jobs.add(Job::new(future).wait_before_exiting());
}
})
.collect();
if quit {
if !force {
buffers_remaining_impl(cx.editor)?;
}
// manually call save for the rest of docs that don't have a formatter
for id in saves {
cx.editor.save::<PathBuf>(id, None, force)?;
}
// close all views
let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
cx.editor.close(view_id);
}
if !errors.is_empty() && !force {
bail!("{:?}", errors);
}
bail!(errors)
Ok(())
}
fn write_all(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_all_impl(cx, args, event, false, false)
write_all_impl(cx, false, true)
}
fn write_all_quit(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_all_impl(cx, args, event, true, false)
write_all_impl(cx, false, true)?;
quit_all_impl(cx, false)
}
fn force_write_all_quit(
cx: &mut compositor::Context,
args: &[Cow<str>],
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_all_impl(cx, args, event, true, true)
let _ = write_all_impl(cx, true, true);
quit_all_impl(cx, true)
}
fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<()> {
cx.block_try_flush_writes()?;
if !force {
buffers_remaining_impl(editor)?;
buffers_remaining_impl(cx.editor)?;
}
// close all views
let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
editor.close(view_id);
cx.editor.close(view_id);
}
Ok(())
@ -677,7 +697,7 @@ fn quit_all(
return Ok(());
}
quit_all_impl(cx.editor, false)
quit_all_impl(cx, false)
}
fn force_quit_all(
@ -689,7 +709,7 @@ fn force_quit_all(
return Ok(());
}
quit_all_impl(cx.editor, true)
quit_all_impl(cx, true)
}
fn cquit(
@ -705,9 +725,9 @@ fn cquit(
.first()
.and_then(|code| code.parse::<i32>().ok())
.unwrap_or(1);
cx.editor.exit_code = exit_code;
quit_all_impl(cx.editor, false)
cx.editor.exit_code = exit_code;
quit_all_impl(cx, false)
}
fn force_cquit(
@ -725,7 +745,7 @@ fn force_cquit(
.unwrap_or(1);
cx.editor.exit_code = exit_code;
quit_all_impl(cx.editor, true)
quit_all_impl(cx, true)
}
fn theme(
@ -752,16 +772,21 @@ fn theme(
};
}
PromptEvent::Validate => {
let theme_name = args.first().with_context(|| "Theme name not provided")?;
let theme = cx
.editor
.theme_loader
.load(theme_name)
.with_context(|| "Theme does not exist")?;
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
if let Some(theme_name) = args.first() {
let theme = cx
.editor
.theme_loader
.load(theme_name)
.with_context(|| "Theme does not exist")?;
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
}
cx.editor.set_theme(theme);
} else {
let name = cx.editor.theme.name().to_string();
cx.editor.set_status(name);
}
cx.editor.set_theme(theme);
}
};
@ -883,7 +908,7 @@ fn replace_selections_with_clipboard_impl(
(range.from(), range.to(), Some(contents.as_str().into()))
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
}
@ -1004,11 +1029,29 @@ fn reload(
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
doc.reload(view.id).map(|_| {
doc.reload(view).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})
}
/// Update the [`Document`] if it has been modified.
fn update(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let (_view, doc) = current!(cx.editor);
if doc.is_modified() {
write(cx, args, event)
} else {
Ok(())
}
}
fn lsp_restart(
cx: &mut compositor::Context,
_args: &[Cow<str>],
@ -1057,7 +1100,22 @@ fn tree_sitter_scopes(
let pos = doc.selection(view.id).primary().cursor(text);
let scopes = indent::get_scopes(doc.syntax(), text, pos);
cx.editor.set_status(format!("scopes: {:?}", &scopes));
let contents = format!("```json\n{:?}\n````", scopes);
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup);
},
));
Ok(call)
};
cx.jobs.callback(callback);
Ok(())
}
@ -1398,7 +1456,7 @@ fn sort_impl(
.map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
@ -1442,7 +1500,7 @@ fn reflow(
(range.from(), range.to(), Some(reflowed_text))
});
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
view.ensure_cursor_in_view(doc, scrolloff);
@ -1470,15 +1528,18 @@ fn tree_sitter_subtree(
.root_node()
.descendant_for_byte_range(from, to)
{
let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
let mut contents = String::from("```tsq\n");
helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?;
contents.push_str("\n```");
let callback = async move {
let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup);
});
},
));
Ok(call)
};
@ -1586,8 +1647,8 @@ fn run_shell_command(
if !output.is_empty() {
let callback = async move {
let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(
format!("```sh\n{}\n```", output),
editor.syn_loader.clone(),
@ -1596,7 +1657,8 @@ fn run_shell_command(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
});
},
));
Ok(call)
};
@ -1809,7 +1871,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "theme",
aliases: &[],
doc: "Change the editor theme.",
doc: "Change the editor theme (show current theme if no name specified).",
fun: theme,
completer: Some(completers::theme),
},
@ -1918,6 +1980,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: reload,
completer: None,
},
TypableCommand {
name: "update",
aliases: &[],
doc: "Write changes only if the file has been modified.",
fun: update,
completer: None,
},
TypableCommand {
name: "lsp-restart",
aliases: &[],
@ -2068,7 +2137,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "insert-output",
aliases: &[],
doc: "Run shell command, inserting output after each selection.",
doc: "Run shell command, inserting output before each selection.",
fun: insert_output,
completer: None,
},
@ -2106,7 +2175,7 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
.collect()
});
pub fn command_mode(cx: &mut Context) {
pub(super) fn command_mode(cx: &mut Context) {
let mut prompt = Prompt::new(
":".into(),
Some(':'),

@ -27,6 +27,16 @@ pub struct Context<'a> {
pub jobs: &'a mut Jobs,
}
impl<'a> Context<'a> {
/// Waits on all pending jobs, and then tries to flush all pending write
/// operations for all documents.
pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> {
tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?;
tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
Ok(())
}
}
pub trait Component: Any + AnyComponent {
/// Process input events, return true if handled.
fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {

@ -5,7 +5,11 @@ use crate::compositor::Compositor;
use futures_util::future::{BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt};
pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
pub enum Callback {
EditorCompositor(Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>),
Editor(Box<dyn FnOnce(&mut Editor) + Send>),
}
pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
pub struct Job {
@ -68,9 +72,10 @@ impl Jobs {
) {
match call {
Ok(None) => {}
Ok(Some(call)) => {
call(editor, compositor);
}
Ok(Some(call)) => match call {
Callback::EditorCompositor(call) => call(editor, compositor),
Callback::Editor(call) => call(editor),
},
Err(e) => {
editor.set_error(format!("Async job failed: {}", e));
}
@ -93,13 +98,32 @@ impl Jobs {
}
/// Blocks until all the jobs that need to be waited on are done.
pub async fn finish(&mut self) -> anyhow::Result<()> {
pub async fn finish(
&mut self,
editor: &mut Editor,
mut compositor: Option<&mut Compositor>,
) -> anyhow::Result<()> {
log::debug!("waiting on jobs...");
let mut wait_futures = std::mem::take(&mut self.wait_futures);
while let (Some(job), tail) = wait_futures.into_future().await {
match job {
Ok(_) => {
Ok(callback) => {
wait_futures = tail;
if let Some(callback) = callback {
// clippy doesn't realize this is an error without the derefs
#[allow(clippy::needless_option_as_deref)]
match callback {
Callback::EditorCompositor(call) if compositor.is_some() => {
call(editor, compositor.as_deref_mut().unwrap())
}
Callback::Editor(call) => call(editor),
// skip callbacks for which we don't have the necessary references
_ => (),
}
}
}
Err(e) => {
self.wait_futures = tail;

@ -209,11 +209,11 @@ pub fn default() -> HashMap<Mode, Keymap> {
"j" => jumplist_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
"g" => diagnostics_picker,
"G" => workspace_diagnostics_picker,
"d" => diagnostics_picker,
"D" => workspace_diagnostics_picker,
"a" => code_action,
"'" => last_picker,
"d" => { "Debug (experimental)" sticky=true
"g" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"b" => dap_toggle_breakpoint,
"c" => dap_continue,

@ -139,8 +139,18 @@ FLAGS:
Err(err) => return Err(Error::new(err)),
};
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
helix_core::config::default_syntax_loader()
});
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config).context("unable to create new application")?;
let mut app = Application::new(args, config, syn_loader_conf)
.context("unable to create new application")?;
let exit_code = app.run(&mut EventStream::new()).await?;

@ -1,5 +1,5 @@
use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::editor::CompleteAction;
use helix_view::{apply_transaction, editor::CompleteAction};
use tui::buffer::Buffer as Surface;
use tui::text::Spans;
@ -92,11 +92,15 @@ impl Completion {
pub fn new(
editor: &Editor,
items: Vec<CompletionItem>,
mut items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
) -> Self {
// Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false));
// Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
@ -143,11 +147,11 @@ impl Completion {
let (view, doc) = current!(editor);
// if more text was entered, remove it
doc.restore(view.id);
doc.restore(view);
match event {
PromptEvent::Abort => {
doc.restore(view.id);
doc.restore(view);
editor.last_completion = None;
}
PromptEvent::Update => {
@ -164,7 +168,7 @@ impl Completion {
// initialize a savepoint
doc.savepoint();
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@ -183,7 +187,7 @@ impl Completion {
trigger_offset,
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@ -213,7 +217,7 @@ impl Completion {
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
}
}
}
@ -295,6 +299,27 @@ impl Completion {
pub fn is_empty(&self) -> bool {
self.popup.contents().is_empty()
}
pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
// > If computing full completion items is expensive, servers can additionally provide a
// > handler for the completion item resolve request. ...
// > A typical use case is for example: the `textDocument/completion` request doesn't fill
// > in the `documentation` property for returned completion items since it is expensive
// > to compute. When the item is selected in the user interface then a
// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
// > The returned completion item should have the documentation property filled in.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
match self.popup.contents_mut().selection_mut() {
Some(item) if item.documentation.is_none() => {
let doc = doc!(cx.editor);
if let Some(resolved_item) = Self::resolve_completion_item(doc, item.clone()) {
*item = resolved_item;
}
true
}
_ => false,
}
}
}
impl Component for Completion {

@ -1,7 +1,8 @@
use crate::{
commands,
compositor::{Component, Context, Event, EventResult},
job, key,
job::{self, Callback},
key,
keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners},
};
@ -13,9 +14,10 @@ use helix_core::{
movement::Direction,
syntax::{self, HighlightEvent},
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection, Transaction,
visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
apply_transaction,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig, RainbowIndentOptions},
graphics::{Color, CursorKind, Modifier, Rect, Style},
@ -23,7 +25,7 @@ use helix_view::{
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use std::{borrow::Cow, path::PathBuf};
use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
use tui::buffer::Buffer as Surface;
@ -118,9 +120,19 @@ impl EditorView {
if is_focused && editor.config().cursorline {
Self::highlight_cursorline(doc, view, surface, theme);
}
if is_focused && editor.config().cursorcolumn {
Self::highlight_cursorcolumn(doc, view, surface, theme);
}
let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
if diagnostic.is_empty() {
continue;
}
highlights = Box::new(syntax::merge(highlights, diagnostic));
}
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
@ -264,7 +276,7 @@ impl EditorView {
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity;
let get_scope_of = |scope| {
theme
@ -285,22 +297,38 @@ impl EditorView {
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
doc.diagnostics()
.iter()
.map(|diagnostic| {
let diagnostic_scope = match diagnostic.severity {
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
Some(Severity::Warning) => warning,
Some(Severity::Error) => error,
_ => r#default,
};
(
diagnostic_scope,
diagnostic.range.start..diagnostic.range.end,
)
})
.collect()
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut info_vec = Vec::new();
let mut hint_vec = Vec::new();
let mut warning_vec = Vec::new();
let mut error_vec = Vec::new();
for diagnostic in doc.diagnostics() {
// Separate diagnostics into different Vecs by severity.
let (vec, scope) = match diagnostic.severity {
Some(Severity::Info) => (&mut info_vec, info),
Some(Severity::Hint) => (&mut hint_vec, hint),
Some(Severity::Warning) => (&mut warning_vec, warning),
Some(Severity::Error) => (&mut error_vec, error),
_ => (&mut default_vec, r#default),
};
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, range)) if diagnostic.range.start <= range.end => {
// This branch merges overlapping diagnostics, assuming that the current
// diagnostic starts on range.start or later. If this assertion fails,
// we will discard some part of `diagnostic`. This implies that
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
debug_assert!(range.start <= diagnostic.range.start);
range.end = diagnostic.range.end.max(range.end)
}
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
}
}
[default_vec, info_vec, hint_vec, warning_vec, error_vec]
}
/// Get highlight spans for selections in a document view.
@ -401,7 +429,7 @@ impl EditorView {
let characters = &whitespace.characters;
let mut spans = Vec::new();
let mut visual_x = 0u16;
let mut visual_x = 0usize;
let mut line = 0u16;
let tab_width = doc.tab_width();
let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
@ -439,9 +467,7 @@ impl EditorView {
}
let starting_indent =
(offset.col / tab_width) as u16 + config.indent_guides.skip_levels;
// TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some
// extra loops if the code is deeply nested.
(offset.col / tab_width) + config.indent_guides.skip_levels as usize;
let modifier = if config.indent_guides.rainbow == RainbowIndentOptions::Dim {
Modifier::DIM
@ -505,14 +531,14 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16;
let out_of_bounds = offset.col > (visual_x as usize)
|| (visual_x as usize) >= viewport.width as usize + offset.col;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - offset.col as u16,
(viewport.x as usize + visual_x - offset.col) as u16,
viewport.y + line,
&newline,
style.patch(whitespace_style),
@ -560,7 +586,7 @@ impl EditorView {
if !out_of_bounds {
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
(viewport.x as usize + visual_x - offset.col) as u16,
viewport.y + line,
display_grapheme,
if is_whitespace {
@ -593,7 +619,7 @@ impl EditorView {
last_line_indent_level = visual_x;
}
visual_x = visual_x.saturating_add(width as u16);
visual_x = visual_x.saturating_add(width);
}
}
}
@ -844,6 +870,53 @@ impl EditorView {
}
}
/// Apply the highlighting on the columns where a cursor is active
pub fn highlight_cursorcolumn(
doc: &Document,
view: &View,
surface: &mut Surface,
theme: &Theme,
) {
let text = doc.text().slice(..);
// Manual fallback behaviour:
// ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s}
let primary_style = theme
.try_get_exact("ui.cursorcolumn.primary")
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.primary"));
let secondary_style = theme
.try_get_exact("ui.cursorcolumn.secondary")
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
let inner_area = view.inner_area();
let offset = view.offset.col;
let selection = doc.selection(view.id);
let primary = selection.primary();
for range in selection.iter() {
let is_primary = primary == *range;
let Position { row: _, col } =
visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
// if the cursor is horizontally in the view
if col >= offset && inner_area.width > (col - offset) as u16 {
let area = Rect::new(
inner_area.x + (col - offset) as u16,
view.area.y,
1,
view.area.height,
);
if is_primary {
surface.set_style(area, primary_style)
} else {
surface.set_style(area, secondary_style)
}
}
}
}
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@ -880,9 +953,10 @@ impl EditorView {
// TODO: Use an on_mode_change hook to remove signature help
cxt.jobs.callback(async {
let call: job::Callback = Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID);
});
let call: job::Callback =
Callback::EditorCompositor(Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID);
}));
Ok(call)
});
}
@ -943,37 +1017,40 @@ impl EditorView {
}
// special handling for repeat operator
(key!('.'), _) if self.keymaps.pending().is_empty() => {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
// then replay the inputs
for key in self.last_insert.1.clone() {
match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::CompletionApply(compl) => {
let (view, doc) = current!(cxt.editor);
doc.restore(view.id);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position =
|pos: usize| -> usize { pos + cursor - compl.trigger_offset };
let tx = Transaction::change(
doc.text(),
compl.changes.iter().cloned().map(|(start, end, t)| {
(shift_position(start), shift_position(end), t)
}),
);
doc.apply(&tx, view.id);
}
InsertEvent::TriggerCompletion => {
let (_, doc) = current!(cxt.editor);
doc.savepoint();
for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
// then replay the inputs
for key in self.last_insert.1.clone() {
match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::CompletionApply(compl) => {
let (view, doc) = current!(cxt.editor);
doc.restore(view);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position =
|pos: usize| -> usize { pos + cursor - compl.trigger_offset };
let tx = Transaction::change(
doc.text(),
compl.changes.iter().cloned().map(|(start, end, t)| {
(shift_position(start), shift_position(end), t)
}),
);
apply_transaction(&tx, doc, view);
}
InsertEvent::TriggerCompletion => {
let (_, doc) = current!(cxt.editor);
doc.savepoint();
}
}
}
}
cxt.editor.count = None;
}
_ => {
// set the count
@ -1030,23 +1107,20 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger
}
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
if self.completion.is_some()
|| cx.editor.mode != Mode::Insert
|| !cx.editor.config().auto_completion
{
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
if let Some(completion) = &mut self.completion {
return if completion.ensure_item_resolved(cx) {
EventResult::Consumed(None)
} else {
EventResult::Ignored(None)
};
}
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
return EventResult::Ignored(None);
}
let mut cx = commands::Context {
register: None,
editor: cx.editor,
jobs: cx.jobs,
count: None,
callback: None,
on_next_key_callback: None,
};
crate::commands::insert::idle_completion(&mut cx);
crate::commands::insert::idle_completion(cx);
EventResult::Consumed(None)
}
@ -1367,7 +1441,16 @@ impl Component for EditorView {
}
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained => EventResult::Ignored(None),
Event::FocusLost => {
if context.editor.config().auto_save {
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}
}
EventResult::Consumed(None)
}
}
}

@ -0,0 +1,74 @@
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
#[cfg(test)]
mod test;
pub struct FuzzyQuery {
queries: Vec<String>,
}
impl FuzzyQuery {
pub fn new(query: &str) -> FuzzyQuery {
let mut saw_backslash = false;
let queries = query
.split(|c| {
saw_backslash = match c {
' ' if !saw_backslash => return true,
'\\' => true,
_ => false,
};
false
})
.filter_map(|query| {
if query.is_empty() {
None
} else {
Some(query.replace("\\ ", " "))
}
})
.collect();
FuzzyQuery { queries }
}
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
// use the rank of the first query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
if self
.queries
.iter()
.any(|query| matcher.fuzzy_match(item, query).is_none())
{
return None;
}
Some(score)
}
pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
if self.queries.len() == 1 {
return matcher.fuzzy_indices(item, &self.queries[0]);
}
// use the rank of the first query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
// fast path for the common case of not using a space
// during matching this branch should be free thanks to branch prediction
if self.queries.len() == 1 {
return Some((score, indicies));
}
for query in &self.queries[1..] {
let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
indicies.extend_from_slice(&matched_indicies);
}
// deadup and remove duplicate matches
indicies.sort_unstable();
indicies.dedup();
Some((score, indicies))
}
}

@ -0,0 +1,47 @@
use crate::ui::fuzzy_match::FuzzyQuery;
use crate::ui::fuzzy_match::Matcher;
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
let query = FuzzyQuery::new(query);
let matcher = Matcher::default();
items
.iter()
.filter_map(|item| {
let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
let matched_string = indicies
.iter()
.map(|&pos| item.chars().nth(pos).unwrap())
.collect();
Some(matched_string)
})
.collect()
}
#[test]
fn match_single_value() {
let matches = run_test("foo", &["foobar", "foo", "bar"]);
assert_eq!(matches, &["foo", "foo"])
}
#[test]
fn match_multiple_values() {
let matches = run_test(
"foo bar",
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
);
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
}
#[test]
fn space_escape() {
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["foo bar"])
}
#[test]
fn trim() {
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["bar foo"])
}

@ -77,11 +77,12 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect();
Self {
options,
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
matches,
cursor: None,
widths: Vec::new(),
callback_fn: Box::new(callback_fn),
@ -89,12 +90,7 @@ impl<T: Item> Menu<T> {
size: (0, 0),
viewport: (0, 0),
recalculate: true,
};
// TODO: scoring on empty input should just use a fastpath
menu.score("");
menu
}
}
pub fn score(&mut self, pattern: &str) {
@ -105,17 +101,15 @@ impl<T: Item> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text: String = option.filter_text(&self.editor_data).into();
let text = option.filter_text(&self.editor_data);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
// matches.sort_unstable_by_key(|(_, score)| -score);
self.matches.sort_unstable_by_key(|(index, _score)| {
self.options[*index].sort_text(&self.editor_data)
});
// Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority
self.matches.sort_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = None;
@ -213,6 +207,14 @@ impl<T: Item> Menu<T> {
})
}
pub fn selection_mut(&mut self) -> Option<&mut T> {
self.cursor.and_then(|cursor| {
self.matches
.get(cursor)
.map(|(index, _score)| &mut self.options[*index])
})
}
pub fn is_empty(&self) -> bool {
self.matches.is_empty()
}

@ -1,5 +1,6 @@
mod completion;
pub(crate) mod editor;
mod fuzzy_match;
mod info;
pub mod lsp;
mod markdown;
@ -13,7 +14,7 @@ mod statusline;
mod text;
use crate::compositor::{Component, Compositor};
use crate::job;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
pub use markdown::Markdown;
@ -120,7 +121,7 @@ pub fn regex_prompt(
if event == PromptEvent::Validate {
let callback = async move {
let call: job::Callback = Box::new(
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let contents = Text::new(format!("{}", err));
let size = compositor.size();
@ -134,7 +135,7 @@ pub fn regex_prompt(
compositor.replace_or_push("invalid-regex", popup);
},
);
));
Ok(call)
};

@ -1,7 +1,7 @@
use crate::{
compositor::{Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
ui::{self, EditorView},
ui::{self, fuzzy_match::FuzzyQuery, EditorView},
};
use tui::{
buffer::Buffer as Surface,
@ -9,7 +9,6 @@ use tui::{
};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::time::Instant;
@ -161,6 +160,27 @@ impl<T: Item> FilePicker<T> {
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
}
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
// Try to find a document in the cache
let doc = self
.current_file(cx.editor)
.and_then(|(path, _range)| self.preview_cache.get_mut(&path))
.and_then(|cache| match cache {
CachedPreview::Document(doc) => Some(doc),
_ => None,
});
// Then attempt to highlight it if it has no language set
if let Some(doc) = doc {
if doc.language_config().is_none() {
let loader = cx.editor.syn_loader.clone();
doc.detect_language(loader);
}
}
EventResult::Consumed(None)
}
}
impl<T: Item + 'static> Component for FilePicker<T> {
@ -228,8 +248,14 @@ impl<T: Item + 'static> Component for FilePicker<T> {
let offset = Position::new(first_line, 0);
let highlights =
let mut highlights =
EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
if spans.is_empty() {
continue;
}
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
}
EditorView::render_text_highlights(
doc,
offset,
@ -261,6 +287,9 @@ impl<T: Item + 'static> Component for FilePicker<T> {
}
fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
if let Event::IdleTimeout = event {
return self.handle_idle_timeout(ctx);
}
// TODO: keybinds for scrolling preview
self.picker.handle_event(event, ctx)
}
@ -287,8 +316,6 @@ pub struct Picker<T: Item> {
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
/// Filter over original options.
filters: Vec<usize>, // could be optimized into bit but not worth it now
/// Current height of the completions box
completion_height: u16,
@ -323,7 +350,6 @@ impl<T: Item> Picker<T> {
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
filters: Vec::new(),
cursor: 0,
prompt,
previous_pattern: String::new(),
@ -365,13 +391,14 @@ impl<T: Item> Picker<T> {
.map(|(index, _option)| (index, 0)),
);
} else if pattern.starts_with(&self.previous_pattern) {
let query = FuzzyQuery::new(pattern);
// optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set.
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
let text = option.sort_text(&self.editor_data);
match self.matcher.fuzzy_match(&text, pattern) {
match query.fuzzy_match(&text, &self.matcher) {
Some(s) => {
// Update the score
*score = s;
@ -384,23 +411,17 @@ impl<T: Item> Picker<T> {
self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score));
} else {
let query = FuzzyQuery::new(pattern);
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
if !self.filters.is_empty() {
// TODO: this filters functionality seems inefficient,
// instead store and operate on filters if any
self.filters.binary_search(&index).ok()?;
}
let text = option.filter_text(&self.editor_data);
self.matcher
.fuzzy_match(&text, pattern)
query
.fuzzy_match(&text, &self.matcher)
.map(|score| (index, score))
}),
);
@ -460,14 +481,6 @@ impl<T: Item> Picker<T> {
.map(|(index, _score)| &self.options[*index])
}
pub fn save_filter(&mut self, cx: &Context) {
self.filters.clear();
self.filters
.extend(self.matches.iter().map(|(index, _)| *index));
self.filters.sort_unstable(); // used for binary search later
self.prompt.clear(cx.editor);
}
pub fn toggle_preview(&mut self) {
self.show_preview = !self.show_preview;
}
@ -505,6 +518,9 @@ impl<T: Item + 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop();
})));
// So that idle timeout retriggers
cx.editor.reset_idle_timer();
match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward);
@ -545,9 +561,6 @@ impl<T: Item + 'static> Component for Picker<T> {
}
return close_fn;
}
ctrl!(' ') => {
self.save_filter(cx);
}
ctrl!('t') => {
self.toggle_preview();
}
@ -630,9 +643,8 @@ impl<T: Item + 'static> Component for Picker<T> {
}
let spans = option.label(&self.editor_data);
let (_score, highlights) = self
.matcher
.fuzzy_indices(&String::from(&spans), self.prompt.line())
let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
.fuzzy_indicies(&String::from(&spans), &self.matcher)
.unwrap_or_default();
spans.0.into_iter().fold(inner, |pos, span| {

@ -11,7 +11,7 @@ mod test {
use self::helpers::*;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn hello_world() -> anyhow::Result<()> {
test(("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#")).await?;
Ok(())
@ -22,5 +22,6 @@ mod test {
mod commands;
mod movement;
mod prompt;
mod splits;
mod write;
}

@ -1,6 +1,6 @@
use super::*;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn auto_indent_c() -> anyhow::Result<()> {
test_with_config(
Args {
@ -8,6 +8,7 @@ async fn auto_indent_c() -> anyhow::Result<()> {
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
// switches to append mode?
(
helpers::platform_line("void foo() {#[|}]#").as_ref(),

@ -1,21 +1,547 @@
use helix_core::{auto_pairs::DEFAULT_PAIRS, hashmap};
use super::*;
#[tokio::test]
async fn auto_pairs_basic() -> anyhow::Result<()> {
test(("#[\n|]#", "i(<esc>", "(#[|)]#\n")).await?;
const LINE_END: &str = helix_core::DEFAULT_LINE_ENDING.as_str();
test_with_config(
Args::default(),
Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Enable(false),
..Default::default()
},
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)
}
fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open == close)
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_basic() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> {
// NOTE: these are multi-byte Unicode characters
let pairs = hashmap!('„' => '“', '' => '', '「' => '」');
let config = Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Pairs(pairs.clone()),
..Default::default()
},
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
)
.await?;
..Default::default()
};
for (open, close) in pairs.iter() {
test_with_config(
Args::default(),
config.clone(),
helpers::test_syntax_conf(None),
(
format!("#[{}|]#", LINE_END),
format!("i{}", open),
format!("{}#[|{}]#{}", open, close, LINE_END),
),
)
.await?;
test_with_config(
Args::default(),
config.clone(),
helpers::test_syntax_conf(None),
(
format!("{}#[{}|]#{}", open, close, LINE_END),
format!("i{}", close),
format!("{}{}#[|{}]#", open, close, LINE_END),
),
)
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_after_word() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("foo#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{}]#{}", pair.0, pair.1, LINE_END),
))
.await?;
}
for pair in matching_pairs() {
test((
format!("foo#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{}]#", pair.0, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_before_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[f|]#oo{}", LINE_END),
format!("i{}", pair.0),
format!("{}#[|f]#oo{}", pair.0, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_before_word_selection() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[foo|]#{}", LINE_END),
format!("i{}", pair.0),
format!("{}#[|foo]#{}", pair.0, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("foo#[ wor|]#{}", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END),
format!("i{}", pair.1),
format!("foo{}{}#[| wor]#{}", pair.0, pair.1, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_before_eol() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("{0}#[{0}|]#", LINE_END),
format!("i{}", pair.0),
format!(
"{eol}{open}#[|{close}]#{eol}",
eol = LINE_END,
open = pair.0,
close = pair.1
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_auto_pairs_disabled() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test_with_config(
Args::default(),
Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Enable(false),
..Default::default()
},
..Default::default()
},
helpers::test_syntax_conf(None),
(
format!("#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("{}#[|{}]#", pair.0, LINE_END),
),
)
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_multi_range() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[{eol}|]##({eol}|)##({eol}|)#", eol = LINE_END),
format!("i{}", pair.0),
format!(
"{open}#[|{close}]#{eol}{open}#(|{close})#{eol}{open}#(|{close})#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("hello #[👨‍👩‍👧‍👦|]# goodbye{}", LINE_END),
format!("i{}", pair.1),
format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.1, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_at_end_of_document() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test(TestCase {
in_text: String::from(LINE_END),
in_selection: Selection::single(LINE_END.len(), LINE_END.len()),
in_keys: format!("i{}", pair.0),
out_text: format!("{}{}{}", LINE_END, pair.0, pair.1),
out_selection: Selection::single(LINE_END.len() + 1, LINE_END.len() + 2),
})
.await?;
test(TestCase {
in_text: format!("foo{}", LINE_END),
in_selection: Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
in_keys: format!("i{}", pair.0),
out_text: format!("foo{}{}{}", LINE_END, pair.0, pair.1),
out_selection: Selection::single(LINE_END.len() + 4, LINE_END.len() + 5),
})
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_close_inside_pair() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"{open}#[{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("i{}", pair.1),
format!(
"{open}{close}#[|{eol}]#",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_close_inside_pair_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"{open}#[{close}|]#{eol}{open}#({close}|)#{eol}{open}#({close}|)#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("i{}", pair.1),
format!(
"{open}{close}#[|{eol}]#{open}{close}#(|{eol})#{open}{close}#(|{eol})#",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_nested_open_inside_pair() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!(
"{open}#[{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("i{}", pair.0),
format!(
"{open}{open}#[|{close}]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> {
for outer_pair in DEFAULT_PAIRS {
for inner_pair in DEFAULT_PAIRS {
if inner_pair.0 == outer_pair.0 {
continue;
}
test((
format!(
"{outer_open}#[{outer_close}|]#{eol}{outer_open}#({outer_close}|)#{eol}{outer_open}#({outer_close}|)#{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
eol = LINE_END
),
format!("i{}", inner_pair.0),
format!(
"{outer_open}{inner_open}#[|{inner_close}]#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
inner_open = inner_pair.0,
inner_close = inner_pair.1,
eol = LINE_END
),
))
.await?;
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_basic() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[{}|]#", LINE_END),
format!("a{}", pair.0),
format!(
"#[{eol}{open}{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_multi_range() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[ |]#{eol}#( |)#{eol}#( |)#{eol}", eol = LINE_END),
format!("a{}", pair.0),
format!(
"#[ {open}{close}|]#{eol}#( {open}{close}|)#{eol}#( {open}{close}|)#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_close_inside_pair() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"#[{open}|]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("a{}", pair.1),
format!(
"#[{open}{close}{eol}|]#",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_close_inside_pair_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"#[{open}|]#{close}{eol}#({open}|)#{close}{eol}#({open}|)#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("a{}", pair.1),
format!(
"#[{open}{close}{eol}|]##({open}{close}{eol}|)##({open}{close}{eol}|)#",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_end_of_word() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("fo#[o|]#{}", LINE_END),
format!("a{}", pair.0),
format!(
"fo#[o{open}{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_middle_of_word() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("#[wo|]#rd{}", LINE_END),
format!("a{}", pair.1),
format!("#[wo{}r|]#d{}", pair.1, LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_end_of_word_multi() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("fo#[o|]#{eol}fo#(o|)#{eol}fo#(o|)#{eol}", eol = LINE_END),
format!("a{}", pair.0),
format!(
"fo#[o{open}{close}|]#{eol}fo#(o{open}{close}|)#{eol}fo#(o{open}{close}|)#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_inside_nested_pair() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!(
"f#[oo{open}|]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
format!("a{}", pair.0),
format!(
"f#[oo{open}{open}{close}|]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_inside_nested_pair_multi() -> anyhow::Result<()> {
for outer_pair in DEFAULT_PAIRS {
for inner_pair in DEFAULT_PAIRS {
if inner_pair.0 == outer_pair.0 {
continue;
}
test((
format!(
"f#[oo{outer_open}|]#{outer_close}{eol}f#(oo{outer_open}|)#{outer_close}{eol}f#(oo{outer_open}|)#{outer_close}{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
eol = LINE_END
),
format!("a{}", inner_pair.0),
format!(
"f#[oo{outer_open}{inner_open}{inner_close}|]#{outer_close}{eol}f#(oo{outer_open}{inner_open}{inner_close}|)#{outer_close}{eol}f#(oo{outer_open}{inner_open}{inner_close}|)#{outer_close}{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
inner_open = inner_pair.0,
inner_close = inner_pair.1,
eol = LINE_END
),
))
.await?;
}
}
Ok(())
}

@ -1,21 +1,25 @@
use std::{
io::{Read, Write},
ops::RangeInclusive,
};
use std::ops::RangeInclusive;
use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use super::*;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
&mut app,
Some("ihello<esc>:wq<ret>"),
Some(&|app| {
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}),
false,
@ -25,11 +29,10 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore]
#[tokio::test(flavor = "multi_thread")]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences(
&mut Application::new(Args::default(), Config::default())?,
&mut helpers::AppBuilder::new().build()?,
vec![
(
None,
@ -69,8 +72,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
command.push_str(":buffer<minus>close<ret>");
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
&mut app,
Some(&command),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
@ -82,17 +89,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(RANGE.end().to_string(), file_content);
helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_selection_duplication() -> anyhow::Result<()> {
// Forward
test((

@ -1,10 +1,15 @@
use std::{io::Write, path::PathBuf, time::Duration};
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
time::Duration,
};
use anyhow::bail;
use crossterm::event::{Event, KeyEvent};
use helix_core::{test, Selection, Transaction};
use helix_core::{diagnostic::Severity, test, Selection, Transaction};
use helix_term::{application::Application, args::Args, config::Config};
use helix_view::{doc, input::parse_macro};
use helix_view::{doc, input::parse_macro, Editor};
use tempfile::NamedTempFile;
use tokio_stream::wrappers::UnboundedReceiverStream;
@ -56,7 +61,9 @@ pub async fn test_key_sequences(
for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
if let Some(in_keys) = in_keys {
for key_event in parse_macro(in_keys)?.into_iter() {
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
let key = Event::Key(KeyEvent::from(key_event));
log::trace!("sending key: {:?}", key);
tx.send(Ok(key))?;
}
}
@ -70,7 +77,7 @@ pub async fn test_key_sequences(
// verify if it exited on the last iteration if it should have and
// the inverse
if i == num_inputs - 1 && app_exited != should_exit {
bail!("expected app to exit: {} != {}", app_exited, should_exit);
bail!("expected app to exit: {} != {}", should_exit, app_exited);
}
if let Some(test) = test_fn {
@ -87,7 +94,17 @@ pub async fn test_key_sequences(
tokio::time::timeout(TIMEOUT, event_loop).await?;
}
app.close().await?;
let errs = app.close().await;
if !errs.is_empty() {
log::error!("Errors closing app");
for err in errs {
log::error!("{}", err);
}
bail!("Error closing app");
}
Ok(())
}
@ -101,20 +118,19 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
let test_case = test_case.into();
let mut app = match app {
Some(app) => app,
None => Application::new(Args::default(), Config::default())?,
None => Application::new(Args::default(), Config::default(), test_syntax_conf(None))?,
};
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
// replace the initial text with the input text
doc.apply(
&Transaction::change_by_selection(doc.text(), &sel, |_| {
(0, doc.text().len_chars(), Some((&test_case.in_text).into()))
})
.with_selection(test_case.in_selection.clone()),
view.id,
);
let transaction = Transaction::change_by_selection(doc.text(), &sel, |_| {
(0, doc.text().len_chars(), Some((&test_case.in_text).into()))
})
.with_selection(test_case.in_selection.clone());
helix_view::apply_transaction(&transaction, doc, view);
test_key_sequence(
&mut app,
@ -125,16 +141,48 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
.await
}
/// Generates language configs that merge in overrides, like a user language
/// config. The argument string must be a raw TOML document.
///
/// By default, language server configuration is dropped from the languages.toml
/// document. If a language-server is necessary for a test, it must be explicitly
/// added in `overrides`.
pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Configuration {
let mut lang = helix_loader::config::default_lang_config();
for lang_config in lang
.as_table_mut()
.expect("Expected languages.toml to be a table")
.get_mut("language")
.expect("Expected languages.toml to have \"language\" keys")
.as_array_mut()
.expect("Expected an array of language configurations")
{
lang_config
.as_table_mut()
.expect("Expected language config to be a TOML table")
.remove("language-server");
}
if let Some(overrides) = overrides {
let override_toml = toml::from_str(&overrides).unwrap();
lang = helix_loader::merge_toml_values(lang, override_toml, 3);
}
lang.try_into().unwrap()
}
/// Use this for very simple test cases where there is one input
/// document, selection, and sequence of key presses, and you just
/// want to verify the resulting document and selection.
pub async fn test_with_config<T: Into<TestCase>>(
args: Args,
config: Config,
syn_conf: helix_core::syntax::Configuration,
test_case: T,
) -> anyhow::Result<()> {
let test_case = test_case.into();
let app = Application::new(args, config)?;
let app = Application::new(args, config, syn_conf)?;
test_key_sequence_with_input_text(
Some(app),
@ -155,7 +203,13 @@ pub async fn test_with_config<T: Into<TestCase>>(
}
pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
test_with_config(Args::default(), Config::default(), test_case).await
test_with_config(
Args::default(),
Config::default(),
test_syntax_conf(None),
test_case,
)
.await
}
pub fn temp_file_with_contents<S: AsRef<str>>(
@ -200,14 +254,87 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
Ok(file)
}
/// Creates a new Application with default config that opens the given file
/// path
pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
Application::new(
Args {
files: vec![(path.into(), helix_core::Position::default())],
..Default::default()
},
Config::default(),
)
pub struct AppBuilder {
args: Args,
config: Config,
syn_conf: helix_core::syntax::Configuration,
input: Option<(String, Selection)>,
}
impl Default for AppBuilder {
fn default() -> Self {
Self {
args: Args::default(),
config: Config::default(),
syn_conf: test_syntax_conf(None),
input: None,
}
}
}
impl AppBuilder {
pub fn new() -> Self {
AppBuilder::default()
}
pub fn with_file<P: Into<PathBuf>>(
mut self,
path: P,
pos: Option<helix_core::Position>,
) -> Self {
self.args.files.push((path.into(), pos.unwrap_or_default()));
self
}
// Remove this attribute once `with_config` is used in a test:
#[allow(dead_code)]
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn with_input_text<S: Into<String>>(mut self, input_text: S) -> Self {
self.input = Some(test::print(&input_text.into()));
self
}
pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self {
self.syn_conf = syn_conf;
self
}
pub fn build(self) -> anyhow::Result<Application> {
let mut app = Application::new(self.args, self.config, self.syn_conf)?;
if let Some((text, selection)) = self.input {
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
let trans = Transaction::change_by_selection(doc.text(), &sel, |_| {
(0, doc.text().len_chars(), Some((text.clone()).into()))
})
.with_selection(selection);
// replace the initial text with the input text
helix_view::apply_transaction(&trans, doc, view);
}
Ok(app)
}
}
pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
file.flush()?;
file.sync_all()?;
let mut file_content = String::new();
file.read_to_string(&mut file_content)?;
assert_eq!(content, file_content);
Ok(())
}
pub fn assert_status_not_error(editor: &Editor) {
if let Some((_, sev)) = editor.get_status() {
assert_ne!(&Severity::Error, sev);
}
}

@ -1,6 +1,6 @@
use super::*;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn insert_mode_cursor_position() -> anyhow::Result<()> {
test(TestCase {
in_text: String::new(),
@ -19,7 +19,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
}
/// Range direction is preserved when escaping insert mode to normal
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
test(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
test((
@ -66,11 +66,13 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
/// Ensure the very initial cursor in an opened file is the width of
/// the first grapheme
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
let file = helpers::temp_file_with_contents(content)?;
let mut app = helpers::app_with_file(file.path())?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
@ -86,7 +88,28 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn cursor_position_append_eof() -> anyhow::Result<()> {
// Selection is fowards
test((
"#[foo|]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n").as_ref(),
))
.await?;
// Selection is backwards
test((
"#[|foo]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n").as_ref(),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> {
test_with_config(
Args {
@ -94,6 +117,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
(
helpers::platform_line(indoc! {"\
#[/|]#// Increments
@ -117,7 +141,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> {
test_with_config(
Args {
@ -125,6 +149,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
(
helpers::platform_line(indoc! {"\
/// Increments
@ -148,7 +173,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> anyhow::Result<()> {
// Note: the anchor stays put and the head moves back.
test_with_config(
@ -157,6 +182,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
(
helpers::platform_line(indoc! {"\
/// Increments
@ -187,6 +213,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
(
helpers::platform_line(indoc! {"\
/// Increments

@ -1,11 +1,9 @@
use super::*;
use helix_term::application::Application;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_history_completion() -> anyhow::Result<()> {
test_key_sequence(
&mut Application::new(Args::default(), Config::default())?,
&mut AppBuilder::new().build()?,
Some(":asdf<ret>:theme d<C-n><tab>"),
Some(&|app| {
assert!(!app.editor.is_err());

@ -0,0 +1,129 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_all() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new()?;
let mut file2 = tempfile::NamedTempFile::new()?;
let mut file3 = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some(&format!(
"ihello1<esc>:sp<ret>:o {}<ret>ihello2<esc>:sp<ret>:o {}<ret>ihello3<esc>",
file2.path().to_string_lossy(),
file3.path().to_string_lossy()
)),
Some(&|app| {
let docs: Vec<_> = app.editor.documents().collect();
assert_eq!(3, docs.len());
let doc1 = docs
.iter()
.find(|doc| doc.path().unwrap() == file1.path())
.unwrap();
assert_eq!("hello1", doc1.text().to_string());
let doc2 = docs
.iter()
.find(|doc| doc.path().unwrap() == file2.path())
.unwrap();
assert_eq!("hello2", doc2.text().to_string());
let doc3 = docs
.iter()
.find(|doc| doc.path().unwrap() == file3.path())
.unwrap();
assert_eq!("hello3", doc3.text().to_string());
helpers::assert_status_not_error(&app.editor);
assert_eq!(3, app.editor.tree.views().count());
}),
),
(
Some(":wqa<ret>"),
Some(&|app| {
helpers::assert_status_not_error(&app.editor);
assert_eq!(0, app.editor.tree.views().count());
}),
),
],
true,
)
.await?;
helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"),
Some(&|app| {
assert_eq!(2, app.editor.tree.views().count());
helpers::assert_status_not_error(&app.editor);
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
doc.text().to_string()
);
assert!(doc.is_modified());
}),
),
(
Some(":wq<ret>"),
Some(&|app| {
helpers::assert_status_not_error(&app.editor);
assert_eq!(1, app.editor.tree.views().count());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
doc.text().to_string()
);
assert!(!doc.is_modified());
}),
),
],
false,
)
.await?;
helpers::assert_file_has_content(
file.as_file_mut(),
&helpers::platform_line("hello\ngoodbye"),
)?;
Ok(())
}

@ -4,17 +4,19 @@ use std::{
};
use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use helix_view::doc;
use super::*;
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_write() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
&mut app,
Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
None,
false,
@ -35,12 +37,15 @@ async fn test_write() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
&mut app,
Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
None,
true,
@ -61,25 +66,21 @@ async fn test_write_quit() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore]
#[tokio::test(flavor = "multi_thread")]
async fn test_write_concurrent() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=5000;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i);
command.push_str(&cmd);
}
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some(&command),
None,
false,
)
.await?;
test_key_sequence(&mut app, Some(&command), None, false).await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
@ -91,13 +92,15 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore]
#[tokio::test(flavor = "multi_thread")]
async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequences(
&mut helpers::app_with_file(file.path())?,
&mut app,
vec![
(
None,
@ -130,13 +133,128 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore]
#[tokio::test(flavor = "multi_thread")]
async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
test_key_sequence(
&mut AppBuilder::new().build()?,
Some(format!("ihello<esc>:w {}<ret>", file.path().to_string_lossy()).as_ref()),
Some(&|app| {
assert!(!app.editor.is_err());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(&file.path().to_path_buf()), doc.path());
}),
false,
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> {
helpers::test_key_sequence_with_input_text(
None,
("#[\n|]#", "ihello<esc>:w<ret>", "hello#[\n|]#"),
&|app| {
assert!(app.editor.is_err());
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(None, doc.path());
},
false,
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> {
let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?;
let lang_conf = indoc! {r#"
[[language]]
name = "rust"
formatter = { command = "bash", args = [ "-c", "exit 1" ] }
"#};
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("#[l|]#et foo = 0;\n")
.with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into())))
.build()?;
test_key_sequences(&mut app, vec![(Some(":w<ret>"), None)], false).await?;
// file still saves
helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_write_new_path() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new().unwrap();
let mut file2 = tempfile::NamedTempFile::new().unwrap();
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.build()?;
test_key_sequences(
&mut app,
vec![
(
Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"),
Some(&|app| {
let doc = doc!(app.editor);
assert!(!app.editor.is_err());
assert_eq!(file1.path(), doc.path().unwrap());
}),
),
(
Some(&format!(":w {}<ret>", file2.path().to_string_lossy())),
Some(&|app| {
let doc = doc!(app.editor);
assert!(!app.editor.is_err());
assert_eq!(file2.path(), doc.path().unwrap());
assert!(app.editor.document_by_path(file1.path()).is_none());
}),
),
],
false,
)
.await?;
helpers::assert_file_has_content(
file1.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
)?;
helpers::assert_file_has_content(
file2.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_write_fail_new_path() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
test_key_sequences(
&mut Application::new(Args::default(), Config::default())?,
&mut AppBuilder::new().build()?,
vec![
(
None,

@ -20,6 +20,7 @@ bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.10"
crossterm = { version = "0.25", optional = true }
termini = "0.1"
serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" }

@ -7,12 +7,45 @@ use crossterm::{
SetForegroundColor,
},
terminal::{self, Clear, ClearType},
Command,
};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
use std::{
fmt,
io::{self, Write},
};
fn vte_version() -> Option<usize> {
std::env::var("VTE_VERSION").ok()?.parse().ok()
}
/// Describes terminal capabilities like extended underline, truecolor, etc.
#[derive(Copy, Clone, Debug, Default)]
struct Capabilities {
/// Support for undercurled, underdashed, etc.
has_extended_underlines: bool,
}
impl Capabilities {
/// Detect capabilities from the terminfo database located based
/// on the $TERM environment variable. If detection fails, returns
/// a default value where no capability is supported.
pub fn from_env_or_default() -> Self {
match termini::TermInfo::from_env() {
Err(_) => Capabilities::default(),
Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
has_extended_underlines: t.extended_cap("Smulx").is_some()
|| t.extended_cap("Su").is_some()
|| vte_version() >= Some(5102),
},
}
}
}
pub struct CrosstermBackend<W: Write> {
buffer: W,
capabilities: Capabilities,
}
impl<W> CrosstermBackend<W>
@ -20,7 +53,10 @@ where
W: Write,
{
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
CrosstermBackend {
buffer,
capabilities: Capabilities::from_env_or_default(),
}
}
}
@ -47,6 +83,8 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut underline_color = Color::Reset;
let mut underline_style = UnderlineStyle::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
@ -74,11 +112,32 @@ where
bg = cell.bg;
}
let mut new_underline_style = cell.underline_style;
if self.capabilities.has_extended_underlines {
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
underline_color = cell.underline_color;
}
} else {
match new_underline_style {
UnderlineStyle::Reset | UnderlineStyle::Line => (),
_ => new_underline_style = UnderlineStyle::Line,
}
}
if new_underline_style != underline_style {
let attr = CAttribute::from(new_underline_style);
map_error(queue!(self.buffer, SetAttribute(attr)))?;
underline_style = new_underline_style;
}
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
map_error(queue!(
self.buffer,
SetUnderlineColor(CColor::Reset),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
@ -153,9 +212,6 @@ impl ModifierDiff {
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
@ -176,9 +232,6 @@ impl ModifierDiff {
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
@ -195,3 +248,58 @@ impl ModifierDiff {
Ok(())
}
}
/// Crossterm uses semicolon as a seperator for colors
/// this is actually not spec compliant (altough commonly supported)
/// However the correct approach is to use colons as a seperator.
/// This usually doesn't make a difference for emulators that do support colored underlines.
/// However terminals that do not support colored underlines will ignore underlines colors with colons
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts.
/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SetUnderlineColor(pub CColor);
impl Command for SetUnderlineColor {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
let color = self.0;
if color == CColor::Reset {
write!(f, "\x1b[59m")?;
return Ok(());
}
f.write_str("\x1b[58:")?;
let res = match color {
CColor::Black => f.write_str("5:0"),
CColor::DarkGrey => f.write_str("5:8"),
CColor::Red => f.write_str("5:9"),
CColor::DarkRed => f.write_str("5:1"),
CColor::Green => f.write_str("5:10"),
CColor::DarkGreen => f.write_str("5:2"),
CColor::Yellow => f.write_str("5:11"),
CColor::DarkYellow => f.write_str("5:3"),
CColor::Blue => f.write_str("5:12"),
CColor::DarkBlue => f.write_str("5:4"),
CColor::Magenta => f.write_str("5:13"),
CColor::DarkMagenta => f.write_str("5:5"),
CColor::Cyan => f.write_str("5:14"),
CColor::DarkCyan => f.write_str("5:6"),
CColor::White => f.write_str("5:15"),
CColor::Grey => f.write_str("5:7"),
CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b),
CColor::AnsiValue(val) => write!(f, "5:{}", val),
_ => Ok(()),
};
res?;
write!(f, "m")?;
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> crossterm::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"SetUnderlineColor not supported by winapi.",
))
}
}

@ -3,7 +3,7 @@ use helix_core::unicode::width::UnicodeWidthStr;
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use helix_view::graphics::{Color, Modifier, Rect, Style};
use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
@ -11,6 +11,8 @@ pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
pub underline_color: Color,
pub underline_style: UnderlineStyle,
pub modifier: Modifier,
}
@ -44,6 +46,13 @@ impl Cell {
if let Some(c) = style.bg {
self.bg = c;
}
if let Some(c) = style.underline_color {
self.underline_color = c;
}
if let Some(style) = style.underline_style {
self.underline_style = style;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
@ -53,6 +62,8 @@ impl Cell {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.underline_style(self.underline_style)
.add_modifier(self.modifier)
}
@ -61,6 +72,8 @@ impl Cell {
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
self.underline_color = Color::Reset;
self.underline_style = UnderlineStyle::Reset;
self.modifier = Modifier::empty();
}
}
@ -71,6 +84,8 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
underline_color: Color::Reset,
underline_style: UnderlineStyle::Reset,
modifier: Modifier::empty(),
}
}
@ -87,7 +102,7 @@ impl Default for Cell {
///
/// ```
/// use helix_tui::buffer::{Buffer, Cell};
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x");
@ -97,7 +112,9 @@ impl Default for Cell {
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// underline_color: Color::Reset,
/// underline_style: UnderlineStyle::Reset,
/// modifier: Modifier::empty(),
/// });
/// buf[(5, 0)].set_char('x');
/// assert_eq!(buf[(5, 0)].symbol, "x");
@ -120,7 +137,7 @@ impl Buffer {
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
let size = area.area() as usize;
let size = area.area();
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
@ -222,7 +239,7 @@ impl Buffer {
y,
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize)
}
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates,
@ -261,8 +278,8 @@ impl Buffer {
self.content.len()
);
(
self.area.x + i as u16 % self.area.width,
self.area.y + i as u16 / self.area.width,
(self.area.x as usize + (i % self.area.width as usize)) as u16,
(self.area.y as usize + (i / self.area.width as usize)) as u16,
)
}
@ -463,7 +480,7 @@ impl Buffer {
/// Resize the buffer so that the mapped area matches the given area and that the buffer
/// length is equal to area.width * area.height
pub fn resize(&mut self, area: Rect) {
let length = area.area() as usize;
let length = area.area();
if self.content.len() > length {
self.content.truncate(length);
} else {
@ -570,8 +587,8 @@ impl Buffer {
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
let x = i as u16 % width;
let y = i as u16 / width;
let x = (i % width as usize) as u16;
let y = (i / width as usize) as u16;
updates.push((x, y, &next_buffer[i]));
}

@ -134,6 +134,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
@ -143,6 +145,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
@ -152,6 +156,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
@ -161,6 +167,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },

@ -4,12 +4,12 @@ use helix_tui::{
};
#[test]
fn terminal_buffer_size_should_be_limited() {
fn terminal_buffer_size_should_not_be_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
assert_eq!(size.width, 400);
assert_eq!(size.height, 400);
}
// #[test]

@ -17,7 +17,7 @@ pub trait ClipboardProvider: std::fmt::Debug {
#[cfg(not(windows))]
macro_rules! command_provider {
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
log::info!(
log::debug!(
"Using {} to interact with the system clipboard",
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
);

@ -24,7 +24,7 @@ use helix_core::{
DEFAULT_LINE_ENDING,
};
use crate::{DocumentId, Editor, ViewId};
use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
@ -83,6 +83,18 @@ impl Serialize for Mode {
}
}
/// A snapshot of the text of a document that we want to write out to disk
#[derive(Debug, Clone)]
pub struct DocumentSavedEvent {
pub revision: usize,
pub doc_id: DocumentId,
pub path: PathBuf,
pub text: Rope,
}
pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
@ -492,45 +504,61 @@ impl Document {
Some(fut.boxed())
}
pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
self.save_impl::<futures_util::future::Ready<_>>(None, force)
}
pub fn format_and_save(
pub fn save<P: Into<PathBuf>>(
&mut self,
formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
path: Option<P>,
force: bool,
) -> impl Future<Output = anyhow::Result<()>> {
self.save_impl(formatting, force)
) -> Result<
impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
anyhow::Error,
> {
let path = path.map(|path| path.into());
self.save_impl(path, force)
// futures_util::future::Ready<_>,
}
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
// or is that handled by the OS/async layer
/// The `Document`'s text is encoded according to its encoding and written to the file located
/// at its `path()`.
///
/// If `formatting` is present, it supplies some changes that we apply to the text before saving.
fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
fn save_impl(
&mut self,
formatting: Option<F>,
path: Option<PathBuf>,
force: bool,
) -> impl Future<Output = Result<(), anyhow::Error>> {
) -> Result<
impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
anyhow::Error,
> {
log::debug!(
"submitting save of doc '{:?}'",
self.path().map(|path| path.to_string_lossy())
);
// we clone and move text + path into the future so that we asynchronously save the current
// state without blocking any further edits.
let text = self.text().clone();
let mut text = self.text().clone();
let path = self.path.clone().expect("Can't save with no path set!");
let identifier = self.identifier();
let path = match path {
Some(path) => helix_core::path::get_canonicalized_path(&path)?,
None => {
if self.path.is_none() {
bail!("Can't save with no path set!");
}
self.path.as_ref().unwrap().clone()
}
};
let identifier = self.path().map(|_| self.identifier());
let language_server = self.language_server.clone();
// mark changes up to now as saved
self.reset_modified();
let current_rev = self.get_current_revision();
let doc_id = self.id();
let encoding = self.encoding;
// We encode the file according to the `Document`'s encoding.
async move {
let future = async move {
use tokio::fs::File;
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
@ -543,39 +571,34 @@ impl Document {
}
}
if let Some(fmt) = formatting {
match fmt.await {
Ok(transaction) => {
let success = transaction.changes().apply(&mut text);
if !success {
// This shouldn't happen, because the transaction changes were generated
// from the same text we're saving.
log::error!("failed to apply format changes before saving");
}
}
Err(err) => {
// formatting failed: report error, and save file without modifications
log::error!("{}", err);
}
}
}
let mut file = File::create(path).await?;
let mut file = File::create(&path).await?;
to_writer(&mut file, encoding, &text).await?;
let event = DocumentSavedEvent {
revision: current_rev,
doc_id,
path,
text: text.clone(),
};
if let Some(language_server) = language_server {
if !language_server.is_initialized() {
return Ok(());
return Ok(event);
}
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
{
notification.await?;
if let Some(identifier) = identifier {
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
{
notification.await?;
}
}
}
Ok(())
}
Ok(event)
};
Ok(future)
}
/// Detect the programming language based on the file type.
@ -601,7 +624,7 @@ impl Document {
}
/// Reload the document from its path.
pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> {
pub fn reload(&mut self, view: &mut View) -> Result<(), Error> {
let encoding = &self.encoding;
let path = self.path().filter(|path| path.exists());
@ -617,8 +640,8 @@ impl Document {
// This is not considered a modification of the contents of the file regardless
// of the encoding.
let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
self.apply(&transaction, view_id);
self.append_changes_to_history(view_id);
apply_transaction(&transaction, self, view);
self.append_changes_to_history(view.id);
self.reset_modified();
self.detect_indent_and_line_ending();
@ -810,6 +833,9 @@ impl Document {
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
/// Instead of calling this function directly, use [crate::apply_transaction]
/// to ensure that the transaction is applied to the appropriate [`View`] as
/// well.
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
// store the state just before any changes are made. This allows us to undo to the
// state just before a transaction was applied.
@ -831,11 +857,11 @@ impl Document {
success
}
fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool {
let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn {
self.apply_impl(txn, view_id)
self.apply_impl(txn, view.id) && view.apply(txn, self)
} else {
false
};
@ -849,26 +875,26 @@ impl Document {
}
/// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
pub fn undo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, true)
pub fn undo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, true)
}
/// Redo the last modification to the [`Document`]. Returns whether the redo was successful.
pub fn redo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, false)
pub fn redo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, false)
}
pub fn savepoint(&mut self) {
self.savepoint = Some(Transaction::new(self.text()));
}
pub fn restore(&mut self, view_id: ViewId) {
pub fn restore(&mut self, view: &mut View) {
if let Some(revert) = self.savepoint.take() {
self.apply(&revert, view_id);
apply_transaction(&revert, self, view);
}
}
fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool {
let txns = if earlier {
self.history.get_mut().earlier(uk)
} else {
@ -876,7 +902,7 @@ impl Document {
};
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view_id) {
if self.apply_impl(&txn, view.id) && view.apply(&txn, self) {
success = true;
}
}
@ -888,13 +914,13 @@ impl Document {
}
/// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, true)
pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, true)
}
/// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, false)
pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, false)
}
/// Commit pending changes to history
@ -927,6 +953,12 @@ impl Document {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
log::debug!(
"id {} modified - last saved: {}, current: {}",
self.id,
self.last_saved_revision,
current_revision
);
current_revision != self.last_saved_revision || !self.changes.is_empty()
}
@ -938,6 +970,30 @@ impl Document {
self.last_saved_revision = current_revision;
}
/// Set the document's latest saved revision to the given one.
pub fn set_last_saved_revision(&mut self, rev: usize) {
log::debug!(
"doc {} revision updated {} -> {}",
self.id,
self.last_saved_revision,
rev
);
self.last_saved_revision = rev;
}
/// Get the document's latest saved revision.
pub fn get_last_saved_revision(&mut self) -> usize {
self.last_saved_revision
}
/// Get the current revision number
pub fn get_current_revision(&mut self) -> usize {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
current_revision
}
/// Corresponding language scope name. Usually `source.<lang>`.
pub fn language_scope(&self) -> Option<&str> {
self.language
@ -996,14 +1052,6 @@ impl Document {
.map_or(4, |config| config.tab_width) // fallback to 4 columns
}
/// Returns a string containing a single level of indentation.
///
/// TODO: we might not need this function anymore, since the information
/// is conveniently available in `Document::indent_style` now.
pub fn indent_unit(&self) -> &'static str {
self.indent_style.as_str()
}
pub fn changes(&self) -> &ChangeSet {
&self.changes
}
@ -1306,84 +1354,66 @@ mod test {
);
}
macro_rules! test_decode {
($label:expr, $label_override:expr) => {
let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_in.txt", $label));
let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
assert!(path.exists());
assert!(ref_path.exists());
let mut file = std::fs::File::open(path).unwrap();
let text = from_reader(&mut file, Some(encoding))
.unwrap()
.0
.to_string();
let expectation = std::fs::read_to_string(ref_path).unwrap();
assert_eq!(text[..], expectation[..]);
};
}
macro_rules! test_encode {
($label:expr, $label_override:expr) => {
let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_out.txt", $label));
let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
assert!(path.exists());
assert!(ref_path.exists());
let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
let mut buf: Vec<u8> = Vec::new();
helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
let expectation = std::fs::read(ref_path).unwrap();
assert_eq!(buf, expectation);
};
}
macro_rules! test_decode_fn {
macro_rules! decode {
($name:ident, $label:expr, $label_override:expr) => {
#[test]
fn $name() {
test_decode!($label, $label_override);
let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_in.txt", $label));
let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
assert!(path.exists());
assert!(ref_path.exists());
let mut file = std::fs::File::open(path).unwrap();
let text = from_reader(&mut file, Some(encoding))
.unwrap()
.0
.to_string();
let expectation = std::fs::read_to_string(ref_path).unwrap();
assert_eq!(text[..], expectation[..]);
}
};
($name:ident, $label:expr) => {
#[test]
fn $name() {
test_decode!($label, $label);
}
decode!($name, $label, $label);
};
}
macro_rules! test_encode_fn {
macro_rules! encode {
($name:ident, $label:expr, $label_override:expr) => {
#[test]
fn $name() {
test_encode!($label, $label_override);
let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_out.txt", $label));
let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
assert!(path.exists());
assert!(ref_path.exists());
let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
let mut buf: Vec<u8> = Vec::new();
helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
let expectation = std::fs::read(ref_path).unwrap();
assert_eq!(buf, expectation);
}
};
($name:ident, $label:expr) => {
#[test]
fn $name() {
test_encode!($label, $label);
}
encode!($name, $label, $label);
};
}
test_decode_fn!(test_big5_decode, "big5");
test_encode_fn!(test_big5_encode, "big5");
test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR");
test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR");
test_decode_fn!(test_gb18030_decode, "gb18030");
test_encode_fn!(test_gb18030_encode, "gb18030");
test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP");
test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP");
test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP");
test_decode_fn!(test_shift_jis_decode, "shift_jis");
test_encode_fn!(test_shift_jis_encode, "shift_jis");
decode!(big5_decode, "big5");
encode!(big5_encode, "big5");
decode!(euc_kr_decode, "euc_kr", "EUC-KR");
encode!(euc_kr_encode, "euc_kr", "EUC-KR");
decode!(gb18030_decode, "gb18030");
encode!(gb18030_encode, "gb18030");
decode!(iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
encode!(iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
decode!(jis0208_decode, "jis0208", "EUC-JP");
encode!(jis0208_encode, "jis0208", "EUC-JP");
decode!(jis0212_decode, "jis0212", "EUC-JP");
decode!(shift_jis_decode, "shift_jis");
encode!(shift_jis_encode, "shift_jis");
}

@ -1,16 +1,18 @@
use crate::{
align_view,
clipboard::{get_clipboard_provider, ClipboardProvider},
document::Mode,
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
graphics::{CursorKind, Rect},
info::Info,
input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
Align, Document, DocumentId, View, ViewId,
};
use futures_util::future;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
use helix_lsp::Call;
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
@ -28,7 +30,7 @@ use tokio::{
time::{sleep, Duration, Instant, Sleep},
};
use anyhow::Error;
use anyhow::{anyhow, bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@ -65,7 +67,7 @@ where
)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig {
/// IgnoreOptions
@ -124,6 +126,8 @@ pub struct Config {
pub line_number: LineNumber,
/// Highlight the lines cursors are currently on. Defaults to false.
pub cursorline: bool,
/// Highlight the columns cursors are currently on. Defaults to false.
pub cursorcolumn: bool,
/// Gutters. Default ["diagnostics", "line-numbers"]
pub gutters: Vec<GutterType>,
/// Middle click paste support. Defaults to true.
@ -136,6 +140,8 @@ pub struct Config {
pub auto_completion: bool,
/// Automatic formatting on save. Defaults to true.
pub auto_format: bool,
/// Automatic save on focus lost. Defaults to false.
pub auto_save: bool,
/// Time in milliseconds since last keypress before idle timers trigger.
/// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
#[serde(
@ -170,7 +176,7 @@ pub struct Config {
pub color_modes: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig {
pub command: String,
@ -223,7 +229,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
None
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig {
/// Display LSP progress messages below statusline
@ -244,7 +250,7 @@ impl Default for LspConfig {
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct SearchConfig {
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
@ -253,7 +259,7 @@ pub struct SearchConfig {
pub wrap_around: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>,
@ -277,7 +283,7 @@ impl Default for StatusLineConfig {
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ModeConfig {
pub normal: String,
@ -456,7 +462,7 @@ impl std::str::FromStr for GutterType {
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceConfig {
pub render: WhitespaceRender,
@ -566,6 +572,7 @@ pub struct IndentGuidesConfig {
pub character: char,
pub rainbow: RainbowIndentOptions,
pub skip_levels: u16,
pub skip_levels: u8,
}
impl Default for IndentGuidesConfig {
@ -592,11 +599,13 @@ impl Default for Config {
},
line_number: LineNumber::Absolute,
cursorline: false,
cursorcolumn: false,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
auto_format: true,
auto_save: false,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
@ -650,12 +659,21 @@ pub struct Breakpoint {
pub log_message: Option<String>,
}
use futures_util::stream::{Flatten, Once};
pub struct Editor {
/// Current editing mode.
pub mode: Mode,
pub tree: Tree,
pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,
// We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
// https://stackoverflow.com/a/66875668
pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
pub write_count: usize,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
@ -695,6 +713,15 @@ pub struct Editor {
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
}
#[derive(Debug)]
pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
ConfigEvent(ConfigEvent),
LanguageServerMessage((usize, Call)),
DebuggerEvent(dap::Payload),
IdleTimer,
}
#[derive(Debug, Clone)]
pub enum ConfigEvent {
Refresh,
@ -726,6 +753,8 @@ pub enum CloseError {
DoesNotExist,
/// Buffer is modified
BufferModified(String),
/// Document failed to save
SaveError(anyhow::Error),
}
impl Editor {
@ -746,6 +775,9 @@ impl Editor {
tree: Tree::new(area),
next_document_id: DocumentId::default(),
documents: BTreeMap::new(),
saves: HashMap::new(),
save_queue: SelectAll::new(),
write_count: 0,
count: None,
selected_register: None,
macro_recording: None,
@ -811,12 +843,16 @@ impl Editor {
#[inline]
pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
self.status_msg = Some((status.into(), Severity::Info));
let status = status.into();
log::debug!("editor status: {}", status);
self.status_msg = Some((status, Severity::Info));
}
#[inline]
pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
self.status_msg = Some((error.into(), Severity::Error));
let error = error.into();
log::error!("editor error: {}", error);
self.status_msg = Some((error, Severity::Error));
}
#[inline]
@ -936,13 +972,7 @@ impl Editor {
let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view.id);
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
align_view(doc, view, Align::Center);
}
pub fn switch(&mut self, id: DocumentId, action: Action) {
@ -1041,6 +1071,13 @@ impl Editor {
DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
doc.id = id;
self.documents.insert(id, doc);
let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
self.saves.insert(id, save_sender);
let stream = UnboundedReceiverStream::new(save_receiver).flatten();
self.save_queue.push(stream);
id
}
@ -1087,16 +1124,19 @@ impl Editor {
}
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
let doc = match self.documents.get(&doc_id) {
let doc = match self.documents.get_mut(&doc_id) {
Some(doc) => doc,
None => return Err(CloseError::DoesNotExist),
};
if !force && doc.is_modified() {
return Err(CloseError::BufferModified(doc.display_name().into_owned()));
}
// This will also disallow any follow-up writes
self.saves.remove(&doc_id);
if let Some(language_server) = doc.language_server() {
// TODO: track error
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
@ -1159,6 +1199,32 @@ impl Editor {
Ok(())
}
pub fn save<P: Into<PathBuf>>(
&mut self,
doc_id: DocumentId,
path: Option<P>,
force: bool,
) -> anyhow::Result<()> {
// convert a channel of futures to pipe into main queue one by one
// via stream.then() ? then push into main future
let path = path.map(|path| path.into());
let doc = doc_mut!(self, &doc_id);
let future = doc.save(path, force)?;
use futures_util::stream;
self.saves
.get(&doc_id)
.ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
.send(stream::once(Box::pin(future)))
.map_err(|err| anyhow!("failed to send save event: {}", err))?;
self.write_count += 1;
Ok(())
}
pub fn resize(&mut self, area: Rect) {
if self.tree.resize(area) {
self._refresh();
@ -1259,14 +1325,14 @@ impl Editor {
}
}
/// Closes language servers with timeout. The default timeout is 500 ms, use
/// Closes language servers with timeout. The default timeout is 10000 ms, use
/// `timeout` parameter to override this.
pub async fn close_language_servers(
&self,
timeout: Option<u64>,
) -> Result<(), tokio::time::error::Elapsed> {
tokio::time::timeout(
Duration::from_millis(timeout.unwrap_or(500)),
Duration::from_millis(timeout.unwrap_or(3000)),
future::join_all(
self.language_servers
.iter_clients()
@ -1276,4 +1342,48 @@ impl Editor {
.await
.map(|_| ())
}
pub async fn wait_event(&mut self) -> EditorEvent {
tokio::select! {
biased;
Some(event) = self.save_queue.next() => {
self.write_count -= 1;
EditorEvent::DocumentSaved(event)
}
Some(config_event) = self.config_events.1.recv() => {
EditorEvent::ConfigEvent(config_event)
}
Some(message) = self.language_servers.incoming.next() => {
EditorEvent::LanguageServerMessage(message)
}
Some(event) = self.debugger_events.next() => {
EditorEvent::DebuggerEvent(event)
}
_ = &mut self.idle_timer => {
EditorEvent::IdleTimer
}
}
}
pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
while self.write_count > 0 {
if let Some(save_event) = self.save_queue.next().await {
self.write_count -= 1;
let save_event = match save_event {
Ok(event) => event,
Err(err) => {
self.set_error(err.to_string());
bail!(err);
}
};
let doc = doc_mut!(self, &save_event.doc_id);
doc.set_last_saved_revision(save_event.revision);
}
}
Ok(())
}
}

@ -95,31 +95,19 @@ pub struct Rect {
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
/// Creates a new rect, with width and height
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
width,
height,
}
}
#[inline]
pub fn area(self) -> u16 {
self.width * self.height
pub fn area(self) -> usize {
(self.width as usize) * (self.height as usize)
}
#[inline]
@ -315,6 +303,44 @@ impl From<Color> for crossterm::style::Color {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UnderlineStyle {
Reset,
Line,
Curl,
Dotted,
Dashed,
DoubleLine,
}
impl FromStr for UnderlineStyle {
type Err = &'static str;
fn from_str(modifier: &str) -> Result<Self, Self::Err> {
match modifier {
"line" => Ok(Self::Line),
"curl" => Ok(Self::Curl),
"dotted" => Ok(Self::Dotted),
"dashed" => Ok(Self::Dashed),
"double_line" => Ok(Self::DoubleLine),
_ => Err("Invalid underline style"),
}
}
}
impl From<UnderlineStyle> for crossterm::style::Attribute {
fn from(style: UnderlineStyle) -> Self {
match style {
UnderlineStyle::Line => crossterm::style::Attribute::Underlined,
UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled,
UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted,
UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed,
UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined,
UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline,
}
}
}
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
@ -332,7 +358,6 @@ bitflags! {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
const ITALIC = 0b0000_0000_0100;
const UNDERLINED = 0b0000_0000_1000;
const SLOW_BLINK = 0b0000_0001_0000;
const RAPID_BLINK = 0b0000_0010_0000;
const REVERSED = 0b0000_0100_0000;
@ -349,7 +374,6 @@ impl FromStr for Modifier {
"bold" => Ok(Self::BOLD),
"dim" => Ok(Self::DIM),
"italic" => Ok(Self::ITALIC),
"underlined" => Ok(Self::UNDERLINED),
"slow_blink" => Ok(Self::SLOW_BLINK),
"rapid_blink" => Ok(Self::RAPID_BLINK),
"reversed" => Ok(Self::REVERSED),
@ -375,7 +399,7 @@ impl FromStr for Modifier {
/// just S3.
///
/// ```rust
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@ -391,6 +415,8 @@ impl FromStr for Modifier {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD,
/// underline_color: Some(Color::Reset),
/// underline_style: Some(UnderlineStyle::Reset),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer[(0, 0)].style(),
@ -401,7 +427,7 @@ impl FromStr for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@ -415,6 +441,8 @@ impl FromStr for Modifier {
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// underline_color: Some(Color::Reset),
/// underline_style: Some(UnderlineStyle::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
@ -426,6 +454,8 @@ impl FromStr for Modifier {
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub underline_color: Option<Color>,
pub underline_style: Option<UnderlineStyle>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
@ -435,6 +465,8 @@ impl Default for Style {
Style {
fg: None,
bg: None,
underline_color: None,
underline_style: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
@ -447,6 +479,8 @@ impl Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
underline_color: None,
underline_style: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
@ -482,6 +516,36 @@ impl Style {
self
}
/// Changes the underline color.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Style};
/// let style = Style::default().underline_color(Color::Blue);
/// let diff = Style::default().underline_color(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red));
/// ```
pub fn underline_color(mut self, color: Color) -> Style {
self.underline_color = Some(color);
self
}
/// Changes the underline style.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{UnderlineStyle, Style};
/// let style = Style::default().underline_style(UnderlineStyle::Line);
/// let diff = Style::default().underline_style(UnderlineStyle::Curl);
/// assert_eq!(style.patch(diff), Style::default().underline_style(UnderlineStyle::Curl));
/// ```
pub fn underline_style(mut self, style: UnderlineStyle) -> Style {
self.underline_style = Some(style);
self
}
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
@ -538,6 +602,8 @@ impl Style {
pub fn patch(mut self, other: Style) -> Style {
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
self.underline_color = other.underline_color.or(self.underline_color);
self.underline_style = other.underline_style.or(self.underline_style);
self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier);
@ -552,33 +618,6 @@ impl Style {
mod tests {
use super::*;
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {

@ -1,7 +1,7 @@
use std::fmt::Write;
use crate::{
graphics::{Color, Modifier, Style},
graphics::{Color, Style, UnderlineStyle},
Document, Editor, Theme, View,
};
@ -147,7 +147,7 @@ pub fn breakpoints<'doc>(
.find(|breakpoint| breakpoint.line == line)?;
let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
error.add_modifier(Modifier::UNDERLINED)
error.underline_style(UnderlineStyle::Line)
} else if breakpoint.condition.is_some() {
error
} else if breakpoint.log_message.is_some() {

@ -262,7 +262,7 @@ impl Editor {
log::info!("{}", output);
self.set_status(format!("{} {}", prefix, output));
}
Event::Initialized => {
Event::Initialized(_) => {
// send existing breakpoints
for (path, breakpoints) in &mut self.breakpoints {
// TODO: call futures in parallel, await all

@ -16,7 +16,11 @@ pub struct Info {
}
impl Info {
pub fn new(title: &str, body: Vec<(String, String)>) -> Self {
pub fn new<T, U>(title: &str, body: &[(T, U)]) -> Self
where
T: AsRef<str>,
U: AsRef<str>,
{
if body.is_empty() {
return Self {
title: title.to_string(),
@ -26,11 +30,21 @@ impl Info {
};
}
let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap();
let item_width = body
.iter()
.map(|(item, _)| item.as_ref().width())
.max()
.unwrap();
let mut text = String::new();
for (item, desc) in &body {
let _ = writeln!(text, "{:width$} {}", item, desc, width = item_width);
for (item, desc) in body {
let _ = writeln!(
text,
"{:width$} {}",
item.as_ref(),
desc.as_ref(),
width = item_width
);
}
Self {
@ -42,19 +56,19 @@ impl Info {
}
pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self {
let body = body
let body: Vec<_> = body
.into_iter()
.map(|(desc, events)| {
let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
(events.join(", "), desc.to_string())
(events.join(", "), desc)
})
.collect();
Self::new(title, body)
Self::new(title, &body)
}
pub fn from_registers(registers: &Registers) -> Self {
let body = registers
let body: Vec<_> = registers
.inner()
.iter()
.map(|(ch, reg)| {
@ -62,13 +76,12 @@ impl Info {
.read()
.get(0)
.and_then(|s| s.lines().next())
.map(String::from)
.unwrap_or_default();
(ch.to_string(), content)
})
.collect();
let mut infobox = Self::new("Registers", body);
let mut infobox = Self::new("Registers", &body);
infobox.width = 30; // copied content could be very long
infobox
}

@ -14,6 +14,7 @@ pub enum Event {
Mouse(MouseEvent),
Paste(String),
Resize(u16, u16),
IdleTimeout,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]

@ -53,17 +53,30 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
let height = view.inner_area().height as usize;
let last_line_height = view.inner_area().height.saturating_sub(1) as usize;
let relative = match align {
Align::Center => height / 2,
Align::Center => last_line_height / 2,
Align::Top => 0,
Align::Bottom => height,
Align::Bottom => last_line_height,
};
view.offset.row = line.saturating_sub(relative);
}
/// Applies a [`helix_core::Transaction`] to the given [`Document`]
/// and [`View`].
pub fn apply_transaction(
transaction: &helix_core::Transaction,
doc: &mut Document,
view: &mut View,
) -> bool {
// This is a short function but it's easy to call `Document::apply`
// without calling `View::apply` or in the wrong order. The transaction
// must be applied to the document before the view.
doc.apply(transaction, view.id) && view.apply(transaction, doc)
}
pub use document::Document;
pub use editor::Editor;
pub use theme::Theme;

@ -11,21 +11,17 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer};
use toml::{map::Map, Value};
use crate::graphics::UnderlineStyle;
pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml"))
// .expect("Failed to parse default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
name: "default".into(),
..toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml"))
// .expect("Failed to parse base 16 default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../base16_theme.toml"))
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
name: "base16_theme".into(),
..toml::from_slice(include_bytes!("../../base16_theme.toml"))
.expect("Failed to parse base 16 default theme")
});
@ -52,7 +48,12 @@ impl Loader {
return Ok(self.base16_default());
}
self.load_theme(name, name, false).map(Theme::from)
let theme = self.load_theme(name, name, false).map(Theme::from)?;
Ok(Theme {
name: name.into(),
..theme
})
}
// load the theme and its parent recursively and merge them
@ -179,8 +180,10 @@ impl Loader {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct Theme {
name: String,
// UI styles are stored in a HashMap
styles: HashMap<String, Style>,
// tree-sitter highlight styles are stored in a Vec to optimize lookups
@ -201,6 +204,7 @@ impl From<Value> for Theme {
scopes,
rainbow_length,
highlights,
..Default::default()
}
}
}
@ -269,6 +273,7 @@ impl<'de> Deserialize<'de> for Theme {
scopes,
rainbow_length,
highlights,
..Default::default()
})
}
}
@ -340,6 +345,10 @@ impl Theme {
self.highlights[index]
}
pub fn name(&self) -> &str {
&self.name
}
pub fn get(&self, scope: &str) -> Style {
self.try_get(scope).unwrap_or_default()
}
@ -352,6 +361,13 @@ impl Theme {
.find_map(|s| self.styles.get(s).copied())
}
/// Get the style of a scope, without falling back to dot separated broader
/// scopes. For example if `ui.text.focus` is not defined in the theme, it
/// will return `None`, even if `ui.text` is.
pub fn try_get_exact(&self, scope: &str) -> Option<Style> {
self.styles.get(scope).copied()
}
#[inline]
pub fn scopes(&self) -> &[String] {
&self.scopes
@ -465,19 +481,48 @@ impl ThemePalette {
.ok_or(format!("Theme: invalid modifier: {}", value))
}
pub fn parse_underline_style(value: &Value) -> Result<UnderlineStyle, String> {
value
.as_str()
.and_then(|s| s.parse().ok())
.ok_or(format!("Theme: invalid underline style: {}", value))
}
pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
if let Value::Table(entries) = value {
for (name, value) in entries {
for (name, mut value) in entries {
match name.as_str() {
"fg" => *style = style.fg(self.parse_color(value)?),
"bg" => *style = style.bg(self.parse_color(value)?),
"underline" => {
let table = value
.as_table_mut()
.ok_or("Theme: underline must be table")?;
if let Some(value) = table.remove("color") {
*style = style.underline_color(self.parse_color(value)?);
}
if let Some(value) = table.remove("style") {
*style = style.underline_style(Self::parse_underline_style(&value)?);
}
if let Some(attr) = table.keys().next() {
return Err(format!("Theme: invalid underline attribute: {attr}"));
}
}
"modifiers" => {
let modifiers = value
.as_array()
.ok_or("Theme: modifiers should be an array")?;
for modifier in modifiers {
*style = style.add_modifier(Self::parse_modifier(modifier)?);
if modifier
.as_str()
.map_or(false, |modifier| modifier == "underlined")
{
*style = style.underline_style(UnderlineStyle::Line);
} else {
*style = style.add_modifier(Self::parse_modifier(modifier)?);
}
}
}
_ => return Err(format!("Theme: invalid style attribute: {}", name)),

@ -3,7 +3,9 @@ use crate::{
gutter::{self, Gutter},
Document, DocumentId, ViewId,
};
use helix_core::{pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection};
use helix_core::{
pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
};
use std::fmt;
@ -62,6 +64,22 @@ impl JumpList {
pub fn get(&self) -> &[Jump] {
&self.jumps
}
/// Applies a [`Transaction`] of changes to the jumplist.
/// This is necessary to ensure that changes to documents do not leave jump-list
/// selections pointing to parts of the text which no longer exist.
fn apply(&mut self, transaction: &Transaction, doc: &Document) {
let text = doc.text().slice(..);
for (doc_id, selection) in &mut self.jumps {
if doc.id() == *doc_id {
*selection = selection
.clone()
.map(transaction.changes())
.ensure_invariants(text);
}
}
}
}
#[derive(Clone)]
@ -334,6 +352,14 @@ impl View {
// (None, None) => return,
// }
// }
/// Applies a [`Transaction`] to the view.
/// Instead of calling this function directly, use [crate::apply_transaction]
/// which applies a transaction to the [`Document`] and view together.
pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
self.jumps.apply(transaction, doc);
true
}
}
#[cfg(test)]

@ -107,7 +107,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "elixir"
source = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "1dabc1c790e07115175057863808085ea60dd08a" }
source = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "b20eaa75565243c50be5e35e253d8beb58f45d56" }
[[language]]
name = "fish"
@ -184,7 +184,7 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
[[grammar]]
name = "c"
source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "f05e279aedde06a25801c3f2b2cc8ac17fac52ae" }
source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd5fc1cee660dce6fe23f6043d75af424a" }
[[language]]
name = "cpp"
@ -221,7 +221,7 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
[[grammar]]
name = "cpp"
source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "e8dcc9d2b404c542fd236ea5f7208f90be8a6e89" }
source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "d5e90fba898f320db48d81ddedd78d52c67c1fed" }
[[language]]
name = "c-sharp"
@ -233,9 +233,28 @@ comment-token = "//"
indent = { tab-width = 4, unit = "\t" }
language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
[language.debugger]
name = "netcoredbg"
transport = "tcp"
command = "netcoredbg"
args = [ "--interpreter=vscode" ]
port-arg = "--server={}"
[[language.debugger.templates]]
name = "launch"
request = "launch"
completion = [ { name = "path to dll", completion = "filename" } ]
args = { type = "coreclr", console = "internalConsole", internalConsoleOptions = "openOnSessionStart", program = "{0}" }
[[language.debugger.templates]]
name = "attach"
request = "attach"
completion = [ "pid" ]
args = { processId = "{0}" }
[[grammar]]
name = "c-sharp"
source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "9c494a503c8e2044bfffce57f70b480c01a82f03" }
source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "5b60f99545fea00a33bbfae5be956f684c4c69e2" }
[[language]]
name = "go"
@ -537,7 +556,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "twig"
source = { git = "https://github.com/eirabben/tree-sitter-twig", rev = "b7444181fb38e603e25ea8fcdac55f9492e49c27" }
source = { git = "https://github.com/gbprod/tree-sitter-twig", rev = "807b293fec3fead64f54c64fdf6fb05516c032b9" }
[[language]]
name = "latex"
@ -572,7 +591,7 @@ name = "julia"
scope = "source.julia"
injection-regex = "julia"
file-types = ["jl"]
roots = []
roots = ["Manifest.toml", "Project.toml"]
comment-token = "#"
language-server = { command = "julia", args = [
"--startup-file=no",
@ -667,7 +686,7 @@ language-server = { command = "lua-language-server", args = [] }
[[grammar]]
name = "lua"
source = { git = "https://github.com/nvim-treesitter/tree-sitter-lua", rev = "6f5d40190ec8a0aa8c8410699353d820f4f7d7a6" }
source = { git = "https://github.com/MunifTanjim/tree-sitter-lua", rev = "887dfd4e83c469300c279314ff1619b1d0b85b91" }
[[language]]
name = "svelte"
@ -723,6 +742,19 @@ indent = { tab-width = 2, unit = " " }
name = "haskell"
source = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "b6ec26f181dd059eedd506fa5fbeae1b8e5556c8" }
[[language]]
name = "purescript"
scope = "source.purescript"
injection-regex = "purescript"
file-types = ["purs"]
roots = ["spago.dhall", "bower.json"]
comment-token = "--"
language-server = { command = "purescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
auto-format = true
formatter = { command = "purs-tidy", args = ["format"] }
grammar = "haskell"
[[language]]
name = "zig"
scope = "source.zig"
@ -911,7 +943,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "markdown"
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown" }
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown" }
[[language]]
name = "markdown.inline"
@ -923,7 +955,7 @@ grammar = "markdown_inline"
[[grammar]]
name = "markdown_inline"
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown-inline" }
source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown-inline" }
[[language]]
name = "dart"
@ -950,7 +982,7 @@ language-server = { command = "metals" }
[[grammar]]
name = "scala"
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "0a3dd53a7fc4b352a538397d054380aaa28be54c" }
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "140c96cf398693189d4e50f76d19ddfcd8a018f8" }
[[language]]
name = "dockerfile"
@ -1021,8 +1053,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf
name = "git-config"
scope = "source.gitconfig"
roots = []
# TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`)
file-types = [".gitmodules", ".gitconfig"]
file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }]
injection-regex = "git-config"
comment-token = "#"
indent = { tab-width = 4, unit = "\t" }
@ -1108,7 +1139,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "rescript"
source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "4cd7ba91696886fdaca086fb32b5fd8cc294a129" }
source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "65609807c628477f3b94052e7ef895885ac51c3c" }
[[language]]
name = "erlang"
@ -1408,12 +1439,12 @@ scope = "source.cairo"
injection-regex = "cairo"
file-types = ["cairo"]
roots = []
comment-token = "#"
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "cairo"
source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "5155c6eb40db6d437f4fa41b8bcd8890a1c91716" }
source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "b249662a1eefeb4d71c9529cdd971e74fecc10fe" }
[[language]]
name = "cpon"
@ -1459,7 +1490,7 @@ source = { git = "https://github.com/bearcove/tree-sitter-meson", rev = "feea83b
[[language]]
name = "sshclientconfig"
scope = "source.sshclientconfig"
file-types = [".ssh/config", "/etc/ssh/ssh_config"]
file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }]
roots = []
[[grammar]]
@ -1803,3 +1834,18 @@ roots = []
[[grammar]]
name = "wast"
source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f9d709847bf7a3de0942a84e912f59088", subpath = "wast" }
[[language]]
name = "d"
scope = "source.d"
file-types = [ "d", "dd" ]
roots = []
comment-token = "//"
injection-regex = "d"
indent = { tab-width = 4, unit = " "}
language-server = { command = "serve-d" }
formatter = { command = "dfmt" }
[[grammar]]
name = "d"
source = { git = "https://github.com/gdamore/tree-sitter-d", rev="601c4a1e8310fb2f3c43fa8a923d0d27497f3c04" }

@ -9,25 +9,23 @@
(member_access_expression
name: (identifier) @function))
(invocation_expression
(member_access_expression
expression: (identifier) @variable))
(invocation_expression
function: (conditional_access_expression
(member_binding_expression
name: (identifier) @function)))
(invocation_expression
[(identifier) (qualified_name)] @function)
[(identifier) (qualified_name)] @function)
(local_function_statement
name: (identifier) @function)
; Generic Method invocation with generic type
(invocation_expression
function: (generic_name
. (identifier) @function))
. (identifier) @function))
;; Namespaces
(namespace_declaration
name: [(identifier) (qualified_name)] @namespace)
@ -40,8 +38,11 @@
(namespace_declaration name: (identifier) @type)
(using_directive (_) @namespace)
(constructor_declaration name: (identifier) @type)
(destructor_declaration name: (identifier) @type)
(object_creation_expression [(identifier) (qualified_name)] @type)
(type_parameter_list (type_parameter) @type)
(array_type (identifier) @type)
(for_each_statement type: (identifier) @type)
[
(implicit_type)
@ -66,7 +67,7 @@
(object_creation_expression
(generic_name
(identifier) @type))
(identifier) @type))
(property_declaration
(generic_name
@ -74,7 +75,7 @@
(_
type: (generic_name
(identifier) @type))
(identifier) @type))
;; Enum
(enum_member_declaration (identifier) @variable.other.member)
@ -91,6 +92,7 @@
(verbatim_string_literal)
(interpolated_string_text)
(interpolated_verbatim_string_text)
(interpolation_format_clause)
"\""
"$\""
"@$\""
@ -107,6 +109,9 @@
(comment) @comment
;; Tokens
(type_argument_list ["<" ">"] @punctuation.bracket)
(type_parameter_list ["<" ">"] @punctuation.bracket)
[
";"
"."
@ -150,97 +155,112 @@
"%"
"%="
":"
"::"
".."
"&="
"->"
"??="
] @operator
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
;; Keywords
(modifier) @keyword
(modifier) @keyword.storage.modifier
(this_expression) @keyword
(escape_sequence) @constant.character.escape
[
"as"
"await"
"base"
"break"
"case"
"catch"
"checked"
"class"
"continue"
"default"
"delegate"
"do"
"else"
"enum"
"event"
"explicit"
"finally"
"for"
"foreach"
"goto"
"if"
"implicit"
"interface"
"from"
"get"
"in"
"init"
"is"
"let"
"lock"
"namespace"
"new"
"operator"
"out"
"params"
"return"
"ref"
"select"
"set"
"sizeof"
"stackalloc"
"static"
"struct"
"switch"
"throw"
"try"
"typeof"
"unchecked"
"using"
"while"
"new"
"await"
"in"
"yield"
"get"
"set"
"when"
"out"
"ref"
"from"
"where"
"select"
"record"
"init"
"with"
"let"
"yield"
] @keyword
(nullable_directive) @keyword.directive
(define_directive) @keyword.directive
(undef_directive) @keyword.directive
(if_directive) @keyword.directive
(else_directive) @keyword.directive
(elif_directive) @keyword.directive
(endif_directive) @keyword.directive
(region_directive) @keyword.directive
(endregion_directive) @keyword.directive
(error_directive) @keyword.directive
(warning_directive) @keyword.directive
(line_directive) @keyword.directive
(pragma_directive) @keyword.directive
[
"class"
"delegate"
"enum"
"event"
"interface"
"namespace"
"struct"
"record"
] @keyword.storage.type
[
"explicit"
"implicit"
"static"
] @keyword.storage.modifier
[
"break"
"continue"
"goto"
] @keyword.control
[
"catch"
"finally"
"throw"
"try"
] @keyword.control.exception
[
"do"
"for"
"foreach"
"while"
] @keyword.control.repeat
[
"case"
"default"
"else"
"if"
"switch"
] @keyword.control.conditional
"return" @keyword.control.return
[
(nullable_directive)
(define_directive)
(undef_directive)
(if_directive)
(else_directive)
(elif_directive)
(endif_directive)
(region_directive)
(endregion_directive)
(error_directive)
(warning_directive)
(line_directive)
(pragma_directive)
] @keyword.directive
;; Linq
(from_clause (identifier) @variable)
@ -259,10 +279,16 @@
(binary_expression [(identifier) (qualified_name)] @variable [(identifier) (qualified_name)] @variable)
(binary_expression [(identifier) (qualified_name)]* @variable)
(conditional_expression [(identifier) (qualified_name)] @variable)
(conditional_access_expression [(identifier) (qualified_name)] @variable)
(prefix_unary_expression [(identifier) (qualified_name)] @variable)
(postfix_unary_expression [(identifier) (qualified_name)]* @variable)
(assignment_expression [(identifier) (qualified_name)] @variable)
(cast_expression [(identifier) (qualified_name)] @type [(identifier) (qualified_name)] @variable)
(element_access_expression (identifier) @variable)
(member_access_expression
expression: ([(identifier) (qualified_name)] @type
(#match? @type "^[A-Z]")))
(member_access_expression [(identifier) (qualified_name)] @variable)
;; Class
(base_list (identifier) @type)
@ -278,7 +304,6 @@
name: (identifier) @variable)
;; Delegate
(delegate_declaration (identifier) @type)
;; Lambda
@ -296,11 +321,11 @@
(parameter_list
(parameter
name: (identifier) @parameter))
name: (identifier) @parameter))
(parameter_list
(parameter
type: [(identifier) (qualified_name)] @type))
type: [(identifier) (qualified_name)] @type))
;; Typeof
(type_of_expression [(identifier) (qualified_name)] @type)
@ -315,7 +340,7 @@
;; Type
(generic_name (identifier) @type)
(type_parameter [(identifier) (qualified_name)] @variable.parameter)
(type_parameter [(identifier) (qualified_name)] @type)
(type_argument_list [(identifier) (qualified_name)] @type)
;; Type constraints
@ -333,15 +358,21 @@
;; Lock statement
(lock_statement (identifier) @variable)
;; Declaration expression
(declaration_expression
type: (identifier) @type
name: (identifier) @variable)
;; Rest
(member_access_expression) @variable
(element_access_expression (identifier) @variable)
(argument (identifier) @variable)
(name_colon (identifier) @variable)
(if_statement (identifier) @variable)
(for_statement (identifier) @variable)
(for_each_statement (identifier) @variable)
(expression_statement (identifier) @variable)
(member_access_expression expression: (identifier) @variable)
(member_access_expression name: (identifier) @variable)
(conditional_access_expression [(identifier) (qualified_name)] @variable)
(array_rank_specifier (identifier) @variable)
(equals_value_clause (identifier) @variable)
(interpolation (identifier) @variable)
(cast_expression (identifier) @variable)
((identifier) @comment.unused
(#eq? @comment.unused "_"))
(#eq? @comment.unused "_"))

@ -1,71 +1,111 @@
(storage_class_specifier) @keyword.storage
"goto" @keyword
"register" @keyword
"break" @keyword
"case" @keyword
"continue" @keyword
"default" @keyword
"do" @keyword
"else" @keyword
"enum" @keyword
"extern" @keyword
"for" @keyword
"if" @keyword
"inline" @keyword
"return" @keyword
"sizeof" @keyword
"struct" @keyword
"switch" @keyword
"typedef" @keyword
"union" @keyword
"volatile" @keyword
"while" @keyword
"const" @keyword
[
"#define"
"#elif"
"#else"
"#endif"
"#if"
"#ifdef"
"#ifndef"
"#include"
(preproc_directive)
"enum"
"struct"
"typedef"
"union"
] @keyword.storage.type
[
"extern"
"register"
(type_qualifier)
(storage_class_specifier)
] @keyword.storage.modifier
[
"goto"
"break"
"continue"
] @keyword.control
[
"do"
"for"
"while"
] @keyword.control.repeat
[
"if"
"else"
"switch"
"case"
"default"
] @keyword.control.conditional
"return" @keyword.control.return
[
"defined"
"#define"
"#elif"
"#else"
"#endif"
"#if"
"#ifdef"
"#ifndef"
"#include"
(preproc_directive)
] @keyword.directive
"--" @operator
"-" @operator
"-=" @operator
"->" @operator
"=" @operator
"!=" @operator
"*" @operator
"&" @operator
"&&" @operator
"+" @operator
"++" @operator
"+=" @operator
"<" @operator
"==" @operator
">" @operator
"||" @operator
">=" @operator
"<=" @operator
"." @punctuation.delimiter
";" @punctuation.delimiter
(pointer_declarator "*" @type.builtin)
(abstract_pointer_declarator "*" @type.builtin)
[
"+"
"-"
"*"
"/"
"++"
"--"
"%"
"=="
"!="
">"
"<"
">="
"<="
"&&"
"||"
"!"
"&"
"|"
"^"
"~"
"<<"
">>"
"="
"+="
"-="
"*="
"/="
"%="
"<<="
">>="
"&="
"^="
"|="
"?"
] @operator
(conditional_expression ":" @operator)
"..." @punctuation
["," "." ":" ";" "->" "::"] @punctuation.delimiter
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
[(true) (false)] @constant.builtin.boolean
(enumerator) @type.enum.variant
(enumerator name: (identifier) @type.enum.variant)
(string_literal) @string
(system_lib_string) @string
(null) @constant
(number_literal) @constant.numeric.integer
(number_literal) @constant.numeric
(char_literal) @constant.character
(call_expression
@ -73,19 +113,28 @@
(call_expression
function: (field_expression
field: (field_identifier) @function))
(call_expression (argument_list (identifier) @variable))
(function_declarator
declarator: (identifier) @function)
declarator: [(identifier) (field_identifier)] @function)
(parameter_declaration
declarator: (identifier) @variable.parameter)
(parameter_declaration
(pointer_declarator
declarator: (identifier) @variable.parameter))
(preproc_function_def
name: (identifier) @function.special)
(attribute
name: (identifier) @attribute)
(field_identifier) @variable.other.member
(statement_identifier) @label
(type_identifier) @type
(primitive_type) @type
(sized_type_specifier) @type
(primitive_type) @type.builtin
(sized_type_specifier) @type.builtin
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
(identifier) @variable

@ -36,7 +36,6 @@
[
"if"
"else"
"end"
"assert"
"with"
"with_attr"
@ -54,7 +53,6 @@
"const"
"local"
"struct"
"member"
"alloc_locals"
"tempvar"
] @keyword

@ -1,7 +1,11 @@
; inherits: c
; Functions
; These casts are parsed as function calls, but are not.
((identifier) @keyword (#eq? @keyword "static_cast"))
((identifier) @keyword (#eq? @keyword "dynamic_cast"))
((identifier) @keyword (#eq? @keyword "reinterpret_cast"))
((identifier) @keyword (#eq? @keyword "const_cast"))
(call_expression
function: (qualified_identifier
name: (identifier) @function))
@ -12,56 +16,114 @@
(template_method
name: (field_identifier) @function)
(template_function
name: (identifier) @function)
(function_declarator
declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
declarator: (qualified_identifier
name: (identifier) @function))
name: (qualified_identifier
name: (identifier) @function)))
(function_declarator
declarator: (field_identifier) @function)
; Types
((namespace_identifier) @type
(#match? @type "^[A-Z]"))
(using_declaration ("using" "namespace" (identifier) @namespace))
(using_declaration ("using" "namespace" (qualified_identifier name: (identifier) @namespace)))
(namespace_definition name: (identifier) @namespace)
(namespace_identifier) @namespace
(qualified_identifier name: (identifier) @type.enum.variant)
(auto) @type
"decltype" @type
(ref_qualifier ["&" "&&"] @type.builtin)
(reference_declarator ["&" "&&"] @type.builtin)
(abstract_reference_declarator ["&" "&&"] @type.builtin)
; Constants
(this) @variable.builtin
(nullptr) @constant
(nullptr) @constant.builtin
; Keywords
"catch" @keyword
"class" @keyword
"constexpr" @keyword
"delete" @keyword
"explicit" @keyword
"final" @keyword
"friend" @keyword
"mutable" @keyword
"namespace" @keyword
"noexcept" @keyword
"new" @keyword
"override" @keyword
"private" @keyword
"protected" @keyword
"public" @keyword
"template" @keyword
"throw" @keyword
"try" @keyword
"typename" @keyword
"using" @keyword
"virtual" @keyword
(template_argument_list (["<" ">"] @punctuation.bracket))
(template_parameter_list (["<" ">"] @punctuation.bracket))
(default_method_clause "default" @keyword)
"static_assert" @function.special
[
"<=>"
"[]"
"()"
] @operator
[
"co_await"
"co_return"
"co_yield"
"concept"
"delete"
"new"
"operator"
"requires"
"using"
] @keyword
[
"catch"
"noexcept"
"throw"
"try"
] @keyword.control.exception
[
"and"
"and_eq"
"bitor"
"bitand"
"not"
"not_eq"
"or"
"or_eq"
"xor"
"xor_eq"
] @keyword.operator
[
"class"
"namespace"
"typename"
"template"
] @keyword.storage.type
[
"constexpr"
"constinit"
"consteval"
"mutable"
] @keyword.storage.modifier
; Modifiers that aren't plausibly type/storage related.
[
"explicit"
"friend"
"virtual"
(virtual_specifier) ; override/final
"private"
"protected"
"public"
"inline" ; C++ meaning differs from C!
] @keyword
; Strings
(raw_string_literal) @string
; inherits: c

@ -0,0 +1,231 @@
; highlights.scm
;
; Highlighting queries for D code for use by Tree-Sitter.
;
; Copyright 2022 Garrett D'Amore
;
; Distributed under the MIT License.
; (See accompanying file LICENSE.txt or https://opensource.org/licenses/MIT)
; SPDX-License-Identifier: MIT
; these are listed first, because they override keyword queries
(identity_expression (in) @operator)
(identity_expression (is) @operator)
(storage_class) @keyword.storage
(function_declaration (identifier) @function)
(call_expression (identifier) @function)
(call_expression (type (identifier) @function))
(module_fqn) @namespace
[
(abstract)
(alias)
(align)
(asm)
(assert)
(auto)
(cast)
(const)
(debug)
(delete)
(deprecated)
(export)
(extern)
(final)
(immutable)
(in)
(inout)
(invariant)
(is)
(lazy)
; "macro" - obsolete
(mixin)
(module)
(new)
(nothrow)
(out)
(override)
(package)
(pragma)
(private)
(protected)
(public)
(pure)
(ref)
(scope)
(shared)
(static)
(super)
(synchronized)
(template)
(this)
(throw)
(typeid)
(typeof)
(unittest)
(version)
(with)
(gshared)
(traits)
(vector)
(parameters_)
] @keyword
[
(class)
(struct)
(interface)
(union)
(enum)
(function)
(delegate)
] @keyword.storage.type
[
(break)
(case)
(catch)
(continue)
(do)
(default)
(finally)
(else)
(goto)
(if)
(switch)
(try)
] @keyword.control
(return) @keyword.control.return
(import) @keyword.control.import
[
(for)
(foreach)
(foreach_reverse)
(while)
] @keyword.control.repeat
[
(not_in)
(not_is)
"/="
"/"
".."
"..."
"&"
"&="
"&&"
"|"
"|="
"||"
"-"
"-="
"--"
"+"
"+="
"++"
"<"
"<="
"<<"
"<<="
">"
">="
">>="
">>>="
">>"
">>>"
"!"
"!="
"?"
"$"
"="
"=="
"*"
"*="
"%"
"%="
"^"
"^="
"^^"
"^^="
"~"
"~="
"@"
"=>"
] @operator
[
"("
")"
"["
"]"
] @punctuation.bracket
[
";"
"."
":"
","
] @punctuation.delimiter
[
(true)
(false)
] @constant.builtin.boolean
(null) @constant.builtin
(special_keyword) @constant.builtin
(directive) @keyword.directive
(shebang) @keyword.directive
(comment) @comment
[
(void)
(bool)
(byte)
(ubyte)
(char)
(short)
(ushort)
(wchar)
(dchar)
(int)
(uint)
(long)
(ulong)
(real)
(double)
] @type.builtin
[
(cent)
(ucent)
(ireal)
(idouble)
(ifloat)
(creal)
(double)
(cfloat)
] @warning ; these types are deprecated
(label (identifier) @label)
(goto_statement (goto) @keyword (identifier) @label)
(string_literal) @string
(int_literal) @constant.numeric.integer
(float_literal) @constant.numeric.float
(char_literal) @constant.character
(identifier) @variable
(at_attribute) @attribute
; everything after __EOF_ is plain text
(end_file) @ui.text

@ -0,0 +1,17 @@
[
(parameters)
(template_parameters)
(expression_statement)
(aggregate_body)
(function_body)
(scope_statement)
(block_statement)
(case_statement)
] @indent
[
(case)
(default)
"}"
"]"
] @outdent

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

@ -0,0 +1,9 @@
(function_declaration (function_body) @function.inside) @function.around
(comment) @comment.inside
(comment)+ @comment.around
(class_declaration (aggregate_body) @class.inside) @class.around
(interface_declaration (aggregate_body) @class.inside) @class.around
(struct_declaration (aggregate_body) @class.inside) @class.around
(unittest_declaration (block_statement) @test.insid) @test.around
(parameter) @parameter.inside
(template_parameter) @parameter.inside

@ -0,0 +1,10 @@
[
(do_statement)
(while_statement)
(repeat_statement)
(if_statement)
(for_statement)
(function_declaration)
(function_definition)
(table_constructor)
] @fold

@ -1,7 +1,8 @@
;;; Highlighting for lua
;;; Builtins
(self) @variable.builtin
((identifier) @variable.builtin
(#eq? @variable.builtin "self"))
;; Keywords
@ -12,20 +13,20 @@
"end"
] @keyword.control.conditional)
(elseif_statement
[
"else"
"elseif"
"then"
] @keyword.control.conditional
"end"
] @keyword.control.conditional)
(for_statement
(else_statement
[
"for"
"do"
"else"
"end"
] @keyword.control.repeat)
] @keyword.control.conditional)
(for_in_statement
(for_statement
[
"for"
"do"
@ -51,21 +52,34 @@
"end"
] @keyword)
"return" @keyword.control.return
[
"in"
"local"
(break_statement)
"goto"
"return"
] @keyword
(function_declaration
[
"function"
"end"
] @keyword.function)
(function_definition
[
"function"
"end"
] @keyword.function)
;; Operators
[
"not"
"and"
"or"
] @operator
] @keyword.operator
[
"="
@ -95,6 +109,7 @@
["," "." ":" ";"] @punctuation.delimiter
;; Brackets
[
"("
")"
@ -110,7 +125,8 @@
(true)
] @constant.builtin.boolean
(nil) @constant.builtin
(spread) @constant ;; "..."
(vararg_expression) @constant
((identifier) @constant
(#match? @constant "^[A-Z][A-Z_0-9]*$"))
@ -119,45 +135,32 @@
(identifier) @variable.parameter)
; ;; Functions
(function [(function_name) (identifier)] @function)
(function ["function" "end"] @keyword.function)
(function
(function_name
(function_name_field
(property_identifier) @function .)))
(local_function (identifier) @function)
(local_function ["function" "end"] @keyword.function)
(function_declaration name: (identifier) @function)
(function_call name: (identifier) @function.call)
(variable_declaration
(variable_declarator (identifier) @function) (function_definition))
(local_variable_declaration
(variable_declarator (identifier) @function) (function_definition))
(function_declaration name: (dot_index_expression field: (identifier) @function))
(function_call name: (dot_index_expression field: (identifier) @function.call))
(function_definition ["function" "end"] @keyword.function)
; TODO: incorrectly highlights variable N in `N, nop = 42, function() end`
(assignment_statement
(variable_list
name: (identifier) @function)
(expression_list
value: (function_definition)))
(function_call
[
((identifier) @variable (method) @function.method)
((_) (method) @function.method)
(identifier) @function
(field_expression (property_identifier) @function)
]
. (arguments))
(method_index_expression method: (identifier) @function.method)
;; Nodes
(table ["{" "}"] @constructor)
(comment) @comment
(string) @string
(number) @constant.numeric.integer
(label_statement) @label
; A bit of a tricky one, this will only match field names
(field . (identifier) @variable.other.member (_))
(shebang) @comment
(hash_bang_line) @comment
;; Property
(property_identifier) @variable.other.member
(dot_index_expression field: (identifier) @variable.other.member)
;; Variable
(identifier) @variable

@ -1,17 +1,13 @@
[
(function_definition)
(variable_declaration)
(local_variable_declaration)
(function_declaration)
(method_index_expression)
(field)
(local_function)
(function)
(if_statement)
(for_statement)
(for_in_statement)
(repeat_statement)
(return_statement)
(while_statement)
(table)
(table_constructor)
(arguments)
(do_statement)
] @indent

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

@ -0,0 +1,15 @@
(function_definition
body: (_) @function.inside) @function.around
(function_declaration
body: (_) @function.inside) @function.around
(parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(arguments
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(comment) @comment.inside
(comment)+ @comment.around

@ -1,2 +1,2 @@
((html_tag) @injection.content (#set! injection.language "html"))
((html_tag) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children))

@ -27,12 +27,32 @@
(class_definition)
] @indent
[
(if_statement)
(for_statement)
(while_statement)
(with_statement)
(try_statement)
(function_definition)
(class_definition)
] @extend
[
(return_statement)
(break_statement)
(continue_statement)
(raise_statement)
(pass_statement)
] @extend.prevent-once
[
")"
"]"
"}"
(return_statement)
(pass_statement)
(raise_statement)
] @outdent
(elif_clause
"elif" @outdent)
(else_clause
"else" @outdent)

@ -10,20 +10,23 @@
[
(type_identifier)
(unit_type)
"list"
] @type
(list ["list{" "}"] @type)
(list_pattern ["list{" "}"] @type)
[
(variant_identifier)
(polyvar_identifier)
] @constant
] @constructor
(property_identifier) @variable.other.member
(record_type_field (property_identifier) @type)
(object_type (field (property_identifier) @type))
(record_field (property_identifier) @variable.other.member)
(object (field (property_identifier) @variable.other.member))
(member_expression (property_identifier) @variable.other.member)
(module_identifier) @namespace
(jsx_identifier) @tag
(jsx_attribute (property_identifier) @variable.parameter)
; Parameters
;----------------
@ -42,8 +45,8 @@
"${" @punctuation.bracket
"}" @punctuation.bracket) @embedded
(character) @constant.character
(escape_sequence) @constant.character.escape
(character) @string.special
(escape_sequence) @string.special
; Other literals
;---------------
@ -60,11 +63,13 @@
; Functions
;----------
; parameter(s) in parens
[
(formal_parameters (value_identifier))
(parameter (value_identifier))
(labeled_parameter (value_identifier))
] @variable.parameter
; single parameter with no parens
(function parameter: (value_identifier) @variable.parameter)
; Meta
@ -74,7 +79,7 @@
"@"
"@@"
(decorator_identifier)
] @label
] @keyword.directive
(extension_identifier) @keyword
("%") @keyword
@ -82,13 +87,13 @@
; Misc
;-----
(subscript_expression index: (string) @variable.other.member)
; (subscript_expression index: (string) @attribute)
(polyvar_type_pattern "#" @constant)
[
("include")
("open")
] @keyword
] @keyword.control.import
[
"as"
@ -96,25 +101,43 @@
"external"
"let"
"module"
"mutable"
"private"
"rec"
"type"
"and"
] @keyword
"assert"
"async"
"await"
"with"
"unpack"
] @keyword.storage.type
"mutable" @keyword.storage.modifier
[
"if"
"else"
"switch"
] @keyword
"when"
] @keyword.control.conditional
[
"exception"
"try"
"catch"
"raise"
] @keyword
] @keyword.control.exception
(call_expression
function: (value_identifier) @keyword.control.exception
(#eq? @keyword.control.exception "raise"))
[
"for"
"in"
"to"
"downto"
"while"
] @keyword.control.conditional
[
"."
@ -129,17 +152,15 @@
"-"
"-."
"*"
"**"
"*."
"/"
"/."
"<"
"<="
"=="
"==="
"!"
"!="
"!=="
">"
">="
"&&"
"||"
@ -151,6 +172,10 @@
(uncurry)
] @operator
; Explicitly enclose these operators with binary_expression
; to avoid confusion with JSX tag delimiters
(binary_expression ["<" ">" "/"] @operator)
[
"("
")"
@ -172,7 +197,24 @@
"~"
"?"
"=>"
".."
"..."
] @punctuation
] @punctuation.special
(ternary_expression ["?" ":"] @operator)
; JSX
;----------
(jsx_identifier) @tag
(jsx_element
open_tag: (jsx_opening_element ["<" ">"] @punctuation.special))
(jsx_element
close_tag: (jsx_closing_element ["<" "/" ">"] @punctuation.special))
(jsx_self_closing_element ["/" ">" "<"] @punctuation.special)
(jsx_fragment [">" "<" "/"] @punctuation.special)
(jsx_attribute (property_identifier) @attribute)
; Error
;----------
(ERROR) @keyword.control.exception

@ -5,4 +5,4 @@
(#set! injection.language "javascript"))
((raw_gql) @injection.content
(#set! injection.language "graphql"))
(#set! injection.language "graphql"))

@ -0,0 +1,7 @@
(switch_expression) @local.scope
(if_expression) @local.scope
; Definitions
;------------
(type_declaration) @local.defintion
(let_binding) @local.defintion

@ -3,14 +3,110 @@
(module_declaration definition: ((_) @class.inside)) @class.around
; Blocks
;-------
(block (_) @function.inside) @function.around
; Functions
;----------
(function body: (_) @function.inside) @function.around
; Calls
;------
(call_expression arguments: ((_) @parameter.inside)) @parameter.around
; Comments
;---------
(comment) @comment.inside
(comment)+ @comment.around
; Parameters
;-----------
(function parameter: (_) @parameter.inside @parameter.around)
(formal_parameters
","
. (_) @parameter.inside
@parameter.around)
(formal_parameters
. (_) @parameter.inside
. ","?
@parameter.around)
(arguments
"," @_arguments_start
. (_) @parameter.inside
@parameter.around)
(arguments
. (_) @parameter.inside
. ","?
@parameter.around)
(function_type_parameters
","
. (_) @parameter.inside
@parameter.around)
(function_type_parameters
. (_) @parameter.inside
. ","?
@parameter.around)
(functor_parameters
","
. (_) @parameter.inside
@parameter.around)
(functor_parameters
. (_) @parameter.inside
. ","?
@parameter.around)
(type_parameters
","
. (_) @parameter.inside
@parameter.around)
(type_parameters
. (_) @parameter.inside
. ","?
@parameter.around)
(type_arguments
","
. (_) @parameter.inside
@parameter.around)
(type_arguments
. (_) @parameter.inside
. ","?
@parameter.around)
(decorator_arguments
","
. (_) @parameter.inside
@parameter.around)
(decorator_arguments
. (_) @parameter.inside
. ","?
@parameter.around)
(variant_parameters
","
. (_) @parameter.inside
@parameter.around)
(variant_parameters
. (_) @parameter.inside
. ","?
@parameter.around)
(polyvar_parameters
","
. (_) @parameter.inside
@parameter.around)
(polyvar_parameters
. (_) @parameter.inside
. ","?
@parameter.around)

@ -1,16 +1,60 @@
(comment_directive) @comment
(comment) @comment
(filter_identifier) @function.method
(function_identifier) @function.method
(test) @function.builtin
(variable) @variable
(string) @string
(interpolated_string) @string
(operator) @operator
(number) @constant.numeric.integer
(boolean) @constant.builtin.boolean
(null) @constant.builtin
(keyword) @keyword
(attribute) @attribute
(tag) @tag
(conditional) @keyword.control.conditional
(repeat) @keyword.control.repeat
(method) @function.method
(parameter) @variable.parameter
[
"{%"
"{%-"
"{%~"
"%}"
"-%}"
"~%}"
"{{"
"{{-"
"{{~"
"}}"
"-}}"
"~}}"
"{{"
"}}"
"{{-"
"-}}"
"{{~"
"~}}"
"{%"
"%}"
"{%-"
"-%}"
"{%~"
"~%}"
] @keyword
[
","
"."
"?"
":"
"="
] @punctuation.delimiter
(interpolated_string [
"#{"
"}"
] @punctuation.delimiter)
[
"("
")"
"["
"]"
"{"
] @punctuation.bracket
(hash [
"}"
] @punctuation.bracket)

@ -2,57 +2,26 @@
(float_literal) @constant.numeric.float
(bool_literal) @constant.builtin.boolean
(global_constant_declaration) @variable
(global_variable_declaration) @variable
(compound_statement) @variable
(const_expression) @function
(variable_identifier_declaration
(identifier) @variable
(type_declaration) @type)
(function_declaration
(identifier) @function
(function_return_type_declaration
(type_declaration) @type))
(parameter
(variable_identifier_declaration
(identifier) @variable.parameter
(type_declaration) @type))
(struct_declaration
(identifier) @type)
(struct_declaration
(struct_member
(variable_identifier_declaration
(identifier) @variable.other.member
(type_declaration) @type)))
[
"bitcast"
"discard"
"enable"
"fallthrough"
] @keyword
(type_constructor_or_function_call_expression
(type_declaration) @function)
[
"let"
"override"
"struct"
"type"
"var"
(texel_format)
] @keyword.storage.type
[
"struct"
"bitcast"
"discard"
"enable"
"fallthrough"
"fn"
"let"
"private"
"read"
"read_write"
"storage"
"type"
"uniform"
"var"
"workgroup"
"write"
"override"
(texel_format)
] @keyword
(access_mode)
(address_space)
] @keyword.storage.modifier
"fn" @keyword.function
@ -62,53 +31,87 @@
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
(type_declaration ["<" ">"] @punctuation.bracket)
[
"loop"
"for"
"while"
"break"
"continue"
"continuing"
"break"
"continue"
"continuing"
] @keyword.control
[
"loop"
"for"
"while"
] @keyword.control.repeat
[
"if"
"else"
"switch"
"case"
"default"
"if"
"else"
"switch"
"case"
"default"
] @keyword.control.conditional
[
"&"
"&&"
"/"
"!"
"="
"=="
"!="
">"
">="
">>"
"<"
"<="
"<<"
"%"
"-"
"+"
"|"
"||"
"*"
"~"
"^"
"@"
"++"
"--"
"!"
"!="
"%"
"%="
"&"
"&&"
"&="
"*"
"*="
"+"
"++"
"+="
"-"
"--"
"-="
"->"
"/"
"/="
"<"
"<<"
"<="
"="
"=="
">"
">="
">>"
"@"
"^"
"^="
"|"
"|="
"||"
"~"
] @operator
(function_declaration
(identifier) @function)
(parameter
(variable_identifier_declaration
(identifier) @variable.parameter))
(struct_declaration
(identifier) @type)
(struct_declaration
(struct_member
(variable_identifier_declaration
(identifier) @variable.other.member)))
(type_constructor_or_function_call_expression
(type_declaration (identifier) @function))
(type_declaration _ @type)
(attribute
(identifier) @attribute)
(identifier) @attribute)
(comment) @comment
(identifier) @variable
(ERROR) @error
(comment) @comment

@ -1,12 +1,14 @@
[
(AsmExpr)
(AssignExpr)
(Block)
(BlockExpr)
(ContainerDecl)
(SwitchExpr)
(AssignExpr)
(ErrorUnionExpr)
(Statement)
(InitList)
(Statement)
(SwitchExpr)
(TestDecl)
] @indent
[

@ -0,0 +1,23 @@
(TopLevelDecl (FnProto)
(_) @function.inside) @function.around
(TestDecl (_) @test.inside) @test.around
; matches all of: struct, enum, union
; this unfortunately cannot be split up because
; of the way struct "container" types are defined
(TopLevelDecl (VarDecl (ErrorUnionExpr (SuffixExpr (ContainerDecl
(_) @class.inside))))) @class.around
(TopLevelDecl (VarDecl (ErrorUnionExpr (SuffixExpr (ErrorSetDecl
(_) @class.inside))))) @class.around
(ParamDeclList
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
[
(doc_comment)
(line_comment)
] @comment.inside
(line_comment)+ @comment.around
(doc_comment)+ @comment.around

@ -34,23 +34,22 @@
# Interface
"ui.background"= { bg = "background" }
"ui.cursor" = { bg = "yellow", fg = "dark_gray" }
"ui.cursor" = { bg = "yellow", fg = "light_gray" }
"ui.cursor.match" = { fg = "orange" }
"ui.linenr" = { fg = "dark_gray" }
"ui.linenr" = { fg = "light_gray" }
"ui.linenr.selected" = { fg = "orange" }
"ui.cursorline" = { bg = "black" }
"ui.statusline" = { fg = "foreground", bg = "black" }
"ui.popup" = { bg = "black" }
"ui.window" = { fg = "dark_gray" }
"ui.window" = { fg = "light_gray" }
"ui.help" = { fg = "foreground", bg = "black" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { bg = "dark_gray", fg = "foreground" }
"ui.text.focus" = { bg = "light_gray", fg = "foreground" }
"ui.text.info" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "dark_gray" }
"ui.virtual.whitespace" = { fg = "light_gray" }
"ui.virtual.ruler" = { bg = "black" }
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "orange", fg = "background" }
"ui.selection" = { bg = "dark_gray" }
"ui.selection" = { bg = "light_gray" }
"warning" = { fg = "yellow" }
"error" = { fg = "red", modifiers = ["bold"] }
"info" = { fg = "blue", modifiers = ["bold"] }
@ -59,8 +58,13 @@
"diagnostic.info"= { fg = "blue", modifiers = ["underlined"] }
"diagnostic.warning"= { fg = "yellow", modifiers = ["underlined"] }
"diagnostic.error"= { fg = "red", modifiers = ["underlined"] }
"ui.bufferline" = { fg = "gray", bg = "dark_gray" }
"ui.bufferline.active" = { fg = "dark", bg = "background" }
"ui.bufferline" = { fg = "ui_foreground", bg = "ui_background" }
"ui.bufferline.active" = { fg = "ui_background", bg = "ui_foreground" }
"ui.statusline" = { fg = "ui_foreground", bg = "ui_background" }
"ui.statusline.inactive" = { fg = "ui_foreground", bg = "ui_background" }
"ui.statusline.normal" = { fg = "white", bg = "light_blue" }
"ui.statusline.insert" = { fg = "white", bg = "orange" }
"ui.statusline.select" = { fg = "white", bg = "magenta" }
"special" = { fg = "orange" }
@ -68,14 +72,18 @@
background = "#fcfcfc"
foreground = "#5c6166"
ui_foreground = "#8a9199"
ui_background = "#f8f9fa"
black = "#e7eaed"
white = "#fcfcfc"
blue = "#399ee6"
light_blue = "#55b4d4"
cyan = "#478acc"
dark_gray = "#e7eaed"
light_gray = "#e7eaed"
gray = "#787b8099"
green = "#86b300"
magenta = "#a37acc"
orange = "#fa8d3e"
red = "#f07171"
yellow = "#ffaa33"
dark = "#131721"

@ -34,7 +34,8 @@
# Interface
"ui.background"= { bg = "background" }
"ui.cursor" = { bg = "yellow", fg = "dark_gray" }
"ui.cursor" = { bg = "green", fg = "dark_gray" }
"ui.cursor.primary" = { bg = "orange", fg = "dark_gray" }
"ui.cursor.match" = { fg = "orange" }
"ui.linenr" = { fg = "dark_gray" }
"ui.linenr.selected" = { fg = "orange" }

@ -1,97 +1,96 @@
# Author : Wojciech Kępka <wojciech@wkepka.dev>
"attribute" = "bogster0"
"keyword" = { fg = "bogster1", modifiers = ["bold"] }
"keyword.directive" = "bogster1"
"namespace" = "bogster2"
"punctuation" = "bogster0"
"punctuation.delimiter" = "bogster0"
"operator" = { fg = "bogster0", modifiers = ["bold"] }
"special" = "bogster3"
"variable.other.member" = "bogster4"
"variable" = "bogster4"
"variable.parameter" = "bogster4"
"type" = "bogster5"
"type.builtin" = { fg = "bogster2", modifiers = ["bold"] }
"constructor" = "bogster5"
"function" = "bogster6"
"function.macro" = { fg = "bogster0", modifiers = ["bold"] }
"function.builtin" = { fg = "bogster6", modifiers = ["bold"] }
"comment" = "bogster7"
"variable.builtin" = "bogster4"
"constant" = "bogster8"
"constant.builtin" = "bogster8"
"string" = "bogster8"
"constant.numeric" = "bogster9"
"constant.character.escape" = { fg = "bogster3", modifiers = ["bold"] }
"label" = "bogster9"
"attribute" = "bogster-orange"
"keyword" = { fg = "bogster-yellow", modifiers = ["bold"] }
"keyword.directive" = "bogster-yellow"
"namespace" = "bogster-red"
"punctuation" = "bogster-orange"
"punctuation.delimiter" = "bogster-orange"
"operator" = { fg = "bogster-orange", modifiers = ["bold"] }
"special" = "bogster-lgreen"
"variable.other.member" = "bogster-fg0"
"variable" = "bogster-fg0"
"variable.parameter" = "bogster-fg0"
"type" = "bogster-lred"
"type.builtin" = { fg = "bogster-red", modifiers = ["bold"] }
"constructor" = "bogster-lred"
"function" = "bogster-lblue"
"function.macro" = { fg = "bogster-orange", modifiers = ["bold"] }
"function.builtin" = { fg = "bogster-lblue", modifiers = ["bold"] }
"comment" = "bogster-base5"
"variable.builtin" = "bogster-fg0"
"constant" = "bogster-teal"
"constant.builtin" = "bogster-teal"
"string" = "bogster-teal"
"constant.numeric" = "bogster-blue"
"constant.character.escape" = { fg = "bogster-lgreen", modifiers = ["bold"] }
"label" = "bogster-blue"
"module" = "bogster-red"
"module" = "bogster2"
"markup.heading" = "bogster-blue"
"markup.list" = "bogster-red"
"markup.bold" = { fg = "bogster-yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "bogster-purp", modifiers = ["italic"] }
"markup.link.url" = { fg = "bogster-yellow", modifiers = ["underlined"] }
"markup.link.text" = "bogster-red"
"markup.quote" = "bogster-teal"
"markup.raw" = "bogster-lgreen"
# TODO
"markup.heading" = "blue"
"markup.list" = "red"
"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
"markup.link.text" = "red"
"markup.quote" = "cyan"
"markup.raw" = "green"
"diff.plus" = "bogster-teal"
"diff.delta" = "bogster-orange"
"diff.minus" = "bogster-lred"
"diff.plus" = "bogster8"
"diff.delta" = "bogster0"
"diff.minus" = "bogster5"
"ui.background" = { bg = "bogster-base1" }
"ui.linenr" = { fg = "bogster-base4" }
"ui.linenr.selected" = { fg = "bogster-fg1" }
"ui.cursorline" = { bg = "bogster-base0" }
"ui.statusline" = { fg = "bogster-fg1", bg = "bogster-base2" }
"ui.statusline.inactive" = { fg = "bogster-fg0", bg = "bogster-base2" }
"ui.popup" = { bg = "bogster-base2" }
"ui.window" = { bg = "bogster-base2" }
"ui.help" = { bg = "bogster-base2", fg = "bogster-fg1" }
"ui.background" = { bg = "bogster10" }
"ui.linenr" = { fg = "bogster11" }
"ui.linenr.selected" = { fg = "bogster12" } # TODO
"ui.cursorline" = { bg = "bogster13" }
"ui.statusline" = { fg = "bogster12", bg = "bogster14" }
"ui.statusline.inactive" = { fg = "bogster4", bg = "bogster14" }
"ui.popup" = { bg = "bogster14" }
"ui.window" = { bg = "bogster14" }
"ui.help" = { bg = "bogster14", fg = "bogster12" }
"ui.statusline.normal" = { fg = "bogster-base1", bg = "bogster-blue", modifiers = [ "bold" ]}
"ui.statusline.insert" = { fg = "bogster-base1", bg = "bogster-lgreen", modifiers = [ "bold" ]}
"ui.statusline.select" = { fg = "bogster-base1", bg = "bogster-red", modifiers = [ "bold" ] }
"ui.statusline.normal" = { fg = "bogster10", bg = "bogster9", modifiers = [ "bold" ]}
"ui.statusline.insert" = { fg = "bogster10", bg = "bogster3", modifiers = [ "bold" ]}
"ui.statusline.select" = { fg = "bogster10", bg = "bogster2", modifiers = [ "bold" ] }
"ui.text" = { fg = "bogster-fg1" }
"ui.text.focus" = { fg = "bogster-fg1", modifiers= ["bold"] }
"ui.virtual.whitespace" = "bogster-base5"
"ui.virtual.ruler" = { bg = "bogster-base0" }
"ui.text" = { fg = "bogster12" }
"ui.text.focus" = { fg = "bogster12", modifiers= ["bold"] }
"ui.virtual.whitespace" = "bogster7"
"ui.virtual.ruler" = { bg = "bogster13" }
"ui.selection" = { bg = "bogster-base3" }
"ui.cursor.match" = { fg = "bogster-base3", bg = "bogster-orange" }
"ui.cursor" = { fg = "bogster-base5", modifiers = ["reversed"] }
"ui.selection" = { bg = "bogster15" }
# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported
"ui.cursor.match" = { fg = "bogster15", bg = "bogster0" }
"ui.cursor" = { fg = "bogster16", modifiers = ["reversed"] }
"ui.menu" = { fg = "bogster-fg1", bg = "bogster-base2" }
"ui.menu.selected" = { bg = "bogster-base3" }
"ui.menu" = { fg = "bogster12", bg = "bogster14" }
"ui.menu.selected" = { bg = "bogster15" }
"warning" = "bogster0"
"error" = "bogster5"
"info" = "bogster8"
"hint" = "bogster9"
"warning" = "bogster-orange"
"error" = "bogster-lred"
"info" = "bogster-teal"
"hint" = "bogster-blue"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
bogster0 = "#dc7759"
bogster1 = "#dcb659"
bogster2 = "#d32c5d"
bogster3 = "#7fdc59"
bogster4 = "#c6b8ad"
bogster5 = "#dc597f"
bogster6 = "#59dcd8"
bogster7 = "#627d9d"
bogster8 = "#59dcb7"
bogster9 = "#59c0dc"
bogster10 = "#161c23"
bogster11 = "#415367"
bogster12 = "#e5ded6"
bogster13 = "#131920"
bogster14 = "#232d38"
bogster15 = "#313f4e"
bogster16 = "#ABB2BF"
bogster-yellow = "#dcb659"
bogster-lblue = "#59dcd8"
bogster-teal = "#59dcb7"
bogster-blue = "#36b2d4"
bogster-orange = "#dc7759"
bogster-red = "#d32c5d"
bogster-lgreen = "#7fdc59"
bogster-lred = "#dc597f"
bogster-purp = "#b759dc"
bogster-base0 = "#13181e"
bogster-base1 = "#161c23"
bogster-base2 = "#232d38"
bogster-base3 = "#313f4e"
bogster-base4 = "#415367"
bogster-base5 = "#abb2bf"
bogster-fg0 = "#c6b8ad"
bogster-fg1 = "#e5ded6"

@ -0,0 +1,97 @@
# Author : Wojciech Kępka <wojciech@wkepka.dev>
"attribute" = "bogster-orange"
"keyword" = { fg = "bogster-yellow", modifiers = ["bold"] }
"keyword.directive" = "bogster-yellow"
"namespace" = "bogster-red"
"punctuation" = "bogster-orange"
"punctuation.delimiter" = "bogster-orange"
"operator" = { fg = "bogster-orange", modifiers = ["bold"] }
"special" = "bogster-lgreen"
"variable.other.member" = "bogster-fg0"
"variable" = "bogster-fg0"
"variable.parameter" = "bogster-fg0"
"type" = "bogster-lred"
"type.builtin" = { fg = "bogster-red", modifiers = ["bold"] }
"constructor" = "bogster-lred"
"function" = "bogster-lblue"
"function.macro" = { fg = "bogster-orange", modifiers = ["bold"] }
"function.builtin" = { fg = "bogster-lblue", modifiers = ["bold"] }
"comment" = "bogster-base5"
"variable.builtin" = "bogster-fg0"
"constant" = "bogster-teal"
"constant.builtin" = "bogster-teal"
"string" = "bogster-teal"
"constant.numeric" = "bogster-blue"
"constant.character.escape" = { fg = "bogster-lgreen", modifiers = ["bold"] }
"label" = "bogster-blue"
"module" = "bogster-red"
"markup.heading" = "bogster-blue"
"markup.list" = "bogster-red"
"markup.bold" = { fg = "bogster-yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
"markup.link.url" = { fg = "bogster-yellow", modifiers = ["underlined"] }
"markup.link.text" = "bogster-red"
"markup.quote" = "bogster-lblue"
"markup.raw" = "bogster-teal"
"diff.plus" = "bogster-teal"
"diff.delta" = "bogster-orange"
"diff.minus" = "bogster-lred"
"ui.background" = { bg = "bogster-base0" }
"ui.linenr" = { fg = "bogster-base3" }
"ui.linenr.selected" = { fg = "bogster-fg1" }
"ui.cursorline" = { bg = "bogster-base00" }
"ui.statusline" = { fg = "bogster-fg1", bg = "bogster-base1" }
"ui.statusline.inactive" = { fg = "bogster-fg0", bg = "bogster-base1" }
"ui.popup" = { bg = "bogster-base1" }
"ui.window" = { bg = "bogster-base1" }
"ui.help" = { bg = "bogster-base1", fg = "bogster-fg1" }
"ui.statusline.normal" = { fg = "bogster-base0", bg = "bogster-blue", modifiers = [ "bold" ]}
"ui.statusline.insert" = { fg = "bogster-base0", bg = "bogster-lgreen", modifiers = [ "bold" ]}
"ui.statusline.select" = { fg = "bogster-base0", bg = "bogster-red", modifiers = [ "bold" ] }
"ui.text" = { fg = "bogster-fg1" }
"ui.text.focus" = { fg = "bogster-fg1", modifiers= ["bold"] }
"ui.virtual.whitespace" = "bogster-base5"
"ui.virtual.ruler" = { bg = "bogster-base00" }
"ui.selection" = { bg = "bogster-base1" }
"ui.cursor.match" = { fg = "bogster-base2", bg = "bogster-orange" }
"ui.cursor" = { fg = "bogster-gray", modifiers = ["reversed"] }
"ui.menu" = { fg = "bogster-fg1", bg = "bogster-base1" }
"ui.menu.selected" = { bg = "bogster-base2" }
"warning" = "bogster-orange"
"error" = "bogster-lred"
"info" = "bogster-teal"
"hint" = "bogster-blue"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
bogster-orange = "#dc7759"
bogster-yellow = "#a58023"
bogster-red = "#d32c5d"
bogster-blue = "#59c0dc"
bogster-gray = "#abb2bf"
bogster-lgreen = "#7fdc59"
bogster-lred = "#dc597f"
bogster-lblue = "#289cbc"
bogster-teal = "#23a580"
bogster-fg1 = "#161c23"
bogster-fg0 = "#232d38"
bogster-base00 = "#d7dbbb"
bogster-base0 = "#f6fbd6"
bogster-base1 = "#c7c7ba"
bogster-base2 = "#aaaa97"
bogster-base3 = "#415367"
bogster-base5 = "#627d9d"

@ -92,7 +92,8 @@
"info" = { fg = "light_blue" }
"hint" = { fg = "light_gray3" }
diagnostic = { modifiers = ["underlined"] }
"diagnostic.error".underline = { color = "red", style = "curl" }
"diagnostic".underline = { color = "gold", style = "curl" }
[palette]
white = "#ffffff"

@ -8,11 +8,12 @@
# Email: sainnhe@gmail.com
# License: MIT License
"constant.character.escape" = "orange"
"type" = "yellow"
"constant" = "purple"
"constant.numeric" = "purple"
"constant.character.escape" = "orange"
"string" = "green"
"string.regexp" = "blue"
"comment" = "grey0"
"variable" = "fg"
"variable.builtin" = "blue"
@ -23,6 +24,7 @@
"punctuation.delimiter" = "grey2"
"punctuation.bracket" = "fg"
"keyword" = "red"
"keyword.directive" = "aqua"
"operator" = "orange"
"function" = "green"
"function.builtin" = "blue"
@ -54,25 +56,33 @@
"diff.minus" = "red"
"ui.background" = { bg = "bg0" }
"ui.background.separator" = "grey0"
"ui.cursor" = { fg = "bg0", bg = "fg" }
"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
"ui.cursor.insert" = { fg = "bg0", bg = "grey1" }
"ui.cursor.select" = { fg = "bg0", bg = "blue" }
"ui.cursorline.primary" = { bg = "bg1" }
"ui.cursorline.secondary" = { bg = "bg1" }
"ui.selection" = { bg = "bg3" }
"ui.linenr" = "grey0"
"ui.linenr.selected" = "fg"
"ui.cursorline" = { bg = "bg1" }
"ui.statusline" = { fg = "grey2", bg = "bg2" }
"ui.statusline" = { fg = "grey2", bg = "bg3" }
"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" }
"ui.popup" = { fg = "grey2", bg = "bg1" }
"ui.window" = { fg = "grey2", bg = "bg1" }
"ui.help" = { fg = "fg", bg = "bg1" }
"ui.statusline.normal" = { fg = "bg0", bg = "grey2", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "bg0", bg = "yellow", modifiers = ["bold"] }
"ui.statusline.select" = { fg = "bg0", bg = "blue", modifiers = ["bold"] }
"ui.bufferline" = { fg = "grey0", bg = "bg1" }
"ui.bufferline.active" = { fg = "fg", bg = "bg3", modifiers = ["bold"] }
"ui.popup" = { fg = "grey2", bg = "bg2" }
"ui.window" = { fg = "grey0", bg = "bg0" }
"ui.help" = { fg = "fg", bg = "bg2" }
"ui.text" = "fg"
"ui.text.focus" = "fg"
"ui.menu" = { fg = "fg", bg = "bg2" }
"ui.menu" = { fg = "fg", bg = "bg3" }
"ui.menu.selected" = { fg = "bg0", bg = "green" }
"ui.selection" = { bg = "bg3" }
"ui.virtual.whitespace" = "grey0"
"ui.virtual.ruler" = { bg = "bg1" }
"ui.virtual.whitespace" = { fg = "bg4" }
"ui.virtual.indent-guide" = { fg = "bg4" }
"ui.virtual.ruler" = { bg = "bg3" }
"hint" = "blue"
"info" = "aqua"
@ -80,7 +90,6 @@
"error" = "red"
"diagnostic" = { modifiers = ["underlined"] }
[palette]
bg0 = "#2b3339"

@ -8,11 +8,12 @@
# Email: sainnhe@gmail.com
# License: MIT License
"constant.character.escape" = "orange"
"type" = "yellow"
"constant" = "purple"
"constant.numeric" = "purple"
"constant.character.escape" = "orange"
"string" = "green"
"string.regexp" = "blue"
"comment" = "grey0"
"variable" = "fg"
"variable.builtin" = "blue"
@ -23,6 +24,7 @@
"punctuation.delimiter" = "grey2"
"punctuation.bracket" = "fg"
"keyword" = "red"
"keyword.directive" = "aqua"
"operator" = "orange"
"function" = "green"
"function.builtin" = "blue"
@ -54,25 +56,33 @@
"diff.minus" = "red"
"ui.background" = { bg = "bg0" }
"ui.background.separator" = "grey0"
"ui.cursor" = { fg = "bg0", bg = "fg" }
"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
"ui.cursor.insert" = { fg = "bg0", bg = "grey1" }
"ui.cursor.select" = { fg = "bg0", bg = "blue" }
"ui.cursorline.primary" = { bg = "bg1" }
"ui.cursorline.secondary" = { bg = "bg1" }
"ui.selection" = { bg = "bg3" }
"ui.linenr" = "grey0"
"ui.linenr.selected" = "fg"
"ui.cursorline" = { bg = "bg1" }
"ui.statusline" = { fg = "grey2", bg = "bg2" }
"ui.statusline" = { fg = "grey2", bg = "bg3" }
"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" }
"ui.popup" = { fg = "grey2", bg = "bg1" }
"ui.window" = { fg = "grey2", bg = "bg1" }
"ui.help" = { fg = "fg", bg = "bg1" }
"ui.statusline.normal" = { fg = "bg0", bg = "grey2", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "bg0", bg = "yellow", modifiers = ["bold"] }
"ui.statusline.select" = { fg = "bg0", bg = "blue", modifiers = ["bold"] }
"ui.bufferline" = { fg = "grey0", bg = "bg1" }
"ui.bufferline.active" = { fg = "fg", bg = "bg3", modifiers = ["bold"] }
"ui.popup" = { fg = "grey2", bg = "bg2" }
"ui.window" = { fg = "grey0", bg = "bg0" }
"ui.help" = { fg = "fg", bg = "bg2" }
"ui.text" = "fg"
"ui.text.focus" = "fg"
"ui.menu" = { fg = "fg", bg = "bg2" }
"ui.menu" = { fg = "fg", bg = "bg3" }
"ui.menu.selected" = { fg = "bg0", bg = "green" }
"ui.selection" = { bg = "bg3" }
"ui.virtual.whitespace" = "grey0"
"ui.virtual.ruler" = { bg = "bg1" }
"ui.virtual.whitespace" = { fg = "bg4" }
"ui.virtual.indent-guide" = { fg = "bg4" }
"ui.virtual.ruler" = { bg = "bg3" }
"hint" = "blue"
"info" = "aqua"
@ -80,7 +90,6 @@
"error" = "red"
"diagnostic" = { modifiers = ["underlined"] }
[palette]
bg0 = "#fff9e8"

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

Loading…
Cancel
Save