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}