Merge branch 'master'

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

5
Cargo.lock generated

@ -388,6 +388,7 @@ name = "helix-core"
version = "0.6.0" version = "0.6.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"bitflags",
"chrono", "chrono",
"encoding_rs", "encoding_rs",
"etcetera", "etcetera",
@ -937,9 +938,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",

@ -67,17 +67,15 @@ 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.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the Helix needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows). config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
| OS | Command | | OS | Command |
| -------------------- | -------------------------------------------- | | -------------------- | ------------------------------------------------ |
| Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` | | Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env: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. This location can be overridden via the `HELIX_RUNTIME` environment variable.
@ -87,12 +85,32 @@ that sets the variable to the install dir.
> NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically > NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root. > detect the `runtime` directory in the project root.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
In order to use LSP features like auto-complete, you will need to In order to use LSP features like auto-complete, you will need to
[install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) [install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
for a language. for a language.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Adding Helix to your desktop environment
If installing from source, to use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## MacOS ## MacOS
Helix can be installed on MacOS through homebrew: Helix can be installed on MacOS through homebrew:

@ -38,13 +38,14 @@ on unix operating systems.
| Key | Description | Default | | Key | Description | Default |
|--|--|---------| |--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` | | `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `5` |
| `mouse` | Enable mouse mode. | `true` | | `mouse` | Enable mouse mode. | `true` |
| `middle-click-paste` | Middle click paste support. | `true` | | `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | | `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` | | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` | | `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `cursorline` | Highlight all lines with a cursor. | `false` | | `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "line-numbers"]` | | `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-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` | | `auto-format` | Enable automatic formatting on save. | `true` |
@ -251,24 +252,12 @@ Sets explorer side width and style.
Options for rendering vertical indent guides. Options for rendering vertical indent guides.
<<<<<<< HEAD
| Key | Description | Default | | Key | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `render` | Whether to render indent guides. | `false` | | `render` | Whether to render indent guides. | `true` |
| `character` | Literal character to use for rendering the indent guide | `│` | | `character` | Literal character to use for rendering the indent guide | `│` |
| `rainbow` | Whether or not the indent guides shall have changing colors. | `false` | | `rainbow` | Whether or not the indent guides shall have changing colors. | `false` |
||||||| 60aa7d36 | `skip-levels` | Number of indent levels to skip | `0` |
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
=======
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
>>>>>>> seperate_code_action
Example: Example:
@ -276,12 +265,8 @@ Example:
[editor.indent-guides] [editor.indent-guides]
render = true render = true
character = "╎" character = "╎"
<<<<<<< HEAD
rainbow = true rainbow = true
||||||| 60aa7d36
=======
skip-levels = 1 skip-levels = 1
>>>>>>> seperate_code_action
``` ```
### `[editor.explorer]` Section ### `[editor.explorer]` Section

@ -86,7 +86,7 @@
| prisma | ✓ | | | `prisma-language-server` | | prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` | | prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | | | protobuf | ✓ | | ✓ | |
| python | ✓ | ✓ | | `pylsp` | | python | ✓ | ✓ | | `pylsp` |
| r | ✓ | | | `R` | | r | ✓ | | | `R` |
| racket | | | | `racket` | | racket | | | | `racket` |
| regex | ✓ | | | | | regex | ✓ | | | |

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

@ -60,24 +60,40 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`. This will install the `hx` binary to `$HOME/.cargo/bin`.
Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden
via the `HELIX_RUNTIME` environment variable. via the `HELIX_RUNTIME` environment variable.
| OS | command | | OS | command |
|-------------------|-----------| | ------------------- | ------------------------------------------------ |
|windows(cmd.exe) |`xcopy /e /i runtime %AppData%/helix/runtime` | | windows(cmd.exe) | `xcopy /e /i runtime %AppData%/helix/runtime` |
|windows(powershell)|`xcopy /e /i runtime $Env: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` |
## Finishing up the installation To use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## Finishing up the installation
To make sure everything is set up as expected you should finally run the helix healthcheck via
To make sure everything is set up as expected you should finally run the helix healthcheck via
``` ```
hx --health hx --health
``` ```
For more information on the information displayed in the healthcheck results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck).
For more information on the information displayed in the health check results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck).
### Building tree-sitter grammars ### Building tree-sitter grammars

@ -393,7 +393,6 @@ Keys to use within picker. Remapping currently not supported.
| `PageDown`, `Ctrl-d` | Page down | | `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry | | `Home` | Go to first entry |
| `End` | Go to last entry | | `End` | Go to last entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected | | `Enter` | Open selected |
| `Ctrl-s` | Open horizontally | | `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically | | `Ctrl-v` | Open vertically |

