Vectrum
Sign In
113 lines · 2.8 KB · Go
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]
}