diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 82ab2736b8bc207aa30952ae9f79f161eb9db8db..c0f62ed8fc1c990b3bb4aaef5fff5ae23bebff86 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -843,6 +843,16 @@ impl BufferDiffInner> { .end .saturating_sub(prev_unstaged_hunk_buffer_end); 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 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] async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) { let head_text = "