@ -240,51 +240,53 @@ These scopes are used for theming the editor interface.
- `hover` - for hover popup ui - `hover` - for hover popup ui
| Key | Notes | | Key | Notes |
| --- | --- | | --- | --- |
| `ui.background` | | | `ui.background` | |
| `ui.background.separator` | Picker separator below input line | | `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | | | `ui.cursor` | |
| `ui.cursor.insert` | | | `ui.cursor.insert` | |
| `ui.cursor.select` | | | `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. | | `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection | | `ui.cursor.primary` | Cursor with primary selection |
| `ui.gutter` | Gutter | | `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on | | `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.linenr` | Line numbers | | `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline | | `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) | | `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) | | `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) | | `ui.statusline.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.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline | | `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.popup.info` | Prompt for multiple key options |
| `ui.window` | Border lines separating splits | | `ui.window` | Border lines separating splits |
| `ui.help` | Description box for commands | | `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. | | `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | | | `ui.text.focus` | |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `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.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `ui.virtual.whitespace` | Visible white-space characters | | `ui.virtual.whitespace` | Visible white-space characters |
| `ui.virtual.indent-guide` | Vertical indent width guides | | `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.menu` | Code and command completion menus | | `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item | | `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar | | `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area | | `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | | | `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor | | `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors | | `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) | | `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `error` | Diagnostics error (gutter) | | `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `info` | Diagnostics info (gutter) | | `warning` | Diagnostics warning (gutter) |
| `hint` | Diagnostics hint (gutter) | | `error` | Diagnostics error (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) | | `info` | Diagnostics info (gutter) |
| `diagnostic.hint` | Diagnostics hint (editing area) | | `hint` | Diagnostics hint (gutter) |
| `diagnostic.info` | Diagnostics info (editing area) | | `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) | | `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) | | `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
You can check compliance to spec with You can check compliance to spec with

@ -29,6 +29,7 @@ tree-sitter = "0.20"
once_cell = "1.15" once_cell = "1.15"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
bitflags = "1.3"
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

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

