Merge branch 'master'

pull/6/head
trivernis 2 years ago
commit 46b135bdc5
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -0,0 +1,26 @@
# Publish the Nix flake outputs to Cachix
name: Cachix
on:
push:
branches:
- master
jobs:
publish:
name: Publish Flake
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v18
- name: Authenticate with Cachix
uses: cachix/cachix-action@v11
with:
name: helix
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Build nix flake
run: nix build -L

14
Cargo.lock generated

@ -508,6 +508,7 @@ dependencies = [
"helix-core",
"helix-view",
"serde",
"termini",
"unicode-segmentation",
]
@ -1100,6 +1101,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termini"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9"
dependencies = [
"dirs-next",
]
[[package]]
name = "textwrap"
version = "0.15.1"
@ -1197,9 +1207,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",

@ -67,18 +67,41 @@ cd helix
cargo install --path helix-term
```
This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars.
This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`.
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 (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` |
| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
This location can be overridden via the `HELIX_RUNTIME` environment variable.
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).
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.

@ -49,6 +49,7 @@ on unix operating systems.
| `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` |
| `completion-trigger-chars` | The chars that trigger completion (additional to all word chars) | `['.', ':']` |

@ -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,6 +87,7 @@
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
| r | ✓ | | | `R` |
| racket | | | | `racket` |
@ -124,4 +126,4 @@
| wgsl | ✓ | | | `wgsl_analyzer` |
| xit | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server` |
| zig | ✓ | | ✓ | `zls` |
| zig | ✓ | | ✓ | `zls` |

@ -65,7 +65,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. |
<<<<<<< HEAD

@ -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,17 +75,42 @@ 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 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:

@ -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` |
@ -280,7 +289,7 @@ This layer is a kludge of mappings, mostly pickers.
| `e` | Open or focus explorer | `toggle_or_focus_explorer` |
| `E` | open explorer recursion | `open_explorer_recursion` |
> 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
@ -313,8 +322,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
@ -416,8 +425,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` |
<<<<<<< HEAD
### Rainbow
@ -259,7 +277,7 @@ These scopes are used for theming the editor interface.
| `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` | 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 |
@ -267,7 +285,7 @@ These scopes are used for theming the editor interface.
| `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.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 |

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

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

@ -190,6 +190,8 @@ pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize
pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize {
if char_idx == slice.len_chars() {
char_idx
} else if char_idx > slice.len_chars() {
slice.len_chars()
} else {
prev_grapheme_boundary(slice, char_idx + 1)
}

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

@ -61,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>,
@ -119,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 {
@ -355,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,
@ -420,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()
}
}
@ -438,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>>,
@ -448,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()),
};
@ -459,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
@ -481,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())
@ -2024,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::*;
@ -2044,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 };
@ -2104,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")
@ -2195,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")
);
}
}

@ -394,10 +394,12 @@ impl ChangeSet {
}
if pos > old_pos {
panic!(
log::error!(
"Position {} is out of range for changeset len {}!",
pos, old_pos
)
pos,
old_pos
);
return old_pos;
}
new_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, Explorer},
};
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")?;
@ -257,6 +261,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;
@ -287,9 +295,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;
@ -306,26 +311,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();
@ -334,26 +319,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();
}
}
@ -458,6 +439,111 @@ impl Application {
}
}
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,
@ -523,14 +609,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(),
@ -852,7 +938,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)?;
@ -878,11 +969,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);
}
@ -890,13 +980,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
}
}

@ -47,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::{
@ -107,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);
@ -1869,10 +1871,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();
@ -1924,8 +1931,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;
@ -1961,7 +1968,8 @@ fn global_search(cx: &mut Context) {
},
);
compositor.push(Box::new(overlayed(picker)));
});
},
));
Ok(call)
};
cx.jobs.callback(show_picker);
@ -2226,7 +2234,7 @@ 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(),
@ -2449,29 +2457,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(),
},
}
}
@ -2511,12 +2518,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")
}));
}
@ -2540,13 +2547,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.
//
@ -2555,34 +2555,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 {
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);
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,
@ -2921,7 +2931,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()),
@ -3077,7 +3087,7 @@ 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(..)),
@ -3177,7 +3187,7 @@ pub mod insert {
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);
@ -3293,8 +3303,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);
@ -3307,10 +3317,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);
@ -3323,7 +3334,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;
}
@ -3334,7 +3345,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;
}
@ -3346,7 +3357,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;
}
@ -3358,7 +3369,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;
}
@ -3410,9 +3421,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);
@ -3441,6 +3458,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
@ -3450,7 +3472,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(())
}
@ -3691,7 +3713,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(),

@ -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,
};
@ -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);
@ -457,37 +519,52 @@ pub fn code_action(cx: &mut Context) {
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 by CodeActionKind
// this ensures that the most relevant codeactions (quickfix) show up first
// while more situational commands (like refactors) show up later
// this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts)
actions.sort_by_key(|action| match &action {
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
kind: Some(kind), ..
}) => {
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,
}
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// 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;
}
_ => 7,
// 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 =

@ -1,5 +1,7 @@
use std::ops::Deref;
use crate::job::Job;
use super::*;
use helix_view::{
@ -19,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(());
}
@ -30,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(())
@ -46,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(())
@ -70,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
@ -86,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: {:?}",
@ -149,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(
@ -162,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> {
@ -184,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(
@ -197,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> {
@ -214,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(
@ -227,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(
@ -274,7 +282,7 @@ fn delete(
let future = doc.delete();
cx.jobs.add(Job::new(future));
helix_lsp::block_on(cx.jobs.finish())?;
cx.block_try_flush_writes()?;
let doc_id = view!(cx.editor).doc;
cx.editor.close_document(doc_id, true)?;
@ -286,39 +294,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(())
@ -371,10 +370,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);
}
@ -508,7 +506,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");
}
@ -527,7 +525,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");
}
@ -545,7 +543,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)
}
@ -559,6 +557,7 @@ fn force_write_quit(
}
write_impl(cx, args.first(), true)?;
cx.block_try_flush_writes()?;
force_quit(cx, &[], event)
}
@ -587,110 +586,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(())
@ -705,7 +722,7 @@ fn quit_all(
return Ok(());
}
quit_all_impl(cx.editor, false)
quit_all_impl(cx, false)
}
fn force_quit_all(
@ -717,7 +734,7 @@ fn force_quit_all(
return Ok(());
}
quit_all_impl(cx.editor, true)
quit_all_impl(cx, true)
}
fn cquit(
@ -733,9 +750,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(
@ -753,7 +770,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(
@ -1085,7 +1102,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(())
}
@ -1498,15 +1530,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)
};
@ -1614,8 +1649,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(),
@ -1624,7 +1659,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)
};
@ -2103,7 +2139,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,
},
@ -2148,7 +2184,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;

@ -211,11 +211,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,

@ -140,8 +140,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?;

@ -295,6 +295,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::{overlay::Overlay, Completion, Explorer, ProgressSpinners},
};
@ -24,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, path::PathBuf};
use tui::buffer::Buffer as Surface;
@ -304,16 +305,7 @@ impl EditorView {
let mut warning_vec = Vec::new();
let mut error_vec = Vec::new();
let diagnostics = doc.diagnostics();
// Diagnostics must be sorted by range. Otherwise, the merge strategy
// below would not be accurate.
debug_assert!(diagnostics
.windows(2)
.all(|window| window[0].range.start <= window[1].range.start
&& window[0].range.end <= window[1].range.end));
for diagnostic in diagnostics {
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),
@ -327,6 +319,11 @@ impl EditorView {
// 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)),
@ -434,7 +431,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 {
@ -472,25 +469,36 @@ 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;
for i in starting_indent..(indent_level / tab_width as u16) {
for i in starting_indent..(indent_level / tab_width) {
let style = if config.indent_guides.rainbow {
let color_index = i as usize % theme.rainbow_length();
indent_guide_style.patch(theme.get(&format!("rainbow.{}", color_index)))
} else {
indent_guide_style
};
surface.set_string(
viewport.x + (i * tab_width as u16) - offset.col as u16,
viewport.x + ((i * tab_width) - offset.col) as u16,
viewport.y + line,
&indent_guide_char,
style,
);
}
// Don't draw indent guides outside of view
let end_indent = min(
indent_level,
// Add tab_width - 1 to round up, since the first visible
// indent might be a bit after offset.col
offset.col + viewport.width as usize + (tab_width - 1),
) / tab_width;
for i in starting_indent..end_indent {
let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
let y = viewport.y + line;
debug_assert!(surface.in_bounds(x, y));
surface.set_string(x, y, &indent_guide_char, indent_guide_style);
}
};
'outer: for event in highlights {
@ -531,14 +539,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),
@ -586,7 +594,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 {
@ -619,7 +627,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);
}
}
}
@ -953,9 +961,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)
});
}
@ -1100,11 +1109,16 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger
}
pub fn handle_idle_timeout(&mut self, cx: &mut crate::commands::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);
}
@ -1444,7 +1458,15 @@ impl Component for EditorView {
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
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)
}
}
}

@ -78,11 +78,12 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect();
let mut menu = Self {
options,
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
matches,
cursor: None,
widths: Vec::new(),
callback_fn: Box::new(callback_fn),
@ -110,7 +111,7 @@ 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)
@ -215,6 +216,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()
}

@ -16,7 +16,7 @@ mod text;
mod tree;
use crate::compositor::{Component, Compositor};
use crate::job;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
pub use explore::Explorer;
@ -125,7 +125,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();
@ -139,7 +139,7 @@ pub fn regex_prompt(
compositor.replace_or_push("invalid-regex", popup);
},
);
));
Ok(call)
};

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

@ -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(),
/// },

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

@ -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,
@ -507,45 +519,61 @@ impl Document {
}
}
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
@ -558,39 +586,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.
@ -850,11 +873,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
};
@ -868,13 +891,13 @@ 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) {
@ -887,7 +910,7 @@ impl Document {
}
}
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 {
@ -895,7 +918,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;
}
}
@ -907,13 +930,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
@ -946,6 +969,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()
}
@ -957,6 +986,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
@ -1015,14 +1068,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
}
@ -1325,84 +1370,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
@ -189,6 +191,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(
@ -226,7 +230,7 @@ pub struct Config {
pub explorer: ExplorerConfig,
}
#[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,
@ -279,7 +283,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
@ -300,7 +304,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.
@ -309,7 +313,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>,
@ -342,7 +346,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,
@ -521,7 +525,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,
@ -622,7 +626,7 @@ pub struct IndentGuidesConfig {
pub render: bool,
pub character: char,
pub rainbow: bool,
pub skip_levels: u16,
pub skip_levels: u8,
}
impl Default for IndentGuidesConfig {
@ -655,6 +659,7 @@ impl Default for Config {
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,
@ -710,12 +715,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,
@ -755,6 +769,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,
@ -781,12 +804,14 @@ pub enum Action {
}
/// Error thrown on failed document closed
#[derive(Debug, Clone)]
#[derive(Debug)]
pub enum CloseError {
/// Document doesn't exist
DoesNotExist,
/// Buffer is modified
BufferModified(String),
/// Document failed to save
SaveError(anyhow::Error),
}
impl std::error::Error for CloseError {}
@ -796,6 +821,7 @@ impl std::fmt::Display for CloseError {
match self {
CloseError::DoesNotExist => "Buffer does not exist".fmt(f),
CloseError::BufferModified(s) => write!(f, "The buffer {s} has been modified"),
CloseError::SaveError(e) => write!(f, "Failed to save document {e}"),
}
}
}
@ -818,6 +844,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,
@ -887,12 +916,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]
@ -1033,13 +1066,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) {
@ -1138,6 +1165,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
}
@ -1184,16 +1218,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()));
}
@ -1256,6 +1293,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();
@ -1356,14 +1419,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()
@ -1373,4 +1436,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(())
}
}

@ -315,6 +315,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 +370,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 +386,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 +411,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 +427,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 +439,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 +453,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 +466,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 +477,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 +491,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 +528,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 +614,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);

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

@ -11,6 +11,7 @@ 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(|| {
@ -414,19 +415,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)),

@ -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,6 +233,25 @@ 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 = "5b60f99545fea00a33bbfae5be956f684c4c69e2" }
@ -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"
@ -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" }

@ -109,6 +109,9 @@
(comment) @comment
;; Tokens
(type_argument_list ["<" ">"] @punctuation.bracket)
(type_parameter_list ["<" ">"] @punctuation.bracket)
[
";"
"."
@ -159,14 +162,7 @@
"??="
] @operator
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
;; Keywords
(modifier) @keyword.storage.modifier
@ -175,36 +171,32 @@
[
"as"
"await"
"base"
"catch"
"checked"
"finally"
"from"
"get"
"in"
"init"
"is"
"let"
"lock"
"new"
"operator"
"out"
"params"
"ref"
"select"
"set"
"sizeof"
"stackalloc"
"throw"
"try"
"typeof"
"unchecked"
"using"
"new"
"await"
"in"
"yield"
"get"
"set"
"when"
"out"
"ref"
"from"
"where"
"select"
"init"
"with"
"let"
"yield"
] @keyword
[
@ -225,21 +217,31 @@
] @keyword.storage.modifier
[
"break"
"continue"
"goto"
] @keyword.control
[
"catch"
"finally"
"throw"
"try"
] @keyword.control.exception
[
"do"
"for"
"foreach"
"do"
"while"
"break"
"continue"
] @keyword.control.repeat
[
"goto"
"if"
"else"
"switch"
"case"
"default"
"else"
"if"
"switch"
] @keyword.control.conditional
"return" @keyword.control.return

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

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

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

@ -1,90 +1,84 @@
# 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"] }
<<<<<<< HEAD
"ui.explorer.file" = { fg = "#e5ded6" }
"ui.explorer.dir" = { fg = "#59dcd8" }
"ui.explorer.exe" = { fg = "#e5ded6" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#313f4e" }
||||||| 60aa7d36
=======
[palette]
bogster0 = "#dc7759"
@ -104,4 +98,22 @@ bogster13 = "#131920"
bogster14 = "#232d38"
bogster15 = "#313f4e"
bogster16 = "#ABB2BF"
>>>>>>> seperate_code_action
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" }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "blue" }

@ -0,0 +1,86 @@
# Author: IrishMaestro <github.com/irishmaestro>
# Syntax highlighting
"type" = { fg = "crystal_blue" }
"type.builtin" = { fg = "crystal_blue" }
"constructor" = { fg = "barium_green" }
"constant" = { fg = "hazmat_yellow" }
"string" = { fg = "crystal_blue" }
"string.regexp" = { fg = "orange" }
"string.special" = { fg = "vapor_yellow" }
"comment" = { fg = "teddy_bear_pink", modifiers = ["slow_blink", "italic"] }
"variable" = { fg = "element_green" }
"variable.parameter" = { fg = "hazmat_yellow" }
"label" = { fg = "vapor_yellow" }
"punctuation" = { fg = "barium_green" }
"keyword" = { fg = "barium_green" }
"keyword.control" = { fg = "barium_green" }
"keyword.directive" = { fg = "teddy_bear_pink" }
"operator" = { fg = "hazmat_yellow" }
"function" = { fg = "cash_green", modifiers = ["bold"] }
"tag" = { fg = "crystal_blue" }
"namespace" = { fg = "hazmat_yellow" }
"markup.heading" = { fg = "hazmat_yellow" }
"markup.list" = { fg = "vapor_yellow" }
"markup.raw.block" = { bg = "background", fg = "orange" }
"markup.link.url" = { fg = "crystal_blue" }
"markup.link.text" = { fg = "vapor_yellow" }
"markup.link.label" = { fg = "barium_green" }
"markup.quote" = { fg = "vapor_yellow" }
"diff.plus" = { fg = "cash_green" }
"diff.minus" = { fg = "chili_powder_red" }
"diff.delta" = { fg = "orange" }
# Interface
"ui.background" = { bg = "background" }
"ui.cursor" = { bg = "crystal_blue", fg = "background" }
"ui.cursor.match" = { fg = "hazmat_yellow" }
"ui.linenr" = { fg = "crystal_blue" }
"ui.linenr.selected" = { fg = "barium_green" }
"ui.statusline" = { fg = "crystal_blue", bg = "black" }
"ui.statusline.normal" = { fg = "crystal_blue", bg = "black" }
"ui.statusline.insert" = { fg = "barium_green", bg = "black"}
"ui.statusline.select" = { fg = "hazmat_yellow", bg = "black" }
"ui.cursorline.primary" = { bg = "#041B0E" }
"ui.popup" = { fg = "crystal_blue", bg = "background" }
"ui.window" = { fg = "barium_green" }
"ui.help" = { fg = "crystal_blue", bg = "background"}
"ui.text" = { fg = "crystal_blue" }
"ui.text.focus" = { bg = "background", fg = "barium_green" }
"ui.text.info" = { fg = "crystal_blue" }
"ui.virtual.whitespace" = { fg = "#08341B" }
"ui.virtual.ruler" = { bg = "#041B0E" }
"ui.virtual.indent-guide" = { fg = "#08341B" }
"ui.menu" = { fg = "crystal_blue", bg = "background" }
"ui.menu.selected" = { bg = "hazmat_yellow", fg = "background" }
"ui.selection" = { bg = "#1B0334" }
"ui.selection.primary" = { bg = "desert_maroon" }
"warning" = { fg = "vapor_yellow" }
"error" = { fg = "chili_powder_red", modifiers = ["bold"] }
"info" = { fg = "crystal_blue", modifiers = ["bold"] }
"hint" = { fg = "crystal_blue", modifiers = ["bold"] }
"diagnostic"= { fg = "chili_powder_red", modifiers = ["underlined"] }
"diagnostic.info"= { fg = "crystal_blue", modifiers = ["underlined"] }
"diagnostic.warning"= { fg = "vapor_yellow", modifiers = ["underlined"] }
"diagnostic.error"= { fg = "chili_powder_red", modifiers = ["underlined"] }
"ui.bufferline" = { fg = "gray", bg = "background" }
"ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" }
"special" = { fg = "cash_green" }
[palette]
background = "#000000"
foreground = "#cccccc"
black = "#121212"
dark_gray = "#2d3640"
gray = "#5c6773"
orange = "#ff8f40"
barium_green = "#009669"
hazmat_yellow = "#f7b90c"
vapor_yellow = "#cecd19"
teddy_bear_pink = "#bd5173"
crystal_blue = "#32c9fa"
cash_green = "#00ff80"
element_green = "#186800"
desert_maroon = "#2B0C02"
chili_powder_red = "#c32101"

@ -0,0 +1,105 @@
"comment" = { fg = "highlight_three" }
"comment.block.documentation" = { bg = "t4", modifiers = ["italic"] }
"constant" = { fg = "t11" }
"function" = { fg = "t10" }
"function.method" = { fg = "t10" }
"function.macro" = { fg = "t7" }
"keyword.storage.modifier" = { fg = "t7" }
"keyword.control.import" = { fg = "t8" }
"keyword.control" = { fg = "t8" }
"keyword.function" = { fg = "t7" }
"keyword" = { fg = "t6" }
"operator" = { fg = "t8" }
"punctuation" = { fg = "t9" }
"string" = { fg = "t6", modifiers = ["italic"] }
"string.regexp" = { fg = "t6" }
"tag" = { fg = "t4" }
"type" = { fg = "t8", modifiers = ["bold"] }
"namespace" = { fg = "t6", modifiers = ["bold"] }
"variable" = { fg = "t4" }
"label" = { fg = "t4" }
"diff.plus" = { fg = "t4" }
"diff.delta" = { fg = "t4" }
"diff.minus" = { fg = "t4" }
"ui.cursor.insert" = { fg = "t2", bg = "highlight" }
"ui.cursor.select" = { fg = "t2", bg = "highlight_two" }
"ui.cursor" = { fg = "t1", bg = "highlight_three" }
"ui.cursor.match" = { fg = "highlight", bg = "t1", modifiers = ["bold"] }
"ui.linenr" = { fg = "t3", bg = "t1" }
"ui.linenr.selected" = { fg = "highlight_three", bg = "t1" }
"ui.gutter" = { bg = "t1" }
"ui.background" = { fg = "t4", bg = "t2" }
"ui.background.separator" = { fg = "t3" }
"ui.help" = { fg = "t4", bg = "t1" }
"ui.menu" = { fg = "t4", bg = "t1" }
"ui.menu.selected" = { fg = "highlight_three", bg = "t1" }
"ui.popup" = { fg = "t4", bg = "t1" }
"ui.window" = { fg = "t4" }
"ui.selection.primary" = { bg = "selection" }
"ui.selection" = { bg = "selection" }
"ui.cursorline.primary" = { bg = "t1" }
"ui.statusline" = { fg = "t4", bg = "t1" }
"ui.statusline.inactive" = { fg = "t4", bg = "t1" }
"ui.statusline.normal" = { fg = "t3", bg = "t1" }
"ui.statusline.insert" = { fg = "t3", bg = "t1" }
"ui.statusline.select" = { fg = "highlight", bg = "t4" }
"ui.text" = { fg = "t4" }
"ui.text.focus" = { fg = "highlight_three", modifiers = ["bold"] }
#
"ui.virtual.ruler" = { bg = "t1" }
"ui.virtual.indent-guide" = { fg = "t3" }
"ui.virtual.whitespace" = { fg = "t3" }
"diagnostic" = { modifiers = ["underlined"] }
"error" = { fg = "error", modifiers = ["bold"] }
"warning" = { fg = "warning", modifiers = ["bold"] }
"info" = { fg = "info", modifiers = ["bold"] }
"hint" = { fg = "display", modifiers = ["bold"] }
"special" = { fg = "t7", modifiers = ["bold"] }
"markup.heading" = { fg = "t4" }
"markup.list" = { fg = "t4" }
"markup.bold" = { fg = "t4" }
"markup.italic" = { fg = "t4" }
"markup.link.url" = { fg = "t4", modifiers = ["underlined"] }
"markup.link.text" = { fg = "t4" }
"markup.quote" = { fg = "t4" }
"markup.raw" = { fg = "t4" }
[palette]
t1 = "#0f0b0b"
t2 = "#161010"
t3 = "#5b5555"
t4 = "#656869"
t5 = "#727b7c"
t6 = "#6e8789"
t7 = "#d85c60"
t8 = "#9bc1bb"
t9 = "#b5c5c5"
t10 = "#c0d0ce"
t11 = "#f78c5e"
highlight = "#3f36f2"
highlight_two = "#f69c3c"
highlight_three = "#d4d987"
selection = "#032d4a"
black = "#000000"
comment = "#396884"
comment_doc = "#234048"
error = "#ff0900"
warning = "#ffbf00"
display = "#57ff89"
info = "#dad7d5"

@ -0,0 +1,30 @@
inherits = "hex_steel"
[palette]
t1 = "#101719"
t2 = "#152432"
t3 = "#4b5968"
t4 = "#8792ab"
t5 = "#6f91bc"
t6 = "#8bb2b9"
t7 = "#eeac90"
t8 = "#b0bd9f"
t9 = "#b3ccd0"
t10 = "#b0d4d8"
t11 = "#ffbf52"
highlight = "#ff2e5f"
highlight_two = "#0affa9"
highlight_three = "#d7ff52"
black = "#000000"
selection = "#290019"
comment = "#396884"
comment_doc = "#234048"
error = "#ff0900"
warning = "#ffbf00"
display = "#57ff89"
info = "#dad7d5"

@ -0,0 +1,125 @@
# Kanagawa
# Author: zetashift
# Adaptation of https://github.com/rebelot/kanagawa.nvim
# Original author: rebelot
# All credits to the original author, the palette is taken from the README
# because of some theming differences, it's not an exact copy of the original.
## User interface
"ui.selection" = { bg = "waveBlue1" }
"ui.background" = { fg = "fujiWhite", bg = "sumiInk1" }
"ui.linenr" = { fg = "sumiInk4" }
"ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" }
"ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" }
"ui.statusline.normal" = { fg = "sumiInk0", bg = "crystalBlue", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen" }
"ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet" }
"ui.bufferline" = { fg = "oldWhite", bg = "sumiInk0" }
"ui.bufferline.inactive" = { fg = "fujiGray", bg = "sumiInk0" }
"ui.popup" = { fg = "fujiWhite", bg = "sumiInk0" }
"ui.window" = { fg = "fujiWhite" }
"ui.help" = { fg = "fujiWhite", bg = "waveBlue1" }
"ui.text" = "fujiWhite"
"ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] }
"ui.virtual" = "waveBlue1"
"ui.cursor" = { fg = "fujiWhite", bg = "waveBlue1"}
"ui.cursor.primary" = { fg = "seaFoam", bg = "waveBlue1" }
"ui.cursor.match" = { fg = "seaFoam", modifiers = ["bold"] }
"ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" }
diagnostic = { modifiers = ["underlined"] }
error = "samuraiRed"
warning = "roninYellow"
info = "waveAqua1"
hint = "dragonBlue"
## Syntax highlighting
"type" = "waveAqua2"
"constant" = "surimiOrange"
"constant.numeric" = "sakuraPink"
"constant.character.escape" = "springBlue"
"string" = "springGreen"
"string.regexp" = "boatYellow2"
"comment" = "fujiGray"
"variable" = "fujiWhite"
"variable.builtin" = "waveRed"
"variable.parameter" = "carpYellow"
"variable.other.member" = "carpYellow"
"label" = "springBlue"
"punctuation" = "springViolet2"
"punctuation.delimiter" = "springViolet2"
"punctuation.bracket" = "springViolet2"
"keyword" = "oniViolet"
"keyword.directive" = "peachRed"
"operator" = "boatYellow2"
"function" = "crystalBlue"
"function.builtin" = "peachRed"
"function.macro" = "waveRed"
"tag" = "springBlue"
"namespace" = "surimiOrange"
"attribute" = "peachRed"
"constructor" = "springBlue"
"module" = "waveAqua2"
"special" = "peachRed"
## Markup modifiers
"markup.heading.marker" = "fujiGray"
"markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] }
"markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] }
"markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] }
"markup.heading.4" = { fg = "springGreen", modifiers = ["bold"] }
"markup.heading.5" = { fg = "waveRed", modifiers = ["bold"] }
"markup.heading.6" = { fg = "autumnRed", modifiers = ["bold"] }
"markup.list" = "oniViolet"
"markup.bold" = { modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.link.url" = { fg = "springBlue", modifiers = ["underlined"] }
"markup.link.text" = "crystalBlue"
"markup.quote" = "seaFoam"
"markup.raw" = "seaFoam"
[palette]
seaFoam = "#C7CCD1" # custom lighter foreground
fujiWhite = "#DCD7BA" # default foreground
oldWhite = "#C8C093" # dark foreground, e.g. statuslines
sumiInk0 = "#16161D" # dark background, e.g. statuslines, floating windows
sumiInk1 = "#1F1F28" # default background
sumiInk3 = "#363646" # lighter background, e.g. colorcolumns and folds
sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column
waveBlue1 = "#223249" # popup background, visual selection background
waveBlue2 = "#2D4F67" # popup selection background, search background
winterGreen = "#2B3328" # diff add background
winterYellow = "#49443C" # diff change background
winterRed = "#43242B" # diff deleted background
winterBlue = "#252535" # diff line background
autumnGreen = "#76946A" # git add
autumnRed = "#C34043" # git delete
autumnYellow = "#DCA561" # git change
samuraiRed = "#E82424" # diagnostic error
roninYellow = "#FF9E3B" # diagnostic warning
waveAqua1 = "#6A9589" # diagnostic info
dragonBlue = "#658594" # diagnostic hint
fujiGray = "#727169" # comments
springViolet1 = "#938AA9" # light foreground
oniViolet = "#957FB8" # statements and keywords
crystalBlue = "#7E9CD8" # functions and titles
springViolet2 = "#9CABCA" # brackets and punctuation
springBlue = "#7FB4CA" # specials and builtins
lightBlue = "#A3D4D5" # not used!
waveAqua2 = "#7AA89F" # types
springGreen = "#98BB6C" # strings
boatYellow1 = "#938056" # not used
boatYellow2 = "#C0A36E" # operators, regex
carpYellow = "#E6C384" # identifiers
sakuraPink = "#D27E99" # numbers
waveRed = "#E46876" # standout specials 1, e.g. builtin variables
peachRed = "#FF5D62" # standout specials 2, e.g. exception handling, returns
surimiOrange = "#FFA066" # constants, imports, booleans
katanaGray = "#717C7C" # deprecated

@ -18,6 +18,9 @@
# status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "base8", bg = "base4" }
"ui.statusline.normal" = { fg = "base4", bg = "blue" }
"ui.statusline.insert" = { fg = "base4", bg = "green" }
"ui.statusline.select" = { fg = "base4", bg = "purple" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { fg = "base8", bg = "base3" }

@ -39,7 +39,10 @@
"diff.delta" = "gold"
"diff.minus" = "red"
diagnostic = { modifiers = ["underlined"] }
"diagnostic.info".underline = { color = "blue", style = "curl" }
"diagnostic.hint".underline = { color = "green", style = "curl" }
"diagnostic.warning".underline = { color = "yellow", style = "curl" }
"diagnostic.error".underline = { color = "red", style = "curl" }
"info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "green", modifiers = ["bold"] }
"warning" = { fg = "yellow", modifiers = ["bold"] }

@ -97,7 +97,7 @@ bright6="#00afaf"
bright7="#5f8787"
selection_foreground="#585858"
selection_background="#8787AF"
cursorline_background="#d0d0d0"
cursorline_background="#303030"
paper_bar_bg="#5F8787"
black="#1c1c1c"
red="#af005f"

@ -43,7 +43,6 @@ module = { bg = 'orangeL' }
special = { fg = 'orangeW' }
operator = { fg = 'orangeY' }
attribute = { fg = 'orangeL' }
attribute = { fg = 'orangeL' }
namespace = { fg = 'orangeL' }
'type' = { fg = 'redH' }
'type.builtin' = { fg = 'orangeL' }

@ -85,6 +85,6 @@ rose = "#ebbcba"
pine = "#31748f"
foam = "#9ccfd8"
iris = "#c4a7e7"
highlight = "#2a2837"
highlightInactive = "#211f2d"
highlightOverlay = "#3a384a"
highlight = "#403d52"
highlightInactive = "#21202e"
highlightOverlay = "#524f67"

@ -82,6 +82,6 @@ rose = "#d7827e"
pine = "#286983"
foam = "#56949f"
iris = "#907aa9"
highlight = "#eee9e6"
highlightInactive = "#f2ede9"
highlightOverlay = "#e4dfde"
highlight = "#dfdad9"
highlightInactive = "#f4ede8"
highlightOverlay = "#cecacd"

@ -83,6 +83,6 @@ rose = "#ea9a97"
pine = "#3e8fb0"
foam = "#9ccfd8"
iris = "#c4a7e7"
highlight = "#2a283e"
highlightInactive = "#44415a"
highlight = "#44415a"
highlightInactive = "#2a283e"
highlightOverlay = "#56526e"

@ -41,6 +41,9 @@
"ui.selection.primary" = { bg = "background_highlight" }
"ui.statusline" = { fg = "foreground", bg = "background_menu" }
"ui.statusline.inactive" = { fg = "foreground_gutter", bg = "background_menu" }
"ui.statusline.normal" = { fg = "black", bg = "blue" }
"ui.statusline.insert" = { fg = "black", bg = "green" }
"ui.statusline.select" = { fg = "black", bg = "magenta" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { fg = "cyan" }
"ui.virtual.ruler" = { bg = "foreground_gutter" }

@ -1,67 +1,6 @@
# Author: Paul Graydon <p.y.graydon@gmail.com>
"comment" = { fg = "comment", modifiers = ["italic"] }
"constant" = { fg = "orange" }
"constant.character.escape" = { fg = "magenta" }
"function" = { fg = "blue", modifiers = ["italic"] }
"function.macro" = { fg = "cyan" }
"keyword" = { fg = "cyan", modifiers = ["italic"] }
"keyword.control" = { fg = "magenta" }
"keyword.control.import" = { fg = "cyan" }
"keyword.operator" = { fg = "turquoise" }
"keyword.function" = { fg = "magenta", modifiers = ["italic"] }
"operator" = { fg = "turquoise" }
"punctuation" = { fg = "turquoise" }
"string" = { fg = "light-green" }
"string.regexp" = { fg = "light-blue" }
"tag" = { fg = "red" }
"type" = { fg = "teal" }
"namespace" = { fg = "blue" }
"variable" = { fg = "white" }
"variable.builtin" = { fg = "red" }
"variable.other.member" = { fg = "green" }
"variable.parameter" = { fg = "yellow", modifiers = ["italic"] }
"diff.plus" = { fg = "green" }
"diff.delta" = { fg = "orange" }
"diff.minus" = { fg = "red" }
"ui.background" = { fg = "foreground", bg = "background" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursor.match" = { fg = "orange", modifiers = ["bold"] }
"ui.cursor.primary" = { modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "background_menu" }
"ui.help" = { fg = "foreground", bg = "background_menu" }
"ui.linenr" = { fg = "foreground_gutter" }
"ui.linenr.selected" = { fg = "foreground" }
"ui.menu" = { fg = "foreground", bg = "background_menu" }
"ui.menu.selected" = { bg = "background_highlight" }
"ui.popup" = { fg = "foreground", bg = "background_menu" }
"ui.selection" = { bg = "background_highlight" }
"ui.selection.primary" = { bg = "background_highlight" }
"ui.statusline" = { fg = "foreground", bg = "background_menu" }
"ui.statusline.inactive" = { fg = "foreground_gutter", bg = "background_menu" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { fg = "cyan" }
"ui.virtual.ruler" = { bg = "foreground_gutter" }
"ui.virtual.whitespace" = { fg = "foreground_gutter" }
"ui.window" = { fg = "black" }
"error" = { fg = "red" }
"warning" = { fg = "yellow" }
"info" = { fg = "blue" }
"hint" = { fg = "teal" }
"diagnostic" = { modifiers = ["underlined"] }
"special" = { fg = "orange" }
"markup.heading" = { fg = "cyan", modifiers = ["bold"] }
"markup.list" = { fg = "cyan" }
"markup.bold" = { fg = "orange", modifiers = ["bold"] }
"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
"markup.link.url" = { fg = "green" }
"markup.link.text" = { fg = "light-gray" }
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "cyan" }
inherits = "tokyonight"
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
@ -70,25 +9,6 @@
"ui.explorer.unfocus" = { bg = "background_highlight" }
[palette]
red = "#f7768e"
orange = "#ff9e64"
yellow = "#e0af68"
light-green = "#9ece6a"
green = "#73daca"
turquoise = "#89ddff"
light-cyan = "#b4f9f8"
teal = "#2ac3de"
cyan = "#7dcfff"
blue = "#7aa2f7"
magenta = "#bb9af7"
white = "#c0caf5"
light-gray = "#9aa5ce"
parameters = "#cfc9c2"
comment = "#565f89"
black = "#414868"
foreground = "#a9b1d6"
foreground_highlight = "#c0caf5"
foreground_gutter = "#3b4261"
background = "#24283b"
background_highlight = "#373d5a"
background_menu = "#1f2335"

@ -16,7 +16,7 @@ _________________________________________________________________
perform various actions with the text. This allows for more
efficient editing. This tutor will teach you how you can make
use of Helix's modal editing features. To begin, ensure your
caps-lock key is not pressed and hold the j key until you reach
CapsLock key is not pressed and hold the j key until you reach
the first lesson.
@ -31,7 +31,7 @@ _________________________________________________________________
The cursor can be moved using the h, j, k, l keys, as shown
above. The cursor/arrow keys will also work, but it is faster
above. The cursor / arrow keys will also work, but it is faster
to use the hjkl keys as they are closer to the other keys you
will be using. Try moving around to get a feel for hjkl.
Once you're ready, hold j to continue to the next lesson.
@ -48,13 +48,13 @@ _________________________________________________________________
1. Type : to enter Command mode. Your cursor will
move to the bottom of the screen.
2. Type q or quit and type <ENTER> to exit Helix.
2. Type q or quit and type Enter to exit Helix.
Note: The quit command will fail if there are unsaved changes.
To force quit and DISCARD these changes, type q! or quit!.
You will learn how to save files later.
To exit Command mode without entering a command, type <ESC>.
To exit Command mode without entering a command, type Escape.
Now, move on to the next lesson.
@ -96,7 +96,7 @@ _________________________________________________________________
2. Move to a place in the line which is missing text and type
i to enter Insert mode. Keys you type will now type text.
3. Enter the missing text.
4. type <ESC> to exit Insert mode and return to Normal mode.
4. type Escape to exit Insert mode and return to Normal mode.
5. Repeat until the line matches the line below it.
--> Th stce misg so.
@ -112,18 +112,18 @@ _________________________________________________________________
= 1.5 SAVING A FILE =
=================================================================
Type :w/:write to save a file.
Type :w / :write to save a file.
1. Exit Helix using :q! as explained before, or open a new
terminal.
2. Open a file in Helix by running: hx FILENAME
3. Make some edits to the file.
4. Type : to enter Command mode.
5. Type w or write, and type <ENTER> to save the file.
5. Type w or write, and type Enter to save the file.
You can also type wq or write-quit to save and exit.
Note: You can optionally enter a filepath after the w/write
Note: You can optionally enter a filepath after the w / write
command in order to save to that path.
Note: If there are any unsaved changes to a file, a plus [+]
will appear next to the file name in the status bar.
@ -137,15 +137,15 @@ _________________________________________________________________
* Use the h,j,k,l keys to move the cursor.
* Type : to enter Command mode.
* The q/quit and q!/quit! commands will exit Helix. The
* The q / quit and q! / quit! commands will exit Helix. The
former fails when there are unsaved changes. The latter
discards them.
* The w/write command will save the file.
* The wq/write-quit command will do both.
* The w / write command will save the file.
* The wq / write-quit command will do both.
* Type d to delete the character at the cursor.
* Type i to enter Insert mode and type text. Type <ESC> to
* Type i to enter Insert mode and type text. Type Escape to
return to Normal mode.
@ -167,7 +167,7 @@ _________________________________________________________________
A - Insert at the end of the line.
1. Move to anywhere in the line marked '-->' below.
2. Type A (<SHIFT> + a), your cursor will move to the end of
2. Type A (Shift-a), your cursor will move to the end of
the line and you will be able to type.
3. Type the text necessary to match the line below.
@ -207,7 +207,7 @@ _________________________________________________________________
* Type A to enter Insert mode at the end of a line.
* Use o and O to open lines below/above the cursor respectively.
* Use o and O to open lines below and above the cursor respectively.
@ -307,11 +307,11 @@ _________________________________________________________________
=================================================================
= 3.5 SELECT/EXTEND MODE =
= 3.5 SELECT / EXTEND MODE =
=================================================================
Type v to enter Select mode.
Type v again or <ESC> to return to Normal mode
Type v again or Escape to return to Normal mode
In Select mode every movement will extend the selection, as
opposed to replacing it.
@ -366,8 +366,8 @@ _________________________________________________________________
--> This is an error-free line with words to move around in.
Note: This works the same in select mode.
Note: Another related command is A-; which flips selections.
Note: This works the same in Select mode.
Note: Another related command is Alt-; which flips selections.
@ -405,7 +405,7 @@ _________________________________________________________________
3. Type u to undo your deletion.
4. Fix all the errors on the line.
5. Type u several times to undo your fixes.
6. Type U (<SHIFT> + u) several times to redo your fixes.
6. Type U (Shift-u) several times to redo your fixes.
--> Fiix the errors on thhis line and reeplace them witth undo.
@ -434,32 +434,32 @@ _________________________________________________________________
1 banana 2 banana 3 banana 4
Note: Whenever you delete or change text, Helix will copy the
altered text. Use alt-d/c instead to avoid this.
altered text. Use Alt-d / Alt-c instead to avoid this.
Note: Helix doesn't share the system clipboard by default. Type
space-y/p to yank/paste on your computer's main clipboard.
Space + y / p to yank / paste on the system's clipboard.
=================================================================
= 4.3 SEARCHING IN FILE =
=================================================================
Type / to search forward in file, enter to confirm search.
Type / to search forward in file, Enter to confirm search.
Type n to go to the next search match.
Type N to go to the previous search match.
1. Type / and type in a common word, like 'banana'.
2. Type enter to confirm the search.
2. Type Enter to confirm the search.
3. Use n and N to cycle through the matches.
Like the select command, searching also uses regex.
Searching uses regular expressions, allowing you to target more
complex expressions, which you'll learn about in the lesson on
the select command.
Note: To search backwards, type ? (shift-/).
Note: To search backwards, type ? (Shift-/).
Note: Unlike Vim, ? doesn't change the search direction.
N always goes backwards and n always goes forwards.
=================================================================
= CHAPTER 4 RECAP =
=================================================================
@ -467,7 +467,7 @@ _________________________________________________________________
* Type u to undo. Type U to redo.
* Type y to yank (copy) text and p to paste.
* Use space-Y and space-P to yank/paste on the system
* Use Space + y and Space + p to yank / paste on the system
clipboard.
* Type / to search forward in file, and ? to search backwards.
@ -488,7 +488,8 @@ _________________________________________________________________
Type C to duplicate the cursor to the next suitable line.
1. Move the cursor to the first line marked '-->' below.
1. Move the cursor to the first line marked '-->' below. Place
the cursor somewhere past the '-->'.
2. Type C to duplicate the cursor to the next suitable line.
Notice how it skips the line in the middle. Keys you type
will now affect both cursors.
@ -499,10 +500,9 @@ _________________________________________________________________
--> Fix th two nes at same ime.
-->
--> Fix th two nes at same ime.
Fix these two lines at the same time.
Note: Type alt-C to do the same above the cursor.
Note: Type Alt-C to do the same above the cursor.
=================================================================
= 5.2 THE SELECT COMMAND =
@ -513,11 +513,11 @@ _________________________________________________________________
1. Move the cursor to the line marked '-->' below.
2. Type x to select the line.
3. Type s. A prompt will appear.
4. Type 'apples' and type <ENTER>. Both occurrences of
4. Type 'apples' and type Enter. Both occurrences of
'apples' in the line will be selected.
5. You can now type c and change 'apples' to something else,
like 'oranges'.
6. Type <ESC> to exit Insert mode.
6. Type Escape to exit Insert mode.
7. Type , to remove the second cursor.
--> I like to eat apples since my favorite fruit is apples.
@ -530,8 +530,8 @@ _________________________________________________________________
= 5.3 SELECTING VIA REGEX =
=================================================================
The select command selects regular expressions, not just exact
matches, allowing you to target more complex patterns.
Like searching, the select command selects regular expressions,
not just exact matches.
1. Move the cursor to the line marked '-->' below.
2. Select the line with x and then type s.
@ -574,11 +574,11 @@ _________________________________________________________________
= 5.5 SPLIT SELECTION INTO LINES =
=================================================================
Type A-s (Alt-s) to split the selection(s) on newlines.
Type Alt-s to split the selection(s) on newlines.
1. Move the cursor to the first row of the table below.
2. Select the entire table with 6x.
3. Type A-s to split into selections at each line.
3. Type Alt-s to split into selections at each line.
4. Align the table with &.
| FRUIT | AMOUNT |
@ -604,7 +604,7 @@ _________________________________________________________________
* Type & to align selections.
* Type A-s to split the selection into lines.
* Type Alt-s to split the selection into lines.
@ -627,7 +627,7 @@ _________________________________________________________________
2. Type f[ to select to the square bracket.
3. Type d to delete your selection.
4. Go to the end of the line and repeat with F].
5. Move to the second line marked -->, just after the arrow.
5. Move to the second line marked '-->', just after the arrow.
6. Use t and T to delete the dashes around the sentence.
--> -----[Free this sentence of its brackets!]-----
@ -663,16 +663,16 @@ _________________________________________________________________
=================================================================
Type . to repeat the last insert command.
Type A-. to repeat the last f / t selection.
Type Alt-. to repeat the last f / t selection.
1. Move the cursor to the line marked '-->' below.
2. Make a change, insertion or appendage and repeat it with . .
3. Try using A-. with f and t, to select multiple sentences for
instance.
3. Try using Alt-. with f and t, to select multiple sentences
for instance.
--> This is some text for you to repeat things. You can repeat
insertions like changing words, or repeat selections like f/t.
insertions like changing words, or repeat selections like
f / t.
@ -690,7 +690,7 @@ _________________________________________________________________
* Type r to replace selected characters.
* Type . to repeat the last insertion.
* Type A-. to repeat the last f / t selection.
* Type Alt-. to repeat the last f / t selection.
@ -772,13 +772,13 @@ lines.
= 7.4 INCREMENTING AND DECREMENTING =
=================================================================
Type C-a to increment the number under selection.
Type C-x to decrement the number under selection.
Type Ctrl-a to increment the number under selection.
Type Ctrl-x to decrement the number under selection.
1. Move the cursor to the third line marked '-->' below.
2. Type C-a to increment the second point marked 2.
2. Type Ctrl-a to increment the second point marked 2.
3. Repeat for the point marked 3.
4. Move to the last point and type C-x to decrement the 6.
4. Move to the last point and type Ctrl-x to decrement the 6.
--> 1) First point.
--> 2) Added point.
@ -800,8 +800,8 @@ lines.
* Type < and > to indent / outdent lines.
* Type C-a to increment the selected number.
* Type C-x to decrement the selected number.
* Type Ctrl-a to increment the selected number.
* Type Ctrl-x to decrement the selected number.
@ -886,8 +886,9 @@ lines.
n and N both refer to register /, this means we can set that
register without having to type in a search.
Type * to copy the primary selection into register /, setting
the search term to the selection.
Type * to copy the selection into register /, setting the search
term to the selection. This copies the primary selection, which
you will learn about in the section on cycling selections.
1. Move the cursor to the line marked '-->' below.
2. Select "horse" with e and type *.
@ -899,18 +900,17 @@ lines.
Note: * is like a shorthand for "/ y as all it really does is
copy the selection into the / register.
=================================================================
= 9.2 ADDING SELECTION ON NEXT SEARCH MATCH =
=================================================================
A property of select mode (v) when using n and N is that instead
A property of Select mode (v) when using n and N is that instead
of moving the selection to the next match, it adds a new
selection on each match.
1. Move the cursor to the line marked '-->' below.
2. Select the first "bat" and type * to set it to search.
3. Type v to enter select mode.
3. Type v to enter Select mode.
4. Type n to select the other "bat".
5. Use c or r to change the "bat"s to "cat".
@ -930,15 +930,15 @@ lines.
searching or jumping to the definition of a function in code. It
stores these in what's called the jumplist.
Type C-s (ctrl-s) to manually save your current position to
Type Ctrl-s to manually save your current position to
the jumplist.
Type C-i ("in") and C-o ("out") to move forward and backwards in
the jumplist respectively.
Type Ctrl-i ("in") and Ctrl-o ("out") to move forward and
backwards in the jumplist respectively.
1. Type C-s somewhere.
1. Type Ctrl-s somewhere.
2. Move far away in the file.
3. Type C-o (just once!) to come back to where you saved.
3. Type Ctrl-o (just once!) to come back to where you saved.
@ -950,12 +950,12 @@ lines.
* Type * to set the search register to the primary selection.
* Type n / N in visual mode to add selections on each search
* Type n / N in Visual mode to add selections on each search
match.
* Type C-s to save position to the jumplist.
* Type C-i and C-o to go forward and backward in the jumplist.
* Type Ctrl-s to save position to the jumplist.
* Type Ctrl-i and Ctrl-o to go forward and backward in the
jumplist.
@ -973,19 +973,19 @@ lines.
Type ) and ( to cycle the primary selection forward and backward
through selections respectively.
Type A-, to remove the primary selection.
Type Alt-, to remove the primary selection.
1. Move the cursor to the line marked '-->' below.
2. Select both lines with xx or 2x.
3. Type s to select, type "would" and enter.
4. Use ( and ) to cycle the primary selection and remove the
very second "would" with A-, .
very second "would" with Alt-, .
5. Type c "wood" to change the remaining "would"s to "wood".
--> How much would would a wouldchuck chuck
--> if a wouldchuck could chuck would?
Note: Additionally, A-( and A-) cycle the *contents* of the
Note: Additionally, Alt-( and Alt-) cycle the *contents* of the
selections as well.
=================================================================
@ -999,10 +999,10 @@ lines.
1. Move the cursor to the first line marked '-->' below.
2. Select each wrongly capitalised or lowercase letter
and type ~ over them.
3. Move to the second line marked -->.
3. Move to the second line marked '-->'.
4. Type x to select the line.
5. Type ` to change the line to lowercase.
6. Move to the third line marked -->.
6. Move to the third line marked '-->'.
7. Type x to select the line.
8. Type Alt-` to change the line to uppercase.
@ -1018,13 +1018,13 @@ lines.
1. Move the cursor to the line under ---.
2. Type xx / 2x to select the lines.
3. Type S then \. |! <enter> (note the spaces after . and !).
3. Type S then \. |! Enter (note the spaces after . and !).
This effectively splits the selection into sentences at each
dot or exclamation mark.
4. Type A-; to reverse the selections.
4. Type Alt-; to reverse the selections.
5. Type ; to reduce selections to a single character - the first
letter of each sentence.
6. Type A-` to convert all selected letters to uppercase.
6. Type Alt-` to convert all selected letters to uppercase.
---
these are sentences. some sentences don't start with uppercase
@ -1038,10 +1038,10 @@ letters! that is not good grammar. you can fix this.
* Use ) and ( to cycle the primary selection back and forward
through selections respectively.
* Type A-, to remove the primary selection.
* Type Alt-, to remove the primary selection.
* Type ~ to alternate case of selected letters.
* Use ` and A-` to set the case of selected letters to
* Use ` and Alt-` to set the case of selected letters to
upper and lower respectively.
* Type S to split selections on regex.

@ -66,6 +66,49 @@
"ui.explorer.unfocus" = { bg = "comment" }
rainbow = ["#7c5ea3", "#9c5b95", "#9c5e80", "#6b4466"]
"ui.selection" = { bg = "#540099" }
"ui.selection.primary" = { bg = "#540099" }
# TODO: namespace ui.cursor as ui.selection.cursor?
"ui.cursor.select" = { bg = "delta" }
"ui.cursor.insert" = { bg = "white" }
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "bossanova" }
"ui.highlight" = { bg = "bossanova" }
"ui.menu" = { fg = "lavender", bg = "revolver" }
"ui.menu.selected" = { fg = "revolver", bg = "white" }
"ui.menu.scroll" = { fg = "lavender", bg = "comet" }
diagnostic = { modifiers = ["underlined"] }
# "diagnostic.hint" = { fg = "revolver", bg = "lilac" }
# "diagnostic.info" = { fg = "revolver", bg = "lavender" }
# "diagnostic.warning" = { fg = "revolver", bg = "honey" }
# "diagnostic.error" = { fg = "revolver", bg = "apricot" }
"ui.selection" = { bg = "#540099" }
"ui.selection.primary" = { bg = "#540099" }
# TODO: namespace ui.cursor as ui.selection.cursor?
"ui.cursor.select" = { bg = "delta" }
"ui.cursor.insert" = { bg = "white" }
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "bossanova" }
"ui.highlight" = { bg = "bossanova" }
"ui.menu" = { fg = "lavender", bg = "revolver" }
"ui.menu.selected" = { fg = "revolver", bg = "white" }
"ui.menu.scroll" = { fg = "lavender", bg = "comet" }
"diagnostic.hint" = { underline = { color = "silver", style = "curl" } }
"diagnostic.info" = { underline = { color = "delta", style = "curl" } }
"diagnostic.warning" = { underline = { color = "lightning", style = "curl" } }
"diagnostic.error" = { underline = { color = "apricot", style = "curl" } }
warning = "lightning"
error = "apricot"
info = "delta"
hint = "silver"
[palette]
background = "#3A2A4D"

@ -14,8 +14,8 @@ pub fn query_check() -> Result<(), DynError> {
];
for language in lang_config().language {
let language_name = language.language_id.to_ascii_lowercase();
let grammar_name = language.grammar.unwrap_or(language.language_id);
let language_name = &language.language_id;
let grammar_name = language.grammar.as_ref().unwrap_or(language_name);
for query_file in query_files {
let language = get_language(&grammar_name);
let query_text = read_query(&language_name, query_file);

Loading…
Cancel
Save