From a95da07db20cf36bd750ecc4bdfecdf2b29e3f0f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 15 Sep 2026 15:27:26 +0000 Subject: [PATCH] helix: Fix paste in deleted diff hunks (#64245) Closes ZED-73F # Objective Pasting in Helix mode while the cursor was inside an expanded deleted diff hunk could create a selection past the buffer and crash. ## Solution Track the inserted range with before/after anchors instead of deriving it from the clipboard text length. If the edit is rejected for non-editable diff text, both anchors resolve to an empty valid range. ## Testing Added a regression test for pasting inside an expanded deleted hunk and ran the Helix paste tests. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed a crash when pasting in an expanded deleted diff hunk in Helix mode --- crates/vim/src/helix/paste.rs | 54 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs index b67c6243d3..17b325a420 100644 --- a/crates/vim/src/helix/paste.rs +++ b/crates/vim/src/helix/paste.rs @@ -1,8 +1,7 @@ -use editor::{ToOffset, movement}; +use editor::movement; use gpui::{Action, Context, Window}; use schemars::JsonSchema; use serde::Deserialize; -use text::LineEnding; use crate::{Vim, state::Mode}; @@ -121,33 +120,16 @@ impl Vim { sel.end }; let point = display_point.to_point(&display_map); - let anchor = if action.before { - display_map.buffer_snapshot().anchor_after(point) - } else { - display_map.buffer_snapshot().anchor_before(point) - }; - let mut to_insert = to_insert.repeat(count); - // Buffer edits normalize line endings, so measure the normalized text. - // Otherwise CRLF paste text can produce a selection range past the inserted text. - // which can cause panics (selection offset greater than snapshot len) or invalid - // selections - LineEnding::normalize(&mut to_insert); - new_selections.push((anchor, to_insert.len())); - edits.push((point..point, to_insert)); + let snapshot = display_map.buffer_snapshot(); + new_selections + .push(snapshot.anchor_before(point)..snapshot.anchor_after(point)); + edits.push((point..point, to_insert.repeat(count))); } editor.edit(edits, cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(new_selections.into_iter().map(|(anchor, len)| { - let offset = anchor.to_offset(&snapshot); - if action.before { - offset.saturating_sub_usize(len)..offset - } else { - offset..(offset + len) - } - })); + s.select_ranges(new_selections); }) }); }); @@ -224,6 +206,30 @@ mod test { cx.assert_state("«a\nbˇ»", Mode::HelixNormal); } + #[gpui::test] + async fn test_paste_in_expanded_deleted_hunk(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + cx.set_state("ˇkept\n", Mode::HelixNormal); + cx.set_head_text("kept\ndeleted\n"); + cx.update_editor(|editor, window, cx| { + editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx); + }); + + cx.write_to_clipboard(ClipboardItem::new_string("replacement text".to_string())); + cx.simulate_keystrokes("j p"); + + let buffer_text = cx.update_editor(|editor, _window, cx| { + let buffer = editor + .buffer() + .read(cx) + .as_singleton() + .expect("test editor should contain one buffer"); + buffer.read(cx).snapshot().text() + }); + assert_eq!(buffer_text, "kept\n"); + } + #[gpui::test] async fn test_read_only_paste(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await;