Fix crash: vim paste panics on editor-copied entire-line selections (#49134)

Eric Holk created

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.

Change summary

crates/vim/src/helix/paste.rs  |  6 +++
crates/vim/src/normal/paste.rs | 54 +++++++++++++++++++++++++++++++++++
2 files changed, 58 insertions(+), 2 deletions(-)

Detailed changes

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.

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,
+        );
+    }
 }