From b4a441f12fad28ec472deade760025b00c1e8671 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Aug 2025 06:52:22 +0200 Subject: [PATCH] Add UnwrapSyntaxNode action (#31421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remake of #8967 > Hey there, > > I have started relying on this action, that I've also put into VSCode as [an extension](https://github.com/Gregoor/soy). On some level I don't know how people code (cope?) without it: > > Release Notes: > > * Added UnwrapSyntaxNode action > > https://github.com/zed-industries/zed/assets/4051932/d74c98c0-96d8-4075-9b63-cea55bea42f6 > > Since I had to put it into Zed anyway to make it my daily driver, I thought I'd also check here if there's an interest in shipping it by default (that would ofc also personally make my life better, not having to maintain my personal fork and all). > > If there is interest, I'd be happy to make any changes to make this more mergeable. Two TODOs on my mind are: > > * unwrap multiple into single (e.g. `fn(≤a≥, b)` to `fn(≤a≥)`) > * multi-cursor > * syntax awareness, i.e. only unwrap if it does not break syntax (I added [a coarse version of that for my VSC extension](https://github.com/Gregoor/soy/blob/main/src/actions/unwrap.ts#L29)) > > Somewhat off-topic: I was happy to see that you're [also](https://github.com/Gregoor/soy/blob/main/src/actions/unwrap.test.ts) using rare special chars in test code to denote cursor positions. Release Notes: - Added UnwrapSyntaxNode action --------- Co-authored-by: Peter Tripp --- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 75 +++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 32 +++++++++++++ crates/editor/src/element.rs | 1 + 4 files changed, 109 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3a3a57ca6481d932887256b4ba7070264b56fe9c..39433b3c279e101f47ad4b2eed4d180f82a38997 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -745,5 +745,6 @@ actions!( UniqueLinesCaseInsensitive, /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, + UnwrapSyntaxNode ] ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 156fda1b376a81a05a82ba97a997bbb5ba20f2e7..73a81bea19471be2800ffdab91caab482e0011eb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14711,6 +14711,81 @@ impl Editor { } } + pub fn unwrap_syntax_node( + &mut self, + _: &UnwrapSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections: Box<[_]> = self.selections.all::(cx).into(); + + let edits = old_selections + .iter() + // only consider the first selection for now + .take(1) + .map(|selection| { + // Only requires two branches once if-let-chains stabilize (#53667) + let selection_range = if !selection.is_empty() { + selection.range() + } else if let Some((_, ancestor_range)) = + buffer.syntax_ancestor(selection.start..selection.end) + { + match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + } + } else { + selection.range() + }; + + let mut new_range = selection_range.clone(); + while let Some((_, ancestor_range)) = buffer.syntax_ancestor(new_range.clone()) { + new_range = match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + }; + if new_range.start < selection_range.start + || new_range.end > selection_range.end + { + break; + } + } + + (selection, selection_range, new_range) + }) + .collect::>(); + + self.transact(window, cx, |editor, window, cx| { + for (_, child, parent) in &edits { + let text = buffer.text_for_range(child.clone()).collect::(); + editor.replace_text_in_range(Some(parent.clone()), &text, window, cx); + } + + editor.change_selections( + SelectionEffects::scroll(Autoscroll::fit()), + window, + cx, + |s| { + s.select( + edits + .iter() + .map(|(s, old, new)| Selection { + id: s.id, + start: new.start, + end: new.start + old.len(), + goal: SelectionGoal::None, + reversed: s.reversed, + }) + .collect(), + ); + }, + ); + }); + } + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { if !EditorSettings::get_global(cx).gutter.runnables { self.clear_tasks(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1cb356573320775810d214139c00676e9ee5435b..b31963c9c8c694acebf072e05f693f36a81185af 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7969,6 +7969,38 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte }); } +#[gpui::test] +async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &r#" + use mod1::mod2::{«mod3ˇ», mod4}; + "# + .unindent(), + ); + cx.update_editor(|editor, window, cx| { + editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); + }); + cx.assert_editor_state( + &r#" + use mod1::mod2::«mod3ˇ»; + "# + .unindent(), + ); +} + #[gpui::test] async fn test_fold_function_bodies(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e1647215bca28ddfe6af42b719b37ff81c394c5c..17a43f9640ba670e4f2873133ac92ebbbadaf452 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -357,6 +357,7 @@ impl EditorElement { register_action(editor, window, Editor::toggle_comments); register_action(editor, window, Editor::select_larger_syntax_node); register_action(editor, window, Editor::select_smaller_syntax_node); + register_action(editor, window, Editor::unwrap_syntax_node); register_action(editor, window, Editor::select_enclosing_symbol); register_action(editor, window, Editor::move_to_enclosing_bracket); register_action(editor, window, Editor::undo_selection);