Skip to content

Commit d1c987d

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 f175ee9 commit d1c987d

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

helix-term/src/commands.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ impl MappableCommand {
408408
rotate_selection_contents_backward, "Rotate selections contents backward",
409409
expand_selection, "Expand selection to parent syntax node",
410410
shrink_selection, "Shrink selection to previously expanded syntax node",
411+
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
411412
select_next_sibling, "Select next sibling in syntax tree",
412413
select_prev_sibling, "Select previous sibling in syntax tree",
413414
jump_forward, "Jump forward on jumplist",
@@ -4469,6 +4470,8 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
44694470
// tree sitter node selection
44704471

44714472
const EXPAND_KEY: &str = "expand";
4473+
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
4474+
const PARENTS_KEY: &str = "parents";
44724475

44734476
fn expand_selection(cx: &mut Context) {
44744477
let motion = |editor: &mut Editor| {
@@ -4513,6 +4516,33 @@ fn shrink_selection(cx: &mut Context) {
45134516
if let Some(prev_selection) = prev_expansions.pop() {
45144517
// allow shrinking the selection only if current selection contains the previous object selection
45154518
doc.set_selection_clear(view.id, prev_selection, false);
4519+
4520+
// Do a corresponding pop of the parents from `expand_selection_around`
4521+
doc.view_data_mut(view.id)
4522+
.object_selections
4523+
.entry(PARENTS_KEY)
4524+
.and_modify(|parents| {
4525+
parents.pop();
4526+
});
4527+
4528+
// need to do this again because borrowing
4529+
let prev_expansions = doc
4530+
.view_data_mut(view.id)
4531+
.object_selections
4532+
.entry(EXPAND_KEY)
4533+
.or_default();
4534+
4535+
// if we've emptied out the previous expansions, then clear out the
4536+
// base history as well so it doesn't get used again erroneously
4537+
if prev_expansions.is_empty() {
4538+
doc.view_data_mut(view.id)
4539+
.object_selections
4540+
.entry(EXPAND_AROUND_BASE_KEY)
4541+
.and_modify(|base| {
4542+
base.clear();
4543+
});
4544+
}
4545+
45164546
return;
45174547
}
45184548

@@ -4528,6 +4558,82 @@ fn shrink_selection(cx: &mut Context) {
45284558
cx.editor.last_motion = Some(Motion(Box::new(motion)));
45294559
}
45304560

4561+
fn expand_selection_around(cx: &mut Context) {
4562+
let motion = |editor: &mut Editor| {
4563+
let (view, doc) = current!(editor);
4564+
4565+
if doc.syntax().is_some() {
4566+
// [NOTE] we do this pop and push dance because if we don't take
4567+
// ownership of the objects, then we require multiple
4568+
// mutable references to the view's object selections
4569+
let mut parents_selection = doc
4570+
.view_data_mut(view.id)
4571+
.object_selections
4572+
.entry(PARENTS_KEY)
4573+
.or_default()
4574+
.pop();
4575+
4576+
let mut base_selection = doc
4577+
.view_data_mut(view.id)
4578+
.object_selections
4579+
.entry(EXPAND_AROUND_BASE_KEY)
4580+
.or_default()
4581+
.pop();
4582+
4583+
let current_selection = doc.selection(view.id).clone();
4584+
4585+
if parents_selection.is_none() || base_selection.is_none() {
4586+
parents_selection = Some(current_selection.clone());
4587+
base_selection = Some(current_selection.clone());
4588+
}
4589+
4590+
let text = doc.text().slice(..);
4591+
let syntax = doc.syntax().unwrap();
4592+
4593+
let outside_selection =
4594+
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
4595+
4596+
let target_selection = match outside_selection
4597+
.clone()
4598+
.without(&base_selection.clone().unwrap())
4599+
{
4600+
Some(sel) => sel,
4601+
None => outside_selection.clone(),
4602+
};
4603+
4604+
// check if selection is different from the last one
4605+
if target_selection != current_selection {
4606+
// save current selection so it can be restored using shrink_selection
4607+
doc.view_data_mut(view.id)
4608+
.object_selections
4609+
.entry(EXPAND_KEY)
4610+
.or_default()
4611+
.push(current_selection);
4612+
4613+
doc.set_selection_clear(view.id, target_selection, false);
4614+
}
4615+
4616+
let parents = doc
4617+
.view_data_mut(view.id)
4618+
.object_selections
4619+
.entry(PARENTS_KEY)
4620+
.or_default();
4621+
4622+
parents.push(parents_selection.unwrap());
4623+
parents.push(outside_selection);
4624+
4625+
doc.view_data_mut(view.id)
4626+
.object_selections
4627+
.entry(EXPAND_AROUND_BASE_KEY)
4628+
.or_default()
4629+
.push(base_selection.unwrap());
4630+
}
4631+
};
4632+
4633+
motion(cx.editor);
4634+
cx.editor.last_motion = Some(Motion(Box::new(motion)));
4635+
}
4636+
45314637
fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
45324638
where
45334639
F: Fn(Node) -> Option<Node>,

helix-term/src/keymap/default.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
8585
";" => collapse_selection,
8686
"A-;" => flip_selections,
8787
"A-o" | "A-up" => expand_selection,
88+
"A-O" => expand_selection_around,
8889
"A-i" | "A-down" => shrink_selection,
8990
"A-p" | "A-left" => select_prev_sibling,
9091
"A-n" | "A-right" => select_next_sibling,

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,68 @@ async fn expand_shrink_selection() -> anyhow::Result<()> {
129129

130130
Ok(())
131131
}
132+
133+
#[tokio::test(flavor = "multi_thread")]
134+
async fn expand_selection_around() -> anyhow::Result<()> {
135+
let tests = vec![
136+
// single cursor stays single cursor, first goes to end of current
137+
// node, then parent
138+
(
139+
helpers::platform_line(indoc! {r##"
140+
Some(#[thing|]#)
141+
"##}),
142+
"<A-O><A-O>",
143+
helpers::platform_line(indoc! {r##"
144+
#[Some(|]#thing#()|)#
145+
"##}),
146+
),
147+
// shrinking restores previous selection
148+
(
149+
helpers::platform_line(indoc! {r##"
150+
Some(#[thing|]#)
151+
"##}),
152+
"<A-O><A-O><A-i><A-i>",
153+
helpers::platform_line(indoc! {r##"
154+
Some(#[thing|]#)
155+
"##}),
156+
),
157+
// multi range collision merges expand as normal, except with the
158+
// original selection removed from the result
159+
(
160+
helpers::platform_line(indoc! {r##"
161+
(
162+
Some(#[thing|]#),
163+
Some(#(other_thing|)#),
164+
)
165+
"##}),
166+
"<A-O><A-O><A-O>",
167+
helpers::platform_line(indoc! {r##"
168+
#[(
169+
Some(|]#thing#(),
170+
Some(|)#other_thing#(),
171+
)|)#
172+
"##}),
173+
),
174+
(
175+
helpers::platform_line(indoc! {r##"
176+
(
177+
Some(#[thing|]#),
178+
Some(#(other_thing|)#),
179+
)
180+
"##}),
181+
"<A-O><A-O><A-O><A-i><A-i><A-i>",
182+
helpers::platform_line(indoc! {r##"
183+
(
184+
Some(#[thing|]#),
185+
Some(#(other_thing|)#),
186+
)
187+
"##}),
188+
),
189+
];
190+
191+
for test in tests {
192+
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
193+
}
194+
195+
Ok(())
196+
}

0 commit comments

Comments
 (0)