package tools
import (
"fmt"
"strings"
)
// diffContext is how many unchanged lines to show either side of a change.
const diffContext = 3
type diffOp struct {
kind byte // ' ' keep, '-' remove, '+' add
text string
}
// diffLines is a line-level LCS diff. The regions being diffed are edit-sized,
// so the quadratic table is not a problem; the guard below is for the
// pathological case of a whole-file replacement.
func diffLines(a, b []string) []diffOp {
if len(a)*len(b) > 4_000_000 {
ops := make([]diffOp, 0, len(a)+len(b))
for _, l := range a {
ops = append(ops, diffOp{'-', l})
}
for _, l := range b {
ops = append(ops, diffOp{'+', l})
}
return ops
}
// lcs[i][j] is the length of the longest common subsequence of a[i:] and b[j:].
lcs := make([][]int, len(a)+1)
for i := range lcs {
lcs[i] = make([]int, len(b)+1)
}
for i := len(a) - 1; i >= 0; i-- {
for j := len(b) - 1; j >= 0; j-- {
if a[i] == b[j] {
lcs[i][j] = lcs[i+1][j+1] + 1
} else if lcs[i+1][j] >= lcs[i][j+1] {
lcs[i][j] = lcs[i+1][j]
} else {
lcs[i][j] = lcs[i][j+1]
}
}
}
var ops []diffOp
i, j := 0, 0
for i < len(a) && j < len(b) {
switch {
case a[i] == b[j]:
ops = append(ops, diffOp{' ', a[i]})
i++
j++
case lcs[i+1][j] >= lcs[i][j+1]:
ops = append(ops, diffOp{'-', a[i]})
i++
default:
ops = append(ops, diffOp{'+', b[j]})
j++
}
}
for ; i < len(a); i++ {
ops = append(ops, diffOp{'-', a[i]})
}
for ; j < len(b); j++ {
ops = append(ops, diffOp{'+', b[j]})
}
return ops
}
// unifiedDiff renders the change to a region as a unified diff hunk. startLine
// is the 1-indexed line in the original file where oldRegion begins, and before
// and after are the surrounding context lines.
func unifiedDiff(path string, before, oldRegion, newRegion, after []string, startLine int) string {
ops := diffLines(oldRegion, newRegion)
oldStart := startLine - len(before)
if oldStart < 1 {
oldStart = 1
}
oldCount := len(before) + len(oldRegion) + len(after)
newCount := len(before) + len(newRegion) + len(after)
var b strings.Builder
fmt.Fprintf(&b, "--- %s\n+++ %s\n", path, path)
fmt.Fprintf(&b, "@@ -%d,%d +%d,%d @@\n", oldStart, oldCount, oldStart, newCount)
for _, l := range before {
fmt.Fprintf(&b, " %s\n", l)
}
for _, op := range ops {
fmt.Fprintf(&b, "%c%s\n", op.kind, op.text)
}
for _, l := range after {
fmt.Fprintf(&b, " %s\n", l)
}
return strings.TrimRight(b.String(), "\n")
}
// contextAround returns up to diffContext lines either side of [start, end) in
// lines, where start and end are 0-indexed.
func contextAround(lines []string, start, end int) (before, after []string) {
lo := start - diffContext
if lo < 0 {
lo = 0
}
hi := end + diffContext
if hi > len(lines) {
hi = len(lines)
}
return lines[lo:start], lines[end:hi]
}