@ -8,13 +8,15 @@ use crate::{
}; };
use arc_swap::{ArcSwap, Guard}; use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags;
use slotmap::{DefaultKey as LayerId, HopSlotMap}; use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, VecDeque},
fmt, fmt,
mem::replace,
path::Path, path::Path,
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
@ -594,6 +596,7 @@ impl Syntax {
tree: None, tree: None,
config, config,
depth: 0, depth: 0,
flags: LayerUpdateFlags::empty(),
ranges: vec![Range { ranges: vec![Range {
start_byte: 0, start_byte: 0,
end_byte: usize::MAX, end_byte: usize::MAX,
@ -656,9 +659,10 @@ impl Syntax {
} }
} }
for layer in &mut self.layers.values_mut() { for layer in self.layers.values_mut() {
// The root layer always covers the whole range (0..usize::MAX) // The root layer always covers the whole range (0..usize::MAX)
if layer.depth == 0 { if layer.depth == 0 {
layer.flags = LayerUpdateFlags::MODIFIED;
continue; continue;
} }
@ -689,6 +693,8 @@ impl Syntax {
edit.new_end_position, edit.new_end_position,
point_sub(range.end_point, edit.old_end_position), point_sub(range.end_point, edit.old_end_position),
); );
layer.flags |= LayerUpdateFlags::MOVED;
} }
// if the edit starts in the space before and extends into the range // if the edit starts in the space before and extends into the range
else if edit.start_byte < range.start_byte { else if edit.start_byte < range.start_byte {
@ -703,11 +709,13 @@ impl Syntax {
edit.new_end_position, edit.new_end_position,
point_sub(range.end_point, edit.old_end_position), point_sub(range.end_point, edit.old_end_position),
); );
layer.flags = LayerUpdateFlags::MODIFIED;
} }
// If the edit is an insertion at the start of the tree, shift // If the edit is an insertion at the start of the tree, shift
else if edit.start_byte == range.start_byte && is_pure_insertion { else if edit.start_byte == range.start_byte && is_pure_insertion {
range.start_byte = edit.new_end_byte; range.start_byte = edit.new_end_byte;
range.start_point = edit.new_end_position; range.start_point = edit.new_end_position;
layer.flags |= LayerUpdateFlags::MOVED;
} else { } else {
range.end_byte = range range.end_byte = range
.end_byte .end_byte
@ -717,6 +725,7 @@ impl Syntax {
edit.new_end_position, edit.new_end_position,
point_sub(range.end_point, edit.old_end_position), point_sub(range.end_point, edit.old_end_position),
); );
layer.flags = LayerUpdateFlags::MODIFIED;
} }
} }
} }
@ -731,27 +740,33 @@ impl Syntax {
let source_slice = source.slice(..); let source_slice = source.slice(..);
let mut touched = HashSet::new();
// TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit
while let Some(layer_id) = queue.pop_front() { while let Some(layer_id) = queue.pop_front() {
// Mark the layer as touched
touched.insert(layer_id);
let layer = &mut self.layers[layer_id]; let layer = &mut self.layers[layer_id];
// Mark the layer as touched
layer.flags |= LayerUpdateFlags::TOUCHED;
// If a tree already exists, notify it of changes. // If a tree already exists, notify it of changes.
if let Some(tree) = &mut layer.tree { if let Some(tree) = &mut layer.tree {
for edit in edits.iter().rev() { if layer
// Apply the edits in reverse. .flags
// If we applied them in order then edit 1 would disrupt the positioning of edit 2. .intersects(LayerUpdateFlags::MODIFIED | LayerUpdateFlags::MOVED)
tree.edit(edit); {
for edit in edits.iter().rev() {
// Apply the edits in reverse.
// If we applied them in order then edit 1 would disrupt the positioning of edit 2.
tree.edit(edit);
}
} }
}
// Re-parse the tree. if layer.flags.contains(LayerUpdateFlags::MODIFIED) {
layer.parse(&mut ts_parser.parser, source)?; // Re-parse the tree.
layer.parse(&mut ts_parser.parser, source)?;
}
} else {
// always parse if this layer has never been parsed before
layer.parse(&mut ts_parser.parser, source)?;
}
// Switch to an immutable borrow. // Switch to an immutable borrow.
let layer = &self.layers[layer_id]; let layer = &self.layers[layer_id];
@ -855,6 +870,8 @@ impl Syntax {
config, config,
depth, depth,
ranges, ranges,
// set the modified flag to ensure the layer is parsed
flags: LayerUpdateFlags::empty(),
}) })
}); });
@ -868,8 +885,11 @@ impl Syntax {
// Return the cursor back in the pool. // Return the cursor back in the pool.
ts_parser.cursors.push(cursor); ts_parser.cursors.push(cursor);
// Remove all untouched layers // Reset all `LayerUpdateFlags` and remove all untouched layers
self.layers.retain(|id, _| touched.contains(&id)); self.layers.retain(|_, layer| {
replace(&mut layer.flags, LayerUpdateFlags::empty())
.contains(LayerUpdateFlags::TOUCHED)
});
Ok(()) Ok(())
}) })
@ -968,6 +988,16 @@ impl Syntax {
// TODO: Folding // TODO: Folding
} }
bitflags! {
/// Flags that track the status of a layer
/// in the `Sytaxn::update` function
struct LayerUpdateFlags : u32{
const MODIFIED = 0b001;
const MOVED = 0b010;
const TOUCHED = 0b100;
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct LanguageLayer { pub struct LanguageLayer {
// mode // mode
@ -975,7 +1005,8 @@ pub struct LanguageLayer {
pub config: Arc<HighlightConfiguration>, pub config: Arc<HighlightConfiguration>,
pub(crate) tree: Option<Tree>, pub(crate) tree: Option<Tree>,
pub ranges: Vec<Range>, pub ranges: Vec<Range>,
pub depth: usize, pub depth: u32,
flags: LayerUpdateFlags,
} }
impl LanguageLayer { impl LanguageLayer {
@ -1191,7 +1222,7 @@ struct HighlightIter<'a> {
layers: Vec<HighlightIterLayer<'a>>, layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize, iter_count: usize,
next_event: Option<HighlightEvent>, next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>, last_highlight_range: Option<(usize, usize, u32)>,
} }
// Adapter to convert rope chunks to bytes // Adapter to convert rope chunks to bytes
@ -1224,7 +1255,7 @@ struct HighlightIterLayer<'a> {
config: &'a HighlightConfiguration, config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>, highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>, scope_stack: Vec<LocalScope<'a>>,
depth: usize, depth: u32,
ranges: &'a [Range], ranges: &'a [Range],
} }

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

@ -447,18 +447,13 @@ impl Application {
} }
pub fn handle_idle_timeout(&mut self) { pub fn handle_idle_timeout(&mut self) {
use crate::compositor::EventResult;
let editor_view = self
.compositor
.find::<ui::EditorView>()
.expect("expected at least one EditorView");
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
jobs: &mut self.jobs, jobs: &mut self.jobs,
scroll: None, scroll: None,
}; };
if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) { let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
if should_render {
self.render(); self.render();
} }
} }

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

