From 2e115644dbd9f9a633d93fef5fef611b5ab794d3 Mon Sep 17 00:00:00 2001 From: Finn Eitreim <48069764+feitreim@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:24:19 -0400 Subject: [PATCH] helix: Fix pasting from the system clipboard (#51703) 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. --- crates/vim/src/helix/paste.rs | 81 +++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs index 32f636b41046a5f8c8ade054594218890e23758f..c43281421462ee66e75d226b8769367f4db417b9 100644 --- a/crates/vim/src/helix/paste.rs +++ b/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 = 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;