From 03d8e9aee95ea6117d75a48bcac2e19241f6e667 Mon Sep 17 00:00:00 2001 From: Alexis Purslane Date: Wed, 28 Jan 2026 10:21:16 -0500 Subject: [PATCH] editor: Add select to {start,end} of larger syntax node commands (#47571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm used to having the Emacs mark-sexp command, which selects/extends the selection to the start or end of a sexp/larger syntax node, so when I found out the `move to {start,end} of larger syntax node` commands were added, these seemed like a natural addition; especially since most other commands in Zed seem to have movement and selection pairs, but those didn't really feel like they did. I did a really *lot* of tests (brainstormed ideas with GLM 4.7, then wrote the actual test case strings myself, then had it convert them to Rust, then made sure the tests made sense and passed), I'm not sure if it's too much or too little, but I looked at the PR that added the `move to` versions of these commands and it was also +~550 lines, so this seemed in the ballpark 😅 I factored out the core syntax node finding logic from those commands into a common function, so that both sets of commands could use it, just with different code for modifying the editor state wrapped around — mine just moves each selection's head, instead of totally resetting it. Release Notes: - Added commands for extending selections to syntax node boundaries. i.e. `editor: select to start of larger syntax node` and `editor: select to end of larger syntax node`. --- crates/editor/src/actions.rs | 4 + crates/editor/src/editor.rs | 159 +++++++---- crates/editor/src/editor_tests.rs | 443 ++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 6 + 4 files changed, 566 insertions(+), 46 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 8361a832a483bffd3d9daf0fe1d47061ad6cb5fc..9a32fe2230785ae900e8529f4e9d86184863e74b 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -733,6 +733,10 @@ actions!( SelectDown, /// Selects the enclosing symbol. SelectEnclosingSymbol, + /// Selects to the start of the next larger syntax node. + SelectToStartOfLargerSyntaxNode, + /// Selects to the end of the next larger syntax node. + SelectToEndOfLargerSyntaxNode, /// Selects the next larger syntax node. SelectLargerSyntaxNode, /// Selects the next syntax node sibling. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5db8b42933d49faa135bffee87b2fc0d1ba1bf06..6b691af3577ed49f677b7d1cc4c57db540c2718b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16557,6 +16557,62 @@ impl Editor { self.move_cursors_to_syntax_nodes(window, cx, true); } + fn find_syntax_node_boundary( + &self, + selection_pos: MultiBufferOffset, + move_to_end: bool, + display_map: &DisplaySnapshot, + buffer: &MultiBufferSnapshot, + ) -> MultiBufferOffset { + let old_range = selection_pos..selection_pos; + let mut new_pos = selection_pos; + let mut search_range = old_range; + while let Some((node, range)) = buffer.syntax_ancestor(search_range.clone()) { + search_range = range.clone(); + if !node.is_named() + || display_map.intersects_fold(range.start) + || display_map.intersects_fold(range.end) + // If cursor is already at the end of the syntax node, continue searching + || (move_to_end && range.end == selection_pos) + // If cursor is already at the start of the syntax node, continue searching + || (!move_to_end && range.start == selection_pos) + { + continue; + } + + // If we found a string_content node, find the largest parent that is still string_content + // Enables us to skip to the end of strings without taking multiple steps inside the string + let (_, final_range) = if node.kind() == "string_content" { + let mut current_node = node; + let mut current_range = range; + while let Some((parent, parent_range)) = + buffer.syntax_ancestor(current_range.clone()) + { + if parent.kind() == "string_content" { + current_node = parent; + current_range = parent_range; + } else { + break; + } + } + + (current_node, current_range) + } else { + (node, range) + }; + + new_pos = if move_to_end { + final_range.end + } else { + final_range.start + }; + + break; + } + + new_pos + } + fn move_cursors_to_syntax_nodes( &mut self, window: &mut Window, @@ -16585,52 +16641,12 @@ impl Editor { } let selection_pos = selection.head(); - let old_range = selection_pos..selection_pos; - - let mut new_pos = selection_pos; - let mut search_range = old_range; - while let Some((node, range)) = buffer.syntax_ancestor(search_range.clone()) { - search_range = range.clone(); - if !node.is_named() - || display_map.intersects_fold(range.start) - || display_map.intersects_fold(range.end) - // If cursor is already at the end of the syntax node, continue searching - || (move_to_end && range.end == selection_pos) - // If cursor is already at the start of the syntax node, continue searching - || (!move_to_end && range.start == selection_pos) - { - continue; - } - - // If we found a string_content node, find the largest parent that is still string_content - // Enables us to skip to the end of strings without taking multiple steps inside the string - let (_, final_range) = if node.kind() == "string_content" { - let mut current_node = node; - let mut current_range = range; - while let Some((parent, parent_range)) = - buffer.syntax_ancestor(current_range.clone()) - { - if parent.kind() == "string_content" { - current_node = parent; - current_range = parent_range; - } else { - break; - } - } - - (current_node, current_range) - } else { - (node, range) - }; - - new_pos = if move_to_end { - final_range.end - } else { - final_range.start - }; - - break; - } + let new_pos = self.find_syntax_node_boundary( + selection_pos, + move_to_end, + &display_map, + &buffer, + ); any_cursor_moved |= new_pos != selection_pos; @@ -16652,6 +16668,57 @@ impl Editor { any_cursor_moved } + pub fn select_to_start_of_larger_syntax_node( + &mut self, + _: &SelectToStartOfLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.select_to_syntax_nodes(window, cx, false); + } + + pub fn select_to_end_of_larger_syntax_node( + &mut self, + _: &SelectToEndOfLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.select_to_syntax_nodes(window, cx, true); + } + + fn select_to_syntax_nodes( + &mut self, + window: &mut Window, + cx: &mut Context, + move_to_end: bool, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections = self.selections.all::(&display_map); + + let new_selections = old_selections + .iter() + .map(|selection| { + let new_pos = self.find_syntax_node_boundary( + selection.head(), + move_to_end, + &display_map, + &buffer, + ); + + let mut new_selection = selection.clone(); + new_selection.set_head(new_pos, SelectionGoal::None); + new_selection + }) + .collect::>(); + + self.change_selections(Default::default(), window, cx, |s| { + s.select(new_selections); + }); + } + 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 fabac3c0271bdb6fbeaeccfd58451b7ff4ed9a66..6a5242d30d53a4408b126a5cb9c35521bf7203f1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32038,3 +32038,446 @@ async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_string ); }); } + +#[gpui::test] +async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + // Test Group 1.1: Cursor in String - First Jump (Select to End) + let text = r#"let msg = "foo bar baz";"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx); + }); + + // Test Group 1.2: Cursor in String - Second Jump (Select to End) + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx); + }); + + // Test Group 1.3: Cursor in String - Third Jump (Select to End) + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx); + }); + + // Test Group 1.4: Cursor in String - First Jump (Select to Start) + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx); + }); + + // Test Group 1.5: Cursor in String - Second Jump (Select to Start) + editor.update_in(cx, |editor, window, cx| { + editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx); + }); + + // Test Group 1.6: Cursor in String - Third Jump (Select to Start) + editor.update_in(cx, |editor, window, cx| { + editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx); + }); + + // Test Group 2.1: Let Statement Progression (Select to End) + let text = r#" +fn main() { + let x = "hello"; +} +"# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let xˇ = "hello"; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r##" + fn main() { + let x« = "hello";ˇ» + } + "##}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x« = "hello"; + }ˇ» + "#}, + cx, + ); + }); + + // Test Group 2.2a: From Inside String Content Node To String Content Boundary + let text = r#"let x = "hello";"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx); + }); + + // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx); + }); + + // Test Group 3.1: Create Selection from Cursor (Select to End) + let text = r#"let x = "hello world";"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx); + }); + + // Test Group 3.2: Extend Existing Selection (Select to End) + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx); + }); + + // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes + let text = r#"let x = "hello"; let y = 42;"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + // Cursor inside string content + DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12), + // Cursor at let statement semicolon + DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18), + // Cursor inside integer literal + DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26), + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx); + }); + + // Test Group 4.2: Multiple Cursors on Separate Lines + let text = r#" +let x = "hello"; +let y = 42; +"# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12), + DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9), + ]); + }); + }); + + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + let x = "helˇlo"; + let y = 4ˇ2; + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + let x = "hel«loˇ»"; + let y = 4«2ˇ»; + "#}, + cx, + ); + }); + + // Test Group 5.1: Nested Function Calls + let text = r#"let result = foo(bar("arg"));"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx); + }); + + // Test Group 6.1: Block Comments + let text = r#"let x = /* multi + line + comment */;"# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" +let x = /* multiˇ +line +comment */;"#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" +let x = /* multi« +line +comment */ˇ»;"#}, + cx, + ); + }); + + // Test Group 6.2: Array/Vector Literals + let text = r#"let arr = [1, 2, 3];"#.unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx); + }); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index dc37965779cc70ed8177c720b38dbc530c4ab6b6..a62a645f0916cd0cc899ece65b59eadb8033bbdb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -416,6 +416,12 @@ impl EditorElement { register_action(editor, window, Editor::select_smaller_syntax_node); register_action(editor, window, Editor::select_next_syntax_node); register_action(editor, window, Editor::select_prev_syntax_node); + register_action( + editor, + window, + Editor::select_to_start_of_larger_syntax_node, + ); + register_action(editor, window, Editor::select_to_end_of_larger_syntax_node); register_action(editor, window, Editor::unwrap_syntax_node); register_action(editor, window, Editor::move_to_start_of_larger_syntax_node); register_action(editor, window, Editor::move_to_end_of_larger_syntax_node);