Add option to either use system clipboard or vim clipboard (#7936)

Mahdy M. Karam and Conrad Irwin created

Release Notes:

- vim: Added a setting to control default clipboard behaviour. `{"vim":
{"use_system_clipboard": "never"}}` disables writing to the clipboard.
`"on_yank"` writes to the system clipboard only on yank, and `"always"`
preserves the current behavior. ([#4390
](https://github.com/zed-industries/zed/issues/4390))

---------

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

Change summary

Cargo.lock                          |   1 
assets/keymaps/vim.json             | 220 ++++++++++++++++++++++++------
assets/settings/default.json        |  15 +
crates/vim/Cargo.toml               |   1 
crates/vim/src/insert.rs            |   2 
crates/vim/src/normal.rs            |  16 +-
crates/vim/src/normal/case.rs       |   2 
crates/vim/src/normal/change.rs     |   8 
crates/vim/src/normal/delete.rs     |   8 
crates/vim/src/normal/increment.rs  |   2 
crates/vim/src/normal/paste.rs      | 161 ++++++++++++++++++++--
crates/vim/src/normal/scroll.rs     |   2 
crates/vim/src/normal/substitute.rs |   4 
crates/vim/src/normal/yank.rs       |  10 
crates/vim/src/state.rs             |   3 
crates/vim/src/utils.rs             |  38 ++++
crates/vim/src/vim.rs               |  54 ++++++-
crates/vim/src/visual.rs            |  22 +-
18 files changed, 453 insertions(+), 116 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10903,6 +10903,7 @@ dependencies = [
  "project",
  "regex",
  "release_channel",
+ "schemars",
  "search",
  "serde",
  "serde_derive",

assets/keymaps/vim.json 🔗

@@ -101,8 +101,14 @@
       "ctrl-o": "pane::GoBack",
       "ctrl-i": "pane::GoForward",
       "ctrl-]": "editor::GoToDefinition",
-      "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl-[": ["vim::SwitchMode", "Normal"],
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "ctrl-[": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "v": "vim::ToggleVisual",
       "shift-v": "vim::ToggleVisualLine",
       "ctrl-v": "vim::ToggleVisualBlock",
@@ -235,36 +241,123 @@
         }
       ],
       // Count support
-      "1": ["vim::Number", 1],
-      "2": ["vim::Number", 2],
-      "3": ["vim::Number", 3],
-      "4": ["vim::Number", 4],
-      "5": ["vim::Number", 5],
-      "6": ["vim::Number", 6],
-      "7": ["vim::Number", 7],
-      "8": ["vim::Number", 8],
-      "9": ["vim::Number", 9],
+      "1": [
+        "vim::Number",
+        1
+      ],
+      "2": [
+        "vim::Number",
+        2
+      ],
+      "3": [
+        "vim::Number",
+        3
+      ],
+      "4": [
+        "vim::Number",
+        4
+      ],
+      "5": [
+        "vim::Number",
+        5
+      ],
+      "6": [
+        "vim::Number",
+        6
+      ],
+      "7": [
+        "vim::Number",
+        7
+      ],
+      "8": [
+        "vim::Number",
+        8
+      ],
+      "9": [
+        "vim::Number",
+        9
+      ],
       // window related commands (ctrl-w X)
-      "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
-      "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
-      "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
-      "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
-      "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
-      "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
-      "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
-      "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
-      "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
-      "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
-      "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
-      "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
-      "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
-      "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
-      "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
-      "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
-      "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
-      "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
-      "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
-      "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
+      "ctrl-w left": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w right": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w up": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w down": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w h": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w l": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w k": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w j": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w ctrl-h": [
+        "workspace::ActivatePaneInDirection",
+        "Left"
+      ],
+      "ctrl-w ctrl-l": [
+        "workspace::ActivatePaneInDirection",
+        "Right"
+      ],
+      "ctrl-w ctrl-k": [
+        "workspace::ActivatePaneInDirection",
+        "Up"
+      ],
+      "ctrl-w ctrl-j": [
+        "workspace::ActivatePaneInDirection",
+        "Down"
+      ],
+      "ctrl-w shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
+      "ctrl-w shift-h": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-l": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-k": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-j": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -286,8 +379,14 @@
       "ctrl-w ctrl-q": "pane::CloseAllItems",
       "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
       "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
-      "ctrl-w n": ["workspace::NewFileInDirection", "Up"],
-      "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
+      "ctrl-w n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ],
+      "ctrl-w ctrl-n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ],
       "-": "pane::RevealInProjectPanel"
     }
   },
