From 9b076586f1626cdb3987892424bcf88730c5adad Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Wed, 18 Feb 2026 15:55:38 -0800 Subject: [PATCH] Fix crash: vim paste panics on editor-copied entire-line selections (#49134) When clipboard data was produced by the editor's copy/cut with multiple entire-line selections, vim's paste would panic with `byte index N is out of bounds`. The editor's `do_copy` and `cut_common` skip the `\n` separator between clipboard selections when the previous selection was an entire-line selection (because the text already ends with `\n`). However, vim's paste code unconditionally did `start_offset = end_offset + 1`, always assuming a `\n` separator exists between every pair of selections. This caused the accumulated offset to exceed the text length, resulting in a string slicing panic. The fix checks `clipboard_selection.is_entire_line` to decide whether to skip the separator, matching the behavior of the editor's own `do_paste` method. The same fix is applied to both the vim and helix paste implementations. Release Notes: - Fixed a crash when using vim paste on clipboard data copied with the editor's copy command containing multiple entire-line selections. --- crates/vim/src/helix/paste.rs | 6 +++- crates/vim/src/normal/paste.rs | 54 +++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs index d91b138853abb07dc10957a4ee1f5af158066e06..32f636b41046a5f8c8ade054594218890e23758f 100644 --- a/crates/vim/src/helix/paste.rs +++ b/crates/vim/src/helix/paste.rs @@ -66,7 +66,11 @@ impl Vim { let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) { let end_offset = start_offset + clip_sel.len; let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; + start_offset = if clip_sel.is_entire_line { + end_offset + } else { + end_offset + 1 + }; text } else if let Some(last_text) = replacement_texts.last() { // We have more current selections than clipboard selections: repeat the last one. diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 3143226b5afbfc75b61a3542552eae37f175b2a4..ec964ec9ae3af08b108aa027a0aa62883dbcbcc5 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -107,7 +107,11 @@ impl Vim { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; + start_offset = if clipboard_selection.is_entire_line { + end_offset + } else { + end_offset + 1 + }; (text, Some(clipboard_selection.first_line_indent)) } else { ("".to_string(), first_selection_indent_column) @@ -1083,4 +1087,52 @@ mod test { Mode::Normal, ); } + + #[gpui::test] + async fn test_paste_entire_line_from_editor_copy(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + ˇline one + line two + line three"}, + Mode::Normal, + ); + + // Simulate what the editor's do_copy produces for two entire-line selections: + // entire-line selections are NOT separated by an extra newline in the clipboard text. + let clipboard_text = "line one\nline two\n".to_string(); + let clipboard_selections = vec![ + editor::ClipboardSelection { + len: "line one\n".len(), + is_entire_line: true, + first_line_indent: 0, + file_path: None, + line_range: None, + }, + editor::ClipboardSelection { + len: "line two\n".len(), + is_entire_line: true, + first_line_indent: 0, + file_path: None, + line_range: None, + }, + ]; + cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( + clipboard_text, + clipboard_selections, + )); + + cx.simulate_keystrokes("p"); + cx.assert_state( + indoc! {" + line one + ˇline one + line two + line two + line three"}, + Mode::Normal, + ); + } }