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		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}