Skip to content

Commit

Permalink
feat(command): expand_selection_around
Browse files Browse the repository at this point in the history
Introduces a new command `expand_selection_around` that expands the
selection to the parent node, like `expand_selection`, except it splits
on the selection you start with and continues expansion around this
initial selection.
  • Loading branch information
dead10ck committed Mar 8, 2023
1 parent 3784ed3 commit c2ddb01
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 0 deletions.
86 changes: 86 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ impl MappableCommand {
rotate_selection_contents_backward, "Rotate selections contents backward",
expand_selection, "Expand selection to parent syntax node",
shrink_selection, "Shrink selection to previously expanded syntax node",
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
select_next_sibling, "Select next sibling in syntax tree",
select_prev_sibling, "Select previous sibling in syntax tree",
jump_forward, "Jump forward on jumplist",
Expand Down Expand Up @@ -4300,6 +4301,8 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
// tree sitter node selection

const EXPAND_KEY: &str = "expand";
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
const PARENTS_KEY: &str = "parents";

fn expand_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
Expand Down Expand Up @@ -4335,6 +4338,27 @@ fn shrink_selection(cx: &mut Context) {
if let Some(prev_selection) = prev_expansions.pop() {
// allow shrinking the selection only if current selection contains the previous object selection
doc.set_selection_clear(view, prev_selection, false);

// Do a corresponding pop of the parents from `expand_selection_around`
view.object_selections
.entry(PARENTS_KEY)
.and_modify(|parents| {
parents.pop();
});

// need to do this again because borrowing
let prev_expansions = view.object_selections.entry(EXPAND_KEY).or_default();

// if we've emptied out the previous expansions, then clear out the
// base history as well so it doesn't get used again erroneously
if prev_expansions.is_empty() {
view.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.and_modify(|base| {
base.clear();
});
}

return;
}

Expand All @@ -4350,6 +4374,68 @@ fn shrink_selection(cx: &mut Context) {
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}

fn expand_selection_around(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);

// [NOTE] we do this pop and push dance because if we don't take
// ownership of the objects, then we require multiple
// mutable references to the view's object selections
let mut parents_selection =
view.object_selections.entry(PARENTS_KEY).or_default().pop();
let mut base_selection = view
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.pop();

if parents_selection.is_none() || base_selection.is_none() {
parents_selection = Some(current_selection.clone());
base_selection = Some(current_selection.clone());
}

let outside_selection =
object::expand_selection(syntax, text, parents_selection.clone().unwrap());

let target_selection = match outside_selection
.clone()
.without(&base_selection.clone().unwrap())
{
Some(sel) => sel,
None => outside_selection.clone(),
};

// check if selection is different from the last one
if target_selection != *current_selection {
// save current selection so it can be restored using shrink_selection
view.object_selections
.entry(EXPAND_KEY)
.or_default()
.push(current_selection.clone());

doc.set_selection_clear(view, target_selection, false);
}

let parents = view.object_selections.entry(PARENTS_KEY).or_default();

parents.push(parents_selection.unwrap());
parents.push(outside_selection);

view.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.push(base_selection.unwrap());
}
};

motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}

fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
where
F: Fn(Node) -> Option<Node>,
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 @@ -84,6 +84,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-O" => expand_selection_around,
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
Expand Down
65 changes: 65 additions & 0 deletions helix-term/tests/test/commands/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,68 @@ async fn expand_shrink_selection() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn expand_selection_around() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
Some(#[thing|]#)
"##}),
"<A-O><A-O>",
helpers::platform_line(indoc! {r##"
#[Some(|]#thing#()|)#
"##}),
),
// shrinking restores previous selection
(
helpers::platform_line(indoc! {r##"
Some(#[thing|]#)
"##}),
"<A-O><A-O><A-i><A-i>",
helpers::platform_line(indoc! {r##"
Some(#[thing|]#)
"##}),
),
// multi range collision merges expand as normal, except with the
// original selection removed from the result
(
helpers::platform_line(indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##}),
"<A-O><A-O><A-O>",
helpers::platform_line(indoc! {r##"
#[(
Some(|]#thing#(),
Some(|)#other_thing#(),
)|)#
"##}),
),
(
helpers::platform_line(indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##}),
"<A-O><A-O><A-O><A-i><A-i><A-i>",
helpers::platform_line(indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##}),
),
];

for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}

Ok(())
}

0 comments on commit c2ddb01

Please sign in to comment.