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
This commit is contained in:
Agus Zubiaga authored and GitHub committed 2026-09-15 15:27:26 +00:00
1 parent 3c779a6df0
commit 488309b187
1 file changed
+30 -24
+30 -24
View File
@@ -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;