diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ddac2b32499fd2c3431aeb3388ac0ae7b8daa956..d17c12ad729a4c07e2ca345870f0f93b3e008617 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -638,6 +638,10 @@ actions!( MoveToEndOfExcerpt, /// Moves cursor to the end of the previous excerpt. MoveToEndOfPreviousExcerpt, + /// Moves cursor to the start of the next larger syntax node. + MoveToStartOfLargerSyntaxNode, + /// Moves cursor to the end of the next larger syntax node. + MoveToEndOfLargerSyntaxNode, /// Moves cursor up. MoveUp, /// Inserts a new line and moves cursor to it. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3c2b91c7f65a38c02dfcb9eee322aa2a8ff9a65..6ec753791daf8f7d440d71a9452a034118618c11 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16375,6 +16375,119 @@ impl Editor { } } + pub fn move_to_start_of_larger_syntax_node( + &mut self, + _: &MoveToStartOfLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.move_cursors_to_syntax_nodes(window, cx, false); + } + + pub fn move_to_end_of_larger_syntax_node( + &mut self, + _: &MoveToEndOfLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.move_cursors_to_syntax_nodes(window, cx, true); + } + + fn move_cursors_to_syntax_nodes( + &mut self, + window: &mut Window, + cx: &mut Context, + move_to_end: bool, + ) -> bool { + let old_selections: Box<[_]> = self + .selections + .all::(&self.display_snapshot(cx)) + .into(); + if old_selections.is_empty() { + return false; + } + + 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 mut any_cursor_moved = false; + let new_selections = old_selections + .iter() + .map(|selection| { + if !selection.is_empty() { + return selection.clone(); + } + + 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; + } + + any_cursor_moved |= new_pos != selection_pos; + + Selection { + id: selection.id, + start: new_pos, + end: new_pos, + goal: SelectionGoal::None, + reversed: false, + } + }) + .collect::>(); + + self.change_selections(Default::default(), window, cx, |s| { + s.select(new_selections); + }); + self.request_autoscroll(Autoscroll::newest(), cx); + + any_cursor_moved + } + 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 fe0169cabf0ebdc1246c23c0e2cff2b79feb9bf1..fe761d29c1a2683bf666f44ddd18c3535d4e875e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -30399,3 +30399,475 @@ async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) ); }); } + +#[gpui::test] +async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = r#" + fn main() { + let x = foo(1, 2); + } + "# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, 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; + + // Test case 1: Move to end of syntax nodes + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(ˇ1, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1ˇ, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2)ˇ; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2);ˇ + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2); + }ˇ + "#}, + cx, + ); + }); + + // Test case 2: Move to start of syntax nodes + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2ˇ); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = fooˇ(1, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = ˇfoo(1, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + ˇlet x = foo(1, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() ˇ{ + let x = foo(1, 2); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + ˇfn main() { + let x = foo(1, 2); + } + "#}, + cx, + ); + }); +} + +#[gpui::test] +async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = r#" + fn main() { + let x = foo(1, 2); + let y = bar(3, 4); + } + "# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, 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; + + // Test case 1: Move to end of syntax nodes with two cursors + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20), + DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20), + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2ˇ); + let y = bar(3, 4ˇ); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2)ˇ; + let y = bar(3, 4)ˇ; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2);ˇ + let y = bar(3, 4);ˇ + } + "#}, + cx, + ); + }); + + // Test case 2: Move to start of syntax nodes with two cursors + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19), + DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19), + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, ˇ2); + let y = bar(3, ˇ4); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = fooˇ(1, 2); + let y = barˇ(3, 4); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = ˇfoo(1, 2); + let y = ˇbar(3, 4); + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + ˇlet x = foo(1, 2); + ˇlet y = bar(3, 4); + } + "#}, + cx, + ); + }); +} + +#[gpui::test] +async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = r#" + fn main() { + let x = foo(1, 2); + let msg = "hello world"; + } + "# + .unindent(); + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, 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; + + // Test case 1: With existing selection, move_to_end keeps selection + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = «foo(1, 2)ˇ»; + let msg = "hello world"; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = «foo(1, 2)ˇ»; + let msg = "hello world"; + } + "#}, + cx, + ); + }); + + // Test case 2: Move to end within a string + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2); + let msg = "ˇhello world"; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2); + let msg = "hello worldˇ"; + } + "#}, + cx, + ); + }); + + // Test case 3: Move to start within a string + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21) + ]); + }); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2); + let msg = "hello ˇworld"; + } + "#}, + cx, + ); + }); + editor.update_in(cx, |editor, window, cx| { + editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx); + }); + editor.update(cx, |editor, cx| { + assert_text_with_selections( + editor, + indoc! {r#" + fn main() { + let x = foo(1, 2); + let msg = "ˇhello world"; + } + "#}, + cx, + ); + }); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b865370437caf2e873a056419ba5734c3ca11332..c6c9b6a30045b7c7acb667c1386b7f93dd776edb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -382,6 +382,8 @@ impl EditorElement { register_action(editor, window, Editor::select_next_syntax_node); register_action(editor, window, Editor::select_prev_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); register_action(editor, window, Editor::select_enclosing_symbol); register_action(editor, window, Editor::move_to_enclosing_bracket); register_action(editor, window, Editor::undo_selection);