@@ -303,12 +402,21 @@
     "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
     "bindings": {
       ".": "vim::Repeat",
-      "c": ["vim::PushOperator", "Change"],
+      "c": [
+        "vim::PushOperator",
+        "Change"
+      ],
       "shift-c": "vim::ChangeToEndOfLine",
-      "d": ["vim::PushOperator", "Delete"],
+      "d": [
+        "vim::PushOperator",
+        "Delete"
+      ],
       "shift-d": "vim::DeleteToEndOfLine",
       "shift-j": "vim::JoinLines",
-      "y": ["vim::PushOperator", "Yank"],
+      "y": [
+        "vim::PushOperator",
+        "Yank"
+      ],
       "shift-y": "vim::YankLine",
       "i": "vim::InsertBefore",
       "shift-i": "vim::InsertFirstNonWhitespace",
@@ -339,7 +447,10 @@
       ],
       "*": "vim::MoveToNext",
       "#": "vim::MoveToPrev",
-      "r": ["vim::PushOperator", "Replace"],
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ],
       "s": "vim::Substitute",
       "shift-s": "vim::SubstituteLine",
       "> >": "editor::Indent",
@@ -351,7 +462,10 @@
   {
     "context": "Editor && VimCount",
     "bindings": {
-      "0": ["vim::Number", 0]
+      "0": [
+        "vim::Number",
+        0
+      ]
     }
   },
   {
@@ -454,10 +568,22 @@
       "shift-i": "vim::InsertBefore",
       "shift-a": "vim::InsertAfter",
       "shift-j": "vim::JoinLines",
-      "r": ["vim::PushOperator", "Replace"],
-      "ctrl-c": ["vim::SwitchMode", "Normal"],
-      "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl-[": ["vim::SwitchMode", "Normal"],
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ],
+      "ctrl-c": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "ctrl-[": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       ">": "editor::Indent",
       "<": "editor::Outdent",
       "i": [
@@ -498,8 +624,14 @@
     "bindings": {
       "tab": "vim::Tab",
       "enter": "vim::Enter",
-      "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl-[": ["vim::SwitchMode", "Normal"]
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "ctrl-[": [
+        "vim::SwitchMode",
+        "Normal"
+      ]
     }
   },
   {

assets/settings/default.json 🔗

@@ -331,7 +331,9 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [".env"]
+    "disabled_globs": [
+      ".env"
+    ]
   },
   // Settings specific to journaling
   "journal": {
@@ -440,7 +442,12 @@
         // Default directories to search for virtual environments, relative
         // to the current working directory. We recommend overriding this
         // in your project's settings, rather than globally.
-        "directories": [".env", "env", ".venv", "venv"],
+        "directories": [
+          ".env",
+          "env",
+          ".venv",
+          "venv"
+        ],
         // Can also be 'csh', 'fish', and `nushell`
         "activate_script": "default"
       }
@@ -555,6 +562,10 @@
     //     }
     // }
   },
+  // Vim settings
+  "vim": {
+    "use_system_clipboard": "always"
+  },
   // The server to connect to. If the environment variable
   // ZED_SERVER_URL is set, it will override this setting.
   "server_url": "https://zed.dev",

crates/vim/Cargo.toml 🔗

@@ -39,6 +39,7 @@ tokio = { version = "1.15", "optional" = true }
 ui.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
+schemars.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/vim/src/insert.rs 🔗

