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 Sep 7, 2024
1 parent 161d468 commit 9d6e303
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 0 deletions.
106 changes: 106 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ impl MappableCommand {
select_prev_sibling, "Select previous sibling the in syntax tree",
select_all_siblings, "Select all siblings of the current node",
select_all_children, "Select all children of the current node",
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save current selection to jumplist",
Expand Down Expand Up @@ -4963,6 +4964,8 @@ fn reverse_selection_contents(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 @@ -5006,6 +5009,33 @@ 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.id, prev_selection, false);

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

// need to do this again because borrowing
let prev_expansions = doc
.view_data_mut(view.id)
.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() {
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.and_modify(|base| {
base.clear();
});
}

return;
}

Expand All @@ -5020,6 +5050,81 @@ fn shrink_selection(cx: &mut Context) {
cx.editor.apply_motion(motion);
}

fn expand_selection_around(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);

if doc.syntax().is_some() {
// [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 = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default()
.pop();

let mut base_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.pop();

let current_selection = doc.selection(view.id).clone();

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

let text = doc.text().slice(..);
let syntax = doc.syntax().unwrap();

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
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default()
.push(current_selection);

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

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

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

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

cx.editor.apply_motion(motion);
}

fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: F)
where
F: Fn(&helix_core::Syntax, RopeSlice, Selection) -> Selection + 'static,
Expand All @@ -5034,6 +5139,7 @@ where
doc.set_selection(view.id, selection);
}
};

cx.editor.apply_motion(motion);
}

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 @@ -86,6 +86,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-O" => expand_selection_around,
"A-i" | "A-down" => shrink_selection,
"A-I" | "A-S-down" => select_all_children,
"A-p" | "A-left" => select_prev_sibling,
Expand Down
66 changes: 66 additions & 0 deletions helix-term/tests/test/commands/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,72 @@ async fn expand_shrink_selection() -> anyhow::Result<()> {
#[|Some(thing)]#,
Some(other_thing),
)
"##},
),
];

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

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
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O>",
indoc! {r##"
#[Some(|]#thing#()|)#
"##},
),
// shrinking restores previous selection
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O><A-i><A-i>",
indoc! {r##"
Some(#[thing|]#)
"##},
),
// multi range collision merges expand as normal, except with the
// original selection removed from the result
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O>",
indoc! {r##"
#[(
Some(|]#thing#(),
Some(|)#other_thing#(),
)|)#
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
];
Expand Down

0 comments on commit 9d6e303

Please sign in to comment.