Unnamed repository; edit this file 'description' to name the repository.
feat(command): expand_selection_around
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.
Skyler Hawthorne 5 months ago
parent d445661 · commit 699391d
-rw-r--r--helix-term/src/commands.rs106
-rw-r--r--helix-term/src/keymap/default.rs1
-rw-r--r--helix-term/tests/test/commands/movement.rs66
3 files changed, 173 insertions, 0 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index aafe9619..48c9fc99 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -530,6 +530,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",
@@ -5448,6 +5449,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| {
@@ -5491,6 +5494,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;
}
@@ -5505,6 +5535,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,
@@ -5519,6 +5624,7 @@ where
doc.set_selection(view.id, selection);
}
};
+
cx.editor.apply_motion(motion);
}
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index 5bbbd3f4..45e1ad12 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -87,6 +87,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,
diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs
index dd5a3bee..684f1dca 100644
--- a/helix-term/tests/test/commands/movement.rs
+++ b/helix-term/tests/test/commands/movement.rs
@@ -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|)#),
+ )
"##},
),
];