diffview.go

  1package diffview
  2
  3import (
  4	"fmt"
  5	"image/color"
  6	"strconv"
  7	"strings"
  8
  9	"github.com/alecthomas/chroma/v2"
 10	"github.com/alecthomas/chroma/v2/lexers"
 11	"github.com/aymanbagabas/go-udiff"
 12	"github.com/aymanbagabas/go-udiff/myers"
 13	"github.com/charmbracelet/lipgloss/v2"
 14	"github.com/charmbracelet/x/ansi"
 15)
 16
 17const (
 18	leadingSymbolsSize = 2
 19	lineNumPadding     = 1
 20)
 21
 22type file struct {
 23	path    string
 24	content string
 25}
 26
 27type layout int
 28
 29const (
 30	layoutUnified layout = iota + 1
 31	layoutSplit
 32)
 33
 34// DiffView represents a view for displaying differences between two files.
 35type DiffView struct {
 36	layout          layout
 37	before          file
 38	after           file
 39	contextLines    int
 40	lineNumbers     bool
 41	height          int
 42	width           int
 43	xOffset         int
 44	yOffset         int
 45	infiniteYScroll bool
 46	style           Style
 47	tabWidth        int
 48	chromaStyle     *chroma.Style
 49
 50	isComputed bool
 51	err        error
 52	unified    udiff.UnifiedDiff
 53	edits      []udiff.Edit
 54
 55	splitHunks []splitHunk
 56
 57	totalLines      int
 58	codeWidth       int
 59	fullCodeWidth   int  // with leading symbols
 60	extraColOnAfter bool // add extra column on after panel
 61	beforeNumDigits int
 62	afterNumDigits  int
 63}
 64
 65// New creates a new DiffView with default settings.
 66func New() *DiffView {
 67	dv := &DiffView{
 68		layout:       layoutUnified,
 69		contextLines: udiff.DefaultContextLines,
 70		lineNumbers:  true,
 71		tabWidth:     8,
 72	}
 73	dv.style = DefaultDarkStyle()
 74	return dv
 75}
 76
 77// Unified sets the layout of the DiffView to unified.
 78func (dv *DiffView) Unified() *DiffView {
 79	dv.layout = layoutUnified
 80	return dv
 81}
 82
 83// Split sets the layout of the DiffView to split (side-by-side).
 84func (dv *DiffView) Split() *DiffView {
 85	dv.layout = layoutSplit
 86	return dv
 87}
 88
 89// Before sets the "before" file for the DiffView.
 90func (dv *DiffView) Before(path, content string) *DiffView {
 91	dv.before = file{path: path, content: content}
 92	return dv
 93}
 94
 95// After sets the "after" file for the DiffView.
 96func (dv *DiffView) After(path, content string) *DiffView {
 97	dv.after = file{path: path, content: content}
 98	return dv
 99}
