Add multi selection support to UnwrapSyntaxNode (#35991)

Closes #35932
Closes #35933

I only intended to fix multi select in this, I accidentally drive-by
fixed the VIM issue as well. `replace_text_in_range` which I was using
before has two, to me unexpected, side-effects:
- it no-ops when input is disabled, which is the case in VIM's
Insert/Visual modes
- it takes the current selection into account, and does not just operate
on the given range (which I erroneously assumed before)

Now the code is using `buffer.edit` instead, which seems more lower
level, and does not have those side-effects. I was enthused to see that
it accepts a vec of edits, so I didn't have to calculate offsets for
following edits... until I also wanted to set selections, where I do
need to do it by hand. I'm still wondering if there is a simpler way to
do it, but for now it at least passes my muster

Release Notes:

- Added multiple selection support to UnwrapSyntaxNode action
- Fixed UnwrapSyntaxNode not working in VIM Insert/Visual modes
This commit is contained in:
Gregor 2025-08-19 00:01:46 +02:00 committed by GitHub
parent 9e0e233319
commit bb640c6a1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 52 deletions

View file

@ -14834,15 +14834,18 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let buffer = self.buffer.read(cx).snapshot(cx); let buffer = self.buffer.read(cx).snapshot(cx);
let old_selections: Box<[_]> = self.selections.all::<usize>(cx).into(); let selections = self
.selections
.all::<usize>(cx)
.into_iter()
// subtracting the offset requires sorting
.sorted_by_key(|i| i.start);
let edits = old_selections let full_edits = selections
.iter() .into_iter()
// only consider the first selection for now .filter_map(|selection| {
.take(1)
.map(|selection| {
// Only requires two branches once if-let-chains stabilize (#53667) // Only requires two branches once if-let-chains stabilize (#53667)
let selection_range = if !selection.is_empty() { let child = if !selection.is_empty() {
selection.range() selection.range()
} else if let Some((_, ancestor_range)) = } else if let Some((_, ancestor_range)) =
buffer.syntax_ancestor(selection.start..selection.end) buffer.syntax_ancestor(selection.start..selection.end)
@ -14855,48 +14858,52 @@ impl Editor {
selection.range() selection.range()
}; };
let mut new_range = selection_range.clone(); let mut parent = child.clone();
while let Some((_, ancestor_range)) = buffer.syntax_ancestor(new_range.clone()) { while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) {
new_range = match ancestor_range { parent = match ancestor_range {
MultiOrSingleBufferOffsetRange::Single(range) => range, MultiOrSingleBufferOffsetRange::Single(range) => range,
MultiOrSingleBufferOffsetRange::Multi(range) => range, MultiOrSingleBufferOffsetRange::Multi(range) => range,
}; };
if new_range.start < selection_range.start if parent.start < child.start || parent.end > child.end {
|| new_range.end > selection_range.end
{
break; break;
} }
} }
(selection, selection_range, new_range) if parent == child {
return None;
}
let text = buffer.text_for_range(child.clone()).collect::<String>();
Some((selection.id, parent, text))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.transact(window, cx, |editor, window, cx| { self.transact(window, cx, |this, window, cx| {
for (_, child, parent) in &edits { this.buffer.update(cx, |buffer, cx| {
let text = buffer.text_for_range(child.clone()).collect::<String>(); buffer.edit(
editor.replace_text_in_range(Some(parent.clone()), &text, window, cx); full_edits
} .iter()
.map(|(_, p, t)| (p.clone(), t.clone()))
editor.change_selections( .collect::<Vec<_>>(),
SelectionEffects::scroll(Autoscroll::fit()), None,
window, cx,
cx, );
|s| { });
s.select( this.change_selections(Default::default(), window, cx, |s| {
edits let mut offset = 0;
.iter() let mut selections = vec![];
.map(|(s, old, new)| Selection { for (id, parent, text) in full_edits {
id: s.id, let start = parent.start - offset;
start: new.start, offset += parent.len() - text.len();
end: new.start + old.len(), selections.push(Selection {
goal: SelectionGoal::None, id: id,
reversed: s.reversed, start,
}) end: start + text.len(),
.collect(), reversed: false,
); goal: Default::default(),
}, });
); }
s.select(selections);
});
}); });
} }

View file

@ -8015,7 +8015,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte
} }
#[gpui::test] #[gpui::test]
async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
@ -8029,21 +8029,12 @@ async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) {
buffer.set_language(Some(language), cx); buffer.set_language(Some(language), cx);
}); });
cx.set_state( cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
&r#"
use mod1::mod2::{«mod3ˇ», mod4};
"#
.unindent(),
);
cx.update_editor(|editor, window, cx| { cx.update_editor(|editor, window, cx| {
editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
}); });
cx.assert_editor_state(
&r#" cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
use mod1::mod2::«mod3ˇ»;
"#
.unindent(),
);
} }
#[gpui::test] #[gpui::test]