diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f4b29f75ddb..0a9400c568e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2886,44 +2886,44 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx); + + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); - editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); + assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); - - editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); editor.move_right(&MoveRight, window, cx); editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); assert_selection_ranges( - "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}", + "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", editor, cx, ); editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); assert_selection_ranges( - "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}", + "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", editor, cx, ); editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); assert_selection_ranges( - "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}", + "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", editor, cx, ); diff --git a/crates/editor/src/input.rs b/crates/editor/src/input.rs index b44b1bf6106..14d3cae720c 100644 --- a/crates/editor/src/input.rs +++ b/crates/editor/src/input.rs @@ -988,7 +988,7 @@ impl Editor { s.move_with(&mut |map, selection| { if selection.is_empty() { let mut cursor = if action.ignore_newlines { - movement::previous_word_start(map, selection.head()) + movement::previous_word_start(map, selection.head(), true) } else { movement::previous_word_start_or_newline(map, selection.head()) }; @@ -1053,7 +1053,7 @@ impl Editor { s.move_with(&mut |map, selection| { if selection.is_empty() { let mut cursor = if action.ignore_newlines { - movement::next_word_end(map, selection.head()) + movement::next_word_end(map, selection.head(), true) } else { movement::next_word_end_or_newline(map, selection.head()) }; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 92b1a30cf22..5384c9c4056 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -263,14 +263,18 @@ pub fn line_end( /// Returns a position of the previous word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). -pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { +pub fn previous_word_start( + map: &DisplaySnapshot, + point: DisplayPoint, + skip_punctuation: bool, +) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { - // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello. - if is_first_iteration + if skip_punctuation + && is_first_iteration && classifier.is_punctuation(right) && !classifier.is_punctuation(left) && left != '\n' @@ -438,13 +442,17 @@ pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> /// Returns a position of the next word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). -pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { +pub fn next_word_end( + map: &DisplaySnapshot, + point: DisplayPoint, + skip_punctuation: bool, +) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { - // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello| - if is_first_iteration + if skip_punctuation + && is_first_iteration && classifier.is_punctuation(left) && !classifier.is_punctuation(right) && right != '\n' @@ -1037,42 +1045,127 @@ mod tests { use settings::SettingsStore; use util::post_inc; + #[derive(Clone, Copy, Debug)] + enum WordMovement { + PreviousStart, + NextEnd, + } + #[gpui::test] - fn test_previous_word_start(cx: &mut gpui::App) { + fn test_word_movement(cx: &mut gpui::App) { init_test(cx); - fn assert(marked_text: &str, cx: &mut gpui::App) { - let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - let actual = previous_word_start(&snapshot, display_points[1]); - let expected = display_points[0]; - if actual != expected { - eprintln!( - "previous_word_start mismatch for '{}': actual={:?}, expected={:?}", - marked_text, actual, expected - ); - } - assert_eq!(actual, expected); - } + let cases = [ + (WordMovement::PreviousStart, " ˇlorˇem"), + (WordMovement::PreviousStart, "\nlorem\nˇ ˇipsum"), + (WordMovement::PreviousStart, "\n\nˇ\nˇ"), + (WordMovement::PreviousStart, "ˇlorem_ˇipsum"), + (WordMovement::PreviousStart, " ˇbcΔˇ"), + (WordMovement::PreviousStart, "foo ˇaˇ bar"), + (WordMovement::PreviousStart, "foo ˇ..ˇ bar"), + (WordMovement::PreviousStart, "wordˇ.ˇ"), + (WordMovement::PreviousStart, "wordˇ...ˇ"), + (WordMovement::PreviousStart, "wordˇ,;:!?ˇ"), + (WordMovement::PreviousStart, "wordˇ()[]{}ˇ"), + (WordMovement::PreviousStart, "wordˇ+-=*/&|^~ˇ"), + (WordMovement::PreviousStart, "wordˇ\"'`ˇ"), + (WordMovement::PreviousStart, "wordˇ—…。ˇ"), + (WordMovement::PreviousStart, "foo ˇ.ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ...ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ()[]{}ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ+-=*/&|^~ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ\"'`ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ—…。ˇ bar"), + (WordMovement::PreviousStart, "foo ˇ@ˇbar"), + (WordMovement::PreviousStart, "foo ˇ..ˇ.bar"), + (WordMovement::PreviousStart, "foo @ˇbarˇ baz"), + (WordMovement::PreviousStart, "foo @ˇbˇar"), + (WordMovement::PreviousStart, "foo ..ˇbarˇ baz"), + (WordMovement::PreviousStart, ".ˇhelloˇ"), + (WordMovement::PreviousStart, "@ˇwordˇ"), + (WordMovement::PreviousStart, "*ˇConnectorˇ"), + (WordMovement::PreviousStart, "\"ˇwordˇ\""), + (WordMovement::PreviousStart, "\"expected ˇresultˇ\""), + (WordMovement::PreviousStart, "\"ˇexpected ˇresult\""), + (WordMovement::PreviousStart, "\\\"ˇunexpectedˇ\\\""), + (WordMovement::PreviousStart, "'ˇwordˇ'"), + (WordMovement::PreviousStart, "`ˇcodeˇ`"), + (WordMovement::PreviousStart, "(ˇargsˇ)"), + (WordMovement::PreviousStart, "[ˇitemˇ]"), + (WordMovement::PreviousStart, "{ˇvalueˇ}"), + (WordMovement::PreviousStart, "a-b-ˇcˇ"), + (WordMovement::PreviousStart, "a.b.ˇcˇ"), + (WordMovement::PreviousStart, "foo.ˇbarˇ"), + (WordMovement::PreviousStart, "a@ˇbˇ"), + (WordMovement::PreviousStart, "left::ˇrightˇ"), + (WordMovement::PreviousStart, "foo/ˇbarˇ"), + (WordMovement::PreviousStart, "foo->ˇbarˇ"), + (WordMovement::PreviousStart, "foo&&ˇbarˇ"), + (WordMovement::PreviousStart, "func(ˇargsˇ)"), + (WordMovement::PreviousStart, "map[string]ˇboolˇ"), + (WordMovement::PreviousStart, "if (foo.ˇbarˇ)"), + (WordMovement::PreviousStart, "[2001:4860:4860::8888ˇ] ˇ"), + (WordMovement::NextEnd, " lorˇemˇ"), + (WordMovement::NextEnd, "loremˇ ipsumˇ"), + (WordMovement::NextEnd, "\nˇ\nˇ\n\n"), + (WordMovement::NextEnd, "loremˇ_ipsumˇ"), + (WordMovement::NextEnd, " ˇbcΔˇ"), + (WordMovement::NextEnd, "foo ˇaˇ bar"), + (WordMovement::NextEnd, "foo ˇ..ˇ bar"), + (WordMovement::NextEnd, "wordˇ.ˇ"), + (WordMovement::NextEnd, "wordˇ...ˇ"), + (WordMovement::NextEnd, "wordˇ,;:!?ˇ"), + (WordMovement::NextEnd, "wordˇ()[]{}ˇ"), + (WordMovement::NextEnd, "wordˇ+-=*/&|^~ˇ"), + (WordMovement::NextEnd, "wordˇ\"'`ˇ"), + (WordMovement::NextEnd, "wordˇ—…。ˇ"), + (WordMovement::NextEnd, "foo ˇ.ˇ bar"), + (WordMovement::NextEnd, "foo ˇ...ˇ bar"), + (WordMovement::NextEnd, "foo ˇ()[]{}ˇ bar"), + (WordMovement::NextEnd, "foo ˇ+-=*/&|^~ˇ bar"), + (WordMovement::NextEnd, "foo ˇ\"'`ˇ bar"), + (WordMovement::NextEnd, "foo ˇ—…。ˇ bar"), + (WordMovement::NextEnd, "ˇ.ˇhello"), + (WordMovement::NextEnd, "ˇ@ˇword"), + (WordMovement::NextEnd, "ˇ*ˇConnector"), + (WordMovement::NextEnd, "ˇ\"ˇword\""), + (WordMovement::NextEnd, "\"ˇexpectedˇ result\""), + (WordMovement::NextEnd, "ˇ\\\"ˇunexpected\\\""), + (WordMovement::NextEnd, "ˇ'ˇword'"), + (WordMovement::NextEnd, "ˇ`ˇcode`"), + (WordMovement::NextEnd, "ˇ(ˇargs)"), + (WordMovement::NextEnd, "ˇ[ˇitem]"), + (WordMovement::NextEnd, "ˇ{ˇvalue}"), + (WordMovement::NextEnd, "display_pointsˇ[ˇ0]"), + (WordMovement::NextEnd, "fooˇ.ˇ bar"), + (WordMovement::NextEnd, "foo ˇ@ˇbar baz"), + (WordMovement::NextEnd, "foo.ˇ..ˇbar"), + (WordMovement::NextEnd, "aˇ-ˇb-c"), + (WordMovement::NextEnd, "aˇ.ˇb.c"), + (WordMovement::NextEnd, "aˇ@ˇb"), + (WordMovement::NextEnd, "leftˇ::ˇright"), + (WordMovement::NextEnd, "fooˇ/ˇbar"), + (WordMovement::NextEnd, "fooˇ->ˇbar"), + (WordMovement::NextEnd, "fooˇ&&ˇbar"), + (WordMovement::NextEnd, "funcˇ(ˇargs)"), + (WordMovement::NextEnd, "map[stringˇ]ˇbool"), + (WordMovement::NextEnd, "if ˇ(ˇfoo.bar)"), + (WordMovement::NextEnd, "[2001:4860:4860::8888ˇ]ˇ "), + ]; - assert("\nˇ ˇlorem", cx); - assert("ˇ\nˇ lorem", cx); - assert(" ˇloremˇ", cx); - assert("ˇ ˇlorem", cx); - assert(" ˇlorˇem", cx); - assert("\nlorem\nˇ ˇipsum", cx); - assert("\n\nˇ\nˇ", cx); - assert(" ˇlorem ˇipsum", cx); - assert("ˇlorem-ˇipsum", cx); - assert("loremˇ-#$@ˇipsum", cx); - assert("ˇlorem_ˇipsum", cx); - assert(" ˇdefγˇ", cx); - assert(" ˇbcΔˇ", cx); - // Test punctuation skipping behavior - assert("ˇhello.ˇ", cx); - assert("helloˇ...ˇ", cx); - assert("helloˇ.---..ˇtest", cx); - assert("test ˇ.--ˇtest", cx); - assert("oneˇ,;:!?ˇtwo", cx); + for (movement, marked_text) in cases { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!(display_points.len(), 2, "{marked_text:?}"); + let (start, expected) = match movement { + WordMovement::PreviousStart => (display_points[1], display_points[0]), + WordMovement::NextEnd => (display_points[0], display_points[1]), + }; + let actual = match movement { + WordMovement::PreviousStart => previous_word_start(&snapshot, start, false), + WordMovement::NextEnd => next_word_end(&snapshot, start, false), + }; + assert_eq!(actual, expected, "{movement:?} failed for {marked_text:?}"); + } } #[gpui::test] @@ -1221,43 +1314,6 @@ mod tests { ); } - #[gpui::test] - fn test_next_word_end(cx: &mut gpui::App) { - init_test(cx); - - fn assert(marked_text: &str, cx: &mut gpui::App) { - let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - let actual = next_word_end(&snapshot, display_points[0]); - let expected = display_points[1]; - if actual != expected { - eprintln!( - "next_word_end mismatch for '{}': actual={:?}, expected={:?}", - marked_text, actual, expected - ); - } - assert_eq!(actual, expected); - } - - assert("\nˇ loremˇ", cx); - assert(" ˇloremˇ", cx); - assert(" lorˇemˇ", cx); - assert(" loremˇ ˇ\nipsum\n", cx); - assert("\nˇ\nˇ\n\n", cx); - assert("loremˇ ipsumˇ ", cx); - assert("loremˇ-ipsumˇ", cx); - assert("loremˇ#$@-ˇipsum", cx); - assert("loremˇ_ipsumˇ", cx); - assert(" ˇbcΔˇ", cx); - assert(" abˇ——ˇcd", cx); - // Test punctuation skipping behavior - assert("ˇ.helloˇ", cx); - assert("display_pointsˇ[0ˇ]", cx); - assert("ˇ...ˇhello", cx); - assert("helloˇ.---..ˇtest", cx); - assert("testˇ.--ˇ test", cx); - assert("oneˇ,;:!?ˇtwo", cx); - } - #[gpui::test] fn test_next_subword_end(cx: &mut gpui::App) { init_test(cx); @@ -1546,7 +1602,7 @@ mod tests { // Ctrl+Right from before fold ("hello |⋯ world") should skip past the fold. // Cursor at column 6 = start of fold. let before_fold = DisplayPoint::new(DisplayRow(0), 6); - let after_fold = next_word_end(&snapshot, before_fold); + let after_fold = next_word_end(&snapshot, before_fold, false); // Should land past the fold, not get stuck at fold start. assert!( after_fold > before_fold, @@ -1557,7 +1613,7 @@ mod tests { // Ctrl+Right from "hello" should jump past "hello" to the fold or past it. let at_start = DisplayPoint::new(DisplayRow(0), 0); - let after_hello = next_word_end(&snapshot, at_start); + let after_hello = next_word_end(&snapshot, at_start, false); assert_eq!( after_hello, DisplayPoint::new(DisplayRow(0), 5), @@ -1567,7 +1623,7 @@ mod tests { // Ctrl+Left from after fold should move to before the fold. // "⋯" ends at column 9. " world" starts at 9. Column 15 = end of "world". let after_world = DisplayPoint::new(DisplayRow(0), 15); - let before_world = previous_word_start(&snapshot, after_world); + let before_world = previous_word_start(&snapshot, after_world, false); assert_eq!( before_world, DisplayPoint::new(DisplayRow(0), 10), @@ -1576,7 +1632,7 @@ mod tests { // Ctrl+Left from start of "world" should land before fold. let start_of_world = DisplayPoint::new(DisplayRow(0), 10); - let landed = previous_word_start(&snapshot, start_of_world); + let landed = previous_word_start(&snapshot, start_of_world, false); // The fold acts as a word, so we should land at the fold start (column 6). assert_eq!( landed, diff --git a/crates/editor/src/navigation.rs b/crates/editor/src/navigation.rs index 55a70109953..fef44102f68 100644 --- a/crates/editor/src/navigation.rs +++ b/crates/editor/src/navigation.rs @@ -385,7 +385,7 @@ impl Editor { self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { ( - movement::previous_word_start(map, head), + movement::previous_word_start(map, head, false), SelectionGoal::None, ) }); @@ -417,7 +417,7 @@ impl Editor { self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { ( - movement::previous_word_start(map, head), + movement::previous_word_start(map, head, false), SelectionGoal::None, ) }); @@ -448,7 +448,10 @@ impl Editor { ) { self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(&mut |map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) + ( + movement::next_word_end(map, head, false), + SelectionGoal::None, + ) }); }) } @@ -474,7 +477,10 @@ impl Editor { ) { self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(&mut |map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) + ( + movement::next_word_end(map, head, false), + SelectionGoal::None, + ) }); }) }