@@ -15,7 +15,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
         let count = vim.take_count(cx).unwrap_or(1);
         vim.stop_recording_immediately(action.boxed_clone());
         if count <= 1 || vim.workspace_state.replaying {
-            vim.update_active_editor(cx, |editor, cx| {
+            vim.update_active_editor(cx, |_, editor, cx| {
                 editor.cancel(&Default::default(), cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_cursors_with(|map, mut cursor, _| {

crates/vim/src/normal.rs 🔗

@@ -119,7 +119,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
                 times -= 1;
             }
 
-            vim.update_active_editor(cx, |editor, cx| {
+            vim.update_active_editor(cx, |_, editor, cx| {
                 editor.transact(cx, |editor, cx| {
                     for _ in 0..times {
                         editor.join_lines(&Default::default(), cx)
@@ -182,7 +182,7 @@ pub(crate) fn move_cursor(
     times: Option<usize>,
     cx: &mut WindowContext,
 ) {
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |_, editor, cx| {
         let text_layout_details = editor.text_layout_details(cx);
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {
@@ -198,7 +198,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
     Vim::update(cx, |vim, cx| {
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
             });
@@ -221,7 +221,7 @@ fn insert_first_non_whitespace(
     Vim::update(cx, |vim, cx| {
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.move_cursors_with(|map, cursor, _| {
                     (
@@ -238,7 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
     Vim::update(cx, |vim, cx| {
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.move_cursors_with(|map, cursor, _| {
                     (next_line_end(map, cursor, 1), SelectionGoal::None)
@@ -252,7 +252,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
     Vim::update(cx, |vim, cx| {
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.transact(cx, |editor, cx| {
                 let (map, old_selections) = editor.selections.all_display(cx);
                 let selection_start_rows: HashSet<u32> = old_selections
@@ -285,7 +285,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
     Vim::update(cx, |vim, cx| {
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 let (map, old_selections) = editor.selections.all_display(cx);
@@ -330,7 +330,7 @@ fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
 pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.stop_recording();
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
                 let (map, display_selections) = editor.selections.all_display(cx);

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

@@ -40,7 +40,7 @@ where
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
         let count = vim.take_count(cx).unwrap_or(1) as u32;
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |vim, editor, cx| {
             let mut ranges = Vec::new();
             let mut cursor_positions = Vec::new();
             let snapshot = editor.buffer().read(cx).snapshot(cx);

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

@@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
             | Motion::Backspace
             | Motion::StartOfLine { .. }
     );
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             // We are swapping to insert mode anyway. Just set the line end clipping behavior now
@@ -45,7 +45,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                     };
                 });
             });
-            copy_selections_content(editor, motion.linewise(), cx);
+            copy_selections_content(vim, editor, motion.linewise(), cx);
             editor.insert("", cx);
         });
     });
@@ -59,7 +59,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
 
 pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
     let mut objects_found = false;
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         // We are swapping to insert mode anyway. Just set the line end clipping behavior now
         editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
@@ -69,7 +69,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
                 });
             });
             if objects_found {
-                copy_selections_content(editor, false, cx);
+                copy_selections_content(vim, editor, false, cx);
                 editor.insert("", cx);
             }
         });

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

@@ -6,7 +6,7 @@ use language::Point;
 
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.stop_recording();
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -39,7 +39,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                     }
                 });
             });
-            copy_selections_content(editor, motion.linewise(), cx);
+            copy_selections_content(vim, editor, motion.linewise(), cx);
             editor.insert("", cx);
 
             // Fixup cursor position after the deletion
@@ -62,7 +62,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
 
 pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
     vim.stop_recording();
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             // Emulates behavior in vim where if we expanded backwards to include a newline
@@ -98,7 +98,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
                     }
                 });
             });
-            copy_selections_content(editor, false, cx);
+            copy_selections_content(vim, editor, false, cx);
             editor.insert("", cx);
 
             // Fixup cursor position after the deletion

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

@@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 }
 
 fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         let mut edits = Vec::new();
         let mut new_anchors = Vec::new();
 

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

@@ -1,14 +1,15 @@
-use std::{borrow::Cow, cmp};
+use std::cmp;
 
 use editor::{
     display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,
 };
-use gpui::{impl_actions, ViewContext};
+use gpui::{impl_actions, AppContext, ViewContext};
 use language::{Bias, SelectionGoal};
 use serde::Deserialize;
+use settings::Settings;
 use workspace::Workspace;
 
-use crate::{state::Mode, utils::copy_selections_content, Vim};
+use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
@@ -25,34 +26,60 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
     workspace.register_action(paste);
 }
 
+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.") {
+            last_state != item.text()
+        } else {
+            true
+        }
+    })
+}
+
 fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |vim, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
-                let Some(item) = cx.read_from_clipboard() else {
-                    return;
-                };
-                let clipboard_text = Cow::Borrowed(item.text());
+                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,
+                        )
+                    } 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)
+                        }
+                    };
+
                 if clipboard_text.is_empty() {
                     return;
                 }
 
                 if !action.preserve_clipboard && vim.state().mode.is_visual() {
-                    copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
+                    copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
                 }
 