@ -27,6 +27,7 @@ use helix_core::{
SmallVec, Tendril, Transaction, SmallVec, Tendril, Transaction,
}; };
use helix_view::{ use helix_view::{
apply_transaction,
clipboard::ClipboardType, clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion}, editor::{Action, Motion},
@ -863,7 +864,7 @@ fn align_selections(cx: &mut Context) {
changes.sort_unstable_by_key(|(from, _, _)| *from); changes.sort_unstable_by_key(|(from, _, _)| *from);
let transaction = Transaction::change(doc.text(), changes.into_iter()); let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn goto_window(cx: &mut Context, align: Align) { fn goto_window(cx: &mut Context, align: Align) {
@ -1293,7 +1294,7 @@ fn replace(cx: &mut Context) {
} }
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
}) })
} }
@ -1310,7 +1311,7 @@ where
(range.from(), range.to(), Some(text)) (range.from(), range.to(), Some(text))
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn switch_case(cx: &mut Context) { fn switch_case(cx: &mut Context) {
@ -2116,7 +2117,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None) (range.from(), range.to(), None)
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
match op { match op {
Operation::Delete => { Operation::Delete => {
@ -2130,14 +2131,11 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
} }
#[inline] #[inline]
fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) { fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: &Selection) {
let view_id = view.id;
// then delete
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None) (range.from(), range.to(), None)
}); });
doc.apply(&transaction, view_id); apply_transaction(&transaction, doc, view);
} }
fn delete_selection(cx: &mut Context) { fn delete_selection(cx: &mut Context) {
@ -2233,7 +2231,7 @@ fn append_mode(cx: &mut Context) {
doc.text(), doc.text(),
[(end, end, Some(doc.line_ending.as_str().into()))].into_iter(), [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
@ -2570,7 +2568,7 @@ async fn make_format_callback(
let doc = doc_mut!(editor, &doc_id); let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor); let view = view_mut!(editor);
if doc.version() == doc_version { if doc.version() == doc_version {
doc.apply(&format, view.id); apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
doc.detect_indent_and_line_ending(); doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);
@ -2657,7 +2655,7 @@ fn open(cx: &mut Context, open: Open) {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
// o inserts a new line after each line with a selection // o inserts a new line after each line with a selection
@ -2678,7 +2676,7 @@ fn normal_mode(cx: &mut Context) {
cx.editor.mode = Mode::Normal; cx.editor.mode = Mode::Normal;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
try_restore_indent(doc, view.id); try_restore_indent(doc, view);
// if leaving append mode, move cursor back by 1 // if leaving append mode, move cursor back by 1
if doc.restore_cursor { if doc.restore_cursor {
@ -2695,7 +2693,7 @@ fn normal_mode(cx: &mut Context) {
} }
} }
fn try_restore_indent(doc: &mut Document, view_id: ViewId) { fn try_restore_indent(doc: &mut Document, view: &mut View) {
use helix_core::chars::char_is_whitespace; use helix_core::chars::char_is_whitespace;
use helix_core::Operation; use helix_core::Operation;
@ -2714,18 +2712,18 @@ fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
let doc_changes = doc.changes().changes(); let doc_changes = doc.changes().changes();
let text = doc.text().slice(..); let text = doc.text().slice(..);
let range = doc.selection(view_id).primary(); let range = doc.selection(view.id).primary();
let pos = range.cursor(text); let pos = range.cursor(text);
let line_end_pos = line_end_char_index(&text, range.cursor_line(text)); let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) { if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
// Removes tailing whitespaces. // Removes tailing whitespaces.
let transaction = let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let line_start_pos = text.line_to_char(range.cursor_line(text)); let line_start_pos = text.line_to_char(range.cursor_line(text));
(line_start_pos, pos, None) (line_start_pos, pos, None)
}); });
doc.apply(&transaction, view_id); apply_transaction(&transaction, doc, view);
} }
} }
@ -3061,7 +3059,7 @@ pub mod insert {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
if let Some(t) = transaction { if let Some(t) = transaction {
doc.apply(&t, view.id); apply_transaction(&t, doc, view);
} }
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@ -3083,7 +3081,7 @@ pub mod insert {
&doc.selection(view.id).clone().cursors(doc.text().slice(..)), &doc.selection(view.id).clone().cursors(doc.text().slice(..)),
indent, indent,
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
pub fn insert_newline(cx: &mut Context) { pub fn insert_newline(cx: &mut Context) {
@ -3170,7 +3168,7 @@ pub mod insert {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
pub fn delete_char_backward(cx: &mut Context) { pub fn delete_char_backward(cx: &mut Context) {
@ -3264,7 +3262,7 @@ pub mod insert {
} }
}); });
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
@ -3282,7 +3280,7 @@ pub mod insert {
None, None,
) )
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
} }
@ -3476,7 +3474,7 @@ enum Paste {
Cursor, Cursor,
} }
fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste, count: usize) { fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Paste, count: usize) {
let repeat = std::iter::repeat( let repeat = std::iter::repeat(
values values
.last() .last()
@ -3519,7 +3517,7 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste,
}; };
(pos, pos, values.next()) (pos, pos, values.next())
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) { pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
@ -3611,7 +3609,7 @@ fn replace_with_yanked(cx: &mut Context) {
} }
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
} }
} }
@ -3634,7 +3632,7 @@ fn replace_selections_with_clipboard_impl(
) )
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
Ok(()) Ok(())
} }
@ -3704,7 +3702,7 @@ fn indent(cx: &mut Context) {
Some((pos, pos, Some(indent.clone()))) Some((pos, pos, Some(indent.clone())))
}), }),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn unindent(cx: &mut Context) { fn unindent(cx: &mut Context) {
@ -3743,7 +3741,7 @@ fn unindent(cx: &mut Context) {
let transaction = Transaction::change(doc.text(), changes.into_iter()); let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn format_selections(cx: &mut Context) { fn format_selections(cx: &mut Context) {
@ -3790,7 +3788,7 @@ fn format_selections(cx: &mut Context) {
// language_server.offset_encoding(), // language_server.offset_encoding(),
// ); // );
// doc.apply(&transaction, view.id); // apply_transaction(&transaction, doc, view);
} }
} }
@ -3845,7 +3843,7 @@ fn join_selections_inner(cx: &mut Context, select_space: bool) {
Transaction::change(doc.text(), changes.into_iter()) Transaction::change(doc.text(), changes.into_iter())
}; };
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@ -4017,7 +4015,7 @@ fn toggle_comments(cx: &mut Context) {
.map(|tc| tc.as_ref()); .map(|tc| tc.as_ref());
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token); let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
exit_select_mode(cx); exit_select_mode(cx);
} }
@ -4073,7 +4071,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
.map(|(range, fragment)| (range.from(), range.to(), Some(fragment))), .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
fn rotate_selection_contents_forward(cx: &mut Context) { fn rotate_selection_contents_forward(cx: &mut Context) {
@ -4539,13 +4537,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
(" ", "... or any character acting as a pair"), (" ", "... or any character acting as a pair"),
]; ];
cx.editor.autoinfo = Some(Info::new( cx.editor.autoinfo = Some(Info::new(title, &help_text));
title,
help_text
.into_iter()
.map(|(col1, col2)| (col1.to_string(), col2.to_string()))
.collect(),
));
} }
fn surround_add(cx: &mut Context) { fn surround_add(cx: &mut Context) {
@ -4569,7 +4561,7 @@ fn surround_add(cx: &mut Context) {
} }
let transaction = Transaction::change(doc.text(), changes.into_iter()); let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
}) })
} }
@ -4608,7 +4600,7 @@ fn surround_replace(cx: &mut Context) {
(pos, pos + 1, Some(t)) (pos, pos + 1, Some(t))
}), }),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
}); });
}) })
} }
@ -4635,7 +4627,7 @@ fn surround_delete(cx: &mut Context) {
let transaction = let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
}) })
} }
@ -4810,7 +4802,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
if behavior != &ShellBehavior::Ignore { if behavior != &ShellBehavior::Ignore {
let transaction = Transaction::change(doc.text(), changes.into_iter()); let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
} }
@ -4873,7 +4865,7 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
}); });
let transaction = Transaction::change(text, changes); let transaction = Transaction::change(text, changes);
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
/// Increment object under cursor by count. /// Increment object under cursor by count.
@ -4966,7 +4958,7 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let transaction = Transaction::change(doc.text(), changes); let transaction = Transaction::change(doc.text(), changes);
let transaction = transaction.with_selection(selection.clone()); let transaction = transaction.with_selection(selection.clone());
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
} }

