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
25 layoutUnified layout = iota + 1
29// DiffView represents a view for displaying differences between two files.
43 unified udiff.UnifiedDiff
46 splitHunks []splitHunk
49 fullCodeWidth int // with leading symbols
52// New creates a new DiffView with default settings.
55 layout: layoutUnified,
56 contextLines: udiff.DefaultContextLines,
59 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
60 dv.style = DefaultDarkStyle
62 dv.style = DefaultLightStyle
67// Unified sets the layout of the DiffView to unified.
68func (dv *DiffView) Unified() *DiffView {
69 dv.layout = layoutUnified
73// Split sets the layout of the DiffView to split (side-by-side).
74func (dv *DiffView) Split() *DiffView {
75 dv.layout = layoutSplit
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}
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}
91// ContextLines sets the number of context lines for the DiffView.
92func (dv *DiffView) ContextLines(contextLines int) *DiffView {
93 dv.contextLines = contextLines
97// Style sets the style for the DiffView.
98func (dv *DiffView) Style(style Style) *DiffView {
103// LineNumbers sets whether to display line numbers in the DiffView.
104func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
105 dv.lineNumbers = lineNumbers
109// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
110func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
111 dv.highlight = highlight
115// Height sets the height of the DiffView.
116func (dv *DiffView) Height(height int) *DiffView {
121// Width sets the width of the DiffView.
122func (dv *DiffView) Width(width int) *DiffView {
127// String returns the string representation of the DiffView.
128func (dv *DiffView) String() string {
129 if err := dv.computeDiff(); err != nil {
132 dv.convertDiffToSplit()
137 return dv.renderUnified()
139 return dv.renderSplit()
141 panic("unknown diffview layout")
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 {
153 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
154 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
156 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(h)))
159 beforeLine := h.FromLine
160 afterLine := h.ToLine
162 for _, l := range h.Lines {
163 content := strings.TrimSuffix(l.Content, "\n")
168 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
169 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
171 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + content))
176 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
177 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
179 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
180 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content))
184 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
185 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
187 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
188 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content))
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 {
206 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
208 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
210 b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
212 b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(" "))
215 beforeLine := h.fromLine
216 afterLine := h.toLine
218 for _, l := range h.lines {
219 var beforeContent string
220 var afterContent string
222 beforeContent = strings.TrimSuffix(l.before.Content, "\n")
225 afterContent = strings.TrimSuffix(l.after.Content, "\n")
229 case l.before == nil:
231 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", beforeNumDigits)))
233 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
234 case l.before.Kind == udiff.Equal:
236 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
238 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + beforeContent))
240 case l.before.Kind == udiff.Delete:
242 b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
244 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
245 b.WriteString(dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent))
252 b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", afterNumDigits)))
254 b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
255 case l.after.Kind == udiff.Equal:
257 b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
259 b.WriteString(dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" " + afterContent))
261 case l.after.Kind == udiff.Insert:
263 b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
265 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
266 b.WriteString(dv.style.InsertLine.Code.Width(dv.codeWidth).Render(afterContent))
277func (dv *DiffView) computeDiff() error {
282 dv.edits = myers.ComputeEdits( //nolint:staticcheck
286 dv.unified, dv.err = udiff.ToUnifiedDiff(
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))))
306func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
307 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
310 " @@ -%d,%d +%d,%d @@ ",
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 {
335func (dv *DiffView) detectCodeWidth() {
338 dv.detectUnifiedCodeWidth()
340 dv.detectSplitCodeWidth()
342 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
345func (dv *DiffView) detectUnifiedCodeWidth() {
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)
358func (dv *DiffView) convertDiffToSplit() {
359 if dv.layout != layoutSplit {
363 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
364 for i, h := range dv.unified.Hunks {
365 dv.splitHunks[i] = hunkToSplit(h)
369func (dv *DiffView) detectSplitCodeWidth() {
372 for i, h := range dv.splitHunks {
373 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
375 for _, l := range h.lines {
377 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
378 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
381 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
382 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list