vim: implement <space> in normal mode (#7011)

Thorsten Ball created

This fixes #6815 by implementing `<space>` in normal mode in Vim. Turns
out that `<space>` behaves like a reverse `<backspace>` (which we
already had): it goes to the right and, if at end of line, to the next
line.

That means I had to touch `movement::right`, which is used in a few
places, but it's documentation said that it would go to the next line,
which it did *not*. So I changed the behaviour.

But I would love another pair of eyes on this, because I don't want to
break non-Vim behaviour.

Release Notes:

- Added support for `<space>` in Vim normal mode: `<space>` goes to the
right and to next line if at end of line.
([#6815](https://github.com/zed-industries/zed/issues/6815)).

Change summary

assets/keymaps/vim.json       |  1 +
crates/editor/src/movement.rs |  7 +++----
crates/vim/src/motion.rs      | 25 +++++++++++++++++++++++++
3 files changed, 29 insertions(+), 4 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -31,6 +31,7 @@
       "up": "vim::Up",
       "l": "vim::Right",
       "right": "vim::Right",
+      "space": "vim::Space",
       "$": "vim::EndOfLine",
       "^": "vim::FirstNonWhitespace",
       "_": "vim::StartOfLineDownward",

crates/editor/src/movement.rs 🔗

@@ -50,11 +50,10 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa
     map.clip_point(point, Bias::Left)
 }
 
-/// Returns a column to the right of the current point, wrapping
-/// to the next line if that point is at the end of line.
+/// Returns a column to the right of the current point, doing nothing
+// if that point is at the end of the line.
 pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
-    let max_column = map.line_len(point.row());
-    if point.column() < max_column {
+    if point.column() < map.line_len(point.row()) {
         *point.column_mut() += 1;
     } else if point.row() < map.max_point().row() {
         *point.row_mut() += 1;

crates/vim/src/motion.rs 🔗

@@ -23,6 +23,7 @@ pub enum Motion {
     Down { display_lines: bool },
     Up { display_lines: bool },
     Right,
+    Space,
     NextWordStart { ignore_punctuation: bool },
     NextWordEnd { ignore_punctuation: bool },
     PreviousWordStart { ignore_punctuation: bool },
@@ -124,6 +125,7 @@ actions!(
         Left,
         Backspace,
         Right,
+        Space,
         CurrentLine,
         StartOfParagraph,
         EndOfParagraph,
@@ -163,6 +165,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
         )
     });
     workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
+    workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
     workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
         motion(
             Motion::FirstNonWhitespace {
@@ -306,6 +309,7 @@ impl Motion {
             | Left
             | Backspace
             | Right
+            | Space
             | StartOfLine { .. }
             | EndOfLineDownward
             | GoToColumn
@@ -332,6 +336,7 @@ impl Motion {
             | Left
             | Backspace
             | Right
+            | Space
             | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
@@ -370,6 +375,7 @@ impl Motion {
             Left
             | Backspace
             | Right
+            | Space
             | StartOfLine { .. }
             | StartOfLineDownward
             | StartOfParagraph
@@ -412,6 +418,7 @@ impl Motion {
                 display_lines: true,
             } => up_display(map, point, goal, times, &text_layout_details),
             Right => (right(map, point, times), SelectionGoal::None),
+            Space => (space(map, point, times), SelectionGoal::None),
             NextWordStart { ignore_punctuation } => (
                 next_word_start(map, point, *ignore_punctuation, times),
                 SelectionGoal::None,
@@ -614,6 +621,24 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
     point
 }
 
+fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+    for _ in 0..times {
+        point = wrapping_right(map, point);
+    }
+    point
+}
+
+fn wrapping_right(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;
+    } else if point.row() < map.max_point().row() {
+        *point.row_mut() += 1;
+        *point.column_mut() = 0;
+    }
+    point
+}
+
 pub(crate) fn start_of_relative_buffer_row(
     map: &DisplaySnapshot,
     point: DisplayPoint,