on eb50c92 help sign in
crush/internal/exp/diffview/diffview.go
386 lines 10 kB history raw
1package diffview
3import (
4 "fmt"
5 "os"
6 "strconv"
7 "strings"
9 "github.com/aymanbagabas/go-udiff"
10 "github.com/aymanbagabas/go-udiff/myers"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/x/ansi"
15const leadingSymbolsSize = 2
17type file struct {
18 path string
19 content string
22type layout int
24const (
25 layoutUnified layout = iota + 1
26 layoutSplit
29// DiffView represents a view for displaying differences between two files.
30type DiffView struct {
31 layout layout
32 before file
33 after file
34 contextLines int
35 lineNumbers bool
36 highlight bool
37 height int
38 width int
39 style Style
41 isComputed bool
42 err error
43 unified udiff.UnifiedDiff
44 edits []udiff.Edit
46 splitHunks []splitHunk
48 codeWidth int
49 fullCodeWidth int // with leading symbols
52// New creates a new DiffView with default settings.
53func New() *DiffView {
54 dv := &DiffView{
55 layout: layoutUnified,
56 contextLines: udiff.DefaultContextLines,
57 lineNumbers: true,
58 }
59 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
60 dv.style = DefaultDarkStyle
61 } else {
62 dv.style = DefaultLightStyle
63 }
64 return dv
67// Unified sets the layout of the DiffView to unified.
68func (dv *DiffView) Unified() *DiffView {
69 dv.layout = layoutUnified
70 return dv
73// Split sets the layout of the DiffView to split (side-by-side).
74func (dv *DiffView) Split() *DiffView {
75 dv.layout = layoutSplit
76 return dv
79// Before sets the "before" file for the DiffView.
80func (dv *DiffView) Before(path, content string) *DiffView {
81 dv.before = file{path: path, content: content}
82 return dv
85// After sets the "after" file for the DiffView.
86func (dv *DiffView) After(path, content string) *DiffView {
87 dv.after = file{path: path, content: content}
88 return dv
91// ContextLines sets the number of context lines for the DiffView.
92func (dv *DiffView) ContextLines(contextLines int) *DiffView {
93 dv.contextLines = contextLines
94 return dv
97// Style sets the style for the DiffView.
98func (dv *DiffView) Style(style Style) *DiffView {
99 dv.style = style
100 return dv
103// LineNumbers sets whether to display line numbers in the DiffView.
104func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
105 dv.lineNumbers = lineNumbers
106 return dv
109// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
110func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
111 dv.highlight = highlight
112 return dv
115// Height sets the height of the DiffView.
116func (dv *DiffView) Height(height int) *DiffView {
117 dv.height = height
118 return dv
121// Width sets the width of the DiffView.
122func (dv *DiffView) Width(width int) *DiffView {
123 dv.width = width
124 return dv
127// String returns the string representation of the DiffView.
128func (dv *DiffView) String() string {
129 if err := dv.computeDiff(); err != nil {
130 return err.Error()
131 }
132 dv.convertDiffToSplit()
133 dv.detectCodeWidth()
135 switch dv.layout {
136 case layoutUnified:
137 return dv.renderUnified()
138 case layoutSplit:
139 return dv.renderSplit()
140 default:
141 panic("unknown diffview layout")
142 }
145// renderUnified renders the unified diff view as a string.
146func (dv *DiffView) renderUnified() string {
147 beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
149 var b strings.Builder
151 for _, h := range dv.unified.Hunks {
152 if dv.lineNumbers {
153 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
154 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
155 }
156 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(h)))
157 b.WriteRune('\n')
159 beforeLine := h.FromLine
160 afterLine := h.ToLine
162 for _, l := range h.Lines {
163 content := strings.TrimSuffix(l.Content, "\n")
165 switch l.Kind {
166 case udiff.Equal:
167 if dv.lineNumbers {
168 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
169 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
170 }
171 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + content))
172 beforeLine++
173 afterLine++
174 case udiff.Insert:
175 if dv.lineNumbers {
176 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
177 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
178 }
179 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
180 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content))
181 afterLine++
182 case udiff.Delete:
183 if dv.lineNumbers {
184 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
185 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
186 }
187 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
188 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content))
189 beforeLine++
190 }
191 b.WriteRune('\n')
192 }
193 }
195 return b.String()
198// renderSplit renders the split (side-by-side) diff view as a string.
199func (dv *DiffView) renderSplit() string {
200 beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
202 var b strings.Builder
204 for i, h := range dv.splitHunks {
205 if dv.lineNumbers {
206 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
207 }
208 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
209 if dv.lineNumbers {
210 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
211 }
212 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(" "))
213 b.WriteRune('\n')
215 beforeLine := h.fromLine
216 afterLine := h.toLine
218 for _, l := range h.lines {
219 var beforeContent string
220 var afterContent string
221 if l.before != nil {
222 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
223 }
224 if l.after != nil {
225 afterContent = strings.TrimSuffix(l.after.Content, "\n")
226 }
228 switch {
229 case l.before == nil:
230 if dv.lineNumbers {
231 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", beforeNumDigits)))
232 }
233 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
234 case l.before.Kind == udiff.Equal:
235 if dv.lineNumbers {
236 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
237 }
238 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + beforeContent))
239 beforeLine++
240 case l.before.Kind == udiff.Delete:
241 if dv.lineNumbers {
242 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
243 }
244 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
245 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent))
246 beforeLine++
247 }
249 switch {
250 case l.after == nil:
251 if dv.lineNumbers {
252 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", afterNumDigits)))
253 }
254 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
255 case l.after.Kind == udiff.Equal:
256 if dv.lineNumbers {
257 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
258 }
259 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + afterContent))
260 afterLine++
261 case l.after.Kind == udiff.Insert:
262 if dv.lineNumbers {
263 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
264 }
265 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
266 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(afterContent))
267 afterLine++
268 }
270 b.WriteRune('\n')
271 }
272 }
274 return b.String()
277func (dv *DiffView) computeDiff() error {
278 if dv.isComputed {
279 return dv.err
280 }
281 dv.isComputed = true
282 dv.edits = myers.ComputeEdits( //nolint:staticcheck
283 dv.before.content,
284 dv.after.content,
285 )
286 dv.unified, dv.err = udiff.ToUnifiedDiff(
287 dv.before.path,
288 dv.after.path,
289 dv.before.content,
290 dv.edits,
291 dv.contextLines,
292 )
293 return dv.err
296// lineNumberDigits calculates the maximum number of digits needed for before and
297// after line numbers.
298func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
299 for _, h := range dv.unified.Hunks {
300 maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
301 maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
302 }
303 return
306func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
307 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
309 return fmt.Sprintf(
310 " @@ -%d,%d +%d,%d @@ ",
311 h.FromLine,
312 beforeShownLines,
313 h.ToLine,
314 afterShownLines,
315 )
318// hunkShownLines calculates the number of lines shown in a hunk for both before
319// and after versions.
320func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
321 for _, l := range h.Lines {
322 switch l.Kind {
323 case udiff.Equal:
324 before++
325 after++
326 case udiff.Insert:
327 after++
328 case udiff.Delete:
329 before++
330 }
331 }
332 return
335func (dv *DiffView) detectCodeWidth() {
336 switch dv.layout {
337 case layoutUnified:
338 dv.detectUnifiedCodeWidth()
339 case layoutSplit:
340 dv.detectSplitCodeWidth()
341 }
342 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
345func (dv *DiffView) detectUnifiedCodeWidth() {
346 dv.codeWidth = 0
348 for _, h := range dv.unified.Hunks {
349 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
351 for _, l := range h.Lines {
352 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
353 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
354 }
355 }
358func (dv *DiffView) convertDiffToSplit() {
359 if dv.layout != layoutSplit {
360 return
361 }
363 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
364 for i, h := range dv.unified.Hunks {
365 dv.splitHunks[i] = hunkToSplit(h)
366 }
369func (dv *DiffView) detectSplitCodeWidth() {
370 dv.codeWidth = 0
372 for i, h := range dv.splitHunks {
373 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
375 for _, l := range h.lines {
376 if l.before != nil {
377 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
378 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
379 }
380 if l.after != nil {
381 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
382 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
383 }
384 }
385 }