@ -9,7 +9,7 @@ use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor, Open}; use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, Selection}; use helix_core::{path, Selection};
use helix_view::{editor::Action, theme::Style}; use helix_view::{apply_transaction, editor::Action, theme::Style};
use crate::{ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
@ -662,9 +662,7 @@ pub fn apply_workspace_edit(
} }
}; };
let doc = editor let doc = doc_mut!(editor, &doc_id);
.document_mut(doc_id)
.expect("Document for document_changes not found");
// Need to determine a view for apply/append_changes_to_history // Need to determine a view for apply/append_changes_to_history
let selections = doc.selections(); let selections = doc.selections();
@ -685,7 +683,7 @@ pub fn apply_workspace_edit(
text_edits, text_edits,
offset_encoding, offset_encoding,
); );
doc.apply(&transaction, view_id); apply_transaction(&transaction, doc, view_mut!(editor, view_id));
doc.append_changes_to_history(view_id); doc.append_changes_to_history(view_id);
}; };

@ -2,7 +2,10 @@ use std::ops::Deref;
use super::*; use super::*;
use helix_view::editor::{Action, CloseError, ConfigEvent}; use helix_view::{
apply_transaction,
editor::{Action, CloseError, ConfigEvent},
};
use ui::completers::{self, Completer}; use ui::completers::{self, Completer};
#[derive(Clone)] #[derive(Clone)]
@ -487,7 +490,7 @@ fn set_line_ending(
} }
}), }),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
Ok(()) Ok(())
@ -908,7 +911,7 @@ fn replace_selections_with_clipboard_impl(
(range.from(), range.to(), Some(contents.as_str().into())) (range.from(), range.to(), Some(contents.as_str().into()))
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
Ok(()) Ok(())
} }
@ -1029,7 +1032,7 @@ fn reload(
let scrolloff = cx.editor.config().scrolloff; let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.reload(view.id).map(|_| { doc.reload(view).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);
}) })
} }
@ -1423,7 +1426,7 @@ fn sort_impl(
.map(|(s, fragment)| (s.from(), s.to(), Some(fragment))), .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
Ok(()) Ok(())
@ -1467,7 +1470,7 @@ fn reflow(
(range.from(), range.to(), Some(reflowed_text)) (range.from(), range.to(), Some(reflowed_text))
}); });
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view.id);
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);

