Skip to content

Commit d83b4d8

Browse files
committed
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.
1 parent 7c8cde9 commit d83b4d8

File tree

3 files changed

+173
-0
lines changed

3 files changed

+173
-0
lines changed

helix-term/src/commands.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ impl MappableCommand {
451451
select_prev_sibling, "Select previous sibling the in syntax tree",
452452
select_all_siblings, "Select all siblings of the current node",
453453
select_all_children, "Select all children of the current node",
454+
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
454455
jump_forward, "Jump forward on jumplist",
455456
jump_backward, "Jump backward on jumplist",
456457
save_selection, "Save current selection to jumplist",
@@ -4876,6 +4877,8 @@ fn reverse_selection_contents(cx: &mut Context) {
48764877
// tree sitter node selection
48774878

48784879
const EXPAND_KEY: &str = "expand";
4880+
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
4881+
const PARENTS_KEY: &str = "parents";
48794882

48804883
fn expand_selection(cx: &mut Context) {
48814884
let motion = |editor: &mut Editor| {
@@ -4919,6 +4922,33 @@ fn shrink_selection(cx: &mut Context) {
49194922
if let Some(prev_selection) = prev_expansions.pop() {
49204923
// allow shrinking the selection only if current selection contains the previous object selection
49214924
doc.set_selection_clear(view.id, prev_selection, false);
4925+
4926+
// Do a corresponding pop of the parents from `expand_selection_around`
4927+
doc.view_data_mut(view.id)
4928+
.object_selections
4929+
.entry(PARENTS_KEY)
4930+
.and_modify(|parents| {
4931+
parents.pop();
4932+
});
4933+
4934+
// need to do this again because borrowing
4935+
let prev_expansions = doc
4936+
.view_data_mut(view.id)
4937+
.object_selections
4938+
.entry(EXPAND_KEY)
4939+
.or_default();
4940+
4941+
// if we've emptied out the previous expansions, then clear out the
4942+
// base history as well so it doesn't get used again erroneously
4943+
if prev_expansions.is_empty() {
4944+
doc.view_data_mut(view.id)
4945+
.object_selections
4946+
.entry(EXPAND_AROUND_BASE_KEY)
4947+
.and_modify(|base| {
4948+
base.clear();
4949+
});
4950+
}
4951+
49224952
return;
49234953
}
49244954

@@ -4933,6 +4963,81 @@ fn shrink_selection(cx: &mut Context) {
49334963
cx.editor.apply_motion(motion);
49344964
}
49354965

4966+
fn expand_selection_around(cx: &mut Context) {
4967+
let motion = |editor: &mut Editor| {
4968+
let (view, doc) = current!(editor);
4969+
4970+
if doc.syntax().is_some() {
4971+
// [NOTE] we do this pop and push dance because if we don't take
4972+
// ownership of the objects, then we require multiple
4973+
// mutable references to the view's object selections
4974+
let mut parents_selection = doc
4975+
.view_data_mut(view.id)
4976+
.object_selections
4977+
.entry(PARENTS_KEY)
4978+
.or_default()
4979+
.pop();
4980+
4981+
let mut base_selection = doc
4982+
.view_data_mut(view.id)
4983+
.object_selections
4984+
.entry(EXPAND_AROUND_BASE_KEY)
4985+
.or_default()
4986+
.pop();
4987+
4988+
let current_selection = doc.selection(view.id).clone();
4989+
4990+
if parents_selection.is_none() || base_selection.is_none() {
4991+
parents_selection = Some(current_selection.clone());
4992+
base_selection = Some(current_selection.clone());
4993+
}
4994+
4995+
let text = doc.text().slice(..);
4996+
let syntax = doc.syntax().unwrap();
4997+
4998+
let outside_selection =
4999+
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
5000+
5001+
let target_selection = match outside_selection
5002+
.clone()
5003+
.without(&base_selection.clone().unwrap())
5004+
{
5005+
Some(sel) => sel,
5006+
None => outside_selection.clone(),
5007+
};
5008+
5009+
// check if selection is different from the last one
5010+
if target_selection != current_selection {
5011+
// save current selection so it can be restored using shrink_selection
5012+
doc.view_data_mut(view.id)
5013+
.object_selections
5014+
.entry(EXPAND_KEY)
5015+
.or_default()
5016+
.push(current_selection);
5017+
5018+
doc.set_selection_clear(view.id, target_selection, false);
5019+
}
5020+
5021+
let parents = doc
5022+
.view_data_mut(view.id)
5023+
.object_selections
5024+
.entry(PARENTS_KEY)
5025+
.or_default();
5026+
5027+
parents.push(parents_selection.unwrap());
5028+
parents.push(outside_selection);
5029+
5030+
doc.view_data_mut(view.id)
5031+
.object_selections
5032+
.entry(EXPAND_AROUND_BASE_KEY)
5033+
.or_default()
5034+
.push(base_selection.unwrap());
5035+
}
5036+
};
5037+
5038+
cx.editor.apply_motion(motion);
5039+
}
5040+
49365041
fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: F)
49375042
where
49385043
F: Fn(&helix_core::Syntax, RopeSlice, Selection) -> Selection + 'static,
@@ -4947,6 +5052,7 @@ where
49475052
doc.set_selection(view.id, selection);
49485053
}
49495054
};
5055+
49505056
cx.editor.apply_motion(motion);
49515057
}
49525058

helix-term/src/keymap/default.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
8686
";" => collapse_selection,
8787
"A-;" => flip_selections,
8888
"A-o" | "A-up" => expand_selection,
89+
"A-O" => expand_selection_around,
8990
"A-i" | "A-down" => shrink_selection,
9091
"A-I" | "A-S-down" => select_all_children,
9192
"A-p" | "A-left" => select_prev_sibling,

helix-term/tests/test/commands/movement.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,72 @@ async fn expand_shrink_selection() -> anyhow::Result<()> {
10671067
#[|Some(thing)]#,
10681068
Some(other_thing),
10691069
)
1070+
1071+
"##},
1072+
),
1073+
];
1074+
1075+
for test in tests {
1076+
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
1077+
}
1078+
1079+
Ok(())
1080+
}
1081+
1082+
#[tokio::test(flavor = "multi_thread")]
1083+
async fn expand_selection_around() -> anyhow::Result<()> {
1084+
let tests = vec![
1085+
// single cursor stays single cursor, first goes to end of current
1086+
// node, then parent
1087+
(
1088+
indoc! {r##"
1089+
Some(#[thing|]#)
1090+
"##},
1091+
"<A-O><A-O>",
1092+
indoc! {r##"
1093+
#[Some(|]#thing#()|)#
1094+
"##},
1095+
),
1096+
// shrinking restores previous selection
1097+
(
1098+
indoc! {r##"
1099+
Some(#[thing|]#)
1100+
"##},
1101+
"<A-O><A-O><A-i><A-i>",
1102+
indoc! {r##"
1103+
Some(#[thing|]#)
1104+
"##},
1105+
),
1106+
// multi range collision merges expand as normal, except with the
1107+
// original selection removed from the result
1108+
(
1109+
indoc! {r##"
1110+
(
1111+
Some(#[thing|]#),
1112+
Some(#(other_thing|)#),
1113+
)
1114+
"##},
1115+
"<A-O><A-O><A-O>",
1116+
indoc! {r##"
1117+
#[(
1118+
Some(|]#thing#(),
1119+
Some(|)#other_thing#(),
1120+
)|)#
1121+
"##},
1122+
),
1123+
(
1124+
indoc! {r##"
1125+
(
1126+
Some(#[thing|]#),
1127+
Some(#(other_thing|)#),
1128+
)
1129+
"##},
1130+
"<A-O><A-O><A-O><A-i><A-i><A-i>",
1131+
indoc! {r##"
1132+
(
1133+
Some(#[thing|]#),
1134+
Some(#(other_thing|)#),
1135+
)
10701136
"##},
10711137
),
10721138
];

0 commit comments

Comments
 (0)