arche / internal/diff/hunks.go

commit 154431fd
  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}