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