Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Package diff renders a line-based difference between two texts. The
// classic longest-common-subsequence dynamic program is enough here:
// post-sized inputs are small, and inputs beyond the cell budget fall
// back to an honest full replacement rather than a slow or hungry walk.
package diff
import "strings"
// Op classifies one line of the result.
type Op uint8
const (
// Equal marks a line both texts share.
Equal Op = iota
// Removed marks a line only the old text carries.
Removed
// Added marks a line only the new text carries.
Added
// Skipped marks equal lines the context collapse hid.
Skipped
)
// Is reports whether the operation matches the name a template asks
// for: "equal", "removed", "added" or "skipped".
func (o Op) Is(name string) bool {
switch name {
case "equal":
return o == Equal
case "removed":
return o == Removed
case "added":
return o == Added
case "skipped":
return o == Skipped
}
return false
}
// Chunk is a run of consecutive lines with one operation.
type Chunk struct {
Op Op
Lines []string
Skipped int // Skipped only: how many equal lines the collapse hid
}
// maxCells bounds the dynamic-programming table. Past it, the diff
// degrades to a whole-text replacement: rare, and never wrong.
const maxCells = 4_000_000
// Chunks diffs the old text against the new one line by line and
// collapses long equal runs to context lines around each change. The
// texts are split on newlines; a trailing newline does not create an
// empty final line.
func Chunks(oldText, newText string, context int) []Chunk {
oldLines := split(oldText)
newLines := split(newText)
ops := make([]Op, 0, len(oldLines)+len(newLines))
if len(oldLines)*len(newLines) > maxCells {
ops = appendAll(ops, Removed, oldLines)
ops = appendAll(ops, Added, newLines)
} else {
ops = lcs(oldLines, newLines)
}
return collapse(ops, oldLines, newLines, context)
}
// split breaks a text into lines without a phantom empty line for the
// trailing newline.
func split(text string) []string {
text = strings.TrimSuffix(text, "\n")
if text == "" {
return nil
}
return strings.Split(text, "\n")
}
func appendAll(ops []Op, op Op, lines []string) []Op {
for range lines {
ops = append(ops, op)
}
return ops
}
// lcs walks the dynamic-programming table backwards and yields the
// per-line operations.
func lcs(a, b []string) []Op {
n, m := len(a), len(b)
// table[i][j] holds the LCS length of a[i:] and b[j:].
table := make([]int32, (n+1)*(m+1))
at := func(i, j int) *int32 { return &table[i*(m+1)+j] }
for i := n - 1; i >= 0; i-- {
for j := m - 1; j >= 0; j-- {
if a[i] == b[j] {
*at(i, j) = *at(i+1, j+1) + 1
} else {
down, right := *at(i+1, j), *at(i, j+1)
*at(i, j) = max(down, right)
}
}
}
ops := make([]Op, 0, n+m)
i, j := 0, 0
for i < n && j < m {
switch {
case a[i] == b[j]:
ops = append(ops, Equal)
i++
j++
case *at(i+1, j) >= *at(i, j+1):
ops = append(ops, Removed)
i++
default:
ops = append(ops, Added)
j++
}
}
ops = appendAll(ops, Removed, a[i:])
ops = appendAll(ops, Added, b[j:])
return ops
}
// collapse groups the operations into chunks and trims equal runs to
// context lines around the changes.
func collapse(ops []Op, a, b []string, context int) []Chunk {
// Position each operation over its source line.
type line struct {
op Op
text string
}
out := make([]line, 0, len(ops))
ai, bi := 0, 0
for _, op := range ops {
switch op {
case Removed:
out = append(out, line{Removed, a[ai]})
ai++
case Added:
out = append(out, line{Added, b[bi]})
bi++
default:
out = append(out, line{Equal, a[ai]})
ai++
bi++
}
}
var chunks []Chunk
for start := 0; start < len(out); {
op := out[start].op
end := start
for end < len(out) && out[end].op == op {
end++
}
lines := make([]string, 0, end-start)
for _, l := range out[start:end] {
lines = append(lines, l.text)
}
chunks = append(chunks, Chunk{Op: op, Lines: lines})
start = end
}
// Collapse an equal chunk only when another change follows it; the
// leading context of the first change stays whole.
if context < 0 {
context = 0
}
final := make([]Chunk, 0, len(chunks))
for _, chunk := range chunks {
final = append(final, chunk)
}
for i := 0; i < len(final)-1; i++ {
c := &final[i]
if c.Op != Equal || len(c.Lines) <= 2*context+4 {
continue
}
hidden := len(c.Lines) - 2*context
collapsed := Chunk{Op: Skipped, Skipped: hidden}
with := make([]Chunk, 0, len(final)+1)
with = append(with, final[:i]...)
with = append(with, Chunk{Op: Equal, Lines: c.Lines[:context]})
with = append(with, collapsed)
with = append(with, Chunk{Op: Equal, Lines: c.Lines[len(c.Lines)-context:]})
with = append(with, final[i+1:]...)
final = with
i++
}
return final
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package diff
import (
"strings"
"testing"
)
func render(chunks []Chunk) string {
var b strings.Builder
for _, c := range chunks {
switch c.Op {
case Equal:
for _, l := range c.Lines {
b.WriteString(" " + l + "\n")
}
case Removed:
for _, l := range c.Lines {
b.WriteString("- " + l + "\n")
}
case Added:
for _, l := range c.Lines {
b.WriteString("+ " + l + "\n")
}
case Skipped:
b.WriteString("… " + strings.Repeat("#", c.Skipped) + "\n")
}
}
return b.String()
}
func TestIdentical(t *testing.T) {
got := render(Chunks("alpha\nbeta\n", "alpha\nbeta\n", 3))
if got != " alpha\n beta\n" {
t.Errorf("identical diff = %q", got)
}
}
func TestSingleChange(t *testing.T) {
got := render(Chunks("alpha\nbeta\ngamma\n", "alpha\nbeta+\ngamma\n", 3))
want := " alpha\n- beta\n+ beta+\n gamma\n"
if got != want {
t.Errorf("change diff = %q, want %q", got, want)
}
}
func TestInsertAndDelete(t *testing.T) {
got := render(Chunks("one\ntwo\n", "one\nthree\nfour\ntwo\n", 3))
want := " one\n+ three\n+ four\n two\n"
if got != want {
t.Errorf("insert diff = %q, want %q", got, want)
}
got = render(Chunks("one\nthree\ntwo\n", "one\ntwo\n", 3))
want = " one\n- three\n two\n"
if got != want {
t.Errorf("delete diff = %q, want %q", got, want)
}
}
func TestEmptySides(t *testing.T) {
got := render(Chunks("", "alpha\n", 3))
if got != "+ alpha\n" {
t.Errorf("empty old = %q", got)
}
got = render(Chunks("alpha\n", "", 3))
if got != "- alpha\n" {
t.Errorf("empty new = %q", got)
}
got = render(Chunks("", "", 3))
if got != "" {
t.Errorf("both empty = %q", got)
}
}
func TestContextCollapse(t *testing.T) {
// Twelve equal lines between two changes: with context 3 the middle
// six collapse into one skipped chunk.
old := "x1\nx2\nx3\nx4\nx5\nx6\nx7\nx8\nx9\nx10\nx11\nx12\nx13\nx14\n"
new := "A1\nx2\nx3\nx4\nx5\nx6\nx7\nx8\nx9\nx10\nx11\nx12\nx13\nA14\n"
got := render(Chunks(old, new, 3))
want := "- x1\n+ A1\n" +
" x2\n x3\n x4\n" +
"… ######\n" +
" x11\n x12\n x13\n" +
"- x14\n+ A14\n"
if got != want {
t.Errorf("collapse diff = %q, want %q", got, want)
}
}
func TestTrailingNewlineIsNotALine(t *testing.T) {
with := render(Chunks("alpha\n", "alpha\n", 3))
without := render(Chunks("alpha", "alpha", 3))
if with != without {
t.Errorf("trailing newline changed the diff: %q vs %q", with, without)
}
}
func TestCellBudgetFallsBackToReplace(t *testing.T) {
// 2500 equal lines on both sides exceeds the cell budget; the diff
// must degrade to a full replacement instead of hanging.
var b strings.Builder
for range 2500 {
b.WriteString(strings.Repeat("x", 40) + "\n")
}
big := b.String()
chunks := Chunks(big, big, 3)
var removed, added bool
for _, c := range chunks {
switch c.Op {
case Removed:
removed = true
case Added:
added = true
}
}
if !removed || !added {
t.Errorf("oversized input did not degrade to replacement (removed=%v added=%v)", removed, added)
}
}
func TestOpIs(t *testing.T) {
if !Removed.Is("removed") || Added.Is("removed") || Skipped.Is("skipped") == false {
t.Error("Op.Is misclassifies")
}
}