-                // if we are copying from multi-cursor (of visual block mode), we want
-                // to
-                let clipboard_selections =
-                    item.metadata::<Vec<ClipboardSelection>>()
-                        .filter(|clipboard_selections| {
-                            clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
-                        });
-
                 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
 
                 // unlike zed, if you have a multi-cursor selection from vim block mode,
@@ -201,8 +228,11 @@ mod test {
     use crate::{
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
+        UseSystemClipboard, VimSettings,
     };
+    use gpui::ClipboardItem;
     use indoc::indoc;
+    use settings::SettingsStore;
 
     #[gpui::test]
     async fn test_paste(cx: &mut gpui::TestAppContext) {
@@ -291,6 +321,103 @@ mod test {
             .await;
     }
 
+    #[gpui::test]
+    async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            });
+        });
+
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["v", "i", "w", "y"]);
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                fox ˇjumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystroke("p");
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                fox jjumpˇsumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        assert_eq!(cx.read_from_clipboard(), None);
+    }
+
+    #[gpui::test]
+    async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
+            });
+        });
+
+        // copy in visual mode
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["v", "i", "w", "y"]);
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                fox ˇjumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystroke("p");
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                fox jjumpˇsumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        assert_eq!(
+            cx.read_from_clipboard().map(|item| item.text().clone()),
+            Some("jumps".into())
+        );
+        cx.simulate_keystrokes(["d", "d", "p"]);
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                the lazy dog
+                ˇfox jjumpsumps over"},
+            Mode::Normal,
+        );
+        assert_eq!(
+            cx.read_from_clipboard().map(|item| item.text().clone()),
+            Some("jumps".into())
+        );
+        cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
+        cx.simulate_keystroke("shift-p");
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                the lazy dog
+                test-copˇyfox jjumpsumps over"},
+            Mode::Normal,
+        );
+    }
+
     #[gpui::test]
     async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

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

@@ -52,7 +52,7 @@ fn scroll(
 ) {
     Vim::update(cx, |vim, cx| {
         let amount = by(vim.take_count(cx).map(|c| c as f32));
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             scroll_editor(editor, move_cursor, &amount, cx)
         });
     })

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

@@ -29,7 +29,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
 }
 
 pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);
@@ -72,7 +72,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
                     }
                 })
             });
-            copy_selections_content(editor, line_mode, cx);
+            copy_selections_content(vim, editor, line_mode, cx);
             let selections = editor.selections.all::<Point>(cx).into_iter();
             let edits = selections.map(|selection| (selection.start..selection.end, ""));
             editor.edit(edits, cx);

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

@@ -1,9 +1,9 @@
-use crate::{motion::Motion, object::Object, utils::copy_and_flash_selections_content, Vim};
+use crate::{motion::Motion, object::Object, utils::yank_selections_content, Vim};
 use collections::HashMap;
 use gpui::WindowContext;
 
 pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -15,7 +15,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
                     motion.expand_selection(map, selection, times, true, &text_layout_details);
                 });
             });
