vim: Implement named registers (#12895)

Paul Eguisier and Conrad Irwin created

Release Notes:

- vim: Add support for register selection `"a`-`"z`, `"0`-`"9`, `"-`.
`"_` and `"%`
([#11511](https://github.com/zed-industries/zed/issues/11511))

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

assets/keymaps/vim.json                           |   2 
crates/editor/src/editor.rs                       |   2 
crates/gpui/src/keymap/context.rs                 |   2 
crates/vim/src/mode_indicator.rs                  |  14 +
crates/vim/src/normal/paste.rs                    | 178 ++++++++++++++--
crates/vim/src/state.rs                           |   8 
crates/vim/src/test/neovim_backed_test_context.rs |  27 ++
crates/vim/src/utils.rs                           |  24 -
crates/vim/src/vim.rs                             | 156 ++++++++++++++
crates/vim/test_data/test_named_registers.json    |  26 ++
crates/vim/test_data/test_numbered_registers.json |  45 ++++
crates/vim/test_data/test_special_registers.json  |  30 ++
docs/src/vim.md                                   |   6 
13 files changed, 454 insertions(+), 66 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -385,6 +385,7 @@
       "g u": ["vim::PushOperator", "Lowercase"],
       "g shift-u": ["vim::PushOperator", "Uppercase"],
       "g ~": ["vim::PushOperator", "OppositeCase"],
+      "\"": ["vim::PushOperator", "Register"],
       "ctrl-pagedown": "pane::ActivateNextItem",
       "ctrl-pageup": "pane::ActivatePrevItem",
       // tree-sitter related commands
@@ -399,6 +400,7 @@
   {
     "context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
     "bindings": {
+      "\"": ["vim::PushOperator", "Register"],
       // tree-sitter related commands
       "[ x": "editor::SelectLargerSyntaxNode",
       "] x": "editor::SelectSmallerSyntaxNode"

crates/editor/src/editor.rs 🔗

@@ -1502,7 +1502,7 @@ struct ActiveDiagnosticGroup {
     is_valid: bool,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
 pub struct ClipboardSelection {
     pub len: usize,
     pub is_entire_line: bool,

crates/gpui/src/keymap/context.rs 🔗

@@ -356,7 +356,7 @@ fn is_identifier_char(c: char) -> bool {
 }
 
 fn is_vim_operator_char(c: char) -> bool {
-    c == '>' || c == '<' || c == '~'
+    c == '>' || c == '<' || c == '~' || c == '"'
 }
 
 fn skip_whitespace(source: &str) -> &str {

crates/vim/src/mode_indicator.rs 🔗

@@ -39,9 +39,17 @@ impl ModeIndicator {
 
     fn current_operators_description(&self, vim: &Vim) -> String {
         vim.state()
-            .operator_stack
-            .iter()
-            .map(|item| item.id())
+            .pre_count
+            .map(|count| format!("{}", count))
+            .into_iter()
+            .chain(vim.state().selected_register.map(|reg| format!("\"{reg}")))
+            .chain(
+                vim.state()
+                    .operator_stack
+                    .iter()
+                    .map(|item| item.id().to_string()),
+            )
+            .chain(vim.state().post_count.map(|count| format!("{}", count)))
             .collect::<Vec<_>>()
             .join("")
     }

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

@@ -10,7 +10,11 @@ use serde::Deserialize;
 use settings::Settings;
 use workspace::Workspace;
 
-use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
+use crate::{
+    state::Mode,
+    utils::{copy_selections_content, SYSTEM_CLIPBOARD},
+    UseSystemClipboard, Vim, VimSettings,
+};
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
@@ -29,7 +33,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
 
 fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool {
     cx.read_from_clipboard().is_some_and(|item| {
-        if let Some(last_state) = vim.workspace_state.registers.get(".system.") {
+        if let Some(last_state) = vim.workspace_state.registers.get(&SYSTEM_CLIPBOARD) {
             last_state != item.text()
         } else {
             true
@@ -46,33 +50,36 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
-                let (clipboard_text, clipboard_selections): (String, Option<_>) =
-                    if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never
-                        || VimSettings::get_global(cx).use_system_clipboard
-                            == UseSystemClipboard::OnYank
-                            && !system_clipboard_is_newer(vim, cx)
-                    {
-                        (
-                            vim.workspace_state
-                                .registers
-                                .get("\"")
-                                .cloned()
-                                .unwrap_or_else(|| "".to_string()),
-                            None,
-                        )
+                let (clipboard_text, clipboard_selections): (String, Option<_>) = if let Some(
+                    register,
+                ) =
+                    vim.update_state(|state| state.selected_register.take())
+                {
+                    (
+                        vim.read_register(register, Some(editor), cx)
+                            .unwrap_or_default(),
+                        None,
+                    )
+                } else if VimSettings::get_global(cx).use_system_clipboard
+                    == UseSystemClipboard::Never
+                    || VimSettings::get_global(cx).use_system_clipboard
+                        == UseSystemClipboard::OnYank
+                        && !system_clipboard_is_newer(vim, cx)
+                {
+                    (vim.read_register('"', None, cx).unwrap_or_default(), None)
+                } else {
+                    if let Some(item) = cx.read_from_clipboard() {
+                        let clipboard_selections = item
+                            .metadata::<Vec<ClipboardSelection>>()
+                            .filter(|clipboard_selections| {
+                                clipboard_selections.len() > 1
+                                    && vim.state().mode != Mode::VisualLine
+                            });
+                        (item.text().clone(), clipboard_selections)
                     } else {
-                        if let Some(item) = cx.read_from_clipboard() {
-                            let clipboard_selections = item
-                                .metadata::<Vec<ClipboardSelection>>()
-                                .filter(|clipboard_selections| {
-                                    clipboard_selections.len() > 1
-                                        && vim.state().mode != Mode::VisualLine
-                                });
-                            (item.text().clone(), clipboard_selections)
-                        } else {
-                            ("".into(), None)
-                        }
-                    };
+                        ("".into(), None)
+                    }
+                };
 
                 if clipboard_text.is_empty() {
                     return;
@@ -606,4 +613,119 @@ mod test {
             three
         "});
     }
+
+    #[gpui::test]
+    async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            });
+        });
+
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("y y \" 0 p").await;
+        cx.shared_register('0').await.assert_eq("fox jumps over\n");
+        cx.shared_register('"').await.assert_eq("fox jumps over\n");
+
+        cx.shared_state().await.assert_eq(indoc! {"
+                The quick brown
+                fox jumps over
+                ˇfox jumps over
+                the lazy dog"});
+        cx.simulate_shared_keystrokes("k k d d").await;
+        cx.shared_register('0').await.assert_eq("fox jumps over\n");
+        cx.shared_register('1').await.assert_eq("The quick brown\n");
+        cx.shared_register('"').await.assert_eq("The quick brown\n");
+
+        cx.simulate_shared_keystrokes("d d shift-g d d").await;
+        cx.shared_register('0').await.assert_eq("fox jumps over\n");
+        cx.shared_register('3').await.assert_eq("The quick brown\n");
+        cx.shared_register('2').await.assert_eq("fox jumps over\n");
+        cx.shared_register('1').await.assert_eq("the lazy dog\n");
+
+        cx.shared_state().await.assert_eq(indoc! {"
+        ˇfox jumps over"});
+
+        cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox jumps over
+                ˇthe lazy dog"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_named_registers(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            });
+        });
+
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("\" a d a w").await;
+        cx.shared_register('a').await.assert_eq("jumps ");
+        cx.simulate_shared_keystrokes("\" shift-a d i w").await;
+        cx.shared_register('a').await.assert_eq("jumps over");
+        cx.simulate_shared_keystrokes("\" a p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+                The quick brown
+                fox jumps oveˇr
+                the lazy dog"});
+        cx.simulate_shared_keystrokes("\" a d a w").await;
+        cx.shared_register('a').await.assert_eq(" over");
+    }
+
+    #[gpui::test]
+    async fn test_special_registers(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            });
+        });
+
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("d i w").await;
+        cx.shared_register('-').await.assert_eq("jumps");
+        cx.simulate_shared_keystrokes("\" _ d d").await;
+        cx.shared_register('_').await.assert_eq("");
+
+        cx.shared_state().await.assert_eq(indoc! {"
+                The quick brown
+                the ˇlazy dog"});
+        cx.simulate_shared_keystrokes("\" \" d ^").await;
+        cx.shared_register('0').await.assert_eq("the ");
+        cx.shared_register('"').await.assert_eq("the ");
+
+        cx.simulate_shared_keystrokes("^ \" + d $").await;
+        cx.shared_clipboard().await.assert_eq("lazy dog");
+        cx.shared_register('"').await.assert_eq("lazy dog");
+
+        // not testing nvim as it doesn't have a filename
+        cx.simulate_keystrokes("\" % p");
+        cx.assert_state(
+            indoc! {"
+                    The quick brown
+                    dir/file.rˇs"},
+            Mode::Normal,
+        );
+    }
 }

crates/vim/src/state.rs 🔗

@@ -63,10 +63,10 @@ pub enum Operator {
     Jump { line: bool },
     Indent,
     Outdent,
-
     Lowercase,
     Uppercase,
     OppositeCase,
+    Register,
 }
 
 #[derive(Default, Clone)]
@@ -89,6 +89,8 @@ pub struct EditorState {
     pub current_tx: Option<TransactionId>,
     pub current_anchor: Option<Selection<Anchor>>,
     pub undo_modes: HashMap<TransactionId, Mode>,
+
+    pub selected_register: Option<char>,
 }
 
 #[derive(Default, Clone, Debug)]
@@ -123,7 +125,7 @@ pub struct WorkspaceState {
     pub recorded_actions: Vec<ReplayableAction>,
     pub recorded_selection: RecordedSelection,
 
-    pub registers: HashMap<String, String>,
+    pub registers: HashMap<char, String>,
 }
 
 #[derive(Debug)]
@@ -277,6 +279,7 @@ impl Operator {
             Operator::Uppercase => "gU",
             Operator::Lowercase => "gu",
             Operator::OppositeCase => "g~",
+            Operator::Register => "\"",
         }
     }
 
@@ -287,6 +290,7 @@ impl Operator {
             | Operator::Mark
             | Operator::Jump { .. }
             | Operator::FindBackward { .. }
+            | Operator::Register
             | Operator::Replace
             | Operator::AddSurrounds { target: Some(_) }
             | Operator::ChangeSurrounds { .. }

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -10,7 +10,7 @@ use language::language_settings::{AllLanguageSettings, SoftWrap};
 use util::test::marked_text_offsets;
 
 use super::{neovim_connection::NeovimConnection, VimTestContext};
-use crate::state::Mode;
+use crate::{state::Mode, Vim};
 
 pub struct NeovimBackedTestContext {
     cx: VimTestContext,
@@ -94,6 +94,7 @@ impl SharedState {
 }
 
 pub struct SharedClipboard {
+    register: char,
     neovim: String,
     editor: String,
     state: SharedState,
@@ -120,15 +121,17 @@ impl SharedClipboard {
                 {}
                 # currently expected:
                 {}
-                # neovim clipboard:
+                # neovim register \"{}:
                 {}
-                # zed clipboard:
+                # zed register \"{}:
                 {}"},
             message,
             self.state.initial,
             self.state.recent_keystrokes,
             expected,
+            self.register,
             self.neovim,
+            self.register,
             self.editor
         )
     }
@@ -241,12 +244,30 @@ impl NeovimBackedTestContext {
     #[must_use]
     pub async fn shared_clipboard(&mut self) -> SharedClipboard {
         SharedClipboard {
+            register: '"',
             state: self.shared_state().await,
             neovim: self.neovim.read_register('"').await,
             editor: self.read_from_clipboard().unwrap().text().clone(),
         }
     }
 
+    #[must_use]
+    pub async fn shared_register(&mut self, register: char) -> SharedClipboard {
+        SharedClipboard {
+            register: register,
+            state: self.shared_state().await,
+            neovim: self.neovim.read_register(register).await,
+            editor: self.update(|cx| {
+                Vim::read(cx)
+                    .workspace_state
+                    .registers
+                    .get(&register)
+                    .cloned()
+                    .unwrap_or_default()
+            }),
+        }
+    }
+
     #[must_use]
     pub async fn shared_state(&mut self) -> SharedState {
         let (mode, marked_text) = self.neovim.state().await;

crates/vim/src/utils.rs 🔗

@@ -1,12 +1,13 @@
 use std::time::Duration;
 
 use editor::{ClipboardSelection, Editor};
-use gpui::{ClipboardItem, ViewContext};
+use gpui::ViewContext;
 use language::{CharKind, Point};
 use multi_buffer::MultiBufferRow;
-use settings::Settings;
 
-use crate::{state::Mode, UseSystemClipboard, Vim, VimSettings};
+use crate::{state::Mode, Vim};
+
+pub const SYSTEM_CLIPBOARD: char = '\0';
 
 pub struct HighlightOnYank;
 
@@ -102,21 +103,8 @@ fn copy_selections_content_internal(
         }
     }
 
-    let setting = VimSettings::get_global(cx).use_system_clipboard;
-    if setting == UseSystemClipboard::Always || setting == UseSystemClipboard::OnYank && is_yank {
-        cx.write_to_clipboard(ClipboardItem::new(text.clone()).with_metadata(clipboard_selections));
-        vim.workspace_state
-            .registers
-            .insert(".system.".to_string(), text.clone());
-    } else {
-        vim.workspace_state.registers.insert(
-            ".system.".to_string(),
-            cx.read_from_clipboard()
-                .map(|item| item.text().clone())
-                .unwrap_or_default(),
-        );
-    }
-    vim.workspace_state.registers.insert("\"".to_string(), text);
+    vim.write_registers(is_yank, linewise, text, clipboard_selections, cx);
+
     if !is_yank || vim.state().mode == Mode::Visual {
         return;
     }

crates/vim/src/vim.rs 🔗

@@ -23,11 +23,11 @@ use collections::HashMap;
 use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
 use editor::{
     movement::{self, FindRange},
-    Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint,
+    Anchor, Bias, ClipboardSelection, Editor, EditorEvent, EditorMode, ToPoint,
 };
 use gpui::{
-    actions, impl_actions, Action, AppContext, EntityId, FocusableView, Global, KeystrokeEvent,
-    Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
+    actions, impl_actions, Action, AppContext, ClipboardItem, EntityId, FocusableView, Global,
+    KeystrokeEvent, Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
 };
 use language::{CursorShape, Point, SelectionGoal, TransactionId};
 pub use mode_indicator::ModeIndicator;
@@ -45,6 +45,7 @@ use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 use std::{ops::Range, sync::Arc};
 use surrounds::{add_surrounds, change_surrounds, delete_surrounds};
 use ui::BorrowAppContext;
+use utils::SYSTEM_CLIPBOARD;
 use visual::{visual_block_motion, visual_replace};
 use workspace::{self, Workspace};
 
@@ -70,6 +71,9 @@ pub struct PushOperator(pub Operator);
 #[derive(Clone, Deserialize, PartialEq)]
 struct Number(usize);
 
+#[derive(Clone, Deserialize, PartialEq)]
+struct SelectRegister(String);
+
 actions!(
     vim,
     [
@@ -86,7 +90,7 @@ actions!(
 // in the workspace namespace so it's not filtered out when vim is disabled.
 actions!(workspace, [ToggleVimMode]);
 
-impl_actions!(vim, [SwitchMode, PushOperator, Number]);
+impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]);
 
 /// Initializes the `vim` crate.
 pub fn init(cx: &mut AppContext) {
@@ -129,7 +133,6 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
     workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| {
         Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
     });
-
     workspace.register_action(|_: &mut Workspace, _: &Tab, cx| {
         Vim::active_editor_input_ignored(" ".into(), cx)
     });
@@ -202,7 +205,8 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext)
             | Operator::ChangeSurrounds { .. }
             | Operator::DeleteSurrounds
             | Operator::Mark
-            | Operator::Jump { .. },
+            | Operator::Jump { .. }
+            | Operator::Register,
         ) => {}
         Some(_) => {
             vim.clear_operator(cx);
@@ -531,6 +535,138 @@ impl Vim {
         count
     }
 
+    fn select_register(&mut self, register: Arc<str>, cx: &mut WindowContext) {
+        self.update_state(|state| {
+            if register.chars().count() == 1 {
+                state
+                    .selected_register
+                    .replace(register.chars().next().unwrap());
+            }
+            state.operator_stack.clear();
+        });
+        self.sync_vim_settings(cx);
+    }
+
+    fn write_registers(
+        &mut self,
+        is_yank: bool,
+        linewise: bool,
+        text: String,
+        clipboard_selections: Vec<ClipboardSelection>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        self.workspace_state.registers.insert('"', text.clone());
+        if let Some(register) = self.update_state(|vim| vim.selected_register.take()) {
+            let lower = register.to_lowercase().next().unwrap_or(register);
+            if lower != register {
+                let current = self.workspace_state.registers.entry(lower).or_default();
+                *current += &text;
+            } else {
+                match lower {
+                    '_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
+                    '+' => {
+                        cx.write_to_clipboard(
+                            ClipboardItem::new(text.clone()).with_metadata(clipboard_selections),
+                        );
+                    }
+                    '*' => {
+                        #[cfg(target_os = "linux")]
+                        cx.write_to_primary(
+                            ClipboardItem::new(text.clone()).with_metadata(clipboard_selections),
+                        );
+                        #[cfg(not(target_os = "linux"))]
+                        cx.write_to_clipboard(
+                            ClipboardItem::new(text.clone()).with_metadata(clipboard_selections),
+                        );
+                    }
+                    '"' => {
+                        self.workspace_state.registers.insert('0', text.clone());
+                        self.workspace_state.registers.insert('"', text);
+                    }
+                    _ => {
+                        self.workspace_state.registers.insert(lower, text);
+                    }
+                }
+            }
+        } else {
+            let setting = VimSettings::get_global(cx).use_system_clipboard;
+            if setting == UseSystemClipboard::Always
+                || setting == UseSystemClipboard::OnYank && is_yank
+            {
+                cx.write_to_clipboard(
+                    ClipboardItem::new(text.clone()).with_metadata(clipboard_selections.clone()),
+                );
+                self.workspace_state
+                    .registers
+                    .insert(SYSTEM_CLIPBOARD, text.clone());
+            } else {
+                self.workspace_state.registers.insert(
+                    SYSTEM_CLIPBOARD,
+                    cx.read_from_clipboard()
+                        .map(|item| item.text().clone())
+                        .unwrap_or_default(),
+                );
+            }
+
+            if is_yank {
+                self.workspace_state.registers.insert('0', text);
+            } else {
+                if !text.contains('\n') {
+                    self.workspace_state.registers.insert('-', text.clone());
+                }
+                if linewise || text.contains('\n') {
+                    let mut content = text;
+                    for i in '1'..'8' {
+                        if let Some(moved) = self.workspace_state.registers.insert(i, content) {
+                            content = moved;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    fn read_register(
+        &mut self,
+        register: char,
+        editor: Option<&mut Editor>,
+        cx: &mut WindowContext,
+    ) -> Option<String> {
+        let lower = register.to_lowercase().next().unwrap_or(register);
+        match lower {
+            '_' | ':' | '.' | '#' | '=' | '/' => None,
+            '+' => cx.read_from_clipboard().map(|item| item.text().clone()),
+            '*' => {
+                #[cfg(target_os = "linux")]
+                {
+                    cx.read_from_primary().map(|item| item.text().clone())
+                }
+                #[cfg(not(target_os = "linux"))]
+                {
+                    cx.read_from_clipboard().map(|item| item.text().clone())
+                }
+            }
+            '%' => editor.and_then(|editor| {
+                let selection = editor.selections.newest::<Point>(cx);
+                if let Some((_, buffer, _)) = editor
+                    .buffer()
+                    .read(cx)
+                    .excerpt_containing(selection.head(), cx)
+                {
+                    buffer
+                        .read(cx)
+                        .file()
+                        .map(|file| file.path().to_string_lossy().to_string())
+                } else {
+                    None
+                }
+            }),
+            _ => self.workspace_state.registers.get(&lower).cloned(),
+        }
+    }
+
     fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
         if matches!(
             operator,
@@ -573,7 +709,10 @@ impl Vim {
 
     fn clear_operator(&mut self, cx: &mut WindowContext) {
         self.take_count(cx);
-        self.update_state(|state| state.operator_stack.clear());
+        self.update_state(|state| {
+            state.selected_register.take();
+            state.operator_stack.clear()
+        });
         self.sync_vim_settings(cx);
     }
 
@@ -741,6 +880,9 @@ 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::Jump { line }) => normal::mark::jump(text, line, cx),
             _ => match Vim::read(cx).state().mode {
                 Mode::Replace => multi_replace(text, cx),

crates/vim/test_data/test_named_registers.json 🔗

@@ -0,0 +1,26 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"w"}
+{"Get":{"state":"The quick brown\nfox ˇover\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"a","value":"jumps "}}
+{"Key":"\""}
+{"Key":"shift-a"}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"a","value":"jumps over"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"w"}
+{"Get":{"state":"The quick brown\nfox jumpˇs\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"a","value":" over"}}

crates/vim/test_data/test_numbered_registers.json 🔗

@@ -0,0 +1,45 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"\""}
+{"Key":"0"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"0","value":"fox jumps over\n"}}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"0","value":"fox jumps over\n"}}
+{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"1","value":"The quick brown\n"}}
+{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"The quick brown\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇfox jumps over","mode":"Normal"}}
+{"ReadRegister":{"name":"0","value":"fox jumps over\n"}}
+{"Get":{"state":"ˇfox jumps over","mode":"Normal"}}
+{"ReadRegister":{"name":"3","value":"The quick brown\n"}}
+{"Get":{"state":"ˇfox jumps over","mode":"Normal"}}
+{"ReadRegister":{"name":"2","value":"fox jumps over\n"}}
+{"Get":{"state":"ˇfox jumps over","mode":"Normal"}}
+{"ReadRegister":{"name":"1","value":"the lazy dog\n"}}
+{"Get":{"state":"ˇfox jumps over","mode":"Normal"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"\""}
+{"Key":"3"}
+{"Key":"p"}
+{"Key":"p"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Put":{"state":"The quick brown\nfox jumps over\nˇthe lazy dog"}}

crates/vim/test_data/test_special_registers.json 🔗

@@ -0,0 +1,30 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"The quick brown\nfox ˇ over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"-","value":"jumps"}}
+{"Key":"\""}
+{"Key":"_"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"_","value":""}}
+{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"\""}
+{"Key":"d"}
+{"Key":"^"}
+{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"0","value":"the "}}
+{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"the "}}
+{"Key":"^"}
+{"Key":"\""}
+{"Key":"+"}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy dog"}}
+{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy dog"}}

docs/src/vim.md 🔗

@@ -215,9 +215,9 @@ Some vim settings are available to modify the default vim behavior:
 ```json
 {
   "vim": {
-    // "always": use system clipboard
-    // "never": don't use system clipboard
-    // "on_yank": use system clipboard for yank operations
+    // "always": use system clipboard when no register is specified
+    // "never": don't use system clipboard unless "+ or "* is specified
+    // "on_yank": use system clipboard for yank operations when no register is specified
     "use_system_clipboard": "always",
     // Lets `f` and `t` motions extend across multiple lines
     "use_multiline_find": true