1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152 |
package diff
import (
"strings"
"unicode/utf8"
"git.pablomurad.com/pablo/patchwork/internal/doc"
)
// Segment kinds inside a modified block.
const (
SegEq = "eq"
SegDel = "del"
SegIns = "ins"
)
// Segment is a run of text in a word diff.
type Segment struct {
T string `json:"t"`
S string `json:"s"`
}
// replaceRatio: above this share of changed text, highlights stop helping and
// the block is shown as old text, then new text.
const replaceRatio = 0.6
// Words diffs two texts word by word. Concatenating the eq and del segments
// gives back old; eq and ins give back new.
func Words(old, new string) (segs []Segment, replaced bool, added, removed int) {
ta, tb := doc.Tokenize(old), doc.Tokenize(new)
for _, e := range Myers(ta, tb) {
switch e.Kind {
case Eq:
segs = appendSeg(segs, SegEq, strings.Join(ta[e.A0:e.A1], ""))
case Del:
segs = appendSeg(segs, SegDel, strings.Join(ta[e.A0:e.A1], ""))
case Ins:
segs = appendSeg(segs, SegIns, strings.Join(tb[e.B0:e.B1], ""))
}
}
segs = cleanup(segs)
changed := 0
for _, s := range segs {
switch s.T {
case SegDel:
changed += utf8.RuneCountInString(s.S)
removed += countWords(s.S)
case SegIns:
changed += utf8.RuneCountInString(s.S)
added += countWords(s.S)
}
}
total := utf8.RuneCountInString(old) + utf8.RuneCountInString(new)
if total > 0 && float64(changed)/float64(total) > replaceRatio {
segs = nil
if old != "" {
segs = append(segs, Segment{SegDel, old})
}
if new != "" {
segs = append(segs, Segment{SegIns, new})
}
return segs, true, countWords(new), countWords(old)
}
return segs, false, added, removed
}
// cleanup absorbs short equalities trapped between edits ("the cat sat" →
// "a dog sat" should not keep " " as a lonely match), the semantic cleanup of
// diff-match-patch: an equality no longer than the edits on both sides of it
// becomes part of them. Each edit group is then written as one del followed
// by one ins.
func cleanup(segs []Segment) []Segment {
for changed := true; changed; {
changed = false
groups := group(segs)
for i := 1; i+1 < len(groups); i++ {
g := groups[i]
if g.eq == "" || groups[i-1].eq != "" || groups[i+1].eq != "" {
continue
}
n := utf8.RuneCountInString(g.eq)
if n <= groups[i-1].size() && n <= groups[i+1].size() {
groups[i] = edit{del: g.eq, ins: g.eq}
segs = ungroup(groups)
changed = true
break
}
}
}
return ungroup(group(segs))
}
type edit struct{ eq, del, ins string }
func (e edit) size() int { return utf8.RuneCountInString(e.del) + utf8.RuneCountInString(e.ins) }
// group merges consecutive segments: every equality is its own group and
// every run of del/ins between equalities is one edit group.
func group(segs []Segment) []edit {
var out []edit
for _, s := range segs {
if s.T == SegEq {
out = append(out, edit{eq: s.S})
continue
}
if len(out) == 0 || out[len(out)-1].eq != "" {
out = append(out, edit{})
}
last := &out[len(out)-1]
if s.T == SegDel {
last.del += s.S
} else {
last.ins += s.S
}
}
// Adjacent edit groups (left after absorbing an equality) merge.
merged := out[:0]
for _, g := range out {
if n := len(merged); n > 0 && g.eq == "" && merged[n-1].eq == "" {
merged[n-1].del += g.del
merged[n-1].ins += g.ins
continue
}
merged = append(merged, g)
}
return merged
}
func ungroup(groups []edit) []Segment {
var out []Segment
for _, g := range groups {
if g.eq != "" {
out = appendSeg(out, SegEq, g.eq)
continue
}
out = appendSeg(out, SegDel, g.del)
out = appendSeg(out, SegIns, g.ins)
}
return out
}
func appendSeg(segs []Segment, t, s string) []Segment {
if s == "" {
return segs
}
if n := len(segs); n > 0 && segs[n-1].T == t {
segs[n-1].S += s
return segs
}
return append(segs, Segment{t, s})
}
|