mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
buffer_diff: Fix panic when staging hunks with stale buffer snapshot (#51641)
When the buffer is edited after the diff is computed but before staging, anchor positions shift while diff_base_byte_range values don't. If the primary (HEAD) hunk extends past the unstaged (index) hunk, an edit in the extension region causes the overshoot calculation to produce an index_end that exceeds index_text.len(), panicking in rope::Cursor::suffix. Fix by clamping index_end to index_text.len(). This is safe because the computed index text is an optimistic approximation — the real staging happens at the filesystem level via git add/git reset. Closes ZED-5R2 Release Notes: - Fixed a source of panics when staging diff hunks
This commit is contained in:
parent
a93c66e794
commit
ebd80d7995
1 changed files with 55 additions and 0 deletions
|
|
@ -843,6 +843,16 @@ impl BufferDiffInner<Entity<language::Buffer>> {
|
||||||
.end
|
.end
|
||||||
.saturating_sub(prev_unstaged_hunk_buffer_end);
|
.saturating_sub(prev_unstaged_hunk_buffer_end);
|
||||||
let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
|
let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
|
||||||
|
|
||||||
|
// Clamp to the index text bounds. The overshoot mapping assumes that
|
||||||
|
// text between unstaged hunks is identical in the buffer and index.
|
||||||
|
// When the buffer has been edited since the diff was computed, anchor
|
||||||
|
// positions shift while diff_base_byte_range values don't, which can
|
||||||
|
// cause index_end to exceed index_text.len().
|
||||||
|
// See `test_stage_all_with_stale_buffer` which would hit an assert
|
||||||
|
// without these min calls
|
||||||
|
let index_end = index_end.min(index_text.len());
|
||||||
|
let index_start = index_start.min(index_end);
|
||||||
let index_byte_range = index_start..index_end;
|
let index_byte_range = index_start..index_end;
|
||||||
|
|
||||||
let replacement_text = match new_status {
|
let replacement_text = match new_status {
|
||||||
|
|
@ -2678,6 +2688,51 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_stage_all_with_stale_buffer(cx: &mut TestAppContext) {
|
||||||
|
// Regression test for ZED-5R2: when the buffer is edited after the diff is
|
||||||
|
// computed but before staging, anchor positions shift while diff_base_byte_range
|
||||||
|
// values don't. If the primary (HEAD) hunk extends past the unstaged (index)
|
||||||
|
// hunk, an edit in the extension region shifts the primary hunk end without
|
||||||
|
// shifting the unstaged hunk end. The overshoot calculation then produces an
|
||||||
|
// index_end that exceeds index_text.len().
|
||||||
|
//
|
||||||
|
// Setup:
|
||||||
|
// HEAD: "aaa\nbbb\nccc\n" (primary hunk covers lines 1-2)
|
||||||
|
// Index: "aaa\nbbb\nCCC\n" (unstaged hunk covers line 1 only)
|
||||||
|
// Buffer: "aaa\nBBB\nCCC\n" (both lines differ from HEAD)
|
||||||
|
//
|
||||||
|
// The primary hunk spans buffer offsets 4..12, but the unstaged hunk only
|
||||||
|
// spans 4..8. The pending hunk extends 4 bytes past the unstaged hunk.
|
||||||
|
// An edit at offset 9 (inside "CCC") shifts the primary hunk end from 12
|
||||||
|
// to 13 but leaves the unstaged hunk end at 8, making index_end = 13 > 12.
|
||||||
|
let head_text = "aaa\nbbb\nccc\n";
|
||||||
|
let index_text = "aaa\nbbb\nCCC\n";
|
||||||
|
let buffer_text = "aaa\nBBB\nCCC\n";
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(
|
||||||
|
ReplicaId::LOCAL,
|
||||||
|
BufferId::new(1).unwrap(),
|
||||||
|
buffer_text.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(index_text, &buffer, cx));
|
||||||
|
let uncommitted_diff = cx.new(|cx| {
|
||||||
|
let mut diff = BufferDiff::new_with_base_text(head_text, &buffer, cx);
|
||||||
|
diff.set_secondary_diff(unstaged_diff);
|
||||||
|
diff
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit the buffer in the region between the unstaged hunk end (offset 8)
|
||||||
|
// and the primary hunk end (offset 12). This shifts the primary hunk end
|
||||||
|
// but not the unstaged hunk end.
|
||||||
|
buffer.edit([(9..9, "Z")]);
|
||||||
|
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
|
async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
|
||||||
let head_text = "
|
let head_text = "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue