vim: Support counts for `H` and `L` motions (#7149)

Vishal Bhavsar created

Release Notes:

- Added support for counts to `H` and `L` motions
([#4941](https://github.com/zed-industries/zed/issues/4941)).

Change summary

crates/vim/src/motion.rs                     | 59 +++++++++++++++++++--
crates/vim/test_data/test_window_bottom.json |  4 +
crates/vim/test_data/test_window_top.json    |  4 +
3 files changed, 60 insertions(+), 7 deletions(-)

Detailed changes

crates/vim/src/motion.rs šŸ”—

@@ -513,9 +513,9 @@ impl Motion {
             StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
             EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
             GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
-            WindowTop => window_top(map, point, &text_layout_details),
+            WindowTop => window_top(map, point, &text_layout_details, times - 1),
             WindowMiddle => window_middle(map, point, &text_layout_details),
-            WindowBottom => window_bottom(map, point, &text_layout_details),
+            WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -1044,11 +1044,24 @@ fn window_top(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     text_layout_details: &TextLayoutDetails,
+    times: usize,
 ) -> (DisplayPoint, SelectionGoal) {
     let first_visible_line = text_layout_details.anchor.to_display_point(map);
-    let new_col = point.column().min(map.line_len(first_visible_line.row()));
-    let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
-    (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
+
+    if let Some(visible_rows) = text_layout_details.visible_rows {
+        let bottom_row = first_visible_line.row() + visible_rows as u32;
+        let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row);
+        let new_col = point.column().min(map.line_len(first_visible_line.row()));
+
+        let new_point = DisplayPoint::new(new_row, new_col);
+        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
+    } else {
+        let new_row = first_visible_line.row() + (times as u32);
+        let new_col = point.column().min(map.line_len(first_visible_line.row()));
+
+        let new_point = DisplayPoint::new(new_row, new_col);
+        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
+    }
 }
 
 fn window_middle(
@@ -1072,13 +1085,19 @@ fn window_bottom(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     text_layout_details: &TextLayoutDetails,
+    times: usize,
 ) -> (DisplayPoint, SelectionGoal) {
     if let Some(visible_rows) = text_layout_details.visible_rows {
         let first_visible_line = text_layout_details.anchor.to_display_point(map);
         let bottom_row = first_visible_line.row() + (visible_rows) as u32;
         let bottom_row_capped = bottom_row.min(map.max_buffer_row());
-        let new_col = point.column().min(map.line_len(bottom_row_capped));
-        let new_point = DisplayPoint::new(bottom_row_capped, new_col);
+        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
+            first_visible_line.row()
+        } else {
+            bottom_row_capped.saturating_sub(times as u32)
+        };
+        let new_col = point.column().min(map.line_len(new_row));
+        let new_point = DisplayPoint::new(new_row, new_col);
         (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
     } else {
         (point, SelectionGoal::None)
@@ -1302,6 +1321,18 @@ mod test {
           7 8 9
           "})
             .await;
+
+        cx.set_shared_state(indoc! {r"
+          1 2 3
+          4 5 ˇ6
+          7 8 9"})
+            .await;
+        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
+        cx.assert_shared_state(indoc! {r"
+          1 2 3
+          4 5 6
+          7 8 ˇ9"})
+            .await;
     }
 
     #[gpui::test]
@@ -1466,5 +1497,19 @@ mod test {
           7 8 9
           ˇ"})
             .await;
+
+        cx.set_shared_state(indoc! {r"
+          1 2 3
+          4 5 ˇ6
+          7 8 9
+          "})
+            .await;
+        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
+        cx.assert_shared_state(indoc! {r"
+          1 2 ˇ3
+          4 5 6
+          7 8 9
+          "})
+            .await;
     }
 }

crates/vim/test_data/test_window_bottom.json šŸ”—

@@ -13,3 +13,7 @@
 {"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}}
 {"Key":"shift-l"}
 {"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}}
+{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}}
+{"Key":"9"}
+{"Key":"shift-l"}
+{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}}

crates/vim/test_data/test_window_top.json šŸ”—

@@ -7,3 +7,7 @@
 {"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}}
 {"Key":"shift-h"}
 {"Get":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n","mode":"Normal"}}
+{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9"}}
+{"Key":"9"}
+{"Key":"shift-h"}
+{"Get":{"state":"1 2 3\n4 5 6\n7 8 ˇ9","mode":"Normal"}}