diff options
Diffstat (limited to 'helix-core/src')
-rw-r--r-- | helix-core/src/indent.rs | 193 | ||||
-rw-r--r-- | helix-core/src/movement.rs | 8 | ||||
-rw-r--r-- | helix-core/src/selection.rs | 12 | ||||
-rw-r--r-- | helix-core/src/syntax.rs | 81 |
4 files changed, 239 insertions, 55 deletions
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], } |