Skip to content

Commit 9bbac17

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 b1b1c42 commit 9bbac17

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",
@@ -4356,6 +4357,8 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
43564357
// tree sitter node selection
43574358

43584359
const EXPAND_KEY: &str = "expand";
4360+
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
4361+
const PARENTS_KEY: &str = "parents";
43594362

43604363
fn expand_selection(cx: &mut Context) {
43614364
let motion = |editor: &mut Editor| {
@@ -4400,6 +4403,33 @@ fn shrink_selection(cx: &mut Context) {
44004403
if let Some(prev_selection) = prev_expansions.pop() {
44014404
// allow shrinking the selection only if current selection contains the previous object selection
44024405
doc.set_selection_clear(view.id, prev_selection, false);
4406+
4407+
// Do a corresponding pop of the parents from `expand_selection_around`
4408+
doc.view_data_mut(view.id)
4409+
.object_selections
4410+
.entry(PARENTS_KEY)
4411+
.and_modify(|parents| {
4412+
parents.pop();
4413+
});
4414+
4415+
// need to do this again because borrowing
4416+
let prev_expansions = doc
4417+
.view_data_mut(view.id)
4418+
.object_selections
4419+
.entry(EXPAND_KEY)
4420+
.or_default();
4421+
4422+
// if we've emptied out the previous expansions, then clear out the
4423+
// base history as well so it doesn't get used again erroneously
4424+
if prev_expansions.is_empty() {
4425+
doc.view_data_mut(view.id)
4426+
.object_selections
4427+
.entry(EXPAND_AROUND_BASE_KEY)
4428+
.and_modify(|base| {
4429+
base.clear();
4430+
});
4431+
}
4432+
44034433
return;
44044434
}
44054435

@@ -4415,6 +4445,82 @@ fn shrink_selection(cx: &mut Context) {
44154445
cx.editor.last_motion = Some(Motion(Box::new(motion)));
44164446
}
44174447

4448+
fn expand_selection_around(cx: &mut Context) {
4449+
let motion = |editor: &mut Editor| {
4450+
let (view, doc) = current!(editor);
4451+
4452+
if doc.syntax().is_some() {
4453+
// [NOTE] we do this pop and push dance because if we don't take
4454+
// ownership of the objects, then we require multiple
4455+
// mutable references to the view's object selections
4456+
let mut parents_selection = doc
4457+
.view_data_mut(view.id)
4458+
.object_selections
4459+
.entry(PARENTS_KEY)
4460+
.or_default()
4461+
.pop();
4462+
4463+
let mut base_selection = doc
4464+
.view_data_mut(view.id)
4465+
.object_selections
4466+
.entry(EXPAND_AROUND_BASE_KEY)
4467+
.or_default()
4468+
.pop();
4469+
4470+
let current_selection = doc.selection(view.id).clone();
4471+
4472+
if parents_selection.is_none() || base_selection.is_none() {
4473+
parents_selection = Some(current_selection.clone());
4474+
base_selection = Some(current_selection.clone());
4475+
}
4476+
4477+
let text = doc.text().slice(..);
4478+
let syntax = doc.syntax().unwrap();
4479+
4480+
let outside_selection =
4481+
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
4482+
4483+
let target_selection = match outside_selection
4484+
.clone()
4485+
.without(&base_selection.clone().unwrap())
4486+
{
4487+
Some(sel) => sel,
4488+
None => outside_selection.clone(),
4489+
};
4490+
4491+
// check if selection is different from the last one
4492+
if target_selection != current_selection {
4493+
// save current selection so it can be restored using shrink_selection
4494+
doc.view_data_mut(view.id)
4495+
.object_selections
4496+
.entry(EXPAND_KEY)
4497+
.or_default()
4498+
.push(current_selection);
4499+
4500+
doc.set_selection_clear(view.id, target_selection, false);
4501+
}
4502+
4503+
let parents = doc
4504+
.view_data_mut(view.id)
4505+
.object_selections
4506+
.entry(PARENTS_KEY)
4507+
.or_default();
4508+
4509+
parents.push(parents_selection.unwrap());
4510+
parents.push(outside_selection);
4511+
4512+
doc.view_data_mut(view.id)
4513+
.object_selections
4514+
.entry(EXPAND_AROUND_BASE_KEY)
4515+
.or_default()
4516+
.push(base_selection.unwrap());
4517+
}
4518+
};
4519+
4520+
motion(cx.editor);
4521+
cx.editor.last_motion = Some(Motion(Box::new(motion)));
4522+
}
4523+
44184524
fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
44194525
where
44204526
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)