From 8b5e75135e50e03e0d86e4bd42ce4f964b69f538 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 10 Jan 2024 14:31:05 -0500 Subject: [PATCH] Reimplement tree motions in terms of syntax::TreeCursor This uses the new TreeCursor type from the parent commit to reimplement the tree-sitter motions (`A-p/o/i/n`). Other tree-sitter related features like textobjects are not touched with this change and will need a different, unrelated approach to solve. --- helix-core/src/object.rs | 82 +++++++++++++++---------------- helix-term/src/commands.rs | 14 +++--- helix-term/tests/test/movement.rs | 57 +++++++++++++++++++++ 3 files changed, 103 insertions(+), 50 deletions(-) diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index d2d4fe70ac02..c5282a3281e8 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,42 +1,48 @@ -use crate::{Range, RopeSlice, Selection, Syntax}; -use tree_sitter::Node; +use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { - select_node_impl(syntax, text, selection, |mut node, from, to| { - while node.start_byte() == from && node.end_byte() == to { - node = node.parent()?; + let cursor = &mut syntax.walk(); + + selection.transform(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + let mut byte_range = from..to; + cursor.reset_to_byte_range(from, to); + + loop { + if cursor.node().byte_range() != byte_range { + break; + } + byte_range = cursor.node().byte_range(); + if !cursor.goto_parent() { + break; + } } - Some(node) + + let node = cursor.node(); + let from = text.byte_to_char(node.start_byte()); + let to = text.byte_to_char(node.end_byte()); + + Range::new(to, from).with_direction(range.direction()) }) } pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { - select_node_impl(syntax, text, selection, |descendant, _from, _to| { - descendant.child(0).or(Some(descendant)) + select_node_impl(syntax, text, selection, |cursor| { + cursor.goto_first_child(); }) } -pub fn select_sibling( - syntax: &Syntax, - text: RopeSlice, - selection: Selection, - sibling_fn: &F, -) -> Selection -where - F: Fn(Node) -> Option, -{ - select_node_impl(syntax, text, selection, |descendant, _from, _to| { - find_sibling_recursive(descendant, sibling_fn) +pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |cursor| { + let _ = cursor.goto_next_sibling() || cursor.goto_parent() && cursor.goto_next_sibling(); }) } -fn find_sibling_recursive(node: Node, sibling_fn: F) -> Option -where - F: Fn(Node) -> Option, -{ - sibling_fn(node).or_else(|| { - node.parent() - .and_then(|node| find_sibling_recursive(node, sibling_fn)) +pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |cursor| { + let _ = cursor.goto_prev_sibling() || cursor.goto_parent() && cursor.goto_prev_sibling(); }) } @@ -44,33 +50,25 @@ fn select_node_impl( syntax: &Syntax, text: RopeSlice, selection: Selection, - select_fn: F, + motion: F, ) -> Selection where - F: Fn(Node, usize, usize) -> Option, + F: Fn(&mut TreeCursor), { - let tree = syntax.tree(); + let cursor = &mut syntax.walk(); selection.transform(|range| { let from = text.char_to_byte(range.from()); let to = text.char_to_byte(range.to()); - let node = match tree - .root_node() - .descendant_for_byte_range(from, to) - .and_then(|node| select_fn(node, from, to)) - { - Some(node) => node, - None => return range, - }; + cursor.reset_to_byte_range(from, to); + motion(cursor); + + let node = cursor.node(); let from = text.byte_to_char(node.start_byte()); let to = text.byte_to_char(node.end_byte()); - if range.head < range.anchor { - Range::new(to, from) - } else { - Range::new(from, to) - } + Range::new(to, from).with_direction(range.direction()) }) } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e46109c0fc0e..d9cad7fb0240 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -25,7 +25,6 @@ use helix_core::{ syntax::LanguageServerFeature, text_annotations::TextAnnotations, textobject, - tree_sitter::Node, unicode::width::UnicodeWidthChar, visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -4556,18 +4555,17 @@ fn shrink_selection(cx: &mut Context) { cx.editor.apply_motion(motion); } -fn select_sibling_impl(cx: &mut Context, sibling_fn: &'static F) +fn select_sibling_impl(cx: &mut Context, sibling_fn: F) where - F: Fn(Node) -> Option, + F: Fn(&helix_core::Syntax, RopeSlice, Selection) -> Selection + 'static, { - let motion = |editor: &mut Editor| { + let motion = move |editor: &mut Editor| { let (view, doc) = current!(editor); if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); let current_selection = doc.selection(view.id); - let selection = - object::select_sibling(syntax, text, current_selection.clone(), sibling_fn); + let selection = sibling_fn(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; @@ -4575,11 +4573,11 @@ where } fn select_next_sibling(cx: &mut Context) { - select_sibling_impl(cx, &|node| Node::next_sibling(&node)) + select_sibling_impl(cx, object::select_next_sibling) } fn select_prev_sibling(cx: &mut Context) { - select_sibling_impl(cx, &|node| Node::prev_sibling(&node)) + select_sibling_impl(cx, object::select_prev_sibling) } fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) { diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index e3c2668daa54..4468e4a89df5 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -552,3 +552,60 @@ async fn find_char_line_ending() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + // When the full injected layer is selected, expand_selection jumps to + // a more shallow layer. + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "#[|]#", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + Ok(()) +}