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.
pull/6198/head
Skyler Hawthorne 2 years ago
parent da261724d1
commit 73b4511713

@ -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",
@ -5033,6 +5034,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| {
@ -5076,6 +5079,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;
}
@ -5090,6 +5120,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,
@ -5104,6 +5209,7 @@ where
doc.set_selection(view.id, selection);
}
};
cx.editor.apply_motion(motion);
}

@ -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,

@ -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|)#),
)
"##},
),
];

Loading…
Cancel
Save