vim: Add ctrl-y/e in insert mode (#36017)

Conrad Irwin created

Closes #17292

Release Notes:

- vim: Added ctrl-y/ctrl-e in insert mode to copy the next character
from the line above or below

Change summary

assets/keymaps/vim.json                      |  4 +
crates/vim/src/insert.rs                     | 46 +++++++++++++++++++++
crates/vim/test_data/test_insert_ctrl_y.json |  5 ++
3 files changed, 54 insertions(+), 1 deletion(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -333,10 +333,14 @@
       "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
       "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
       "ctrl-x ctrl-z": "editor::Cancel",
+      "ctrl-x ctrl-e": "vim::LineDown",
+      "ctrl-x ctrl-y": "vim::LineUp",
       "ctrl-w": "editor::DeleteToPreviousWordStart",
       "ctrl-u": "editor::DeleteToBeginningOfLine",
       "ctrl-t": "vim::Indent",
       "ctrl-d": "vim::Outdent",
+      "ctrl-y": "vim::InsertFromAbove",
+      "ctrl-e": "vim::InsertFromBelow",
       "ctrl-k": ["vim::PushDigraph", {}],
       "ctrl-v": ["vim::PushLiteral", {}],
       "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.

crates/vim/src/insert.rs 🔗

@@ -3,7 +3,9 @@ use editor::{Bias, Editor};
 use gpui::{Action, Context, Window, actions};
 use language::SelectionGoal;
 use settings::Settings;
+use text::Point;
 use vim_mode_setting::HelixModeSetting;
+use workspace::searchable::Direction;
 
 actions!(
     vim,
@@ -11,13 +13,23 @@ actions!(
         /// Switches to normal mode with cursor positioned before the current character.
         NormalBefore,
         /// Temporarily switches to normal mode for one command.
-        TemporaryNormal
+        TemporaryNormal,
+        /// Inserts the next character from the line above into the current line.
+        InsertFromAbove,
+        /// Inserts the next character from the line below into the current line.
+        InsertFromBelow
     ]
 );
 
 pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, Vim::normal_before);
     Vim::action(editor, cx, Vim::temporary_normal);
+    Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| {
+        vim.insert_around(Direction::Prev, window, cx)
+    });
+    Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| {
+        vim.insert_around(Direction::Next, window, cx)
+    })
 }
 
 impl Vim {
@@ -71,6 +83,29 @@ impl Vim {
         self.switch_mode(Mode::Normal, true, window, cx);
         self.temp_mode = true;
     }
+
+    fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context<Self>) {
+        self.update_editor(cx, |_, editor, cx| {
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let mut edits = Vec::new();
+            for selection in editor.selections.all::<Point>(cx) {
+                let point = selection.head();
+                let new_row = match direction {
+                    Direction::Next => point.row + 1,
+                    Direction::Prev if point.row > 0 => point.row - 1,
+                    _ => continue,
+                };
+                let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left);
+                if let Some(c) = snapshot.chars_at(source).next()
+                    && c != '\n'
+                {
+                    edits.push((point..point, c.to_string()))
+                }
+            }
+
+            editor.edit(edits, cx);
+        });
+    }
 }
 
 #[cfg(test)]
@@ -156,4 +191,13 @@ mod test {
             .await;
         cx.shared_state().await.assert_eq("hehello\nˇllo\n");
     }
+
+    #[gpui::test]
+    async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("hello\nˇ\nworld").await;
+        cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await;
+        cx.shared_state().await.assert_eq("hello\nhoˇ\nworld");
+    }
 }