diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 9593b882..ff810489 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -63,7 +63,21 @@ pub fn select_all_siblings(tree: &Tree, text: RopeSlice, selection: Selection) - root_node .descendant_for_byte_range(from, to) .and_then(find_parent_with_more_children) - .map(|parent| select_children(parent, text, range.direction())) + .and_then(|parent| select_children(parent, text, range.direction())) + .unwrap_or_else(|| vec![range].into_iter()) + }) +} + +pub fn select_all_children(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(|parent| select_children(parent, text, range.direction())) .unwrap_or_else(|| vec![range].into_iter()) }) } @@ -72,10 +86,11 @@ fn select_children( node: Node, text: RopeSlice, direction: Direction, -) -> as std::iter::IntoIterator>::IntoIter { +) -> Option< as std::iter::IntoIterator>::IntoIter> { let mut cursor = node.walk(); - node.named_children(&mut cursor) + let children = 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()); @@ -86,8 +101,13 @@ fn select_children( Range::new(from, to) } }) - .collect::>() - .into_iter() + .collect::>(); + + if !children.is_empty() { + Some(children.into_iter()) + } else { + None + } } fn find_sibling_recursive(node: Node, sibling_fn: F) -> Option diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7618fd0a..9b203092 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -440,7 +440,8 @@ impl MappableCommand { shrink_selection, "Shrink selection to previously expanded syntax node", 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", + select_all_siblings, "Select all siblings of the current node", + select_all_children, "Select all children of the current node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save current selection to jumplist", @@ -4991,6 +4992,23 @@ fn select_all_siblings(cx: &mut Context) { cx.editor.apply_motion(motion); } +fn select_all_children(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_children(syntax.tree(), text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(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 90088e99..5a3e8eed 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -87,6 +87,7 @@ pub fn default() -> HashMap { "A-;" => flip_selections, "A-o" | "A-up" => expand_selection, "A-i" | "A-down" => shrink_selection, + "A-I" | "A-S-down" => select_all_children, "A-p" | "A-left" => select_prev_sibling, "A-n" | "A-right" => select_next_sibling, "A-e" => move_parent_node_end, diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index f263fbac..84806d5f 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -601,3 +601,128 @@ async fn select_all_siblings() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_children() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + helpers::platform_line(indoc! {r##" + let foo = bar#[(a, b, c)|]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}), + ), + ( + helpers::platform_line(indoc! {r##" + let a = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + ), + // direction is preserved + ( + helpers::platform_line(indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}), + ), + // can't pick any more children - selection stays the same + ( + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + ), + // each cursor does the sibling select independently + ( + helpers::platform_line(indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + + let b = #([ + "one", + "two", + "three", + "four", + "five", + ]|)#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}), + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +}