aboutsummaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/Cargo.toml3
-rw-r--r--helix-core/src/indent.rs193
-rw-r--r--helix-core/src/movement.rs8
-rw-r--r--helix-core/src/selection.rs12
-rw-r--r--helix-core/src/syntax.rs81
-rw-r--r--helix-core/tests/indent.rs1
6 files changed, 242 insertions, 56 deletions
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ba6901ba..4eaadd1e 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -18,7 +18,7 @@ integration = []
helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.5", default-features = false, features = ["simd"] }
-smallvec = "1.9"
+smallvec = "1.10"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
unicode-width = "0.1"
@@ -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"] }
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index ad079c25..9526fc8a 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -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,
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index c232484c..278375e8 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -389,6 +389,8 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
}
}
+/// Finds the range of the next or previous textobject in the syntax sub-tree of `node`.
+/// Returns the range in the forwards direction.
pub fn goto_treesitter_object(
slice: RopeSlice,
range: Range,
@@ -419,8 +421,8 @@ pub fn goto_treesitter_object(
.filter(|n| n.start_byte() > byte_pos)
.min_by_key(|n| n.start_byte())?,
Direction::Backward => nodes
- .filter(|n| n.start_byte() < byte_pos)
- .max_by_key(|n| n.start_byte())?,
+ .filter(|n| n.end_byte() < byte_pos)
+ .max_by_key(|n| n.end_byte())?,
};
let len = slice.len_bytes();
@@ -434,7 +436,7 @@ pub fn goto_treesitter_object(
let end_char = slice.byte_to_char(end_byte);
// head of range should be at beginning
- Some(Range::new(end_char, start_char))
+ Some(Range::new(start_char, end_char))
};
(0..count).fold(range, |range, _| get_range(range).unwrap_or(range))
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 3463c1d3..1f28ecef 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -122,7 +122,7 @@ impl Range {
}
}
- // flips the direction of the selection
+ /// Flips the direction of the selection
pub fn flip(&self) -> Self {
Self {
anchor: self.head,
@@ -131,6 +131,16 @@ impl Range {
}
}
+ /// Returns the selection if it goes in the direction of `direction`,
+ /// flipping the selection otherwise.
+ pub fn with_direction(self, direction: Direction) -> Self {
+ if self.direction() == direction {
+ self
+ } else {
+ self.flip()
+ }
+ }
+
/// Check two ranges for overlap.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index e0a984d2..61d382fd 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -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,
@@ -366,7 +368,13 @@ impl LanguageConfiguration {
None
} else {
let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id))
- .map_err(|e| log::info!("{}", e))
+ .map_err(|err| {
+ log::error!(
+ "Failed to load tree-sitter parser for language {:?}: {}",
+ self.language_id,
+ err
+ )
+ })
.ok()?;
let config = HighlightConfiguration::new(
language,
@@ -594,6 +602,7 @@ impl Syntax {
tree: None,
config,
depth: 0,
+ flags: LayerUpdateFlags::empty(),
ranges: vec![Range {
start_byte: 0,
end_byte: usize::MAX,
@@ -656,9 +665,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 +699,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 +715,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 +731,7 @@ impl Syntax {
edit.new_end_position,
point_sub(range.end_point, edit.old_end_position),
);
+ layer.flags = LayerUpdateFlags::MODIFIED;
}
}
}
@@ -731,27 +746,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 +876,8 @@ impl Syntax {
config,
depth,
ranges,
+ // set the modified flag to ensure the layer is parsed
+ flags: LayerUpdateFlags::empty(),
})
});
@@ -868,8 +891,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 +994,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 +1011,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 +1228,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 +1261,7 @@ struct HighlightIterLayer<'a> {
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
- depth: usize,
+ depth: u32,
ranges: &'a [Range],
}
diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs
index ff04d05f..e1114f4a 100644
--- a/helix-core/tests/indent.rs
+++ b/helix-core/tests/indent.rs
@@ -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,