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);