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] }