vim: Rename wrapping keybindings + document cursor wrapping (#25694)

Asqar Arslanov created

https://github.com/zed-industries/zed/pull/25663#issuecomment-2686095807

Renamed the `vim::Backspace` and `vim::Space` actions to
`vim::WrappingLeft` and `vim::WrappingRight` respectively. The old names
are still available, but they are marked as deprecated and users are
advised to use the new names.

Also added a paragraph to the docs describing how to enable wrapping
cursor navigation.

Change summary

assets/keymaps/vim.json         |  4 +-
crates/vim/src/motion.rs        | 47 +++++++++++++++++++++-------------
crates/vim/src/normal/change.rs |  2 
crates/vim/src/replace.rs       |  2 
docs/src/vim.md                 | 16 +++++++++++
5 files changed, 49 insertions(+), 22 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -6,7 +6,7 @@
       "a": ["vim::PushObject", { "around": true }],
       "left": "vim::Left",
       "h": "vim::Left",
-      "backspace": "vim::Backspace",
+      "backspace": "vim::WrappingLeft",
       "down": "vim::Down",
       "ctrl-j": "vim::Down",
       "j": "vim::Down",
@@ -20,7 +20,7 @@
       "k": "vim::Up",
       "right": "vim::Right",
       "l": "vim::Right",
-      "space": "vim::Space",
+      "space": "vim::WrappingRight",
       "end": "vim::EndOfLine",
       "$": "vim::EndOfLine",
       "^": "vim::FirstNonWhitespace",

crates/vim/src/motion.rs 🔗

@@ -6,7 +6,7 @@ use editor::{
     scroll::Autoscroll,
     Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
 };
-use gpui::{actions, impl_actions, px, Context, Window};
+use gpui::{action_with_deprecated_aliases, actions, impl_actions, px, Context, Window};
 use language::{CharKind, Point, Selection, SelectionGoal};
 use multi_buffer::MultiBufferRow;
 use schemars::JsonSchema;
@@ -24,7 +24,7 @@ use crate::{
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Motion {
     Left,
-    Backspace,
+    WrappingLeft,
     Down {
         display_lines: bool,
     },
@@ -32,7 +32,7 @@ pub enum Motion {
         display_lines: bool,
     },
     Right,
-    Space,
+    WrappingRight,
     NextWordStart {
         ignore_punctuation: bool,
     },
@@ -304,12 +304,19 @@ actions!(
     ]
 );
 
+action_with_deprecated_aliases!(vim, WrappingLeft, ["vim::Backspace"]);
+action_with_deprecated_aliases!(vim, WrappingRight, ["vim::Space"]);
+
 pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Left, window, cx| {
         vim.motion(Motion::Left, window, cx)
     });
+    Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
+        vim.motion(Motion::WrappingLeft, window, cx)
+    });
+    // Deprecated.
     Vim::action(editor, cx, |vim, _: &Backspace, window, cx| {
-        vim.motion(Motion::Backspace, window, cx)
+        vim.motion(Motion::WrappingLeft, window, cx)
     });
     Vim::action(editor, cx, |vim, action: &Down, window, cx| {
         vim.motion(
@@ -332,8 +339,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &Right, window, cx| {
         vim.motion(Motion::Right, window, cx)
     });
+    Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
+        vim.motion(Motion::WrappingRight, window, cx)
+    });
+    // Deprecated.
     Vim::action(editor, cx, |vim, _: &Space, window, cx| {
-        vim.motion(Motion::Space, window, cx)
+        vim.motion(Motion::WrappingRight, window, cx)
     });
     Vim::action(
         editor,
@@ -639,11 +650,11 @@ impl Motion {
             | UnmatchedBackward { .. }
             | FindForward { .. }
             | Left
-            | Backspace
+            | WrappingLeft
             | Right
             | SentenceBackward
             | SentenceForward
-            | Space
+            | WrappingRight
             | StartOfLine { .. }
             | EndOfLineDownward
             | GoToColumn
@@ -679,9 +690,9 @@ impl Motion {
             | FindForward { .. }
             | RepeatFind { .. }
             | Left
-            | Backspace
+            | WrappingLeft
             | Right
-            | Space
+            | WrappingRight
             | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
@@ -747,9 +758,9 @@ impl Motion {
             | NextLineStart
             | PreviousLineStart => true,
             Left
-            | Backspace
+            | WrappingLeft
             | Right
-            | Space
+            | WrappingRight
             | StartOfLine { .. }
             | StartOfLineDownward
             | StartOfParagraph
@@ -796,7 +807,7 @@ impl Motion {
         let infallible = self.infallible();
         let (new_point, goal) = match self {
             Left => (left(map, point, times), SelectionGoal::None),
-            Backspace => (backspace(map, point, times), SelectionGoal::None),
+            WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
             Down {
                 display_lines: false,
             } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
@@ -810,7 +821,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),
+            WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
             NextWordStart { ignore_punctuation } => (
                 next_word_start(map, point, *ignore_punctuation, times),
                 SelectionGoal::None,
@@ -1219,7 +1230,7 @@ impl Motion {
                 // DisplayPoint
 
                 if !inclusive
-                    && self != &Motion::Backspace
+                    && self != &Motion::WrappingLeft
                     && end_point.row > start_point.row
                     && end_point.column == 0
                 {
@@ -1274,7 +1285,7 @@ fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Display
     point
 }
 
-pub(crate) fn backspace(
+pub(crate) fn wrapping_left(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
     times: usize,
@@ -1288,9 +1299,9 @@ pub(crate) fn backspace(
     point
 }
 
-fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
     for _ in 0..times {
-        point = wrapping_right(map, point);
+        point = wrapping_right_single(map, point);
         if point == map.max_point() {
             break;
         }
@@ -1298,7 +1309,7 @@ fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Displa
     point
 }
 
-fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+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;

crates/vim/src/normal/change.rs 🔗

@@ -27,7 +27,7 @@ impl Vim {
             Motion::Left
                 | Motion::Right
                 | Motion::EndOfLine { .. }
-                | Motion::Backspace
+                | Motion::WrappingLeft
                 | Motion::StartOfLine { .. }
         );
         self.update_editor(window, cx, |vim, editor, window, cx| {

crates/vim/src/replace.rs 🔗

@@ -95,7 +95,7 @@ impl Vim {
                     .into_iter()
                     .filter_map(|selection| {
                         let end = selection.head();
-                        let start = motion::backspace(
+                        let start = motion::wrapping_left(
                             &map,
                             end.to_display_point(&map),
                             maybe_times.unwrap_or(1),

docs/src/vim.md 🔗

@@ -408,6 +408,22 @@ Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), b
 }
 ```
 
+In non-modal text editors, cursor navigation typically wraps when moving past line ends. Zed, however, handles this behavior exactly like Vim by default: the cursor stops at line boundaries. If you prefer your cursor to wrap between lines, override these keybindings:
+
+```json
+// In VimScript, this would look like this:
+// set whichwrap+=<,>,[,],h,l
+{
+  "context": "VimControl && !menu",
+  "bindings": {
+    "left": "vim::WrappingLeft",
+    "right": "vim::WrappingRight",
+    "h": "vim::WrappingLeft",
+    "l": "vim::WrappingRight"
+  }
+}
+```
+
 The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for quick navigation to any two-character sequence in your text. You can enable it by adding the following keybindings to your keymap. By default, the `s` key is mapped to `vim::Substitute`. Adding these bindings will override that behavior, so ensure this change aligns with your workflow preferences.
 
 ```json