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"
dependencies = [
"arc-swap",
"bitflags",
"chrono",
"encoding_rs",
"etcetera",
@ -937,9 +938,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa",
"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.
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).
| OS | Command |
| -------------------- | -------------------------------------------- |
| Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux/macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
| OS | Command |
| -------------------- | ------------------------------------------------ |
| Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux/macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
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
> detect the `runtime` directory in the project root.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
In order to use LSP features like auto-complete, you will need to
[install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
for a language.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Adding Helix to your desktop environment
If installing from source, to use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## MacOS
Helix can be installed on MacOS through homebrew:

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

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

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

@ -60,24 +60,40 @@ cargo install --path helix-term
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
via the `HELIX_RUNTIME` environment variable.
| OS | command |
|-------------------|-----------|
|windows(cmd.exe) |`xcopy /e /i runtime %AppData%/helix/runtime` |
|windows(powershell)|`xcopy /e /i runtime $Env:AppData\helix\runtime` |
|linux/macos |`ln -s $PWD/runtime ~/.config/helix/runtime`|
| OS | command |
| ------------------- | ------------------------------------------------ |
| windows(cmd.exe) | `xcopy /e /i runtime %AppData%/helix/runtime` |
| windows(powershell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| linux/macos | `ln -s $PWD/runtime ~/.config/helix/runtime` |
## 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
```
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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::editor::CompleteAction;
use helix_view::{apply_transaction, editor::CompleteAction};
use tui::buffer::Buffer as Surface;
use tui::text::Spans;
@ -143,11 +143,11 @@ impl Completion {
let (view, doc) = current!(editor);
// if more text was entered, remove it
doc.restore(view.id);
doc.restore(view);
match event {
PromptEvent::Abort => {
doc.restore(view.id);
doc.restore(view);
editor.last_completion = None;
}
PromptEvent::Update => {
@ -164,7 +164,7 @@ impl Completion {
// initialize a savepoint
doc.savepoint();
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@ -183,7 +183,7 @@ impl Completion {
trigger_offset,
);
doc.apply(&transaction, view.id);
apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@ -213,7 +213,7 @@ impl Completion {
additional_edits.clone(),
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,
syntax::{self, HighlightEvent},
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection, Transaction,
visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
apply_transaction,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
@ -120,9 +121,19 @@ impl EditorView {
if is_focused && editor.config().cursorline {
Self::highlight_cursorline(doc, view, surface, theme);
}
if is_focused && editor.config().cursorcolumn {
Self::highlight_cursorcolumn(doc, view, surface, theme);
}
let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
if diagnostic.is_empty() {
continue;
}
highlights = Box::new(syntax::merge(highlights, diagnostic));
}
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
@ -266,7 +277,7 @@ impl EditorView {
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity;
let get_scope_of = |scope| {
theme
@ -287,22 +298,42 @@ impl EditorView {
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
doc.diagnostics()
.iter()
.map(|diagnostic| {
let diagnostic_scope = match diagnostic.severity {
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
Some(Severity::Warning) => warning,
Some(Severity::Error) => error,
_ => r#default,
};
(
diagnostic_scope,
diagnostic.range.start..diagnostic.range.end,
)
})
.collect()
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut info_vec = Vec::new();
let mut hint_vec = Vec::new();
let mut warning_vec = Vec::new();
let mut error_vec = Vec::new();
let diagnostics = doc.diagnostics();
// Diagnostics must be sorted by range. Otherwise, the merge strategy
// below would not be accurate.
debug_assert!(diagnostics
.windows(2)
.all(|window| window[0].range.start <= window[1].range.start
&& window[0].range.end <= window[1].range.end));
for diagnostic in diagnostics {
// 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.
@ -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
/// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@ -947,7 +1025,7 @@ impl EditorView {
InsertEvent::CompletionApply(compl) => {
let (view, doc) = current!(cxt.editor);
doc.restore(view.id);
doc.restore(view);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
@ -961,7 +1039,7 @@ impl EditorView {
(shift_position(start), shift_position(end), t)
}),
);
doc.apply(&tx, view.id);
apply_transaction(&tx, doc, view);
}
InsertEvent::TriggerCompletion => {
let (_, doc) = current!(cxt.editor);
@ -1022,21 +1100,24 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger
}
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
let config = cx.editor.config();
if cx.editor.mode != Mode::Insert || !config.auto_completion {
pub fn handle_idle_timeout(&mut self, cx: &mut crate::commands::Context) -> EventResult {
if self.completion.is_some()
|| cx.editor.mode != Mode::Insert
|| !cx.editor.config().auto_completion
{
return EventResult::Ignored(None);
}
self.clear_completion(cx.editor);
commands::completion(&mut commands::Context {
let mut cx = commands::Context {
register: None,
editor: cx.editor,
jobs: cx.jobs,
count: None,
callback: None,
on_next_key_callback: None,
});
};
crate::commands::insert::idle_completion(&mut cx);
EventResult::Consumed(None)
}
}
@ -1362,6 +1443,7 @@ impl Component for EditorView {
}
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
}
}

@ -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;
pub(crate) mod editor;
mod explore;
mod fuzzy_match;
mod info;
pub mod lsp;
mod markdown;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -576,7 +576,7 @@ _________________________________________________________________
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.
3. Type A-s to split into selections at each line.
4. Align the table with &.

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

Loading…
Cancel
Save