vim: Preserve system clipboard when pasting over visual selection (#52948)

João Soares created

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #52352

Release Notes:

- Fixed system clipboard being overwritten when pasting over a visual
selection with Cmd-V/Ctrl-V in vim mode

Change summary

crates/vim/src/normal/paste.rs | 37 +++++++++++++++++++++++++++++++++++
crates/vim/src/vim.rs          |  4 ++
2 files changed, 39 insertions(+), 2 deletions(-)

Detailed changes

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

@@ -25,7 +25,7 @@ pub struct Paste {
     #[serde(default)]
     before: bool,
     #[serde(default)]
-    preserve_clipboard: bool,
+    pub(crate) preserve_clipboard: bool,
 }
 
 impl Vim {
@@ -835,6 +835,41 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_editor_paste_visual_preserves_system_clipboard(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox ˇjumps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+
+        // Put known content on the system clipboard
+        cx.write_to_clipboard(ClipboardItem::new_string("from clipboard".to_string()));
+
+        // Select "jumps" in visual mode, then editor::Paste (Cmd-V / Ctrl-V)
+        cx.simulate_keystrokes("v i w");
+        cx.dispatch_action(editor::actions::Paste);
+
+        // The selected text should be replaced with clipboard content
+        cx.assert_state(
+            indoc! {"
+                The quick brown
+                fox from clipboarˇd over
+                the lazy dog"},
+            Mode::Normal,
+        );
+
+        // System clipboard must still hold the original value, not "jumps"
+        assert_eq!(
+            cx.read_from_clipboard().map(|item| item.text().unwrap()),
+            Some("from clipboard".into()),
+        );
+    }
+
     #[gpui::test]
     async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/vim.rs 🔗

@@ -965,7 +965,9 @@ impl Vim {
                     Mode::Replace => vim.paste_replace(window, cx),
                     Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
                         vim.selected_register.replace('+');
-                        vim.paste(&VimPaste::default(), window, cx);
+                        let mut action = VimPaste::default();
+                        action.preserve_clipboard = true;
+                        vim.paste(&action, window, cx);
                     }
                     _ => {
                         vim.update_editor(cx, |_, editor, cx| editor.paste(&Paste, window, cx));