mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
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:
+19
-23
@@ -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
|
// Strip the terminator for display; also drop a trailing CR so CRLF files
|
||||||
// do not leak raw carriage returns into the rendered diff.
|
// do not leak raw carriage returns into the rendered diff.
|
||||||
text := strings.TrimSuffix(strings.TrimSuffix(raw, "\n"), "\r")
|
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 {
|
switch d.Type {
|
||||||
case diffmatchpatch.DiffEqual:
|
case diffmatchpatch.DiffEqual:
|
||||||
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
|
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
|
||||||
oldLine++
|
oldLine++
|
||||||
newLine++
|
newLine++
|
||||||
case diffmatchpatch.DiffDelete:
|
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++
|
oldLine++
|
||||||
case diffmatchpatch.DiffInsert:
|
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++
|
newLine++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,38 +683,31 @@ func diffHunkRanges(lines []diffLine) []indexRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hunkBounds computes the unified-diff hunk header line numbers and counts for
|
// 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) {
|
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++ {
|
for i := start; i <= end; i++ {
|
||||||
l := lines[i]
|
switch lines[i].op {
|
||||||
switch l.op {
|
|
||||||
case diffmatchpatch.DiffEqual:
|
case diffmatchpatch.DiffEqual:
|
||||||
if oldCount == 0 {
|
|
||||||
oldStart = l.oldN
|
|
||||||
}
|
|
||||||
if newCount == 0 {
|
|
||||||
newStart = l.newN
|
|
||||||
}
|
|
||||||
oldCount++
|
oldCount++
|
||||||
newCount++
|
newCount++
|
||||||
case diffmatchpatch.DiffDelete:
|
case diffmatchpatch.DiffDelete:
|
||||||
if oldCount == 0 {
|
|
||||||
oldStart = l.oldN
|
|
||||||
}
|
|
||||||
if newCount == 0 {
|
|
||||||
newStart = l.oldN // approximate
|
|
||||||
}
|
|
||||||
oldCount++
|
oldCount++
|
||||||
case diffmatchpatch.DiffInsert:
|
case diffmatchpatch.DiffInsert:
|
||||||
if newCount == 0 {
|
|
||||||
newStart = l.newN
|
|
||||||
}
|
|
||||||
if oldCount == 0 {
|
|
||||||
oldStart = l.newN // approximate
|
|
||||||
}
|
|
||||||
newCount++
|
newCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if oldCount == 0 {
|
||||||
|
oldStart = max(0, oldStart-1)
|
||||||
|
}
|
||||||
|
if newCount == 0 {
|
||||||
|
newStart = max(0, newStart-1)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,6 +427,29 @@ func TestGenerateDiffLineLevelAccuracy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hunk @@ headers must be correct at file boundaries (previously approximate).
|
||||||
|
func TestGenerateDiffHunkHeaderBounds(t *testing.T) {
|
||||||
|
e := &Engine{dmp: diffmatchpatch.New()}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
original string
|
||||||
|
modified string
|
||||||
|
wantHdr string
|
||||||
|
}{
|
||||||
|
{name: "insert into empty file", original: "", modified: "X\nY\n", wantHdr: "@@ -0,0 +1,2 @@"},
|
||||||
|
{name: "delete entire file", original: "a\nb\n", modified: "", wantHdr: "@@ -1,2 +0,0 @@"},
|
||||||
|
{name: "mid-file change keeps 1-based start", original: "l1\nl2\nl3\nl4\nl5\n", modified: "l1\nl2\nCHANGED\nl4\nl5\n", wantHdr: "@@ -1,5 +1,5 @@"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
diff := e.generateDiff(tt.original, tt.modified, "f.txt")
|
||||||
|
if !strings.Contains(diff, tt.wantHdr) {
|
||||||
|
t.Errorf("expected hunk header %q, got:\n%s", tt.wantHdr, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateDiffNoPhantomChanges(t *testing.T) {
|
func TestGenerateDiffNoPhantomChanges(t *testing.T) {
|
||||||
// Regression test: replacing a line range should not produce phantom
|
// Regression test: replacing a line range should not produce phantom
|
||||||
// +/- lines for unchanged code after the edit region.
|
// +/- lines for unchanged code after the edit region.
|
||||||
|
|||||||
Reference in New Issue
Block a user