Skip to content

Commit 10edb7c

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 b56f8e7 commit 10edb7c

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
@@ -396,6 +396,7 @@ impl MappableCommand {
396396
rotate_selection_contents_backward, "Rotate selections contents backward",
397397
expand_selection, "Expand selection to parent syntax node",
398398
shrink_selection, "Shrink selection to previously expanded syntax node",
399+
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
399400
select_next_sibling, "Select next sibling in syntax tree",
400401
select_prev_sibling, "Select previous sibling in syntax tree",
401402
jump_forward, "Jump forward on jumplist",
@@ -4372,6 +4373,8 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
43724373
// tree sitter node selection
43734374

43744375
const EXPAND_KEY: &str = "expand";
4376+
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
4377+
const PARENTS_KEY: &str = "parents";
43754378

43764379
fn expand_selection(cx: &mut Context) {
43774380
let motion = |editor: &mut Editor| {
@@ -4416,6 +4419,33 @@ fn shrink_selection(cx: &mut Context) {
44164419
if let Some(prev_selection) = prev_expansions.pop() {
44174420
// allow shrinking the selection only if current selection contains the previous object selection
44184421
doc.set_selection_clear(view.id, prev_selection, false);
4422+
4423+
// Do a corresponding pop of the parents from `expand_selection_around`
4424+
doc.view_data_mut(view.id)
4425+
.object_selections
4426+
.entry(PARENTS_KEY)
4427+
.and_modify(|parents| {
4428+
parents.pop();
4429+
});
4430+
4431+
// need to do this again because borrowing
4432+
let prev_expansions = doc
4433+
.view_data_mut(view.id)
4434+
.object_selections
4435+
.entry(EXPAND_KEY)
4436+
.or_default();
4437+
4438+
// if we've emptied out the previous expansions, then clear out the
4439+
// base history as well so it doesn't get used again erroneously
4440+
if prev_expansions.is_empty() {
4441+
doc.view_data_mut(view.id)
4442+
.object_selections
4443+
.entry(EXPAND_AROUND_BASE_KEY)
4444+
.and_modify(|base| {
4445+
base.clear();
4446+
});
4447+
}
4448+
44194449
return;
44204450
}
44214451

@@ -4431,6 +4461,82 @@ fn shrink_selection(cx: &mut Context) {
44314461
cx.editor.last_motion = Some(Motion(Box::new(motion)));
44324462
}
44334463

4464+
fn expand_selection_around(cx: &mut Context) {
4465+
let motion = |editor: &mut Editor| {
4466+
let (view, doc) = current!(editor);
4467+
4468+
if doc.syntax().is_some() {
4469+
// [NOTE] we do this pop and push dance because if we don't take
4470+
// ownership of the objects, then we require multiple
4471+
// mutable references to the view's object selections
4472+
let mut parents_selection = doc
4473+
.view_data_mut(view.id)
4474+
.object_selections
4475+
.entry(PARENTS_KEY)
4476+
.or_default()
4477+
.pop();
4478+
4479+
let mut base_selection = doc
4480+
.view_data_mut(view.id)
4481+
.object_selections
4482+
.entry(EXPAND_AROUND_BASE_KEY)
4483+
.or_default()
4484+
.pop();
4485+
4486+
let current_selection = doc.selection(view.id).clone();
4487+
4488+
if parents_selection.is_none() || base_selection.is_none() {
4489+
parents_selection = Some(current_selection.clone());
4490+
base_selection = Some(current_selection.clone());
4491+
}
4492+
4493+
let text = doc.text().slice(..);
4494+
let syntax = doc.syntax().unwrap();
4495+
4496+
let outside_selection =
4497+
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
4498+
4499+
let target_selection = match outside_selection
4500+
.clone()
4501+
.without(&base_selection.clone().unwrap())
4502+
{
4503+
Some(sel) => sel,
4504+
None => outside_selection.clone(),
4505+
};
4506+
4507+
// check if selection is different from the last one
4508+
if target_selection != current_selection {
4509+
// save current selection so it can be restored using shrink_selection
4510+
doc.view_data_mut(view.id)
4511+
.object_selections
4512+
.entry(EXPAND_KEY)
4513+
.or_default()
4514+
.push(current_selection);
4515+
4516+
doc.set_selection_clear(view.id, target_selection, false);
4517+
}
4518+
4519+
let parents = doc
4520+
.view_data_mut(view.id)
4521+
.object_selections
4522+
.entry(PARENTS_KEY)
4523+
.or_default();
4524+
4525+
parents.push(parents_selection.unwrap());
4526+
parents.push(outside_selection);
4527+
4528+
doc.view_data_mut(view.id)
4529+
.object_selections
4530+
.entry(EXPAND_AROUND_BASE_KEY)
4531+
.or_default()
4532+
.push(base_selection.unwrap());
4533+
}
4534+
};
4535+
4536+
motion(cx.editor);
4537+
cx.editor.last_motion = Some(Motion(Box::new(motion)));
4538+
}
4539+
44344540
fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
44354541
where
44364542
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
@@ -84,6 +84,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
8484
";" => collapse_selection,
8585
"A-;" => flip_selections,
8686
"A-o" | "A-up" => expand_selection,
87+
"A-O" => expand_selection_around,
8788
"A-i" | "A-down" => shrink_selection,
8889
"A-p" | "A-left" => select_prev_sibling,
8990
"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)