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
This commit is contained in:
Tommy D. Rossi 2025-06-04 17:48:20 +02:00 committed by GitHub
parent 89743117c6
commit 81058ee172
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 19 deletions

View file

@ -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); assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, 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); 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); 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_next_word_end(&MoveToNextWordEnd, window, 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); 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);
@ -1942,7 +1942,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges( 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, editor,
cx, cx,
); );

View file

@ -264,7 +264,18 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point); 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| { 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)) (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
|| left == '\n' || 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 { pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point); let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let mut is_first_iteration = true;
find_boundary(map, point, FindRange::MultiLine, |left, right| { 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)) (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left))
|| right == '\n' || right == '\n'
}) })
@ -782,10 +803,15 @@ mod tests {
fn assert(marked_text: &str, cx: &mut gpui::App) { fn assert(marked_text: &str, cx: &mut gpui::App) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( let actual = previous_word_start(&snapshot, display_points[1]);
previous_word_start(&snapshot, display_points[1]), let expected = display_points[0];
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); assert("\nˇ ˇlorem", cx);
@ -796,12 +822,17 @@ mod tests {
assert("\nlorem\nˇ ˇipsum", cx); assert("\nlorem\nˇ ˇipsum", cx);
assert("\n\nˇ\nˇ", 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("loremˇ-#$@ˇipsum", cx);
assert("ˇlorem_ˇipsum", cx); assert("ˇlorem_ˇipsum", cx);
assert(" ˇdefγˇ", cx); assert(" ˇdefγˇ", cx);
assert(" ˇbcΔˇ", 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] #[gpui::test]
@ -955,10 +986,15 @@ mod tests {
fn assert(marked_text: &str, cx: &mut gpui::App) { fn assert(marked_text: &str, cx: &mut gpui::App) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( let actual = next_word_end(&snapshot, display_points[0]);
next_word_end(&snapshot, display_points[0]), let expected = display_points[1];
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("\nˇ loremˇ", cx);
@ -967,11 +1003,18 @@ mod tests {
assert(" loremˇ ˇ\nipsum\n", cx); assert(" loremˇ ˇ\nipsum\n", cx);
assert("\nˇ\nˇ\n\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("loremˇ#$@-ˇipsum", cx);
assert("loremˇ_ipsumˇ", cx); assert("loremˇ_ipsumˇ", cx);
assert(" ˇbcΔˇ", cx); assert(" ˇbcΔˇ", cx);
assert(" abˇ——ˇcd", 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] #[gpui::test]