vim: Fix space forward bug with non-ASCII characters at EOL (#27860)

Closes https://github.com/zed-industries/zed/issues/27619

Fixes issue with right wrapped movement when a multi-byte character is
at the end of the line. This is done by grabbing the last character on
the current row and using that characters size to calculate the
`max_column` variable, which is used to decide if the next right
movement should move down the line or not.

We did notice a bit of code that could be an issue that we wanted to
call out.
[Here](https://github.com/zed-industries/zed/blob/main/crates/editor/src/display_map.rs#L1070)
inside of `clip_at_line_end` it also does a saturating_sub(1), assuming
a single byte character. We didn't run into any issues due to this line
but felt like a similar bug. We can apply a similar fix if wanted to
pose the question first.

Test case: Moving to next line when eol is a multi-byte character


https://github.com/user-attachments/assets/1021ab1f-f49d-4986-8f9a-8cfc7e5c91bc


Release Notes:

- Fixed issue in vim forward spacing when a multi-byte character is at
the eol

---------

Co-authored-by: KyleBarton <kjbarton4@gmail.com>
This commit is contained in:
Peter Finn 2025-04-01 11:21:41 -07:00 committed by GitHub
parent 64ef3ab09d
commit bda33ec436
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 52 additions and 9 deletions

View file

@ -1273,16 +1273,19 @@ fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize)
point
}
fn wrapping_right_single(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let max_column = map.line_len(point.row()).saturating_sub(1);
if point.column() < max_column {
*point.column_mut() += 1;
point = map.clip_point(point, Bias::Right);
} else if point.row() < map.max_point().row() {
*point.row_mut() += 1;
*point.column_mut() = 0;
fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let mut next_point = point;
*next_point.column_mut() += 1;
next_point = map.clip_point(next_point, Bias::Right);
if next_point == point {
if next_point.row() == map.max_point().row() {
next_point
} else {
DisplayPoint::new(next_point.row().next_row(), 0)
}
} else {
next_point
}
point
}
pub(crate) fn start_of_relative_buffer_row(
@ -3563,4 +3566,32 @@ mod test {
cx.simulate_shared_keystrokes("3 space").await;
cx.shared_state().await.assert_eq("πππˇππ");
}
#[gpui::test]
async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
ππππˇπ
πanotherline"})
.await;
cx.simulate_shared_keystrokes("4 space").await;
cx.shared_state().await.assert_eq(indoc! {"
πππππ
πanˇotherline"});
}
#[gpui::test]
async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
ππππ
πanˇotherline"})
.await;
cx.simulate_shared_keystrokes("4 backspace").await;
cx.shared_state().await.assert_eq(indoc! {"
πππˇπ
πanotherline"});
}
}

View file

@ -0,0 +1,4 @@
{"Put":{"state":"ππππ\nπanˇotherline"}}
{"Key":"4"}
{"Key":"backspace"}
{"Get":{"state":"πππˇπ\nπanotherline","mode":"Normal"}}

View file

@ -0,0 +1,4 @@
{"Put":{"state":"ππππˇπ\nπanotherline"}}
{"Key":"4"}
{"Key":"space"}
{"Get":{"state":"πππππ\nπanˇotherline","mode":"Normal"}}

View file

@ -0,0 +1,4 @@
{"Put":{"state":"aaaaˇaa\nanotherline"}}
{"Key":"4"}
{"Key":"space"}
{"Get":{"state":"aaaaaa\nanˇotherline","mode":"Normal"}}