100
101// ContextLines sets the number of context lines for the DiffView.
102func (dv *DiffView) ContextLines(contextLines int) *DiffView {
103	dv.contextLines = contextLines
104	return dv
105}
106
107// Style sets the style for the DiffView.
108func (dv *DiffView) Style(style Style) *DiffView {
109	dv.style = style
110	return dv
111}
112
113// LineNumbers sets whether to display line numbers in the DiffView.
114func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
115	dv.lineNumbers = lineNumbers
116	return dv
117}
118
119// Height sets the height of the DiffView.
120func (dv *DiffView) Height(height int) *DiffView {
121	dv.height = height
122	return dv
123}
124
125// Width sets the width of the DiffView.
126func (dv *DiffView) Width(width int) *DiffView {
127	dv.width = width
128	return dv
129}
130
131// XOffset sets the horizontal offset for the DiffView.
132func (dv *DiffView) XOffset(xOffset int) *DiffView {
133	dv.xOffset = xOffset
134	return dv
135}
136
137// YOffset sets the vertical offset for the DiffView.
138func (dv *DiffView) YOffset(yOffset int) *DiffView {
139	dv.yOffset = yOffset
140	return dv
141}
142
143// InfiniteYScroll allows the YOffset to scroll beyond the last line.
144func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
145	dv.infiniteYScroll = infiniteYScroll
146	return dv
147}
148
149// TabWidth sets the tab width. Only relevant for code that contains tabs, like
150// Go code.
151func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
152	dv.tabWidth = tabWidth
153	return dv
154}
155
156// ChromaStyle sets the chroma style for syntax highlighting.
157// If nil, no syntax highlighting will be applied.
158func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
159	dv.chromaStyle = style
160	return dv
161}
162
163// String returns the string representation of the DiffView.
164func (dv *DiffView) String() string {
165	dv.replaceTabs()
166	if err := dv.computeDiff(); err != nil {
167		return err.Error()
168	}
169	dv.convertDiffToSplit()
170	dv.adjustStyles()
171	dv.detectNumDigits()
172	dv.detectTotalLines()
173	dv.preventInfiniteYScroll()
174
175	if dv.width <= 0 {
176		dv.detectCodeWidth()
177	} else {
178		dv.resizeCodeWidth()
179	}
180
181	style := lipgloss.NewStyle()
182	if dv.width > 0 {
183		style = style.MaxWidth(dv.width)
184	}
185	if dv.height > 0 {
186		style = style.MaxHeight(dv.height)
187	}
188
189	switch dv.layout {
190	case layoutUnified:
191		return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
192	case layoutSplit:
193		return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
194	default:
195		panic("unknown diffview layout")
196	}
197}
198
199// replaceTabs replaces tabs in the before and after file contents with spaces
200// according to the specified tab width.
201func (dv *DiffView) replaceTabs() {
202	spaces := strings.Repeat(" ", dv.tabWidth)
203	dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
204	dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
205}
206
207// computeDiff computes the differences between the "before" and "after" files.
208func (dv *DiffView) computeDiff() error {
209	if dv.isComputed {
210		return dv.err
211	}
212	dv.isComputed = true
213	dv.edits = myers.ComputeEdits( //nolint:staticcheck
214		dv.before.content,
215		dv.after.content,
216	)
217	dv.unified, dv.err = udiff.ToUnifiedDiff(
218		dv.before.path,
219		dv.after.path,
220		dv.before.content,
221		dv.edits,
222		dv.contextLines,
223	)
224	return dv.err
225}
226
227// convertDiffToSplit converts the unified diff to a split diff if the layout is
228// set to split.
229func (dv *DiffView) convertDiffToSplit() {
230	if dv.layout != layoutSplit {
231		return
232	}
233
234	dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
235	for i, h := range dv.unified.Hunks {
236		dv.splitHunks[i] = hunkToSplit(h)
237	}
238}
239
240// adjustStyles adjusts adds padding and alignment to the styles.
241func (dv *DiffView) adjustStyles() {
242	setPadding := func(s lipgloss.Style) lipgloss.Style {
243		return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
244	}
245	dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
246	dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
247	dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
248	dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
249	dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
250}
251
252// detectNumDigits calculates the maximum number of digits needed for before and
253// after line numbers.
254func (dv *DiffView) detectNumDigits() {
255	dv.beforeNumDigits = 0
256	dv.afterNumDigits = 0
257
258	for _, h := range dv.unified.Hunks {
259		dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
260		dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
261	}
262}
263
264func (dv *DiffView) detectTotalLines() {
265	dv.totalLines = 0
266
267	switch dv.layout {
268	case layoutUnified:
269		for _, h := range dv.unified.Hunks {
270			dv.totalLines += 1 + len(h.Lines)
271		}
272	case layoutSplit:
273		for _, h := range dv.splitHunks {
274			dv.totalLines += 1 + len(h.lines)
275		}
276	}
277}
278
279func (dv *DiffView) preventInfiniteYScroll() {
280	if dv.infiniteYScroll {
281		return
282	}
283
284	// clamp yOffset to prevent scrolling beyond the last line
285	if dv.height > 0 {
286		maxYOffset := max(0, dv.totalLines-dv.height)
287		dv.yOffset = min(dv.yOffset, maxYOffset)
288	} else {
289		// if no height limit, ensure yOffset doesn't exceed total lines
290		dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
291	}
292	dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
293}
294
295// detectCodeWidth calculates the maximum width of code lines in the diff view.
296func (dv *DiffView) detectCodeWidth() {
297	switch dv.layout {
298	case layoutUnified:
299		dv.detectUnifiedCodeWidth()
300	case layoutSplit:
301		dv.detectSplitCodeWidth()
302	}
303	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
304}
305
306// detectUnifiedCodeWidth calculates the maximum width of code lines in a
307// unified diff.
308func (dv *DiffView) detectUnifiedCodeWidth() {
309	dv.codeWidth = 0
310
311	for _, h := range dv.unified.Hunks {
312		shownLines := ansi.StringWidth(dv.hunkLineFor(h))
313
314		for _, l := range h.Lines {
315			lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
316			dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
317		}
318	}
319}
320
321// detectSplitCodeWidth calculates the maximum width of code lines in a
322// split diff.
323func (dv *DiffView) detectSplitCodeWidth() {
324	dv.codeWidth = 0
325
326	for i, h := range dv.splitHunks {
327		shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
328
329		for _, l := range h.lines {
330			if l.before != nil {
331				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
332				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
333			}
334			if l.after != nil {
335				codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
336				dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
337			}
338		}
339	}
340}
341
342// resizeCodeWidth resizes the code width to fit within the specified width.
343func (dv *DiffView) resizeCodeWidth() {
344	fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
345	fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
346
347	switch dv.layout {
348	case layoutUnified:
349		dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
350	case layoutSplit:
351		remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
352		dv.codeWidth = remainingWidth / 2
353		dv.extraColOnAfter = isOdd(remainingWidth)
354	}
355
356	dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
357}
358
359// renderUnified renders the unified diff view as a string.
360func (dv *DiffView) renderUnified() string {
361	var b strings.Builder
362
363	fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
364	printedLines := -dv.yOffset
365
366	write := func(s string) {
367		if printedLines >= 0 {
368			b.WriteString(s)
369		}
370	}
371
372outer:
373	for i, h := range dv.unified.Hunks {
374		ls := dv.style.DividerLine
375		if dv.lineNumbers {
376			write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
377			write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
378		}
379		content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
380		write(ls.Code.Width(dv.fullCodeWidth).Render(content))
381		write("\n")
382		printedLines++
383
384		beforeLine := h.FromLine
385		afterLine := h.ToLine
386
387		for j, l := range h.Lines {
388			// print ellipis if we don't have enough space to print the rest of the diff
389			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
390			isLastHunk := i+1 == len(dv.unified.Hunks)
391			isLastLine := j+1 == len(h.Lines)
392			if hasReachedHeight && (!isLastHunk || !isLastLine) {
393				ls := dv.lineStyleForType(l.Kind)
394				if dv.lineNumbers {
395					write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
396					write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
397				}
398				write(fullContentStyle.Render(
399					ls.Code.Width(dv.fullCodeWidth).Render("  …"),
400				))
401				write("\n")
402				break outer
403			}
404
405			getContent := func(ls LineStyle) string {
406				content := strings.TrimSuffix(l.Content, "\n")
407				content = dv.hightlightCode(content, ls.Code.GetBackground())
408				content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
409				content = ansi.Truncate(content, dv.codeWidth, "…")
410				return content
411			}
412
413			leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
414
415			switch l.Kind {
416			case udiff.Equal:
417				ls := dv.style.EqualLine
418				content := getContent(ls)
419				if dv.lineNumbers {
420					write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
421					write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
422				}
423				write(fullContentStyle.Render(
424					ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
425				))
426				beforeLine++
427				afterLine++
428			case udiff.Insert:
429				ls := dv.style.InsertLine
430				content := getContent(ls)
431				if dv.lineNumbers {
432					write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
433					write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
434				}
435				write(fullContentStyle.Render(
436					ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
437						ls.Code.Width(dv.codeWidth).Render(content),
438				))
439				afterLine++
440			case udiff.Delete:
441				ls := dv.style.DeleteLine
442				content := getContent(ls)
443				if dv.lineNumbers {
444					write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
445					write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
446				}
447				write(fullContentStyle.Render(
448					ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
449						ls.Code.Width(dv.codeWidth).Render(content),
450				))
451				beforeLine++
452			}
453			write("\n")
454
455			printedLines++
456		}
457	}
458
459	for printedLines < dv.height {
460		ls := dv.style.MissingLine
461		if dv.lineNumbers {
462			write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
463			write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
464		}
465		write(ls.Code.Width(dv.fullCodeWidth).Render("  "))
466		write("\n")
467		printedLines++
468	}
469
470	return b.String()
471}
472
473// renderSplit renders the split (side-by-side) diff view as a string.
474func (dv *DiffView) renderSplit() string {
475	var b strings.Builder
476
477	beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
478	afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
479	printedLines := -dv.yOffset
480
481	write := func(s string) {
482		if printedLines >= 0 {
483			b.WriteString(s)
484		}
485	}
486
487outer:
488	for i, h := range dv.splitHunks {
489		ls := dv.style.DividerLine
490		if dv.lineNumbers {
491			write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
492		}
493		content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
494		write(ls.Code.Width(dv.fullCodeWidth).Render(content))
495		if dv.lineNumbers {
496			write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
497		}
498		write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
499		write("\n")
500		printedLines++
501
502		beforeLine := h.fromLine
503		afterLine := h.toLine
504
505		for j, l := range h.lines {
506			// print ellipis if we don't have enough space to print the rest of the diff
507			hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
508			isLastHunk := i+1 == len(dv.unified.Hunks)
509			isLastLine := j+1 == len(h.lines)
510			if hasReachedHeight && (!isLastHunk || !isLastLine) {
511				ls := dv.style.MissingLine
512				if l.before != nil {
513					ls = dv.lineStyleForType(l.before.Kind)
514				}
515				if dv.lineNumbers {
516					write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
517				}
518				write(beforeFullContentStyle.Render(
519					ls.Code.Width(dv.fullCodeWidth).Render("  …"),
520				))
521				ls = dv.style.MissingLine
522				if l.after != nil {
523					ls = dv.lineStyleForType(l.after.Kind)
524				}
525				if dv.lineNumbers {
526					write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
527				}
528				write(afterFullContentStyle.Render(
529					ls.Code.Width(dv.fullCodeWidth).Render("  …"),
530				))
531				write("\n")
532				break outer
533			}
534
535			getContent := func(content string, ls LineStyle) string {
536				content = strings.TrimSuffix(content, "\n")
537				content = dv.hightlightCode(content, ls.Code.GetBackground())
538				content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
539				content = ansi.Truncate(content, dv.codeWidth, "…")
540				return content
541			}
542			getLeadingEllipsis := func(content string) bool {
543				return dv.xOffset > 0 && strings.TrimSpace(content) != ""
544			}
545
546			switch {
547			case l.before == nil:
548				ls := dv.style.MissingLine
549				if dv.lineNumbers {
550					write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
551				}
552				write(beforeFullContentStyle.Render(
553					ls.Code.Width(dv.fullCodeWidth).Render("  "),
554				))
555			case l.before.Kind == udiff.Equal:
556				ls := dv.style.EqualLine
557				content := getContent(l.before.Content, ls)
558				leadingEllipsis := getLeadingEllipsis(content)
559				if dv.lineNumbers {
560					write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
561				}
562				write(beforeFullContentStyle.Render(
563					ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", "  ") + content),
564				))
565				beforeLine++
566			case l.before.Kind == udiff.Delete:
567				ls := dv.style.DeleteLine
568				content := getContent(l.before.Content, ls)
569				leadingEllipsis := getLeadingEllipsis(content)
570				if dv.lineNumbers {
571					write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
572				}
573				write(beforeFullContentStyle.Render(
574					ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
575						ls.Code.Width(dv.codeWidth).Render(content),
576				))
577				beforeLine++
578			}
579
580			switch {
581			case l.after == nil:
582				ls := dv.style.MissingLine
583				if dv.lineNumbers {
584					write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
585				}
586				write(afterFullContentStyle.Render(
587					ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render("  "),
588				))
589			case l.after.Kind == udiff.Equal:
590				ls := dv.style.EqualLine
591				content := getContent(l.after.Content, ls)
592				leadingEllipsis := getLeadingEllipsis(content)
593				if dv.lineNumbers {
594					write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
595				}
596				write(afterFullContentStyle.Render(
597					ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", "  ") + content),
598				))
599				afterLine++
600			case l.after.Kind == udiff.Insert:
601				ls := dv.style.InsertLine
602				content := getContent(l.after.Content, ls)
603				leadingEllipsis := getLeadingEllipsis(content)
604				if dv.lineNumbers {
605					write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
606				}
607				write(afterFullContentStyle.Render(
608					ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
609						ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
610				))
611				afterLine++
612			}
613
614			write("\n")
615
616			printedLines++
617		}
618	}
619
620	for printedLines < dv.height {
621		ls := dv.style.MissingLine
622		if dv.lineNumbers {
623			write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
624		}
625		write(ls.Code.Width(dv.fullCodeWidth).Render(" "))
626		if dv.lineNumbers {
627			write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
628		}
629		write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
630		write("\n")
631		printedLines++
632	}
633
634	return b.String()
635}
636
637// hunkLineFor formats the header line for a hunk in the unified diff view.
638func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
639	beforeShownLines, afterShownLines := dv.hunkShownLines(h)
640
641	return fmt.Sprintf(
642		"  @@ -%d,%d +%d,%d @@ ",
643		h.FromLine,
644		beforeShownLines,
645		h.ToLine,
646		afterShownLines,
647	)
648}
649
650// hunkShownLines calculates the number of lines shown in a hunk for both before
651// and after versions.
652func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
653	for _, l := range h.Lines {
654		switch l.Kind {
655		case udiff.Equal:
656			before++
657			after++
658		case udiff.Insert:
659			after++
660		case udiff.Delete:
661			before++
662		}
663	}
664	return
665}
666
667func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
668	switch t {
669	case udiff.Equal:
670		return dv.style.EqualLine
671	case udiff.Insert:
672		return dv.style.InsertLine
673	case udiff.Delete:
674		return dv.style.DeleteLine
675	default:
676		return dv.style.MissingLine
677	}
678}
679
680func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
681	if dv.chromaStyle == nil {
682		return source
683	}
684
685	l := dv.getChromaLexer(source)
686	f := dv.getChromaFormatter(bgColor)
687
688	it, err := l.Tokenise(nil, source)
689	if err != nil {
690		return source
691	}
692
693	var b strings.Builder
694	if err := f.Format(&b, dv.chromaStyle, it); err != nil {
695		return source
696	}
697	return b.String()
698}
699
700func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
701	l := lexers.Match(dv.before.path)
702	if l == nil {
703		l = lexers.Analyse(source)
704	}
705	if l == nil {
706		l = lexers.Fallback
707	}
708	return chroma.Coalesce(l)
709}
710
711func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
712	return chromaFormatter{
713		bgColor: gbColor,
714	}
715}