From c2ddb017fd0606b1fe9fe5df18dccf450c4b3f85 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 9 Oct 2022 18:04:09 -0400 Subject: [PATCH] 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. --- helix-term/src/commands.rs | 86 ++++++++++++++++++++++ helix-term/src/keymap/default.rs | 1 + helix-term/tests/test/commands/movement.rs | 65 ++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d50b80398f38b..9e66fabe4aa88 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -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", @@ -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| { @@ -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; } @@ -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(cx: &mut Context, sibling_fn: &'static F) where F: Fn(Node) -> Option, diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 9bd002809d613..b1afbd97f03b2 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -84,6 +84,7 @@ pub fn default() -> HashMap { ";" => 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, diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 6aa231d82a1f8..c9b87c1bd2f9e 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -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|]#) + "##}), + "", + helpers::platform_line(indoc! {r##" + #[Some(|]#thing#()|)# + "##}), + ), + // shrinking restores previous selection + ( + helpers::platform_line(indoc! {r##" + Some(#[thing|]#) + "##}), + "", + 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|)#), + ) + "##}), + "", + helpers::platform_line(indoc! {r##" + #[( + Some(|]#thing#(), + Some(|)#other_thing#(), + )|)# + "##}), + ), + ( + helpers::platform_line(indoc! {r##" + ( + Some(#[thing|]#), + Some(#(other_thing|)#), + ) + "##}), + "", + 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(()) +}