From 81058ee1721e2668f472cd0924ae17c0d27c0c7f Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Wed, 4 Jun 2025 17:48:20 +0200 Subject: [PATCH] Make `alt-left` and `alt-right` skip punctuation like VSCode (#31977) Closes https://github.com/zed-industries/zed/discussions/25526 Follow up of #29872 Release Notes: - Make `alt-left` and `alt-right` skip punctuation on Mac OS to respect the Mac default behaviour. When pressing alt-left and the first character is a punctuation character like a dot, this character should be skipped. For example: `hello.|` goes to `|hello.` This change makes the editor feels much snappier, it now follows the same behaviour as VSCode and any other Mac OS native application. @ConradIrwin --- crates/editor/src/editor_tests.rs | 12 +++--- crates/editor/src/movement.rs | 67 +++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5ecd3e32e6fd0ba3f68196fe22a616c10d4db11e..4ba5e55fab28ae3ba21782d52fe643a0de1b0f72 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1912,19 +1912,19 @@ 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); + 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_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); @@ -1942,7 +1942,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); assert_selection_ranges( - "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", + "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}", editor, cx, ); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index c6720d40ff77e006e7e1447b7016f5a366effd4f..a750efe98ab31ad6ec03ea39f580b3823f4b5eec 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -264,7 +264,18 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa 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, |left, right| { + // Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello. + if is_first_iteration + && classifier.is_punctuation(right) + && !classifier.is_punctuation(left) + { + is_first_iteration = false; + return false; + } + is_first_iteration = false; + (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' }) @@ -305,8 +316,18 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> 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, |left, right| { + // Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello| + if is_first_iteration + && classifier.is_punctuation(left) + && !classifier.is_punctuation(right) + { + is_first_iteration = false; + return false; + } + is_first_iteration = false; + (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left)) || right == '\n' }) @@ -782,10 +803,15 @@ mod tests { fn assert(marked_text: &str, cx: &mut gpui::App) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - assert_eq!( - previous_word_start(&snapshot, display_points[1]), - display_points[0] - ); + 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); } assert("\nˇ ˇlorem", cx); @@ -796,12 +822,17 @@ mod tests { 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("ˇlorem_ˇipsum", cx); assert(" ˇdefγˇ", cx); assert(" ˇbcΔˇ", cx); - assert(" abˇ——ˇcd", cx); + // Test punctuation skipping behavior + assert("ˇhello.ˇ", cx); + assert("helloˇ...ˇ", cx); + assert("helloˇ.---..ˇtest", cx); + assert("test ˇ.--ˇtest", cx); + assert("oneˇ,;:!?ˇtwo", cx); } #[gpui::test] @@ -955,10 +986,15 @@ mod tests { fn assert(marked_text: &str, cx: &mut gpui::App) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - assert_eq!( - next_word_end(&snapshot, display_points[0]), - display_points[1] - ); + 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); @@ -967,11 +1003,18 @@ mod tests { 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("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]