Unnamed repository; edit this file 'description' to name the repository.
feat(command): select_all_siblings
Skyler Hawthorne 2024-04-09
parent cf9b88f · commit 87c4161
-rw-r--r--helix-core/src/object.rs62
-rw-r--r--helix-term/src/commands.rs21
-rw-r--r--helix-term/src/keymap/default.rs1
-rw-r--r--helix-term/tests/test/commands/movement.rs151
4 files changed, 232 insertions, 3 deletions
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index 0df105f1..9593b882 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<Node> {
+ 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,
+) -> <Vec<Range> 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::<Vec<_>>()
+ .into_iter()
+}
+
+fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ 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 99e7608f..7618fd0a 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 498a9a3e..90088e99 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -91,6 +91,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"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 34c9d23b..f263fbac 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);
+ "##},
+ "<A-a>",
+ indoc! {r##"
+ let foo = bar(#[a|]#, #(b|)#, #(c|)#);
+ "##},
+ ),
+ (
+ indoc! {r##"
+ let a = [
+ #[1|]#,
+ 2,
+ 3,
+ 4,
+ 5,
+ ];
+ "##},
+ "<A-a>",
+ indoc! {r##"
+ let a = [
+ #[1|]#,
+ #(2|)#,
+ #(3|)#,
+ #(4|)#,
+ #(5|)#,
+ ];
+ "##},
+ ),
+ // direction is preserved
+ (
+ indoc! {r##"
+ let a = [
+ #[|1]#,
+ 2,
+ 3,
+ 4,
+ 5,
+ ];
+ "##},
+ "<A-a>",
+ 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|)#,
+ ];
+ "##},
+ "<A-a>",
+ 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",
+ ];
+ "##},
+ "<A-a>",
+ 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,
+ ];
+ "##},
+ "<A-a>",
+ 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(())
+}