diffview.go

  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		if dv.lineNumbers {
136			b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
137			b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
138		}
139		b.WriteString(dv.style.DividerLine.Code.Width(codeWidth + leadingSymbolsSize).Render(dv.hunkLineFor(i)))
140		b.WriteRune('\n')
141
142		beforeLine := h.FromLine
143		afterLine := h.ToLine
144
145		for _, l := range h.Lines {
146			content := strings.TrimSuffix(l.Content, "\n")
147
148			switch l.Kind {
149			case udiff.Equal:
150				if dv.lineNumbers {
151					b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
152					b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
153				}
154				b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render("  " + content))
155				beforeLine++
156				afterLine++
157			case udiff.Insert:
158				if dv.lineNumbers {
159					b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
160					b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
161				}
162				b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
163				b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
164				afterLine++
165			case udiff.Delete:
166				if dv.lineNumbers {
167					b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
168					b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
169				}
170				b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
171				b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
172				beforeLine++
173			}
174			b.WriteRune('\n')
175		}
176	}
177
178	return b.String()
179}
180
181func (dv *DiffView) computeDiff() error {
182	if dv.isComputed {
183		return dv.err
184	}
185	dv.isComputed = true
186	dv.edits = myers.ComputeEdits(
187		dv.before.content,
188		dv.after.content,
189	)
190	dv.unified, dv.err = udiff.ToUnifiedDiff(
191		dv.before.path,
192		dv.after.path,
193		dv.before.content,
194		dv.edits,
195		dv.contextLines,
196	)
197	return dv.err
198}
199
200// lineNumberDigits calculates the maximum number of digits needed for before and
201// after line numbers.
202func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
203	for _, h := range dv.unified.Hunks {
204		maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
205		maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
206	}
207	return
208}
209
210func (dv *DiffView) hunkLineFor(i int) string {
211	h := dv.unified.Hunks[i]
212	beforeShownLines, afterShownLines := dv.hunkShownLines(i)
213
214	return fmt.Sprintf(
215		"  @@ -%d,%d +%d,%d @@ ",
216		h.FromLine,
217		beforeShownLines,
218		h.ToLine,
219		afterShownLines,
220	)
221}
222
223// hunkShownLines calculates the number of lines shown in a hunk for both before
224// and after versions.
225func (dv *DiffView) hunkShownLines(i int) (before, after int) {
226	for _, l := range dv.unified.Hunks[i].Lines {
227		switch l.Kind {
228		case udiff.Equal:
229			before++
230			after++
231		case udiff.Insert:
232			after++
233		case udiff.Delete:
234			before++
235		}
236	}
237	return
238}
239
240func (dv *DiffView) detectWidth() {
241	if dv.width > 0 {
242		return
243	}
244
245	for i, h := range dv.unified.Hunks {
246		shownLines := ansi.StringWidth(dv.hunkLineFor(i))
247
248		for _, l := range h.Lines {
249			lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n"))
250			lineWidth += leadingSymbolsSize
251			dv.width = max(dv.width, lineWidth, shownLines)
252		}
253	}
254}