1package diffview
2
3import (
4 "fmt"
5 "os"
6 "strconv"
7 "strings"
8
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"
13)
14
15const leadingSymbolsSize = 2
16
17type file struct {
18 path string
19 content string
20}
21
22type layout int
23
24const (
25 layoutUnified layout = iota + 1
26 layoutSplit
27)
28
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
40
41 isComputed bool
42 err error
43 unified udiff.UnifiedDiff
44 edits []udiff.Edit
45}
46
47// New creates a new DiffView with default settings.
48func New() *DiffView {
49 dv := &DiffView{
50 layout: layoutUnified,
51 contextLines: udiff.DefaultContextLines,
52 lineNumbers: true,
53 }
54 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
55 dv.style = DefaultDarkStyle
56 } else {
57 dv.style = DefaultLightStyle
58 }
59 return dv
60}
61
62// Unified sets the layout of the DiffView to unified.
63func (dv *DiffView) Unified() *DiffView {
64 dv.layout = layoutUnified
65 return dv
66}
67
68// Split sets the layout of the DiffView to split (side-by-side).
69func (dv *DiffView) Split() *DiffView {
70 dv.layout = layoutSplit
71 return dv
72}
73
74// Before sets the "before" file for the DiffView.
75func (dv *DiffView) Before(path, content string) *DiffView {
76 dv.before = file{path: path, content: content}
77 return dv
78}
79
80// After sets the "after" file for the DiffView.
81func (dv *DiffView) After(path, content string) *DiffView {
82 dv.after = file{path: path, content: content}
83 return dv
84}
85
86// ContextLines sets the number of context lines for the DiffView.
87func (dv *DiffView) ContextLines(contextLines int) *DiffView {
88 dv.contextLines = contextLines
89 return dv
90}
91
92// Style sets the style for the DiffView.
93func (dv *DiffView) Style(style Style) *DiffView {
94 dv.style = style
95 return dv
96}
97
98// LineNumbers sets whether to display line numbers in the DiffView.
99func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
100 dv.lineNumbers = lineNumbers
101 return dv
102}
103
104// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
105func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
106 dv.highlight = highlight
107 return dv
108}
109
110// Height sets the height of the DiffView.
111func (dv *DiffView) Height(height int) *DiffView {
112 dv.height = height
113 return dv
114}
115
116// Width sets the width of the DiffView.
117func (dv *DiffView) Width(width int) *DiffView {
118 dv.width = width
119 return dv
120}
121
122// String returns the string representation of the DiffView.
123func (dv *DiffView) String() string {
124 if err := dv.computeDiff(); err != nil {
125 return err.Error()
126 }
127 dv.detectWidth()
128
129 codeWidth := dv.width - leadingSymbolsSize
130 beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
131
132 var b strings.Builder
133
134 for i, h := range dv.unified.Hunks {
135 beforeShownLines, afterShownLines := dv.hunkShownLines(i)
136
137 if dv.lineNumbers {
138 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
139 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
140 }
141 b.WriteString(dv.style.DividerLine.Code.Width(codeWidth + leadingSymbolsSize).Render(
142 fmt.Sprintf(
143 " @@ -%d,%d +%d,%d @@",
144 h.FromLine,
145 beforeShownLines,
146 h.ToLine,
147 afterShownLines,
148 ),
149 ))
150 b.WriteRune('\n')
151
152 beforeLine := h.FromLine
153 afterLine := h.ToLine
154
155 for _, l := range h.Lines {
156 content := strings.TrimSuffix(l.Content, "\n")
157
158 switch l.Kind {
159 case udiff.Equal:
160 if dv.lineNumbers {
161 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
162 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
163 }
164 b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
165 beforeLine++
166 afterLine++
167 case udiff.Insert:
168 if dv.lineNumbers {
169 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
170 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
171 }
172 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
173 b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
174 afterLine++
175 case udiff.Delete:
176 if dv.lineNumbers {
177 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
178 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
179 }
180 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
181 b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
182 beforeLine++
183 }
184 b.WriteRune('\n')
185 }
186 }
187
188 return b.String()
189}
190
191func (dv *DiffView) computeDiff() error {
192 if dv.isComputed {
193 return dv.err
194 }
195 dv.isComputed = true
196 dv.edits = myers.ComputeEdits(
197 dv.before.content,
198 dv.after.content,
199 )
200 dv.unified, dv.err = udiff.ToUnifiedDiff(
201 dv.before.path,
202 dv.after.path,
203 dv.before.content,
204 dv.edits,
205 dv.contextLines,
206 )
207 return dv.err
208}
209
210// lineNumberDigits calculates the maximum number of digits needed for before and
211// after line numbers.
212func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
213 for _, h := range dv.unified.Hunks {
214 maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
215 maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
216 }
217 return
218}
219
220// hunkShownLines calculates the number of lines shown in a hunk for both before
221// and after versions.
222func (dv *DiffView) hunkShownLines(i int) (before, after int) {
223 for _, l := range dv.unified.Hunks[i].Lines {
224 switch l.Kind {
225 case udiff.Equal:
226 before++
227 after++
228 case udiff.Insert:
229 after++
230 case udiff.Delete:
231 before++
232 }
233 }
234 return
235}
236
237func (dv *DiffView) detectWidth() {
238 if dv.width > 0 {
239 return
240 }
241
242 for _, h := range dv.unified.Hunks {
243 for _, l := range h.Lines {
244 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
245 lineWidth += leadingSymbolsSize
246 dv.width = max(dv.width, lineWidth)
247 }
248 }
249}