helix: Fix pasting from the system clipboard (#51703)

Finn Eitreim created

Closes #51693

Helix was unable to paste from the system clipboard, ex: `vim: paste`
would work but `helix: paste` would not work. Helix paste was silently
requiring the content it was going to paste to have selection metadata
exist, and just silently fail if it didn't, and the system clipboard
doesn't have that metadata. note: this is not necessarily for parity
with helix, as helix didn't seem to support this either in my testing,
but rather parity with the other parts of zed, editor mode and vim mode.

single-line paste:


https://github.com/user-attachments/assets/c8696032-d265-4025-9c4c-a8c35dfd2529

multi-line paste:


https://github.com/user-attachments/assets/4bf96033-e13d-4ec1-8a7e-8c56bbc12b94

I also added a new test verifying the behavior.

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- helix: fixed helix paste not pasting from system clipboard.

Change summary

crates/vim/src/helix/paste.rs | 81 ++++++++++++++++++++++++++++++------
1 file changed, 67 insertions(+), 14 deletions(-)

Detailed changes

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

@@ -33,16 +33,14 @@ impl Vim {
 
                 let selected_register = vim.selected_register.take();
 
-                let Some((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| {
+                let Some(register) = Vim::update_globals(cx, |globals, cx| {
                     globals.read_register(selected_register, Some(editor), cx)
                 })
-                .and_then(|reg| {
-                    (!reg.text.is_empty())
-                        .then_some(reg.text)
-                        .zip(reg.clipboard_selections)
-                }) else {
+                .filter(|reg| !reg.text.is_empty()) else {
                     return;
                 };
+                let text = register.text;
+                let clipboard_selections = register.clipboard_selections;
 
                 let display_map = editor.display_snapshot(cx);
                 let current_selections = editor.selections.all_adjusted_display(&display_map);
@@ -63,7 +61,9 @@ impl Vim {
                 let mut replacement_texts: Vec<String> = Vec::new();
 
                 for ix in 0..current_selections.len() {
-                    let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) {
+                    let to_insert = if let Some(clip_sel) =
+                        clipboard_selections.as_ref().and_then(|s| s.get(ix))
+                    {
                         let end_offset = start_offset + clip_sel.len;
                         let text = text[start_offset..end_offset].to_string();
                         start_offset = if clip_sel.is_entire_line {
@@ -102,13 +102,16 @@ impl Vim {
                     } else if action.before {
                         sel.start
                     } else if sel.start == sel.end {
-                        // Helix and Zed differ in how they understand
-                        // single-point cursors. In Helix, a single-point cursor
-                        // is "on top" of some character, and pasting after that
-                        // cursor means that the pasted content should go after
-                        // that character. (If the cursor is at the end of a
-                        // line, the pasted content goes on the next line.)
-                        movement::right(&display_map, sel.end)
+                        // In Helix, a single-point cursor is "on top" of a
+                        // character, and pasting after means after that character.
+                        // At line end this means the next line. But on an empty
+                        // line there is no character, so paste at the cursor.
+                        let right = movement::right(&display_map, sel.end);
+                        if right.row() != sel.end.row() && sel.end.column() == 0 {
+                            sel.end
+                        } else {
+                            right
+                        }
                     } else {
                         sel.end
                     };
@@ -146,8 +149,58 @@ impl Vim {
 mod test {
     use indoc::indoc;
 
+    use gpui::ClipboardItem;
+
     use crate::{state::Mode, test::VimTestContext};
 
+    #[gpui::test]
+    async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+        cx.set_state(
+            indoc! {"
+            The quiˇck brown
+            fox jumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
+        cx.simulate_keystrokes("p");
+        cx.assert_state(
+            indoc! {"
+            The quic«clipboardˇ»k brown
+            fox jumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        // Multiple cursors with system clipboard (no metadata) pastes
+        // the same text at each cursor.
+        cx.set_state(
+            indoc! {"
+            ˇThe quick brown
+            fox ˇjumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+        cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
+        cx.simulate_keystrokes("p");
+        cx.assert_state(
+            indoc! {"
+            T«hiˇ»he quick brown
+            fox j«hiˇ»umps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        // Multiple cursors on empty lines should paste on those same lines.
+        cx.set_state("ˇ\nˇ\nˇ\nend", Mode::HelixNormal);
+        cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
+        cx.simulate_keystrokes("p");
+        cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
+    }
+
     #[gpui::test]
     async fn test_paste(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;