Skip to content

Commit

Permalink
feat(command): select_all_siblings
Browse files Browse the repository at this point in the history
  • Loading branch information
dead10ck committed Jul 19, 2023
1 parent d52b790 commit e7d6462
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 4 deletions.
53 changes: 51 additions & 2 deletions helix-core/src/object.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Range, RopeSlice, Selection, Syntax};
use tree_sitter::Node;
use crate::{movement::Direction, Range, RopeSlice, Selection, Syntax};
use tree_sitter::{Node, Tree};

pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |mut node, from, to| {
Expand Down Expand Up @@ -30,6 +30,55 @@ where
})
}

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>,
Expand Down
22 changes: 20 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,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",
Expand Down Expand Up @@ -4691,6 +4692,23 @@ fn select_prev_sibling(cx: &mut Context) {
select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
}

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);
}
};

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;
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-a" => select_all_siblings,

"%" => select_all,
"x" => extend_line_below,
Expand Down
1 change: 1 addition & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use helix_term::application::Application;

use super::*;

mod movement;
mod write;

#[tokio::test(flavor = "multi_thread")]
Expand Down
152 changes: 152 additions & 0 deletions helix-term/tests/test/commands/movement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use super::*;

#[tokio::test(flavor = "multi_thread")]
async fn select_all_siblings() -> anyhow::Result<()> {
let tests = vec![
// basic tests
(
helpers::platform_line(indoc! {r##"
let foo = bar(#[a|]#, b, c);
"##}),
"<A-a>",
helpers::platform_line(indoc! {r##"
let foo = bar(#[a|]#, #(b|)#, #(c|)#);
"##}),
),
(
helpers::platform_line(indoc! {r##"
let a = [
#[1|]#,
2,
3,
4,
5,
];
"##}),
"<A-a>",
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,
];
"##}),
"<A-a>",
helpers::platform_line(indoc! {r##"
let a = [
#[|1]#,
#(|2)#,
#(|3)#,
#(|4)#,
#(|5)#,
];
"##}),
),
// can't pick any more siblings - selection stays the same
(
helpers::platform_line(indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##}),
"<A-a>",
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",
];
"##}),
"<A-a>",
helpers::platform_line(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.
(
helpers::platform_line(indoc! {r##"
let a = [
#[1|]#,
2,
#(3,
4|)#,
5,
];
"##}),
"<A-a>",
helpers::platform_line(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(())
}

0 comments on commit e7d6462

Please sign in to comment.