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 shouldWrite := func() bool { return printedLines >= 0 }
366
367 getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
368 content = strings.TrimSuffix(in, "\n")
369 content = dv.hightlightCode(content, ls.Code.GetBackground())
370 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
371 content = ansi.Truncate(content, dv.codeWidth, "…")
372 leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
373 return
374 }
375
376outer:
377 for i, h := range dv.unified.Hunks {
378 if shouldWrite() {
379 ls := dv.style.DividerLine
380 if dv.lineNumbers {
381 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
382 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
383 }
384 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
385 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
386 b.WriteString("\n")
387 }
388 printedLines++
389
390 beforeLine := h.FromLine
391 afterLine := h.ToLine
392
393 for j, l := range h.Lines {
394 // print ellipis if we don't have enough space to print the rest of the diff
395 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
396 isLastHunk := i+1 == len(dv.unified.Hunks)
397 isLastLine := j+1 == len(h.Lines)
398 if hasReachedHeight && (!isLastHunk || !isLastLine) {
399 if shouldWrite() {
400 ls := dv.lineStyleForType(l.Kind)
401 if dv.lineNumbers {
402 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
403 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
404 }
405 b.WriteString(fullContentStyle.Render(
406 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
407 ))
408 b.WriteRune('\n')
409 }
410 break outer
411 }
412
413 switch l.Kind {
414 case udiff.Equal:
415 if shouldWrite() {
416 ls := dv.style.EqualLine
417 content, leadingEllipsis := getContent(l.Content, ls)
418 if dv.lineNumbers {
419 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
420 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
421 }
422 b.WriteString(fullContentStyle.Render(
423 ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
424 ))
425 }
426 beforeLine++
427 afterLine++
428 case udiff.Insert:
429 if shouldWrite() {
430 ls := dv.style.InsertLine
431 content, leadingEllipsis := getContent(l.Content, ls)
432 if dv.lineNumbers {
433 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
434 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
435 }
436 b.WriteString(fullContentStyle.Render(
437 ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
438 ls.Code.Width(dv.codeWidth).Render(content),
439 ))
440 }
441 afterLine++
442 case udiff.Delete:
443 if shouldWrite() {
444 ls := dv.style.DeleteLine
445 content, leadingEllipsis := getContent(l.Content, ls)
446 if dv.lineNumbers {
447 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
448 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
449 }
450 b.WriteString(fullContentStyle.Render(
451 ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
452 ls.Code.Width(dv.codeWidth).Render(content),
453 ))
454 }
455 beforeLine++
456 }
457 if shouldWrite() {
458 b.WriteRune('\n')
459 }
460
461 printedLines++
462 }
463 }
464
465 for printedLines < dv.height {
466 if shouldWrite() {
467 ls := dv.style.MissingLine
468 if dv.lineNumbers {
469 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
470 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
471 }
472 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
473 b.WriteRune('\n')
474 }
475 printedLines++
476 }
477
478 return b.String()
479}
480
481// renderSplit renders the split (side-by-side) diff view as a string.
482func (dv *DiffView) renderSplit() string {
483 var b strings.Builder
484
485 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
486 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
487 printedLines := -dv.yOffset
488 shouldWrite := func() bool { return printedLines >= 0 }
489
490 getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
491 content = strings.TrimSuffix(in, "\n")
492 content = dv.hightlightCode(content, ls.Code.GetBackground())
493 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
494 content = ansi.Truncate(content, dv.codeWidth, "…")
495 leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
496 return
497 }
498
499outer:
500 for i, h := range dv.splitHunks {
501 if shouldWrite() {
502 ls := dv.style.DividerLine
503 if dv.lineNumbers {
504 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
505 }
506 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
507 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
508 if dv.lineNumbers {
509 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
510 }
511 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
512 b.WriteRune('\n')
513 }
514 printedLines++
515
516 beforeLine := h.fromLine
517 afterLine := h.toLine
518
519 for j, l := range h.lines {
520 // print ellipis if we don't have enough space to print the rest of the diff
521 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
522 isLastHunk := i+1 == len(dv.unified.Hunks)
523 isLastLine := j+1 == len(h.lines)
524 if hasReachedHeight && (!isLastHunk || !isLastLine) {
525 if shouldWrite() {
526 ls := dv.style.MissingLine
527 if l.before != nil {
528 ls = dv.lineStyleForType(l.before.Kind)
529 }
530 if dv.lineNumbers {
531 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
532 }
533 b.WriteString(beforeFullContentStyle.Render(
534 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
535 ))
536 ls = dv.style.MissingLine
537 if l.after != nil {
538 ls = dv.lineStyleForType(l.after.Kind)
539 }
540 if dv.lineNumbers {
541 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
542 }
543 b.WriteString(afterFullContentStyle.Render(
544 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
545 ))
546 b.WriteRune('\n')
547 }
548 break outer
549 }
550
551 switch {
552 case l.before == nil:
553 if shouldWrite() {
554 ls := dv.style.MissingLine
555 if dv.lineNumbers {
556 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
557 }
558 b.WriteString(beforeFullContentStyle.Render(
559 ls.Code.Width(dv.fullCodeWidth).Render(" "),
560 ))
561 }
562 case l.before.Kind == udiff.Equal:
563 if shouldWrite() {
564 ls := dv.style.EqualLine
565 content, leadingEllipsis := getContent(l.before.Content, ls)
566 if dv.lineNumbers {
567 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
568 }
569 b.WriteString(beforeFullContentStyle.Render(
570 ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
571 ))
572 }
573 beforeLine++
574 case l.before.Kind == udiff.Delete:
575 if shouldWrite() {
576 ls := dv.style.DeleteLine
577 content, leadingEllipsis := getContent(l.before.Content, ls)
578 if dv.lineNumbers {
579 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
580 }
581 b.WriteString(beforeFullContentStyle.Render(
582 ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
583 ls.Code.Width(dv.codeWidth).Render(content),
584 ))
585 }
586 beforeLine++
587 }
588
589 switch {
590 case l.after == nil:
591 if shouldWrite() {
592 ls := dv.style.MissingLine
593 if dv.lineNumbers {
594 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
595 }
596 b.WriteString(afterFullContentStyle.Render(
597 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
598 ))
599 }
600 case l.after.Kind == udiff.Equal:
601 if shouldWrite() {
602 ls := dv.style.EqualLine
603 content, leadingEllipsis := getContent(l.after.Content, ls)
604 if dv.lineNumbers {
605 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
606 }
607 b.WriteString(afterFullContentStyle.Render(
608 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
609 ))
610 }
611 afterLine++
612 case l.after.Kind == udiff.Insert:
613 if shouldWrite() {
614 ls := dv.style.InsertLine
615 content, leadingEllipsis := getContent(l.after.Content, ls)
616 if dv.lineNumbers {
617 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
618 }
619 b.WriteString(afterFullContentStyle.Render(
620 ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
621 ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
622 ))
623 }
624 afterLine++
625 }
626
627 if shouldWrite() {
628 b.WriteRune('\n')
629 }
630
631 printedLines++
632 }
633 }
634
635 for printedLines < dv.height {
636 if shouldWrite() {
637 ls := dv.style.MissingLine
638 if dv.lineNumbers {
639 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
640 }
641 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
642 if dv.lineNumbers {
643 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
644 }
645 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
646 b.WriteRune('\n')
647 }
648 printedLines++
649 }
650
651 return b.String()
652}
653
654// hunkLineFor formats the header line for a hunk in the unified diff view.
655func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
656 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
657
658 return fmt.Sprintf(
659 " @@ -%d,%d +%d,%d @@ ",
660 h.FromLine,
661 beforeShownLines,
662 h.ToLine,
663 afterShownLines,
664 )
665}
666
667// hunkShownLines calculates the number of lines shown in a hunk for both before
668// and after versions.
669func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
670 for _, l := range h.Lines {
671 switch l.Kind {
672 case udiff.Equal:
673 before++
674 after++
675 case udiff.Insert:
676 after++
677 case udiff.Delete:
678 before++
679 }
680 }
681 return
682}
683
684func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
685 switch t {
686 case udiff.Equal:
687 return dv.style.EqualLine
688 case udiff.Insert:
689 return dv.style.InsertLine
690 case udiff.Delete:
691 return dv.style.DeleteLine
692 default:
693 return dv.style.MissingLine
694 }
695}
696
697func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
698 if dv.chromaStyle == nil {
699 return source
700 }
701
702 l := dv.getChromaLexer(source)
703 f := dv.getChromaFormatter(bgColor)
704
705 it, err := l.Tokenise(nil, source)
706 if err != nil {
707 return source
708 }
709
710 var b strings.Builder
711 if err := f.Format(&b, dv.chromaStyle, it); err != nil {
712 return source
713 }
714 return b.String()
715}
716
717func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
718 l := lexers.Match(dv.before.path)
719 if l == nil {
720 l = lexers.Analyse(source)
721 }
722 if l == nil {
723 l = lexers.Fallback
724 }
725 return chroma.Coalesce(l)
726}
727
728func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
729 return chromaFormatter{
730 bgColor: gbColor,
731 }
732}