@@ -2,10 +2,13 @@ package diffview
10+ "github.com/alecthomas/chroma/v2"
11+ "github.com/alecthomas/chroma/v2/lexers"
9 "github.com/aymanbagabas/go-udiff"
12 "github.com/aymanbagabas/go-udiff"
10 "github.com/aymanbagabas/go-udiff/myers"
13 "github.com/aymanbagabas/go-udiff/myers"
11 "github.com/charmbracelet/lipgloss/v2"
14 "github.com/charmbracelet/lipgloss/v2"
@@ -43,6 +46,7 @@ type DiffView struct {
43 yOffset int
46 yOffset int
44 style Style
47 style Style
45 tabWidth int
48 tabWidth int
49+ chromaStyle *chroma.Style
47 isComputed bool
51 isComputed bool
@@ -153,6 +157,13 @@ func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
153 return dv
157 return dv
160+// ChromaStyle sets the chroma style for syntax highlighting.
161+// If nil, no syntax highlighting will be applied.
162+func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
163+ dv.chromaStyle = style
156// String returns the string representation of the DiffView.
167// String returns the string representation of the DiffView.
157func (dv *DiffView) String() string {
168func (dv *DiffView) String() string {
158 dv.replaceTabs()
169 dv.replaceTabs()
@@ -361,14 +372,19 @@ outer:
361 break outer
372 break outer
375+ getContent := func(ls LineStyle) string {
364− content := strings.TrimSuffix(l.Content, "\n")
376+ content := strings.TrimSuffix(l.Content, "\n")
377+ content = dv.hightlightCode(content, ls.Code.GetBackground())
365− content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
378+ content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
366− content = ansi.Truncate(content, dv.codeWidth, "…")
379+ content = ansi.Truncate(content, dv.codeWidth, "…")
368 leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
383 leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
370 switch l.Kind {
385 switch l.Kind {
371 case udiff.Equal:
386 case udiff.Equal:
387+ content := getContent(dv.style.EqualLine)
372 if dv.lineNumbers {
388 if dv.lineNumbers {
373 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
389 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
374 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
390 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
@@ -379,6 +395,7 @@ outer:
379 beforeLine++
395 beforeLine++
380 afterLine++
396 afterLine++
381 case udiff.Insert:
397 case udiff.Insert:
398+ content := getContent(dv.style.InsertLine)
382 if dv.lineNumbers {
399 if dv.lineNumbers {
383 write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
400 write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
384 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
401 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
@@ -389,6 +406,7 @@ outer:
390 afterLine++
407 afterLine++
391 case udiff.Delete:
408 case udiff.Delete:
409+ content := getContent(dv.style.DeleteLine)
392 if dv.lineNumbers {
410 if dv.lineNumbers {
393 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
411 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
394 write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
412 write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
@@ -479,22 +497,17 @@ outer:
479 break outer
497 break outer
482− var beforeContent string
500+ getContent := func(content string, ls LineStyle) string {
483− var afterContent string
484− if l.before != nil {
485− beforeContent = strings.TrimSuffix(l.before.Content, "\n")
501+ content = strings.TrimSuffix(content, "\n")
502+ content = dv.hightlightCode(content, ls.Code.GetBackground())
486− beforeContent = ansi.GraphemeWidth.Cut(beforeContent, dv.xOffset, len(beforeContent))
503+ content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
487− beforeContent = ansi.Truncate(beforeContent, dv.codeWidth, "…")
504+ content = ansi.Truncate(content, dv.codeWidth, "…")
489− if l.after != nil {
507+ getLeadingEllipsis := func(content string) bool {
490− afterContent = strings.TrimSuffix(l.after.Content, "\n")
508+ return dv.xOffset > 0 && strings.TrimSpace(content) != ""
491− afterContent = ansi.GraphemeWidth.Cut(afterContent, dv.xOffset, len(afterContent))
492− afterContent = ansi.Truncate(afterContent, dv.codeWidth+btoi(dv.extraColOnAfter), "…")
495− leadingBeforeEllipsis := dv.xOffset > 0 && strings.TrimSpace(beforeContent) != ""
496− leadingAfterEllipsis := dv.xOffset > 0 && strings.TrimSpace(afterContent) != ""
499 case l.before == nil:
512 case l.before == nil:
500 if dv.lineNumbers {
513 if dv.lineNumbers {
@@ -504,20 +517,24 @@ outer:
504 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
517 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
506 case l.before.Kind == udiff.Equal:
519 case l.before.Kind == udiff.Equal:
520+ content := getContent(l.before.Content, dv.style.EqualLine)
521+ leadingEllipsis := getLeadingEllipsis(content)
507 if dv.lineNumbers {
522 if dv.lineNumbers {
508 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
523 write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
510 write(beforeFullContentStyle.Render(
525 write(beforeFullContentStyle.Render(
511− dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingBeforeEllipsis, " …", " ") + beforeContent),
526+ dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
513 beforeLine++
528 beforeLine++
514 case l.before.Kind == udiff.Delete:
529 case l.before.Kind == udiff.Delete:
530+ content := getContent(l.before.Content, dv.style.DeleteLine)
531+ leadingEllipsis := getLeadingEllipsis(content)
515 if dv.lineNumbers {
532 if dv.lineNumbers {
516 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
533 write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
518 write(beforeFullContentStyle.Render(
535 write(beforeFullContentStyle.Render(
519− dv.style.DeleteLine.Symbol.Render(ternary(leadingBeforeEllipsis, "-…", "- ")) +
536+ dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
520− dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
537+ dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
522 beforeLine++
539 beforeLine++
@@ -531,20 +548,24 @@ outer:
531 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
548 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
533 case l.after.Kind == udiff.Equal:
550 case l.after.Kind == udiff.Equal:
551+ content := getContent(l.after.Content, dv.style.EqualLine)
552+ leadingEllipsis := getLeadingEllipsis(content)
534 if dv.lineNumbers {
553 if dv.lineNumbers {
535 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
554 write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
537 write(afterFullContentStyle.Render(
556 write(afterFullContentStyle.Render(
538− dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingAfterEllipsis, " …", " ") + afterContent),
557+ dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
540 afterLine++
559 afterLine++
541 case l.after.Kind == udiff.Insert:
560 case l.after.Kind == udiff.Insert:
561+ content := getContent(l.after.Content, dv.style.InsertLine)
562+ leadingEllipsis := getLeadingEllipsis(content)
542 if dv.lineNumbers {
563 if dv.lineNumbers {
543 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
564 write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
545 write(afterFullContentStyle.Render(
566 write(afterFullContentStyle.Render(
546− dv.style.InsertLine.Symbol.Render(ternary(leadingAfterEllipsis, "+…", "+ ")) +
567+ dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
547− dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
568+ dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
549 afterLine++
570 afterLine++
@@ -613,3 +634,40 @@ func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
613 return dv.style.MissingLine
634 return dv.style.MissingLine
638+func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
639+ if dv.chromaStyle == nil {
643+ l := dv.getChromaLexer(source)
644+ f := dv.getChromaFormatter(bgColor)
646+ it, err := l.Tokenise(nil, source)
651+ var b strings.Builder
652+ if err := f.Format(&b, dv.chromaStyle, it); err != nil {
658+func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
659+ l := lexers.Match(dv.before.path)
661+ l = lexers.Analyse(source)
666+ return chroma.Coalesce(l)
669+func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
670+ return chromaFormatter{