Skip to content

Commit bdb3bbd

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 96e9808 commit bdb3bbd

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

helix-term/src/commands.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ impl MappableCommand {
381381
rotate_selection_contents_backward, "Rotate selections contents backward",
382382
expand_selection, "Expand selection to parent syntax node",
383383
shrink_selection, "Shrink selection to previously expanded syntax node",
384+
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
384385
select_next_sibling, "Select next sibling in syntax tree",
385386
select_prev_sibling, "Select previous sibling in syntax tree",
386387
jump_forward, "Jump forward on jumplist",
@@ -4295,6 +4296,8 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
42954296
// tree sitter node selection
42964297

42974298
const EXPAND_KEY: &str = "expand";
4299+
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
4300+
const PARENTS_KEY: &str = "parents";
42984301

42994302
fn expand_selection(cx: &mut Context) {
43004303
let motion = |editor: &mut Editor| {
@@ -4330,6 +4333,27 @@ fn shrink_selection(cx: &mut Context) {
43304333
if let Some(prev_selection) = prev_expansions.pop() {
43314334
// allow shrinking the selection only if current selection contains the previous object selection
43324335
doc.set_selection_clear(view, prev_selection, false);
4336+
4337+
// Do a corresponding pop of the parents from `expand_selection_around`
4338+
view.object_selections
4339+
.entry(PARENTS_KEY)
4340+
.and_modify(|parents| {
4341+
parents.pop();
4342+
});
4343+
4344+
// need to do this again because borrowing
4345+
let prev_expansions = view.object_selections.entry(EXPAND_KEY).or_default();
4346+
4347+
// if we've emptied out the previous expansions, then clear out the
4348+
// base history as well so it doesn't get used again erroneously
4349+
if prev_expansions.is_empty() {
4350+
view.object_selections
4351+
.entry(EXPAND_AROUND_BASE_KEY)
4352+
.and_modify(|base| {
4353+
base.clear();
4354+
});
4355+
}
4356+
43334357
return;
43344358
}
43354359

@@ -4345,6 +4369,68 @@ fn shrink_selection(cx: &mut Context) {
43454369
cx.editor.last_motion = Some(Motion(Box::new(motion)));
43464370
}
43474371

4372+
fn expand_selection_around(cx: &mut Context) {
4373+
let motion = |editor: &mut Editor| {
4374+
let (view, doc) = current!(editor);
4375+
4376+
if let Some(syntax) = doc.syntax() {
4377+
let text = doc.text().slice(..);
4378+
let current_selection = doc.selection(view.id);
4379+
4380+
// [NOTE] we do this pop and push dance because if we don't take
4381+
// ownership of the objects, then we require multiple
4382+
// mutable references to the view's object selections
4383+
let mut parents_selection =
4384+
view.object_selections.entry(PARENTS_KEY).or_default().pop();
4385+
let mut base_selection = view
4386+
.object_selections
4387+
.entry(EXPAND_AROUND_BASE_KEY)
4388+
.or_default()
4389+
.pop();
4390+
4391+
if parents_selection.is_none() || base_selection.is_none() {
4392+
parents_selection = Some(current_selection.clone());
4393+
base_selection = Some(current_selection.clone());
4394+
}
4395+
4396+
let outside_selection =
4397+
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
4398+
4399+
let target_selection = match outside_selection
4400+
.clone()
4401+
.without(&base_selection.clone().unwrap())
4402+
{
4403+
Some(sel) => sel,
4404+
None => outside_selection.clone(),
4405+
};
4406+
4407+
// check if selection is different from the last one
4408+
if target_selection != *current_selection {
4409+
// save current selection so it can be restored using shrink_selection
4410+
view.object_selections
4411+
.entry(EXPAND_KEY)
4412+
.or_default()
4413+
.push(current_selection.clone());
4414+
4415+
doc.set_selection_clear(view, target_selection, false);
4416+
}
4417+
4418+
let parents = view.object_selections.entry(PARENTS_KEY).or_default();
4419+
4420+
parents.push(parents_selection.unwrap());
4421+
parents.push(outside_selection);
4422+
4423+
view.object_selections
4424+
.entry(EXPAND_AROUND_BASE_KEY)
4425+
.or_default()
4426+
.push(base_selection.unwrap());
4427+
}
4428+
};
4429+
4430+
motion(cx.editor);
4431+
cx.editor.last_motion = Some(Motion(Box::new(motion)));
4432+
}
4433+
43484434
fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
43494435
where
43504436
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)