Fix panic in vim increment (#50311) (cherry-pick to stable) (#50338)

zed-zippy[bot] and Conrad Irwin created

Cherry-pick of #50311 to stable

----
Fixes ZED-59V

Release Notes:

- vim: Fixed panic when incrementing a number preceded by a multibyte
character

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/vim/src/normal/increment.rs | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)

Detailed changes

crates/vim/src/normal/increment.rs šŸ”—

@@ -203,20 +203,25 @@ fn find_target(
     let start_offset = start.to_offset(snapshot);
     let end_offset = end.to_offset(snapshot);
 
-    let mut offset = start_offset;
     let mut first_char_is_num = snapshot
-        .chars_at(offset)
+        .chars_at(start_offset)
         .next()
         .map_or(false, |ch| ch.is_ascii_hexdigit());
     let mut pre_char = String::new();
 
-    let next_offset = offset
+    let next_offset = start_offset
         + snapshot
             .chars_at(start_offset)
             .next()
             .map_or(0, |ch| ch.len_utf8());
-    // Backward scan to find the start of the number, but stop at start_offset
+    // Backward scan to find the start of the number, but stop at start_offset.
+    // We track `offset` as the start position of the current character. Initialize
+    // to `next_offset` and decrement at the start of each iteration so that `offset`
+    // always lands on a valid character boundary (not in the middle of a multibyte char).
+    let mut offset = next_offset;
     for ch in snapshot.reversed_chars_at(next_offset) {
+        offset -= ch.len_utf8();
+
         // Search boundaries
         if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) {
             break;
@@ -238,7 +243,6 @@ fn find_target(
         }
 
         pre_char.insert(0, ch);
-        offset -= ch.len_utf8();
     }
 
     // The backward scan breaks on whitespace, including newlines. Without this
@@ -895,4 +899,15 @@ mod test {
             .await
             .assert_eq("# Title\n2. item\nˇ2. item\n3. item");
     }
+
+    #[gpui::test]
+    async fn test_increment_with_multibyte_characters(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Test cursor after a multibyte character - this would panic before the fix
+        // because the backward scan would land in the middle of the Korean character
+        cx.set_state("지ˇ1", Mode::Normal);
+        cx.simulate_keystrokes("ctrl-a");
+        cx.assert_state("지ˇ2", Mode::Normal);
+    }
 }