Stop unnecessary repeat cursor movements in Vim mode (#7641)

Colin Cai created

Fixes: #7605

When repeating some cursor movements in Vim mode (e.g. `99999999 w`),
Zed tries to repeat the movement that many times, even if further
actions don't have any effect. This causes Zed to hang.

This commit makes those movements like other actions (like moving the
cursor left/right), stopping the repeat movement if a boundary of the
text is reached/the cursor can't move anymore.

Release Notes:

- Fixed [#7605](https://github.com/zed-industries/zed/issues/7605).

Change summary

crates/vim/src/motion.rs | 60 ++++++++++++++++++++++++++++++++---------
1 file changed, 46 insertions(+), 14 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -617,6 +617,9 @@ fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Display
 fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
     for _ in 0..times {
         point = movement::left(map, point);
+        if point.is_zero() {
+            break;
+        }
     }
     point
 }
@@ -624,6 +627,9 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
 fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
     for _ in 0..times {
         point = wrapping_right(map, point);
+        if point == map.max_point() {
+            break;
+        }
     }
     point
 }
@@ -768,7 +774,7 @@ pub(crate) fn next_word_start(
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
         let mut crossed_newline = false;
-        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
+        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
             let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
             let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
             let at_newline = right == '\n';
@@ -779,7 +785,11 @@ pub(crate) fn next_word_start(
 
             crossed_newline |= at_newline;
             found
-        })
+        });
+        if point == new_point {
+            break;
+        }
+        point = new_point;
     }
     point
 }
@@ -792,21 +802,30 @@ fn next_word_end(
 ) -> DisplayPoint {
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
-        if point.column() < map.line_len(point.row()) {
-            *point.column_mut() += 1;
-        } else if point.row() < map.max_buffer_row() {
-            *point.row_mut() += 1;
-            *point.column_mut() = 0;
+        let mut new_point = point;
+        if new_point.column() < map.line_len(new_point.row()) {
+            *new_point.column_mut() += 1;
+        } else if new_point.row() < map.max_buffer_row() {
+            *new_point.row_mut() += 1;
+            *new_point.column_mut() = 0;
         }
 
-        point =
-            movement::find_boundary_exclusive(map, point, FindRange::MultiLine, |left, right| {
+        let new_point = movement::find_boundary_exclusive(
+            map,
+            new_point,
+            FindRange::MultiLine,
+            |left, right| {
                 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
                 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 
                 left_kind != right_kind && left_kind != CharKind::Whitespace
-            });
-        point = map.clip_point(point, Bias::Left);
+            },
+        );
+        let new_point = map.clip_point(new_point, Bias::Left);
+        if point == new_point {
+            break;
+        }
+        point = new_point;
     }
     point
 }
@@ -821,13 +840,17 @@ fn previous_word_start(
     for _ in 0..times {
         // This works even though find_preceding_boundary is called for every character in the line containing
         // cursor because the newline is checked only once.
-        point =
+        let new_point =
             movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
                 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
                 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 
                 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
             });
+        if point == new_point {
+            break;
+        }
+        point = new_point;
     }
     point
 }
@@ -967,10 +990,14 @@ fn find_forward(
 
     for _ in 0..times {
         found = false;
-        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
+        let new_to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
             found = right == target;
             found
         });
+        if to == new_to {
+            break;
+        }
+        to = new_to;
     }
 
     if found {
@@ -995,7 +1022,12 @@ fn find_backward(
     let mut to = from;
 
     for _ in 0..times {
-        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
+        let new_to =
+            find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
+        if to == new_to {
+            break;
+        }
+        to = new_to;
     }
 
     if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {