fix(edit): correct diff @@ hunk headers at file boundaries

hunkBounds approximated one-sided start lines by reusing the opposite side's number, and re-assigned it every iteration inside the count==0 guard. For pure insertion/deletion hunks (e.g. writing into an empty file, or deleting a whole file) this produced wrong headers like '@@ -2,0 +1,2 @@' instead of '@@ -0,0 +1,2 @@'. flattenDiffLines now records both running line numbers on every line, so hunkBounds takes the start from the first line directly and applies GNU's zero-count convention (line-before-change, clamped to 0). Adds TestGenerateDiffHunkHeaderBounds.
This commit is contained in:
2026-06-20 00:33:32 +01:00
parent 53bc06df7f
commit b2c4101b5b
2 changed files with 42 additions and 23 deletions
+19 -23
View File
@@ -642,16 +642,19 @@ func flattenDiffLines(diffs []diffmatchpatch.Diff) (lines []diffLine, maxOldN, m
// Strip the terminator for display; also drop a trailing CR so CRLF files
// do not leak raw carriage returns into the rendered diff.
text := strings.TrimSuffix(strings.TrimSuffix(raw, "\n"), "\r")
// Record both running line numbers on every line (not just the active
// side): the opposite-side counter is the position the change sits at,
// which hunkBounds needs to compute correct @@ header start lines.
switch d.Type {
case diffmatchpatch.DiffEqual:
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
oldLine++
newLine++
case diffmatchpatch.DiffDelete:
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine})
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
oldLine++
case diffmatchpatch.DiffInsert:
lines = append(lines, diffLine{op: d.Type, text: text, newN: newLine})
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
newLine++
}
}
@@ -680,38 +683,31 @@ func diffHunkRanges(lines []diffLine) []indexRange {
}
// hunkBounds computes the unified-diff hunk header line numbers and counts for
// lines[start:end+1]. newStart/oldStart for one-sided lines are approximate.
// lines[start:end+1]. Each line carries both running line numbers (see
// flattenDiffLines), so the hunk start is simply the first line's numbers. When a
// side has zero lines in the hunk (pure insertion or deletion), the header start
// for that side follows GNU's convention: the line number *before* the change,
// clamped to 0 at the start of the file (e.g. "@@ -0,0 +1,N @@").
func hunkBounds(lines []diffLine, start, end int) (oldStart, oldCount, newStart, newCount int) {
oldStart = lines[start].oldN
newStart = lines[start].newN
for i := start; i <= end; i++ {
l := lines[i]
switch l.op {
switch lines[i].op {
case diffmatchpatch.DiffEqual:
if oldCount == 0 {
oldStart = l.oldN
}
if newCount == 0 {
newStart = l.newN
}
oldCount++
newCount++
case diffmatchpatch.DiffDelete:
if oldCount == 0 {
oldStart = l.oldN
}
if newCount == 0 {
newStart = l.oldN // approximate
}
oldCount++
case diffmatchpatch.DiffInsert:
if newCount == 0 {
newStart = l.newN
}
if oldCount == 0 {
oldStart = l.newN // approximate
}
newCount++
}
}
if oldCount == 0 {
oldStart = max(0, oldStart-1)
}
if newCount == 0 {
newStart = max(0, newStart-1)
}
return
}