@ -1,5 +1,5 @@
use crate::compositor::{Component, Context, Event, EventResult}; use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::editor::CompleteAction; use helix_view::{apply_transaction, editor::CompleteAction};
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::text::Spans; use tui::text::Spans;
@ -143,11 +143,11 @@ impl Completion {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
// if more text was entered, remove it // if more text was entered, remove it
doc.restore(view.id); doc.restore(view);
match event { match event {
PromptEvent::Abort => { PromptEvent::Abort => {
doc.restore(view.id); doc.restore(view);
editor.last_completion = None; editor.last_completion = None;
} }
PromptEvent::Update => { PromptEvent::Update => {
@ -164,7 +164,7 @@ impl Completion {
// initialize a savepoint // initialize a savepoint
doc.savepoint(); doc.savepoint();
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction { editor.last_completion = Some(CompleteAction {
trigger_offset, trigger_offset,
@ -183,7 +183,7 @@ impl Completion {
trigger_offset, trigger_offset,
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction { editor.last_completion = Some(CompleteAction {
trigger_offset, trigger_offset,
@ -213,7 +213,7 @@ impl Completion {
additional_edits.clone(), additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client offset_encoding, // TODO: should probably transcode in Client
); );
doc.apply(&transaction, view.id); apply_transaction(&transaction, doc, view);
} }
} }
} }

@ -13,9 +13,10 @@ use helix_core::{
movement::Direction, movement::Direction,
syntax::{self, HighlightEvent}, syntax::{self, HighlightEvent},
unicode::width::UnicodeWidthStr, unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection, Transaction, visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
}; };
use helix_view::{ use helix_view::{
apply_transaction,
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig}, editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style}, graphics::{Color, CursorKind, Modifier, Rect, Style},
@ -120,9 +121,19 @@ impl EditorView {
if is_focused && editor.config().cursorline { if is_focused && editor.config().cursorline {
Self::highlight_cursorline(doc, view, surface, theme); Self::highlight_cursorline(doc, view, surface, theme);
} }
if is_focused && editor.config().cursorcolumn {
Self::highlight_cursorcolumn(doc, view, surface, theme);
}
let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
if diagnostic.is_empty() {
continue;
}
highlights = Box::new(syntax::merge(highlights, diagnostic));
}
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge( Box::new(syntax::merge(
highlights, highlights,
@ -266,7 +277,7 @@ impl EditorView {
pub fn doc_diagnostics_highlights( pub fn doc_diagnostics_highlights(
doc: &Document, doc: &Document,
theme: &Theme, theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> { ) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
let get_scope_of = |scope| { let get_scope_of = |scope| {
theme theme
@ -287,22 +298,42 @@ impl EditorView {
let error = get_scope_of("diagnostic.error"); let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
doc.diagnostics() let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
.iter() let mut info_vec = Vec::new();
.map(|diagnostic| { let mut hint_vec = Vec::new();
let diagnostic_scope = match diagnostic.severity { let mut warning_vec = Vec::new();
Some(Severity::Info) => info, let mut error_vec = Vec::new();
Some(Severity::Hint) => hint,
Some(Severity::Warning) => warning, let diagnostics = doc.diagnostics();
Some(Severity::Error) => error,
_ => r#default, // Diagnostics must be sorted by range. Otherwise, the merge strategy
}; // below would not be accurate.
( debug_assert!(diagnostics
diagnostic_scope, .windows(2)
diagnostic.range.start..diagnostic.range.end, .all(|window| window[0].range.start <= window[1].range.start
) && window[0].range.end <= window[1].range.end));
})
.collect() for diagnostic in diagnostics {
// Separate diagnostics into different Vecs by severity.
let (vec, scope) = match diagnostic.severity {
Some(Severity::Info) => (&mut info_vec, info),
Some(Severity::Hint) => (&mut hint_vec, hint),
Some(Severity::Warning) => (&mut warning_vec, warning),
Some(Severity::Error) => (&mut error_vec, error),
_ => (&mut default_vec, r#default),
};
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, range)) if diagnostic.range.start <= range.end => {
range.end = diagnostic.range.end.max(range.end)
}
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
}
}
[default_vec, info_vec, hint_vec, warning_vec, error_vec]
} }
/// Get highlight spans for selections in a document view. /// Get highlight spans for selections in a document view.
@ -839,6 +870,53 @@ impl EditorView {
} }
} }
/// Apply the highlighting on the columns where a cursor is active
pub fn highlight_cursorcolumn(
doc: &Document,
view: &View,
surface: &mut Surface,
theme: &Theme,
) {
let text = doc.text().slice(..);
// Manual fallback behaviour:
// ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s}
let primary_style = theme
.try_get_exact("ui.cursorcolumn.primary")
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.primary"));
let secondary_style = theme
.try_get_exact("ui.cursorcolumn.secondary")
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
let inner_area = view.inner_area();
let offset = view.offset.col;
let selection = doc.selection(view.id);
let primary = selection.primary();
for range in selection.iter() {
let is_primary = primary == *range;
let Position { row: _, col } =
visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
// if the cursor is horizontally in the view
if col >= offset && inner_area.width > (col - offset) as u16 {
let area = Rect::new(
inner_area.x + (col - offset) as u16,
view.area.y,
1,
view.area.height,
);
if is_primary {
surface.set_style(area, primary_style)
} else {
surface.set_style(area, secondary_style)
}
}
}
}
/// Handle events by looking them up in `self.keymaps`. Returns None /// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was /// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@ -947,7 +1025,7 @@ impl EditorView {
InsertEvent::CompletionApply(compl) => { InsertEvent::CompletionApply(compl) => {
let (view, doc) = current!(cxt.editor); let (view, doc) = current!(cxt.editor);
doc.restore(view.id); doc.restore(view);
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text); let cursor = doc.selection(view.id).primary().cursor(text);
@ -961,7 +1039,7 @@ impl EditorView {
(shift_position(start), shift_position(end), t) (shift_position(start), shift_position(end), t)
}), }),
); );
doc.apply(&tx, view.id); apply_transaction(&tx, doc, view);
} }
InsertEvent::TriggerCompletion => { InsertEvent::TriggerCompletion => {
let (_, doc) = current!(cxt.editor); let (_, doc) = current!(cxt.editor);
@ -1022,21 +1100,24 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger editor.clear_idle_timer(); // don't retrigger
} }
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { pub fn handle_idle_timeout(&mut self, cx: &mut crate::commands::Context) -> EventResult {
let config = cx.editor.config(); if self.completion.is_some()
if cx.editor.mode != Mode::Insert || !config.auto_completion { || cx.editor.mode != Mode::Insert
|| !cx.editor.config().auto_completion
{
return EventResult::Ignored(None); return EventResult::Ignored(None);
} }
self.clear_completion(cx.editor); let mut cx = commands::Context {
commands::completion(&mut commands::Context {
register: None, register: None,
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
count: None, count: None,
callback: None, callback: None,
on_next_key_callback: None, on_next_key_callback: None,
}); };
crate::commands::insert::idle_completion(&mut cx);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
} }
@ -1362,6 +1443,7 @@ impl Component for EditorView {
} }
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), 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 | Event::FocusLost => EventResult::Ignored(None),
} }
} }

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

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

@ -1,6 +1,7 @@
mod completion; mod completion;
pub(crate) mod editor; pub(crate) mod editor;
mod explore; mod explore;
mod fuzzy_match;
mod info; mod info;
pub mod lsp; pub mod lsp;
mod markdown; mod markdown;

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

@ -24,7 +24,7 @@ use helix_core::{
DEFAULT_LINE_ENDING, DEFAULT_LINE_ENDING,
}; };
use crate::{DocumentId, Editor, ViewId}; use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s. /// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192; const BUF_SIZE: usize = 8192;
@ -616,7 +616,7 @@ impl Document {
} }
/// Reload the document from its path. /// Reload the document from its path.
pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> { pub fn reload(&mut self, view: &mut View) -> Result<(), Error> {
let encoding = &self.encoding; let encoding = &self.encoding;
let path = self.path().filter(|path| path.exists()); let path = self.path().filter(|path| path.exists());
@ -632,8 +632,8 @@ impl Document {
// This is not considered a modification of the contents of the file regardless // This is not considered a modification of the contents of the file regardless
// of the encoding. // of the encoding.
let transaction = helix_core::diff::compare_ropes(self.text(), &rope); let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
self.apply(&transaction, view_id); apply_transaction(&transaction, self, view);
self.append_changes_to_history(view_id); self.append_changes_to_history(view.id);
self.reset_modified(); self.reset_modified();
self.detect_indent_and_line_ending(); self.detect_indent_and_line_ending();
@ -826,6 +826,9 @@ impl Document {
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text. /// Apply a [`Transaction`] to the [`Document`] to change its text.
/// Instead of calling this function directly, use [crate::apply_transaction]
/// to ensure that the transaction is applied to the appropriate [`View`] as
/// well.
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
// store the state just before any changes are made. This allows us to undo to the // store the state just before any changes are made. This allows us to undo to the
// state just before a transaction was applied. // state just before a transaction was applied.
@ -878,9 +881,9 @@ impl Document {
self.savepoint = Some((self.version, Transaction::new(self.text()))); self.savepoint = Some((self.version, Transaction::new(self.text())));
} }
pub fn restore(&mut self, view_id: ViewId) { pub fn restore(&mut self, view: &mut View) {
if let Some((_, revert)) = self.savepoint.take() { if let Some((_, revert)) = self.savepoint.take() {
self.apply(&revert, view_id); apply_transaction(&revert, self, view);
} }
} }

@ -175,6 +175,8 @@ pub struct Config {
pub line_number: LineNumber, pub line_number: LineNumber,
/// Highlight the lines cursors are currently on. Defaults to false. /// Highlight the lines cursors are currently on. Defaults to false.
pub cursorline: bool, pub cursorline: bool,
/// Highlight the columns cursors are currently on. Defaults to false.
pub cursorcolumn: bool,
/// Gutters. Default ["diagnostics", "line-numbers"] /// Gutters. Default ["diagnostics", "line-numbers"]
pub gutters: Vec<GutterType>, pub gutters: Vec<GutterType>,
/// Middle click paste support. Defaults to true. /// Middle click paste support. Defaults to true.
@ -646,6 +648,7 @@ impl Default for Config {
}, },
line_number: LineNumber::Absolute, line_number: LineNumber::Absolute,
cursorline: true, cursorline: true,
cursorcolumn: false,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers], gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
middle_click_paste: true, middle_click_paste: true,
auto_pairs: AutoPairConfig::default(), auto_pairs: AutoPairConfig::default(),

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

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

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

@ -298,6 +298,13 @@ impl Theme {
.find_map(|s| self.styles.get(s).copied()) .find_map(|s| self.styles.get(s).copied())
} }
/// Get the style of a scope, without falling back to dot separated broader
/// scopes. For example if `ui.text.focus` is not defined in the theme, it
/// will return `None`, even if `ui.text` is.
pub fn try_get_exact(&self, scope: &str) -> Option<Style> {
self.styles.get(scope).copied()
}
#[inline] #[inline]
pub fn scopes(&self) -> &[String] { pub fn scopes(&self) -> &[String] {
&self.scopes &self.scopes

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

@ -235,7 +235,7 @@ language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
[[grammar]] [[grammar]]
name = "c-sharp" name = "c-sharp"
source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "9c494a503c8e2044bfffce57f70b480c01a82f03" } source = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "5b60f99545fea00a33bbfae5be956f684c4c69e2" }
[[language]] [[language]]
name = "go" name = "go"
@ -537,7 +537,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "twig" name = "twig"
source = { git = "https://github.com/eirabben/tree-sitter-twig", rev = "b7444181fb38e603e25ea8fcdac55f9492e49c27" } source = { git = "https://github.com/gbprod/tree-sitter-twig", rev = "807b293fec3fead64f54c64fdf6fb05516c032b9" }
[[language]] [[language]]
name = "latex" name = "latex"

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

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

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

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

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

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

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

@ -576,7 +576,7 @@ _________________________________________________________________
Type A-s (Alt-s) to split the selection(s) on newlines. Type A-s (Alt-s) to split the selection(s) on newlines.
1. Move the first row of the table below. 1. Move the cursor to the first row of the table below.
2. Select the entire table with 6x. 2. Select the entire table with 6x.
3. Type A-s to split into selections at each line. 3. Type A-s to split into selections at each line.
4. Align the table with &. 4. Align the table with &.

@ -171,7 +171,7 @@ pub fn lint(file: String) -> Result<(), DynError> {
let message = m.replace("$THEME", theme.as_str()); let message = m.replace("$THEME", theme.as_str());
println!("{}", message); println!("{}", message);
}); });
Err(format!("{} has issues", file.clone().as_str()).into()) Err(format!("{} has issues", file).into())
} else { } else {
Ok(()) Ok(())
} }
@ -183,8 +183,7 @@ pub fn lint_all() -> Result<(), DynError> {
let ok_files_count = files let ok_files_count = files
.into_iter() .into_iter()
.filter_map(|path| lint(path.replace(".toml", "")).ok()) .filter_map(|path| lint(path.replace(".toml", "")).ok())
.collect::<Vec<()>>() .count();
.len();
if files_count != ok_files_count { if files_count != ok_files_count {
Err(format!( Err(format!(

Loading…
Cancel
Save