192 lines
4.1 KiB
Go
192 lines
4.1 KiB
Go
package internal
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// DiffOp says a line is kept, removed or added.
|
|
type DiffOp uint8
|
|
|
|
const (
|
|
DiffEqual DiffOp = iota
|
|
DiffDelete
|
|
DiffInsert
|
|
)
|
|
|
|
// DiffLine is a numbered diff line; 0 means absent.
|
|
type DiffLine struct {
|
|
Op DiffOp
|
|
Text string
|
|
Old, New int
|
|
}
|
|
|
|
// SplitLines splits text into lines; "" has none.
|
|
func SplitLines(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(strings.TrimSuffix(s, "\n"), "\n")
|
|
}
|
|
|
|
// LineDiff returns a shortest edit script from a to b.
|
|
func LineDiff(a, b []string) []DiffLine {
|
|
// Common prefix and suffix need no search.
|
|
pre := 0
|
|
for pre < len(a) && pre < len(b) && a[pre] == b[pre] {
|
|
pre++
|
|
}
|
|
suf := 0
|
|
for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] {
|
|
suf++
|
|
}
|
|
out := make([]DiffLine, 0, len(a)+len(b)-pre-suf)
|
|
for i := range pre {
|
|
out = append(out, DiffLine{DiffEqual, a[i], i + 1, i + 1})
|
|
}
|
|
out = myers(out, a[pre:len(a)-suf], b[pre:len(b)-suf], pre, pre)
|
|
for i := range suf {
|
|
ai, bi := len(a)-suf+i, len(b)-suf+i
|
|
out = append(out, DiffLine{DiffEqual, a[ai], ai + 1, bi + 1})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// myers appends the edit script, numbering from ao and bo.
|
|
func myers(out []DiffLine, a, b []string, ao, bo int) []DiffLine {
|
|
n, m := len(a), len(b)
|
|
total := n + m
|
|
if total == 0 {
|
|
return out
|
|
}
|
|
off := total + 1
|
|
v := make([]int, 2*total+3)
|
|
var trace [][]int
|
|
end := -1
|
|
for d := 0; d <= total && end < 0; d++ {
|
|
trace = append(trace, append([]int(nil), v[off-d:off+d+1]...)) // diagonals -d..d
|
|
for k := -d; k <= d; k += 2 {
|
|
var x int
|
|
if k == -d || k != d && v[off+k-1] < v[off+k+1] {
|
|
x = v[off+k+1]
|
|
} else {
|
|
x = v[off+k-1] + 1
|
|
}
|
|
y := x - k
|
|
for x < n && y < m && a[x] == b[y] {
|
|
x, y = x+1, y+1
|
|
}
|
|
v[off+k] = x
|
|
if x >= n && y >= m {
|
|
end = d
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Walk the trace back, collecting moves in reverse.
|
|
var rev []DiffLine
|
|
x, y := n, m
|
|
for d := end; d >= 0; d-- {
|
|
vd := trace[d]
|
|
at := func(k int) int { return vd[k+d] }
|
|
k := x - y
|
|
var pk int
|
|
if k == -d || k != d && at(k-1) < at(k+1) {
|
|
pk = k + 1
|
|
} else {
|
|
pk = k - 1
|
|
}
|
|
px := 0
|
|
if d > 0 {
|
|
px = at(pk)
|
|
}
|
|
py := px - pk
|
|
for x > px && y > py {
|
|
x, y = x-1, y-1
|
|
rev = append(rev, DiffLine{DiffEqual, a[x], ao + x + 1, bo + y + 1})
|
|
}
|
|
if d == 0 {
|
|
break
|
|
}
|
|
if x == px {
|
|
y--
|
|
rev = append(rev, DiffLine{DiffInsert, b[y], 0, bo + y + 1})
|
|
} else {
|
|
x--
|
|
rev = append(rev, DiffLine{DiffDelete, a[x], ao + x + 1, 0})
|
|
}
|
|
}
|
|
for i := len(rev) - 1; i >= 0; i-- {
|
|
out = append(out, rev[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// WordSpan is a run of a line that a word diff kept, removed or added.
|
|
type WordSpan struct {
|
|
Op DiffOp
|
|
Text string
|
|
}
|
|
|
|
// WordDiff compares two lines by words: from holds kept and removed spans, to kept and added.
|
|
func WordDiff(a, b string) (from, to []WordSpan) {
|
|
add := func(spans []WordSpan, op DiffOp, s string) []WordSpan {
|
|
if n := len(spans); n > 0 && spans[n-1].Op == op {
|
|
spans[n-1].Text += s
|
|
return spans
|
|
}
|
|
return append(spans, WordSpan{op, s})
|
|
}
|
|
for _, d := range LineDiff(wordTokens(a), wordTokens(b)) {
|
|
switch d.Op {
|
|
case DiffEqual:
|
|
from, to = add(from, DiffEqual, d.Text), add(to, DiffEqual, d.Text)
|
|
case DiffDelete:
|
|
from = add(from, DiffDelete, d.Text)
|
|
case DiffInsert:
|
|
to = add(to, DiffInsert, d.Text)
|
|
}
|
|
}
|
|
return from, to
|
|
}
|
|
|
|
// wordTokens splits s into words, runs of spaces and single other characters.
|
|
func wordTokens(s string) []string {
|
|
var out []string
|
|
kind := func(r rune) int {
|
|
switch {
|
|
case r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r):
|
|
return 1
|
|
case unicode.IsSpace(r):
|
|
return 2
|
|
}
|
|
return 0
|
|
}
|
|
start, prev := 0, -1
|
|
for i, r := range s {
|
|
k := kind(r)
|
|
if i > start && (k != prev || k == 0) {
|
|
out = append(out, s[start:i])
|
|
start = i
|
|
}
|
|
prev = k
|
|
}
|
|
if start < len(s) {
|
|
out = append(out, s[start:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DiffStats counts the added and removed lines of a diff.
|
|
func DiffStats(d []DiffLine) (added, removed int) {
|
|
for _, l := range d {
|
|
switch l.Op {
|
|
case DiffInsert:
|
|
added++
|
|
case DiffDelete:
|
|
removed++
|
|
}
|
|
}
|
|
return added, removed
|
|
}
|