1package diff
2
3import (
4 "sort"
5 "strings"
6
7 "github.com/sergi/go-diff/diffmatchpatch"
8)
9
10const ContextLines = 3
11
12type HunkLineKind byte
13
14const (
15 LineEqual HunkLineKind = ' '
16 LineAdd HunkLineKind = '+'
17 LineRemove HunkLineKind = '-'
18)
19
20type HunkLine struct {
21 Content string
22 Kind HunkLineKind
23}
24
25type Hunk struct {
26 OldStart int
27 OldCount int
28 NewStart int
29 NewCount int
30 Lines []HunkLine
31}
32
33func (h Hunk) Header() string {
34 return strings.Join([]string{
35 "@@ -",
36 itoa(h.OldStart), ",", itoa(h.OldCount),
37 " +",
38 itoa(h.NewStart), ",", itoa(h.NewCount),
39 " @@",
40 }, "")
41}
42
43type FileHunkDiff struct {
44 Path string
45 Status rune
46 OldContent string
47 NewContent string
48 Hunks []Hunk
49}
50
51func ComputeFileHunks(path, oldContent, newContent string, status rune) FileHunkDiff {
52 fhd := FileHunkDiff{
53 Path: path,
54 Status: status,
55 OldContent: oldContent,
56 NewContent: newContent,
57 }
58 fhd.Hunks = computeHunks(oldContent, newContent)
59 return fhd
60}
61
62func computeHunks(oldText, newText string) []Hunk {
63 dmp := diffmatchpatch.New()
64 chars1, chars2, lineArray := dmp.DiffLinesToChars(oldText, newText)
65 diffs := dmp.DiffMain(chars1, chars2, false)
66 diffs = dmp.DiffCharsToLines(diffs, lineArray)
67
68 type rawLine struct {
69 kind HunkLineKind
70 content string
71 }
72 var flat []rawLine
73 for _, d := range diffs {
74 lines := splitLinesFull(d.Text)
75 switch d.Type {
76 case diffmatchpatch.DiffEqual:
77 for _, l := range lines {
78 flat = append(flat, rawLine{LineEqual, l})
79 }
80 case diffmatchpatch.DiffInsert:
81 for _, l := range lines {
82 flat = append(flat, rawLine{LineAdd, l})
83 }
84 case diffmatchpatch.DiffDelete:
85 for _, l := range lines {
86 flat = append(flat, rawLine{LineRemove, l})
87 }
88 }
89 }
90
91 isChange := make([]bool, len(flat))
92 for i, l := range flat {
93 isChange[i] = l.kind != LineEqual
94 }
95
96 type hunkRange struct{ start, end int }
97 var ranges []hunkRange
98 i := 0
99 for i < len(flat) {
100 if !isChange[i] {
101 i++
102 continue
103 }
104 j := i
105 for j < len(flat) && isChange[j] {
106 j++
107 }
108 start := maxInt(0, i-ContextLines)
109 end := minInt(len(flat), j+ContextLines)
110 if len(ranges) > 0 && start <= ranges[len(ranges)-1].end {
111 ranges[len(ranges)-1].end = end
112 } else {
113 ranges = append(ranges, hunkRange{start, end})
114 }
115 i = j
116 }
117
118 var hunks []Hunk
119 for _, r := range ranges {
120 oldLine, newLine := 1, 1
121 for i := 0; i < r.start; i++ {
122 switch flat[i].kind {
123 case LineEqual:
124 oldLine++
125 newLine++
126 case LineRemove:
127 oldLine++
128 case LineAdd:
129 newLine++
130 }
131 }
132 var lines []HunkLine
133 oldCount, newCount := 0, 0
134 for i := r.start; i < r.end; i++ {
135 lines = append(lines, HunkLine{Content: flat[i].content, Kind: flat[i].kind})
136 switch flat[i].kind {
137 case LineEqual:
138 oldCount++
139 newCount++
140 case LineRemove:
141 oldCount++
142 case LineAdd:
143 newCount++
144 }
145 }
146 hunks = append(hunks, Hunk{
147 OldStart: oldLine,
148 OldCount: oldCount,
149 NewStart: newLine,
150 NewCount: newCount,
151 Lines: lines,
152 })
153 }
154 return hunks
155}
156
157func ApplySelectedHunks(fhd FileHunkDiff, selected []bool) string {
158 if len(fhd.Hunks) == 0 {
159 return fhd.OldContent
160 }
161
162 oldLines := splitLinesFull(fhd.OldContent)
163
164 type replacement struct {
165 start, count int
166 newLines []string
167 }
168 var reps []replacement
169 for i, h := range fhd.Hunks {
170 if !selected[i] {
171 continue
172 }
173 var newLines []string
174 for _, l := range h.Lines {
175 if l.Kind == LineEqual || l.Kind == LineAdd {
176 newLines = append(newLines, l.Content)
177 }
178 }
179 reps = append(reps, replacement{
180 start: h.OldStart - 1,
181 count: h.OldCount,
182 newLines: newLines,
183 })
184 }
185
186 sort.Slice(reps, func(i, j int) bool {
187 return reps[i].start > reps[j].start
188 })
189
190 result := make([]string, len(oldLines))
191 copy(result, oldLines)
192
193 for _, rep := range reps {
194 end := rep.start + rep.count
195 if end > len(result) {
196 end = len(result)
197 }
198 newResult := make([]string, 0, len(result)-rep.count+len(rep.newLines))
199 newResult = append(newResult, result[:rep.start]...)
200 newResult = append(newResult, rep.newLines...)
201 newResult = append(newResult, result[end:]...)
202 result = newResult
203 }
204
205 return strings.Join(result, "")
206}
207
208func splitLinesFull(s string) []string {
209 if s == "" {
210 return nil
211 }
212 var lines []string
213 for len(s) > 0 {
214 i := strings.Index(s, "\n")
215 if i < 0 {
216 lines = append(lines, s)
217 break
218 }
219 lines = append(lines, s[:i+1])
220 s = s[i+1:]
221 }
222 return lines
223}
224
225func maxInt(a, b int) int {
226 if a > b {
227 return a
228 }
229 return b
230}
231
232func minInt(a, b int) int {
233 if a < b {
234 return a
235 }
236 return b
237}
238
239func itoa(n int) string {
240 if n == 0 {
241 return "0"
242 }
243 var buf [20]byte
244 pos := len(buf)
245 neg := n < 0
246 if neg {
247 n = -n
248 }
249 for n > 0 {
250 pos--
251 buf[pos] = byte('0' + n%10)
252 n /= 10
253 }
254 if neg {
255 pos--
256 buf[pos] = '-'
257 }
258 return string(buf[pos:])
259}