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