on c8e653e help sign in
crush/internal/exp/diffview/diffview.go
209 lines 5.1 kB history raw
1package diffview
3import (
4 "os"
5 "strconv"
6 "strings"
8 "github.com/aymanbagabas/go-udiff"
9 "github.com/aymanbagabas/go-udiff/myers"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/ansi"
14const leadingSymbolsSize = 2
16type file struct {
17 path string
18 content string
21type layout int
23const (
24 layoutUnified layout = iota + 1
25 layoutSplit
28// DiffView represents a view for displaying differences between two files.
29type DiffView struct {
30 layout layout
31 before file
32 after file
33 contextLines int
34 lineNumbers bool
35 highlight bool
36 height int
37 width int
38 style Style
40 isComputed bool
41 err error
42 unified udiff.UnifiedDiff
43 edits []udiff.Edit
46// New creates a new DiffView with default settings.
47func New() *DiffView {
48 dv := &DiffView{
49 layout: layoutUnified,
50 contextLines: udiff.DefaultContextLines,
51 lineNumbers: true,
52 }
53 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
54 dv.style = DefaultDarkStyle
55 } else {
56 dv.style = DefaultLightStyle
57 }
58 return dv
61// Unified sets the layout of the DiffView to unified.
62func (dv *DiffView) Unified() *DiffView {
63 dv.layout = layoutUnified
64 return dv
67// Split sets the layout of the DiffView to split (side-by-side).
68func (dv *DiffView) Split() *DiffView {
69 dv.layout = layoutSplit
70 return dv
73// Before sets the "before" file for the DiffView.
74func (dv *DiffView) Before(path, content string) *DiffView {
75 dv.before = file{path: path, content: content}
76 return dv
79// After sets the "after" file for the DiffView.
80func (dv *DiffView) After(path, content string) *DiffView {
81 dv.after = file{path: path, content: content}
82 return dv
85// ContextLines sets the number of context lines for the DiffView.
86func (dv *DiffView) ContextLines(contextLines int) *DiffView {
87 dv.contextLines = contextLines
88 return dv
91// Style sets the style for the DiffView.
92func (dv *DiffView) Style(style Style) *DiffView {
93 dv.style = style
94 return dv
97// LineNumbers sets whether to display line numbers in the DiffView.
98func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
99 dv.lineNumbers = lineNumbers
100 return dv
103// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
104func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
105 dv.highlight = highlight
106 return dv
109// Height sets the height of the DiffView.
110func (dv *DiffView) Height(height int) *DiffView {
111 dv.height = height
112 return dv
115// Width sets the width of the DiffView.
116func (dv *DiffView) Width(width int) *DiffView {
117 dv.width = width
118 return dv
121// String returns the string representation of the DiffView.
122func (dv *DiffView) String() string {
123 if err := dv.computeDiff(); err != nil {
124 return err.Error()
125 }
126 dv.detectWidth()
128 lineNumberWidth := func(start, num int) int {
129 return len(strconv.Itoa(start + num))
130 }
132 var b strings.Builder
134 for _, h := range dv.unified.Hunks {
135 beforeLine := h.FromLine
136 afterLine := h.ToLine
138 beforeNumDigits := lineNumberWidth(h.FromLine, len(h.Lines))
139 afterNumDigits := lineNumberWidth(h.ToLine, len(h.Lines))
141 for _, l := range h.Lines {
142 content := strings.TrimSuffix(l.Content, "\n")
143 codeWidth := dv.width - leadingSymbolsSize
145 switch l.Kind {
146 case udiff.Equal:
147 if dv.lineNumbers {
148 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
149 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
150 }
151 b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
152 beforeLine++
153 afterLine++
154 case udiff.Insert:
155 if dv.lineNumbers {
156 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
157 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
158 }
159 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
160 b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
161 afterLine++
162 case udiff.Delete:
163 if dv.lineNumbers {
164 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
165 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
166 }
167 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
168 b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
169 beforeLine++
170 }
171 b.WriteRune('\n')
172 }
173 }
175 return b.String()
178func (dv *DiffView) computeDiff() error {
179 if dv.isComputed {
180 return dv.err
181 }
182 dv.isComputed = true
183 dv.edits = myers.ComputeEdits(
184 dv.before.content,
185 dv.after.content,
186 )
187 dv.unified, dv.err = udiff.ToUnifiedDiff(
188 dv.before.path,
189 dv.after.path,
190 dv.before.content,
191 dv.edits,
192 dv.contextLines,
193 )
194 return dv.err
197func (dv *DiffView) detectWidth() {
198 if dv.width > 0 {
199 return
200 }
202 for _, h := range dv.unified.Hunks {
203 for _, l := range h.Lines {
204 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
205 lineWidth += leadingSymbolsSize
206 dv.width = max(dv.width, lineWidth)
207 }
208 }