-            copy_and_flash_selections_content(editor, motion.linewise(), cx);
+            yank_selections_content(vim, editor, motion.linewise(), cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|_, selection| {
                     let (head, goal) = original_positions.remove(&selection.id).unwrap();
@@ -27,7 +27,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
 }
 
 pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
-    vim.update_active_editor(cx, |editor, cx| {
+    vim.update_active_editor(cx, |vim, editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_positions: HashMap<_, _> = Default::default();
@@ -38,7 +38,7 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC
                     original_positions.insert(selection.id, original_position);
                 });
             });
-            copy_and_flash_selections_content(editor, false, cx);
+            yank_selections_content(vim, editor, false, cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|_, selection| {
                     let (head, goal) = original_positions.remove(&selection.id).unwrap();

crates/vim/src/state.rs 🔗

@@ -1,5 +1,6 @@
 use std::{ops::Range, sync::Arc};
 
+use collections::HashMap;
 use gpui::{Action, KeyContext};
 use language::CursorShape;
 use serde::{Deserialize, Serialize};
@@ -86,6 +87,8 @@ pub struct WorkspaceState {
     pub recorded_count: Option<usize>,
     pub recorded_actions: Vec<ReplayableAction>,
     pub recorded_selection: RecordedSelection,
+
+    pub registers: HashMap<String, String>,
 }
 
 #[derive(Debug)]

crates/vim/src/utils.rs 🔗

@@ -3,25 +3,35 @@ use std::time::Duration;
 use editor::{ClipboardSelection, Editor};
 use gpui::{ClipboardItem, ViewContext};
 use language::{CharKind, Point};
+use settings::Settings;
+
+use crate::{state::Mode, UseSystemClipboard, Vim, VimSettings};
 
 pub struct HighlightOnYank;
 
-pub fn copy_and_flash_selections_content(
+pub fn yank_selections_content(
+    vim: &mut Vim,
     editor: &mut Editor,
     linewise: bool,
     cx: &mut ViewContext<Editor>,
 ) {
-    copy_selections_content_internal(editor, linewise, true, cx);
+    copy_selections_content_internal(vim, editor, linewise, true, cx);
 }
 
-pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut ViewContext<Editor>) {
-    copy_selections_content_internal(editor, linewise, false, cx);
+pub fn copy_selections_content(
+    vim: &mut Vim,
+    editor: &mut Editor,
+    linewise: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    copy_selections_content_internal(vim, editor, linewise, false, cx);
 }
 
 fn copy_selections_content_internal(
+    vim: &mut Vim,
     editor: &mut Editor,
     linewise: bool,
-    highlight: bool,
+    is_yank: bool,
     cx: &mut ViewContext<Editor>,
 ) {
     let selections = editor.selections.all_adjusted(cx);
@@ -73,8 +83,22 @@ fn copy_selections_content_internal(
         }
     }
 
-    cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
-    if !highlight {
+    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);
+    if !is_yank || vim.state().mode == Mode::Visual {
         return;
     }
 

crates/vim/src/vim.rs 🔗

@@ -27,7 +27,9 @@ use language::{CursorShape, Point, Selection, SelectionGoal};
 pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
+use schemars::JsonSchema;
 use serde::Deserialize;
+use serde_derive::Serialize;
 use settings::{update_settings_file, Settings, SettingsStore};
 use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 use std::{ops::Range, sync::Arc};
@@ -70,6 +72,7 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number]);
 pub fn init(cx: &mut AppContext) {
     cx.set_global(Vim::default());
     VimModeSetting::register(cx);
+    VimSettings::register(cx);
 
     editor_events::init(cx);
 
@@ -261,12 +264,12 @@ impl Vim {
     }
 
     fn update_active_editor<S>(
-        &self,
+        &mut self,
         cx: &mut WindowContext,
-        update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
+        update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext<Editor>) -> S,
     ) -> Option<S> {
         let editor = self.active_editor.clone()?.upgrade()?;
-        Some(editor.update(cx, update))
+        Some(editor.update(cx, |editor, cx| update(self, editor, cx)))
     }
 
     /// When doing an action that modifies the buffer, we start recording so that `.`
@@ -365,7 +368,7 @@ impl Vim {
         }
 
         // Adjust selections
-        self.update_active_editor(cx, |editor, cx| {
+        self.update_active_editor(cx, |_, editor, cx| {
             if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
             {
                 visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
@@ -565,10 +568,9 @@ impl Vim {
         ret
     }
 
-    fn sync_vim_settings(&self, cx: &mut WindowContext) {
-        let state = self.state();
-
-        self.update_active_editor(cx, |editor, cx| {
+    fn sync_vim_settings(&mut self, cx: &mut WindowContext) {
+        self.update_active_editor(cx, |vim, editor, cx| {
+            let state = vim.state();
             editor.set_cursor_shape(state.cursor_shape(), cx);
             editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
             editor.set_collapse_matches(true);
@@ -612,6 +614,42 @@ impl Settings for VimModeSetting {
     }
 }
 
+/// Controls the soft-wrapping behavior in the editor.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum UseSystemClipboard {
+    Never,
+    Always,
+    OnYank,
+}
+
+#[derive(Deserialize)]
+struct VimSettings {
+    // all vim uses vim clipboard
+    // vim always uses system cliupbaord
+    // some magic where yy is system and dd is not.
+    pub use_system_clipboard: UseSystemClipboard,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+struct VimSettingsContent {
+    pub use_system_clipboard: Option<UseSystemClipboard>,
+}
+
+impl Settings for VimSettings {
+    const KEY: Option<&'static str> = Some("vim");
+
+    type FileContent = VimSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
 fn local_selections_changed(
     newest: Selection<usize>,
     is_multicursor: bool,

crates/vim/src/visual.rs 🔗

@@ -16,7 +16,7 @@ use crate::{
     motion::{start_of_line, Motion},
     object::Object,
     state::{Mode, Operator},
-    utils::copy_selections_content,
+    utils::{copy_selections_content, yank_selections_content},
     Vim,
 };
 
@@ -60,7 +60,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |vim, editor, cx| {
             let text_layout_details = editor.text_layout_details(cx);
             if vim.state().mode == Mode::VisualBlock
                 && !matches!(
@@ -251,7 +251,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
                 vim.switch_mode(target_mode, true, cx);
             }
 
-            vim.update_active_editor(cx, |editor, cx| {
+            vim.update_active_editor(cx, |_, editor, cx| {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_with(|map, selection| {
                         let mut head = selection.head();
@@ -298,7 +298,7 @@ fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
 
 pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.change_selections(None, cx, |s| {
                 s.move_with(|_, selection| {
                     selection.reversed = !selection.reversed;
@@ -311,7 +311,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
 pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |vim, editor, cx| {
             let mut original_columns: HashMap<_, _> = Default::default();
             let line_mode = editor.selections.line_mode;
 
@@ -328,7 +328,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
                         selection.goal = SelectionGoal::None;
                     });
                 });
-                copy_selections_content(editor, line_mode, cx);
+                copy_selections_content(vim, editor, line_mode, cx);
                 editor.insert("", cx);
 
                 // Fixup cursor position after the deletion
@@ -355,9 +355,9 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
 
 pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |vim, editor, cx| {
             let line_mode = editor.selections.line_mode;
-            copy_selections_content(editor, line_mode, cx);
+            yank_selections_content(vim, editor, line_mode, cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
                     if line_mode {
@@ -377,7 +377,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
 pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.stop_recording();
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             editor.transact(cx, |editor, cx| {
                 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 
@@ -426,7 +426,7 @@ pub fn select_next(
         let count =
             vim.take_count(cx)
                 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             for _ in 0..count {
                 match editor.select_next(&Default::default(), cx) {
                     Err(a) => return Err(a),
@@ -448,7 +448,7 @@ pub fn select_previous(
         let count =
             vim.take_count(cx)
                 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
-        vim.update_active_editor(cx, |editor, cx| {
+        vim.update_active_editor(cx, |_, editor, cx| {
             for _ in 0..count {
                 match editor.select_previous(&Default::default(), cx) {
                     Err(a) => return Err(a),