diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 0df105f1a517..9593b882f6d3 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,4 +1,5 @@ -use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; +use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; +use tree_sitter::{Node, Tree}; pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { let cursor = &mut syntax.walk(); @@ -40,6 +41,65 @@ pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selectio }) } +fn find_parent_with_more_children(mut node: Node) -> Option { + while let Some(parent) = node.parent() { + if parent.child_count() > 1 { + return Some(parent); + } + + node = parent; + } + + None +} + +pub fn select_all_siblings(tree: &Tree, text: RopeSlice, selection: Selection) -> Selection { + let root_node = &tree.root_node(); + + selection.transform_iter(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + root_node + .descendant_for_byte_range(from, to) + .and_then(find_parent_with_more_children) + .map(|parent| select_children(parent, text, range.direction())) + .unwrap_or_else(|| vec![range].into_iter()) + }) +} + +fn select_children( + node: Node, + text: RopeSlice, + direction: Direction, +) -> as std::iter::IntoIterator>::IntoIter { + let mut cursor = node.walk(); + + node.named_children(&mut cursor) + .map(|child| { + let from = text.byte_to_char(child.start_byte()); + let to = text.byte_to_char(child.end_byte()); + + if direction == Direction::Backward { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) + .collect::>() + .into_iter() +} + +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| { while !cursor.goto_prev_sibling() { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99e7608fcb5a..7618fd0ab617 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -438,8 +438,9 @@ impl MappableCommand { reverse_selection_contents, "Reverse selections contents", expand_selection, "Expand selection to parent syntax node", shrink_selection, "Shrink selection to previously expanded syntax node", - select_next_sibling, "Select next sibling in syntax tree", - select_prev_sibling, "Select previous sibling in syntax tree", + select_next_sibling, "Select next sibling in the syntax tree", + select_prev_sibling, "Select previous sibling the in syntax tree", + select_all_siblings, "Select all siblings in the syntax tree", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save current selection to jumplist", @@ -4974,6 +4975,22 @@ pub fn extend_parent_node_start(cx: &mut Context) { move_node_bound_impl(cx, Direction::Backward, Movement::Extend) } +fn select_all_siblings(cx: &mut Context) { + let motion = |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_all_siblings(syntax.tree(), text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + + cx.editor.apply_motion(motion); +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); let is_select = cx.editor.mode == Mode::Select; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 498a9a3e71ef..90088e991eae 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -91,6 +91,7 @@ pub fn default() -> HashMap { "A-n" | "A-right" => select_next_sibling, "A-e" => move_parent_node_end, "A-b" => move_parent_node_start, + "A-a" => select_all_siblings, "%" => select_all, "x" => extend_line_below, diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 34c9d23b2b8e..f263fbac4ca0 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -450,3 +450,154 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_siblings() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + indoc! {r##" + let foo = bar(#[a|]#, b, c); + "##}, + "", + indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}, + ), + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // direction is preserved + ( + indoc! {r##" + let a = [ + #[|1]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}, + ), + // can't pick any more siblings - selection stays the same + ( + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // each cursor does the sibling select independently + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + + let b = [ + #("one"|)#, + "two", + "three", + "four", + "five", + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}, + ), + // conflicting sibling selections get normalized. Here, the primary + // selection would choose every list item, but because the secondary + // range covers more than one item, the descendent is the entire list, + // which means the sibling is the assignment. The list item ranges just + // get normalized out since the list itself becomes selected. + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + #(3, + 4|)#, + 5, + ]; + "##}, + "", + indoc! {r##" + let #(a|)# = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +}