vim: ctrl-r while we're on a register kick (#13085)

Conrad Irwin created

Release Notes:

- vim: Support `ctrl-r X` to paste in insert mode (#4308)

Change summary

assets/keymaps/vim.json                      |   3 
crates/editor/src/editor.rs                  | 138 ++++++++++++---------
crates/vim/src/insert.rs                     |  26 ++-
crates/vim/src/state.rs                      |   1 
crates/vim/src/vim.rs                        |  22 +++
crates/vim/test_data/test_insert_ctrl_r.json |   7 +
6 files changed, 118 insertions(+), 79 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -634,8 +634,7 @@
       "ctrl-u": "editor::DeleteToBeginningOfLine",
       "ctrl-t": "vim::Indent",
       "ctrl-d": "vim::Outdent",
-      "ctrl-r \"": "editor::Paste",
-      "ctrl-r +": "editor::Paste"
+      "ctrl-r": ["vim::PushOperator", "Register"]
     }
   },
   {

crates/editor/src/editor.rs 🔗

@@ -6413,82 +6413,96 @@ impl Editor {
         cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
     }
 
-    pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+    pub fn do_paste(
+        &mut self,
+        text: &String,
+        clipboard_selections: Option<Vec<ClipboardSelection>>,
+        handle_entire_lines: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         if self.read_only(cx) {
             return;
         }
 
+        let clipboard_text = Cow::Borrowed(text);
+
         self.transact(cx, |this, cx| {
-            if let Some(item) = cx.read_from_clipboard() {
-                let clipboard_text = Cow::Borrowed(item.text());
-                if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
-                    let old_selections = this.selections.all::<usize>(cx);
-                    let all_selections_were_entire_line =
-                        clipboard_selections.iter().all(|s| s.is_entire_line);
-                    let first_selection_indent_column =
-                        clipboard_selections.first().map(|s| s.first_line_indent);
-                    if clipboard_selections.len() != old_selections.len() {
-                        clipboard_selections.drain(..);
-                    }
+            if let Some(mut clipboard_selections) = clipboard_selections {
+                let old_selections = this.selections.all::<usize>(cx);
+                let all_selections_were_entire_line =
+                    clipboard_selections.iter().all(|s| s.is_entire_line);
+                let first_selection_indent_column =
+                    clipboard_selections.first().map(|s| s.first_line_indent);
+                if clipboard_selections.len() != old_selections.len() {
+                    clipboard_selections.drain(..);
+                }
 
-                    this.buffer.update(cx, |buffer, cx| {
-                        let snapshot = buffer.read(cx);
-                        let mut start_offset = 0;
-                        let mut edits = Vec::new();
-                        let mut original_indent_columns = Vec::new();
-                        let line_mode = this.selections.line_mode;
-                        for (ix, selection) in old_selections.iter().enumerate() {
-                            let to_insert;
-                            let entire_line;
-                            let original_indent_column;
-                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
-                                let end_offset = start_offset + clipboard_selection.len;
-                                to_insert = &clipboard_text[start_offset..end_offset];
-                                entire_line = clipboard_selection.is_entire_line;
-                                start_offset = end_offset + 1;
-                                original_indent_column =
-                                    Some(clipboard_selection.first_line_indent);
-                            } else {
-                                to_insert = clipboard_text.as_str();
-                                entire_line = all_selections_were_entire_line;
-                                original_indent_column = first_selection_indent_column
-                            }
+                this.buffer.update(cx, |buffer, cx| {
+                    let snapshot = buffer.read(cx);
+                    let mut start_offset = 0;
+                    let mut edits = Vec::new();
+                    let mut original_indent_columns = Vec::new();
+                    for (ix, selection) in old_selections.iter().enumerate() {
+                        let to_insert;
+                        let entire_line;
+                        let original_indent_column;
+                        if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+                            let end_offset = start_offset + clipboard_selection.len;
+                            to_insert = &clipboard_text[start_offset..end_offset];
+                            entire_line = clipboard_selection.is_entire_line;
+                            start_offset = end_offset + 1;
+                            original_indent_column = Some(clipboard_selection.first_line_indent);
+                        } else {
+                            to_insert = clipboard_text.as_str();
+                            entire_line = all_selections_were_entire_line;
+                            original_indent_column = first_selection_indent_column
+                        }
 
-                            // If the corresponding selection was empty when this slice of the
-                            // clipboard text was written, then the entire line containing the
-                            // selection was copied. If this selection is also currently empty,
-                            // then paste the line before the current line of the buffer.
-                            let range = if selection.is_empty() && !line_mode && entire_line {
-                                let column = selection.start.to_point(&snapshot).column as usize;
-                                let line_start = selection.start - column;
-                                line_start..line_start
-                            } else {
-                                selection.range()
-                            };
+                        // If the corresponding selection was empty when this slice of the
+                        // clipboard text was written, then the entire line containing the
+                        // selection was copied. If this selection is also currently empty,
+                        // then paste the line before the current line of the buffer.
+                        let range = if selection.is_empty() && handle_entire_lines && entire_line {
+                            let column = selection.start.to_point(&snapshot).column as usize;
+                            let line_start = selection.start - column;
+                            line_start..line_start
+                        } else {
+                            selection.range()
+                        };
 
-                            edits.push((range, to_insert));
-                            original_indent_columns.extend(original_indent_column);
-                        }
-                        drop(snapshot);
+                        edits.push((range, to_insert));
+                        original_indent_columns.extend(original_indent_column);
+                    }
+                    drop(snapshot);
 
-                        buffer.edit(
-                            edits,
-                            Some(AutoindentMode::Block {
-                                original_indent_columns,
-                            }),
-                            cx,
-                        );
-                    });
+                    buffer.edit(
+                        edits,
+                        Some(AutoindentMode::Block {
+                            original_indent_columns,
+                        }),
+                        cx,
+                    );
+                });
 
-                    let selections = this.selections.all::<usize>(cx);
-                    this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
-                } else {
-                    this.insert(&clipboard_text, cx);
-                }
+                let selections = this.selections.all::<usize>(cx);
+                this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+            } else {
+                this.insert(&clipboard_text, cx);
             }
         });
     }
 
+    pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if let Some(item) = cx.read_from_clipboard() {
+            self.do_paste(
+                item.text(),
+                item.metadata::<Vec<ClipboardSelection>>(),
+                true,
+                cx,
+            )
+        };
+    }
+
     pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
         if self.read_only(cx) {
             return;

crates/vim/src/insert.rs 🔗

@@ -16,6 +16,11 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 
 fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
     let should_repeat = Vim::update(cx, |vim, cx| {
+        if vim.state().active_operator().is_some() {
+            vim.update_state(|state| state.operator_stack.clear());
+            vim.sync_vim_settings(cx);
+            return false;
+        }
         let count = vim.take_count(cx).unwrap_or(1);
         vim.stop_recording_immediately(action.boxed_clone());
         if count <= 1 || vim.workspace_state.replaying {
@@ -66,30 +71,24 @@ mod test {
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("5 i - escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("----ˇ-hello\n");
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("5 a - escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("h----ˇ-ello\n");
 
         cx.simulate_shared_keystrokes("4 shift-i - escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("---ˇ-h-----ello\n");
 
         cx.simulate_shared_keystrokes("3 shift-a - escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("----h-----ello--ˇ-\n");
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("3 o o i escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("hello\noi\noi\noˇi\n");
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("3 shift-o o i escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("oi\noi\noˇi\nhello\n");
     }
 
@@ -99,28 +98,31 @@ mod test {
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("3 i - escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("--ˇ-hello\n");
         cx.simulate_shared_keystrokes(".").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("----ˇ--hello\n");
         cx.simulate_shared_keystrokes("2 .").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("-----ˇ---hello\n");
 
         cx.set_shared_state("ˇhello\n").await;
         cx.simulate_shared_keystrokes("2 o k k escape").await;
-        cx.run_until_parked();
         cx.shared_state().await.assert_eq("hello\nkk\nkˇk\n");
         cx.simulate_shared_keystrokes(".").await;
-        cx.run_until_parked();
         cx.shared_state()
             .await
             .assert_eq("hello\nkk\nkk\nkk\nkˇk\n");
         cx.simulate_shared_keystrokes("1 .").await;
-        cx.run_until_parked();
         cx.shared_state()
             .await
             .assert_eq("hello\nkk\nkk\nkk\nkk\nkˇk\n");
     }
+
+    #[gpui::test]
+    async fn test_insert_ctrl_r(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("heˇllo\n").await;
+        cx.simulate_shared_keystrokes("y y i ctrl-r \"").await;
+        cx.shared_state().await.assert_eq("hehello\nˇllo\n");
+    }
 }

crates/vim/src/state.rs 🔗

@@ -227,6 +227,7 @@ impl EditorState {
             Some(Operator::FindForward { .. })
                 | Some(Operator::FindBackward { .. })
                 | Some(Operator::Mark)
+                | Some(Operator::Register)
                 | Some(Operator::Jump { .. })
         )
     }

crates/vim/src/vim.rs 🔗

@@ -624,7 +624,7 @@ impl Vim {
         editor: Option<&mut Editor>,
         cx: &mut WindowContext,
     ) -> Option<Register> {
-        let Some(register) = register else {
+        let Some(register) = register.filter(|reg| *reg != '"') else {
             let setting = VimSettings::get_global(cx).use_system_clipboard;
             return match setting {
                 UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
@@ -882,8 +882,24 @@ impl Vim {
             Some(Operator::Mark) => Vim::update(cx, |vim, cx| {
                 normal::mark::create_mark(vim, text, false, cx)
             }),
-            Some(Operator::Register) => Vim::update(cx, |vim, cx| {
-                vim.select_register(text, cx);
+            Some(Operator::Register) => Vim::update(cx, |vim, cx| match vim.state().mode {
+                Mode::Insert => {
+                    vim.update_active_editor(cx, |vim, editor, cx| {
+                        if let Some(register) =
+                            vim.read_register(text.chars().next(), Some(editor), cx)
+                        {
+                            editor.do_paste(
+                                &register.text.to_string(),
+                                register.clipboard_selections.clone(),
+                                false,
+                                cx,
+                            )
+                        }
+                    });
+                }
+                _ => {
+                    vim.select_register(text, cx);
+                }
             }),
             Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx),
             _ => match Vim::read(cx).state().mode {