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/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/x/ansi"
14)
15
16const (
17 leadingSymbolsSize = 2
18 lineNumPadding = 1
19)
20
21type file struct {
22 path string
23 content string
24}
25
26type layout int
27
28const (
29 layoutUnified layout = iota + 1
30 layoutSplit
31)
32
33// DiffView represents a view for displaying differences between two files.
34type DiffView struct {
35 layout layout
36 before file
37 after file
38 contextLines int
39 lineNumbers bool
40 height int
41 width int
42 xOffset int
43 yOffset int
44 infiniteYScroll bool
45 style Style
46 tabWidth int
47 chromaStyle *chroma.Style
48
49 isComputed bool
50 err error
51 unified udiff.UnifiedDiff
52 edits []udiff.Edit
53
54 splitHunks []splitHunk
55
56 totalLines int
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.
65func 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.
77func (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).
83func (dv *DiffView) Split() *DiffView {
84 dv.layout = layoutSplit
85 return dv
86}
87
88// Before sets the "before" file for the DiffView.
89func (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.
95func (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.
101func (dv *DiffView) ContextLines(contextLines int) *DiffView {
102 dv.contextLines = contextLines
103 return dv
104}
105
106// Style sets the style for the DiffView.
107func (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.
113func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
114 dv.lineNumbers = lineNumbers
115 return dv
116}
117
118// Height sets the height of the DiffView.
119func (dv *DiffView) Height(height int) *DiffView {
120 dv.height = height
121 return dv
122}
123
124// Width sets the width of the DiffView.
125func (dv *DiffView) Width(width int) *DiffView {
126 dv.width = width
127 return dv
128}
129
130// XOffset sets the horizontal offset for the DiffView.
131func (dv *DiffView) XOffset(xOffset int) *DiffView {
132 dv.xOffset = xOffset
133 return dv
134}
135
136// YOffset sets the vertical offset for the DiffView.
137func (dv *DiffView) YOffset(yOffset int) *DiffView {
138 dv.yOffset = yOffset
139 return dv
140}
141
142// InfiniteYScroll allows the YOffset to scroll beyond the last line.
143func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
144 dv.infiniteYScroll = infiniteYScroll
145 return dv
146}
147
148// TabWidth sets the tab width. Only relevant for code that contains tabs, like
149// Go code.
150func (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.
157func (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.
163func (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 dv.detectTotalLines()
172 dv.preventInfiniteYScroll()
173
174 if dv.width <= 0 {
175 dv.detectCodeWidth()
176 } else {
177 dv.resizeCodeWidth()
178 }
179
180 style := lipgloss.NewStyle()
181 if dv.width > 0 {
182 style = style.MaxWidth(dv.width)
183 }
184 if dv.height > 0 {
185 style = style.MaxHeight(dv.height)
186 }
187
188 switch dv.layout {
189 case layoutUnified:
190 return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
191 case layoutSplit:
192 return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
193 default:
194 panic("unknown diffview layout")
195 }
196}
197
198// replaceTabs replaces tabs in the before and after file contents with spaces
199// according to the specified tab width.
200func (dv *DiffView) replaceTabs() {
201 spaces := strings.Repeat(" ", dv.tabWidth)
202 dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
203 dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
204}
205
206// computeDiff computes the differences between the "before" and "after" files.
207func (dv *DiffView) computeDiff() error {
208 if dv.isComputed {
209 return dv.err
210 }
211 dv.isComputed = true
212 dv.edits = udiff.Strings(
213 dv.before.content,
214 dv.after.content,
215 )
216 dv.unified, dv.err = udiff.ToUnifiedDiff(
217 dv.before.path,
218 dv.after.path,
219 dv.before.content,
220 dv.edits,
221 dv.contextLines,
222 )
223 return dv.err
224}
225
226// convertDiffToSplit converts the unified diff to a split diff if the layout is
227// set to split.
228func (dv *DiffView) convertDiffToSplit() {
229 if dv.layout != layoutSplit {
230 return
231 }
232
233 dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
234 for i, h := range dv.unified.Hunks {
235 dv.splitHunks[i] = hunkToSplit(h)
236 }
237}
238
239// adjustStyles adjusts adds padding and alignment to the styles.
240func (dv *DiffView) adjustStyles() {
241 setPadding := func(s lipgloss.Style) lipgloss.Style {
242 return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
243 }
244 dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
245 dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
246 dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
247 dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
248 dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
249}
250
251// detectNumDigits calculates the maximum number of digits needed for before and
252// after line numbers.
253func (dv *DiffView) detectNumDigits() {
254 dv.beforeNumDigits = 0
255 dv.afterNumDigits = 0
256
257 for _, h := range dv.unified.Hunks {
258 dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
259 dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
260 }
261}
262
263func (dv *DiffView) detectTotalLines() {
264 dv.totalLines = 0
265
266 switch dv.layout {
267 case layoutUnified:
268 for _, h := range dv.unified.Hunks {
269 dv.totalLines += 1 + len(h.Lines)
270 }
271 case layoutSplit:
272 for _, h := range dv.splitHunks {
273 dv.totalLines += 1 + len(h.lines)
274 }
275 }
276}
277
278func (dv *DiffView) preventInfiniteYScroll() {
279 if dv.infiniteYScroll {
280 return
281 }
282
283 // clamp yOffset to prevent scrolling beyond the last line
284 if dv.height > 0 {
285 maxYOffset := max(0, dv.totalLines-dv.height)
286 dv.yOffset = min(dv.yOffset, maxYOffset)
287 } else {
288 // if no height limit, ensure yOffset doesn't exceed total lines
289 dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
290 }
291 dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
292}
293
294// detectCodeWidth calculates the maximum width of code lines in the diff view.
295func (dv *DiffView) detectCodeWidth() {
296 switch dv.layout {
297 case layoutUnified:
298 dv.detectUnifiedCodeWidth()
299 case layoutSplit:
300 dv.detectSplitCodeWidth()
301 }
302 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
303}
304
305// detectUnifiedCodeWidth calculates the maximum width of code lines in a
306// unified diff.
307func (dv *DiffView) detectUnifiedCodeWidth() {
308 dv.codeWidth = 0
309
310 for _, h := range dv.unified.Hunks {
311 shownLines := ansi.StringWidth(dv.hunkLineFor(h))
312
313 for _, l := range h.Lines {
314 lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
315 dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
316 }
317 }
318}
319
320// detectSplitCodeWidth calculates the maximum width of code lines in a
321// split diff.
322func (dv *DiffView) detectSplitCodeWidth() {
323 dv.codeWidth = 0
324
325 for i, h := range dv.splitHunks {
326 shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
327
328 for _, l := range h.lines {
329 if l.before != nil {
330 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
331 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
332 }
333 if l.after != nil {
334 codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
335 dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
336 }
337 }
338 }
339}
340
341// resizeCodeWidth resizes the code width to fit within the specified width.
342func (dv *DiffView) resizeCodeWidth() {
343 fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
344 fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
345
346 switch dv.layout {
347 case layoutUnified:
348 dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
349 case layoutSplit:
350 remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
351 dv.codeWidth = remainingWidth / 2
352 dv.extraColOnAfter = isOdd(remainingWidth)
353 }
354
355 dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
356}
357
358// renderUnified renders the unified diff view as a string.
359func (dv *DiffView) renderUnified() string {
360 var b strings.Builder
361
362 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
363 printedLines := -dv.yOffset
364 shouldWrite := func() bool { return printedLines >= 0 }
365
366 getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) {
367 content = strings.ReplaceAll(in, "\r\n", "\n")
368 content = strings.TrimSuffix(content, "\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.ReplaceAll(in, "\r\n", "\n")
492 content = strings.TrimSuffix(content, "\n")
493 content = dv.hightlightCode(content, ls.Code.GetBackground())
494 content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
495 content = ansi.Truncate(content, dv.codeWidth, "…")
496 leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != ""
497 return
498 }
499
500outer:
501 for i, h := range dv.splitHunks {
502 if shouldWrite() {
503 ls := dv.style.DividerLine
504 if dv.lineNumbers {
505 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
506 }
507 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
508 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content))
509 if dv.lineNumbers {
510 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
511 }
512 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
513 b.WriteRune('\n')
514 }
515 printedLines++
516
517 beforeLine := h.fromLine
518 afterLine := h.toLine
519
520 for j, l := range h.lines {
521 // print ellipis if we don't have enough space to print the rest of the diff
522 hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
523 isLastHunk := i+1 == len(dv.unified.Hunks)
524 isLastLine := j+1 == len(h.lines)
525 if hasReachedHeight && (!isLastHunk || !isLastLine) {
526 if shouldWrite() {
527 ls := dv.style.MissingLine
528 if l.before != nil {
529 ls = dv.lineStyleForType(l.before.Kind)
530 }
531 if dv.lineNumbers {
532 b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
533 }
534 b.WriteString(beforeFullContentStyle.Render(
535 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
536 ))
537 ls = dv.style.MissingLine
538 if l.after != nil {
539 ls = dv.lineStyleForType(l.after.Kind)
540 }
541 if dv.lineNumbers {
542 b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
543 }
544 b.WriteString(afterFullContentStyle.Render(
545 ls.Code.Width(dv.fullCodeWidth).Render(" …"),
546 ))
547 b.WriteRune('\n')
548 }
549 break outer
550 }
551
552 switch {
553 case l.before == nil:
554 if shouldWrite() {
555 ls := dv.style.MissingLine
556 if dv.lineNumbers {
557 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
558 }
559 b.WriteString(beforeFullContentStyle.Render(
560 ls.Code.Width(dv.fullCodeWidth).Render(" "),
561 ))
562 }
563 case l.before.Kind == udiff.Equal:
564 if shouldWrite() {
565 ls := dv.style.EqualLine
566 content, leadingEllipsis := getContent(l.before.Content, ls)
567 if dv.lineNumbers {
568 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
569 }
570 b.WriteString(beforeFullContentStyle.Render(
571 ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
572 ))
573 }
574 beforeLine++
575 case l.before.Kind == udiff.Delete:
576 if shouldWrite() {
577 ls := dv.style.DeleteLine
578 content, leadingEllipsis := getContent(l.before.Content, ls)
579 if dv.lineNumbers {
580 b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
581 }
582 b.WriteString(beforeFullContentStyle.Render(
583 ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
584 ls.Code.Width(dv.codeWidth).Render(content),
585 ))
586 }
587 beforeLine++
588 }
589
590 switch {
591 case l.after == nil:
592 if shouldWrite() {
593 ls := dv.style.MissingLine
594 if dv.lineNumbers {
595 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
596 }
597 b.WriteString(afterFullContentStyle.Render(
598 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
599 ))
600 }
601 case l.after.Kind == udiff.Equal:
602 if shouldWrite() {
603 ls := dv.style.EqualLine
604 content, leadingEllipsis := getContent(l.after.Content, ls)
605 if dv.lineNumbers {
606 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
607 }
608 b.WriteString(afterFullContentStyle.Render(
609 ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
610 ))
611 }
612 afterLine++
613 case l.after.Kind == udiff.Insert:
614 if shouldWrite() {
615 ls := dv.style.InsertLine
616 content, leadingEllipsis := getContent(l.after.Content, ls)
617 if dv.lineNumbers {
618 b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
619 }
620 b.WriteString(afterFullContentStyle.Render(
621 ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
622 ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
623 ))
624 }
625 afterLine++
626 }
627
628 if shouldWrite() {
629 b.WriteRune('\n')
630 }
631
632 printedLines++
633 }
634 }
635
636 for printedLines < dv.height {
637 if shouldWrite() {
638 ls := dv.style.MissingLine
639 if dv.lineNumbers {
640 b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
641 }
642 b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" "))
643 if dv.lineNumbers {
644 b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
645 }
646 b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
647 b.WriteRune('\n')
648 }
649 printedLines++
650 }
651
652 return b.String()
653}
654
655// hunkLineFor formats the header line for a hunk in the unified diff view.
656func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
657 beforeShownLines, afterShownLines := dv.hunkShownLines(h)
658
659 return fmt.Sprintf(
660 " @@ -%d,%d +%d,%d @@ ",
661 h.FromLine,
662 beforeShownLines,
663 h.ToLine,
664 afterShownLines,
665 )
666}
667
668// hunkShownLines calculates the number of lines shown in a hunk for both before
669// and after versions.
670func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
671 for _, l := range h.Lines {
672 switch l.Kind {
673 case udiff.Equal:
674 before++
675 after++
676 case udiff.Insert:
677 after++
678 case udiff.Delete:
679 before++
680 }
681 }
682 return
683}
684
685func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
686 switch t {
687 case udiff.Equal:
688 return dv.style.EqualLine
689 case udiff.Insert:
690 return dv.style.InsertLine
691 case udiff.Delete:
692 return dv.style.DeleteLine
693 default:
694 return dv.style.MissingLine
695 }
696}
697
698func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
699 if dv.chromaStyle == nil {
700 return source
701 }
702
703 l := dv.getChromaLexer(source)
704 f := dv.getChromaFormatter(bgColor)
705
706 it, err := l.Tokenise(nil, source)
707 if err != nil {
708 return source
709 }
710
711 var b strings.Builder
712 if err := f.Format(&b, dv.chromaStyle, it); err != nil {
713 return source
714 }
715 return b.String()
716}
717
718func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
719 l := lexers.Match(dv.before.path)
720 if l == nil {
721 l = lexers.Analyse(source)
722 }
723 if l == nil {
724 l = lexers.Fallback
725 }
726 return chroma.Coalesce(l)
727}
728
729func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
730 return chromaFormatter{
731 bgColor: gbColor,
732 }
733}