11 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters"
13 "github.com/alecthomas/chroma/v2/lexers"
14 "github.com/alecthomas/chroma/v2/styles"
15 "github.com/aymanbagabas/go-udiff"
16 "github.com/charmbracelet/lipgloss"
17 "github.com/charmbracelet/x/ansi"
18 "github.com/opencode-ai/opencode/internal/config"
19 "github.com/sergi/go-diff/diffmatchpatch"
22// -------------------------------------------------------------------------
24// -------------------------------------------------------------------------
26// LineType represents the kind of line in a diff.
30 LineContext LineType = iota // Line exists in both files
31 LineAdded // Line added in the new file
32 LineRemoved // Line removed from the old file
35// Segment represents a portion of a line for intra-line highlighting
43// DiffLine represents a single line in a diff
45 OldLineNo int // Line number in old file (0 for added lines)
46 NewLineNo int // Line number in new file (0 for removed lines)
47 Kind LineType // Type of line (added, removed, context)
48 Content string // Content of the line
49 Segments []Segment // Segments for intraline highlighting
52// Hunk represents a section of changes in a diff
58// DiffResult contains the parsed result of a diff
59type DiffResult struct {
65// linePair represents a pair of lines for side-by-side display
71// -------------------------------------------------------------------------
73// -------------------------------------------------------------------------
75// StyleConfig defines styling for diff rendering
76type StyleConfig struct {
79 FileNameFg lipgloss.Color
81 RemovedLineBg lipgloss.Color
82 AddedLineBg lipgloss.Color
83 ContextLineBg lipgloss.Color
84 HunkLineBg lipgloss.Color
85 RemovedLineNumberBg lipgloss.Color
86 AddedLineNamerBg lipgloss.Color
89 HunkLineFg lipgloss.Color
90 RemovedFg lipgloss.Color
91 AddedFg lipgloss.Color
92 LineNumberFg lipgloss.Color
93 RemovedHighlightFg lipgloss.Color
94 AddedHighlightFg lipgloss.Color
98 RemovedHighlightBg lipgloss.Color
99 AddedHighlightBg lipgloss.Color
102// StyleOption is a function that modifies a StyleConfig
103type StyleOption func(*StyleConfig)
105// NewStyleConfig creates a StyleConfig with default values
106func NewStyleConfig(opts ...StyleOption) StyleConfig {
107 // Default color scheme
108 config := StyleConfig{
110 ShowHunkHeader: true,
111 FileNameFg: lipgloss.Color("#a0a0a0"),
112 RemovedLineBg: lipgloss.Color("#3A3030"),
113 AddedLineBg: lipgloss.Color("#303A30"),
114 ContextLineBg: lipgloss.Color("#212121"),
115 HunkLineBg: lipgloss.Color("#212121"),
116 HunkLineFg: lipgloss.Color("#a0a0a0"),
117 RemovedFg: lipgloss.Color("#7C4444"),
118 AddedFg: lipgloss.Color("#478247"),
119 LineNumberFg: lipgloss.Color("#888888"),
120 HighlightStyle: "dracula",
121 RemovedHighlightBg: lipgloss.Color("#612726"),
122 AddedHighlightBg: lipgloss.Color("#256125"),
123 RemovedLineNumberBg: lipgloss.Color("#332929"),
124 AddedLineNamerBg: lipgloss.Color("#293229"),
125 RemovedHighlightFg: lipgloss.Color("#FADADD"),
126 AddedHighlightFg: lipgloss.Color("#DAFADA"),
129 // Apply all provided options
130 for _, opt := range opts {
137// Style option functions
138func WithFileNameFg(color lipgloss.Color) StyleOption {
139 return func(s *StyleConfig) { s.FileNameFg = color }
142func WithRemovedLineBg(color lipgloss.Color) StyleOption {
143 return func(s *StyleConfig) { s.RemovedLineBg = color }
146func WithAddedLineBg(color lipgloss.Color) StyleOption {
147 return func(s *StyleConfig) { s.AddedLineBg = color }
150func WithContextLineBg(color lipgloss.Color) StyleOption {
151 return func(s *StyleConfig) { s.ContextLineBg = color }
154func WithRemovedFg(color lipgloss.Color) StyleOption {
155 return func(s *StyleConfig) { s.RemovedFg = color }
158func WithAddedFg(color lipgloss.Color) StyleOption {
159 return func(s *StyleConfig) { s.AddedFg = color }
162func WithLineNumberFg(color lipgloss.Color) StyleOption {
163 return func(s *StyleConfig) { s.LineNumberFg = color }
166func WithHighlightStyle(style string) StyleOption {
167 return func(s *StyleConfig) { s.HighlightStyle = style }
170func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
171 return func(s *StyleConfig) {
172 s.RemovedHighlightBg = bg
173 s.RemovedHighlightFg = fg
177func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
178 return func(s *StyleConfig) {
179 s.AddedHighlightBg = bg
180 s.AddedHighlightFg = fg
184func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
185 return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
188func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
189 return func(s *StyleConfig) { s.AddedLineNamerBg = color }
192func WithHunkLineBg(color lipgloss.Color) StyleOption {
193 return func(s *StyleConfig) { s.HunkLineBg = color }
196func WithHunkLineFg(color lipgloss.Color) StyleOption {
197 return func(s *StyleConfig) { s.HunkLineFg = color }
200func WithShowHeader(show bool) StyleOption {
201 return func(s *StyleConfig) { s.ShowHeader = show }
204func WithShowHunkHeader(show bool) StyleOption {
205 return func(s *StyleConfig) { s.ShowHunkHeader = show }
208// -------------------------------------------------------------------------
209// Parse Configuration
210// -------------------------------------------------------------------------
212// ParseConfig configures the behavior of diff parsing
213type ParseConfig struct {
214 ContextSize int // Number of context lines to include
217// ParseOption modifies a ParseConfig
218type ParseOption func(*ParseConfig)
220// WithContextSize sets the number of context lines to include
221func WithContextSize(size int) ParseOption {
222 return func(p *ParseConfig) {
229// -------------------------------------------------------------------------
230// Side-by-Side Configuration
231// -------------------------------------------------------------------------
233// SideBySideConfig configures the rendering of side-by-side diffs
234type SideBySideConfig struct {
239// SideBySideOption modifies a SideBySideConfig
240type SideBySideOption func(*SideBySideConfig)
242// NewSideBySideConfig creates a SideBySideConfig with default values
243func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
244 config := SideBySideConfig{
245 TotalWidth: 160, // Default width for side-by-side view
246 Style: NewStyleConfig(),
249 for _, opt := range opts {
256// WithTotalWidth sets the total width for side-by-side view
257func WithTotalWidth(width int) SideBySideOption {
258 return func(s *SideBySideConfig) {
265// WithStyle sets the styling configuration
266func WithStyle(style StyleConfig) SideBySideOption {
267 return func(s *SideBySideConfig) {
272// WithStyleOptions applies the specified style options
273func WithStyleOptions(opts ...StyleOption) SideBySideOption {
274 return func(s *SideBySideConfig) {
275 s.Style = NewStyleConfig(opts...)
279// -------------------------------------------------------------------------
281// -------------------------------------------------------------------------
283// ParseUnifiedDiff parses a unified diff format string into structured data
284func ParseUnifiedDiff(diff string) (DiffResult, error) {
285 var result DiffResult
286 var currentHunk *Hunk
288 hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
289 lines := strings.Split(diff, "\n")
291 var oldLine, newLine int
294 for _, line := range lines {
295 // Parse file headers
297 if strings.HasPrefix(line, "--- a/") {
298 result.OldFile = strings.TrimPrefix(line, "--- a/")
301 if strings.HasPrefix(line, "+++ b/") {
302 result.NewFile = strings.TrimPrefix(line, "+++ b/")
308 // Parse hunk headers
309 if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
310 if currentHunk != nil {
311 result.Hunks = append(result.Hunks, *currentHunk)
318 oldStart, _ := strconv.Atoi(matches[1])
319 newStart, _ := strconv.Atoi(matches[3])
325 // Ignore "No newline at end of file" markers
326 if strings.HasPrefix(line, "\\ No newline at end of file") {
330 if currentHunk == nil {
334 // Process the line based on its prefix
338 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
346 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
354 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
364 // Handle empty lines
365 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
376 // Add the last hunk if there is one
377 if currentHunk != nil {
378 result.Hunks = append(result.Hunks, *currentHunk)
384// HighlightIntralineChanges updates lines in a hunk to show character-level differences
385func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
386 var updated []DiffLine
387 dmp := diffmatchpatch.New()
389 for i := 0; i < len(h.Lines); i++ {
390 // Look for removed line followed by added line
391 if i+1 < len(h.Lines) &&
392 h.Lines[i].Kind == LineRemoved &&
393 h.Lines[i+1].Kind == LineAdded {
395 oldLine := h.Lines[i]
396 newLine := h.Lines[i+1]
398 // Find character-level differences
399 patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
400 patches = dmp.DiffCleanupSemantic(patches)
401 patches = dmp.DiffCleanupMerge(patches)
402 patches = dmp.DiffCleanupEfficiency(patches)
404 segments := make([]Segment, 0)
408 for _, patch := range patches {
410 case diffmatchpatch.DiffDelete:
411 segments = append(segments, Segment{
413 End: removeStart + len(patch.Text),
417 removeStart += len(patch.Text)
418 case diffmatchpatch.DiffInsert:
419 segments = append(segments, Segment{
421 End: addStart + len(patch.Text),
425 addStart += len(patch.Text)
427 // Context text, no highlighting needed
428 removeStart += len(patch.Text)
429 addStart += len(patch.Text)
432 oldLine.Segments = segments
433 newLine.Segments = segments
435 updated = append(updated, oldLine, newLine)
436 i++ // Skip the next line as we've already processed it
438 updated = append(updated, h.Lines[i])
445// pairLines converts a flat list of diff lines to pairs for side-by-side display
446func pairLines(lines []DiffLine) []linePair {
451 switch lines[i].Kind {
453 // Check if the next line is an addition, if so pair them
454 if i+1 < len(lines) && lines[i+1].Kind == LineAdded {
455 pairs = append(pairs, linePair{left: &lines[i], right: &lines[i+1]})
458 pairs = append(pairs, linePair{left: &lines[i], right: nil})
462 pairs = append(pairs, linePair{left: nil, right: &lines[i]})
465 pairs = append(pairs, linePair{left: &lines[i], right: &lines[i]})
473// -------------------------------------------------------------------------
474// Syntax Highlighting
475// -------------------------------------------------------------------------
477// SyntaxHighlight applies syntax highlighting to text based on file extension
478func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
479 // Determine the language lexer to use
480 l := lexers.Match(fileName)
482 l = lexers.Analyse(source)
487 l = chroma.Coalesce(l)
490 f := formatters.Get(formatter)
492 f = formatters.Fallback
495 <style name="vscode-dark-plus">
497 <entry type="Background" style="bg:#1E1E1E"/>
498 <entry type="Text" style="#D4D4D4"/>
499 <entry type="Other" style="#D4D4D4"/>
500 <entry type="Error" style="#F44747"/>
501 <!-- Keywords - using the Control flow / Special keywords color -->
502 <entry type="Keyword" style="#C586C0"/>
503 <entry type="KeywordConstant" style="#4FC1FF"/>
504 <entry type="KeywordDeclaration" style="#C586C0"/>
505 <entry type="KeywordNamespace" style="#C586C0"/>
506 <entry type="KeywordPseudo" style="#C586C0"/>
507 <entry type="KeywordReserved" style="#C586C0"/>
508 <entry type="KeywordType" style="#4EC9B0"/>
510 <entry type="Name" style="#D4D4D4"/>
511 <entry type="NameAttribute" style="#9CDCFE"/>
512 <entry type="NameBuiltin" style="#4EC9B0"/>
513 <entry type="NameBuiltinPseudo" style="#9CDCFE"/>
514 <entry type="NameClass" style="#4EC9B0"/>
515 <entry type="NameConstant" style="#4FC1FF"/>
516 <entry type="NameDecorator" style="#DCDCAA"/>
517 <entry type="NameEntity" style="#9CDCFE"/>
518 <entry type="NameException" style="#4EC9B0"/>
519 <entry type="NameFunction" style="#DCDCAA"/>
520 <entry type="NameLabel" style="#C8C8C8"/>
521 <entry type="NameNamespace" style="#4EC9B0"/>
522 <entry type="NameOther" style="#9CDCFE"/>
523 <entry type="NameTag" style="#569CD6"/>
524 <entry type="NameVariable" style="#9CDCFE"/>
525 <entry type="NameVariableClass" style="#9CDCFE"/>
526 <entry type="NameVariableGlobal" style="#9CDCFE"/>
527 <entry type="NameVariableInstance" style="#9CDCFE"/>
529 <entry type="Literal" style="#CE9178"/>
530 <entry type="LiteralDate" style="#CE9178"/>
531 <entry type="LiteralString" style="#CE9178"/>
532 <entry type="LiteralStringBacktick" style="#CE9178"/>
533 <entry type="LiteralStringChar" style="#CE9178"/>
534 <entry type="LiteralStringDoc" style="#CE9178"/>
535 <entry type="LiteralStringDouble" style="#CE9178"/>
536 <entry type="LiteralStringEscape" style="#d7ba7d"/>
537 <entry type="LiteralStringHeredoc" style="#CE9178"/>
538 <entry type="LiteralStringInterpol" style="#CE9178"/>
539 <entry type="LiteralStringOther" style="#CE9178"/>
540 <entry type="LiteralStringRegex" style="#d16969"/>
541 <entry type="LiteralStringSingle" style="#CE9178"/>
542 <entry type="LiteralStringSymbol" style="#CE9178"/>
543 <!-- Numbers - using the numberLiteral color -->
544 <entry type="LiteralNumber" style="#b5cea8"/>
545 <entry type="LiteralNumberBin" style="#b5cea8"/>
546 <entry type="LiteralNumberFloat" style="#b5cea8"/>
547 <entry type="LiteralNumberHex" style="#b5cea8"/>
548 <entry type="LiteralNumberInteger" style="#b5cea8"/>
549 <entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
550 <entry type="LiteralNumberOct" style="#b5cea8"/>
552 <entry type="Operator" style="#D4D4D4"/>
553 <entry type="OperatorWord" style="#C586C0"/>
554 <entry type="Punctuation" style="#D4D4D4"/>
555 <!-- Comments - standard VSCode Dark+ comment color -->
556 <entry type="Comment" style="#6A9955"/>
557 <entry type="CommentHashbang" style="#6A9955"/>
558 <entry type="CommentMultiline" style="#6A9955"/>
559 <entry type="CommentSingle" style="#6A9955"/>
560 <entry type="CommentSpecial" style="#6A9955"/>
561 <entry type="CommentPreproc" style="#C586C0"/>
562 <!-- Generic styles -->
563 <entry type="Generic" style="#D4D4D4"/>
564 <entry type="GenericDeleted" style="#F44747"/>
565 <entry type="GenericEmph" style="italic #D4D4D4"/>
566 <entry type="GenericError" style="#F44747"/>
567 <entry type="GenericHeading" style="bold #D4D4D4"/>
568 <entry type="GenericInserted" style="#b5cea8"/>
569 <entry type="GenericOutput" style="#808080"/>
570 <entry type="GenericPrompt" style="#D4D4D4"/>
571 <entry type="GenericStrong" style="bold #D4D4D4"/>
572 <entry type="GenericSubheading" style="bold #D4D4D4"/>
573 <entry type="GenericTraceback" style="#F44747"/>
574 <entry type="GenericUnderline" style="underline"/>
575 <entry type="TextWhitespace" style="#D4D4D4"/>
579 r := strings.NewReader(theme)
580 style := chroma.MustNewXMLStyle(r)
581 // Modify the style to use the provided background
582 s, err := style.Builder().Transform(
583 func(t chroma.StyleEntry) chroma.StyleEntry {
584 r, g, b, _ := bg.RGBA()
585 t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
593 // Tokenize and format
594 it, err := l.Tokenise(nil, source)
599 return f.Format(w, s, it)
602// highlightLine applies syntax highlighting to a single line
603func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
605 err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
612// createStyles generates the lipgloss styles needed for rendering diffs
613func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
614 removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
615 addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
616 contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
617 lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
622// -------------------------------------------------------------------------
623// Rendering Functions
624// -------------------------------------------------------------------------
626// applyHighlighting applies intra-line highlighting to a piece of text
627func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
629 // Find all ANSI sequences in the content
630 ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
631 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
633 // Build a mapping of visible character positions to their actual indices
635 ansiSequences := make(map[int]string)
636 lastAnsiSeq := "\x1b[0m" // Default reset sequence
638 for i := 0; i < len(content); {
640 for _, match := range ansiMatches {
642 ansiSequences[visibleIdx] = content[match[0]:match[1]]
643 lastAnsiSeq = content[match[0]:match[1]]
653 // For non-ANSI positions, store the last ANSI sequence
654 if _, exists := ansiSequences[visibleIdx]; !exists {
655 ansiSequences[visibleIdx] = lastAnsiSeq
661 // Apply highlighting
662 var sb strings.Builder
666 for i := 0; i < len(content); {
667 // Check if we're at an ANSI sequence
669 for _, match := range ansiMatches {
671 sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence
681 // Check for segment boundaries
682 for _, seg := range segments {
683 if seg.Type == segmentType {
684 if currentPos == seg.Start {
687 if currentPos == seg.End {
693 // Get current character
694 char := string(content[i])
697 // Get the current styling
698 currentStyle := ansiSequences[currentPos]
700 // Apply background highlight
701 sb.WriteString("\x1b[48;2;")
702 r, g, b, _ := highlightBg.RGBA()
703 sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
705 sb.WriteString("\x1b[49m") // Reset only background
707 // Reapply the original ANSI sequence
708 sb.WriteString(currentStyle)
710 // Not in selection, just copy the character
721// renderLeftColumn formats the left side of a side-by-side diff
722func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
724 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
725 return contextLineStyle.Width(colWidth).Render("")
728 removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
730 // Determine line style based on line type
732 var bgStyle lipgloss.Style
735 marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
736 bgStyle = removedLineStyle
737 lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
740 bgStyle = contextLineStyle
742 marker = contextLineStyle.Render(" ")
743 bgStyle = contextLineStyle
746 // Format line number
748 if dl.OldLineNo > 0 {
749 lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
752 // Create the line prefix
753 prefix := lineNumberStyle.Render(lineNum + " " + marker)
755 // Apply syntax highlighting
756 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
758 // Apply intra-line highlighting for removed lines
759 if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
760 content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
763 // Add a padding space for removed lines
764 if dl.Kind == LineRemoved {
765 content = bgStyle.Render(" ") + content
768 // Create the final line and truncate if needed
769 lineText := prefix + content
770 return bgStyle.MaxHeight(1).Width(colWidth).Render(
774 lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
779// renderRightColumn formats the right side of a side-by-side diff
780func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
782 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
783 return contextLineStyle.Width(colWidth).Render("")
786 _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
788 // Determine line style based on line type
790 var bgStyle lipgloss.Style
793 marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
794 bgStyle = addedLineStyle
795 lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
798 bgStyle = contextLineStyle
800 marker = contextLineStyle.Render(" ")
801 bgStyle = contextLineStyle
804 // Format line number
806 if dl.NewLineNo > 0 {
807 lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
810 // Create the line prefix
811 prefix := lineNumberStyle.Render(lineNum + " " + marker)
813 // Apply syntax highlighting
814 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
816 // Apply intra-line highlighting for added lines
817 if dl.Kind == LineAdded && len(dl.Segments) > 0 {
818 content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
821 // Add a padding space for added lines
822 if dl.Kind == LineAdded {
823 content = bgStyle.Render(" ") + content
826 // Create the final line and truncate if needed
827 lineText := prefix + content
828 return bgStyle.MaxHeight(1).Width(colWidth).Render(
832 lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
837// -------------------------------------------------------------------------
839// -------------------------------------------------------------------------
841// RenderSideBySideHunk formats a hunk for side-by-side display
842func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
843 // Apply options to create the configuration
844 config := NewSideBySideConfig(opts...)
846 // Make a copy of the hunk so we don't modify the original
847 hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
848 copy(hunkCopy.Lines, h.Lines)
850 // Highlight changes within lines
851 HighlightIntralineChanges(&hunkCopy, config.Style)
853 // Pair lines for side-by-side display
854 pairs := pairLines(hunkCopy.Lines)
856 // Calculate column width
857 colWidth := config.TotalWidth / 2
859 leftWidth := colWidth
860 rightWidth := config.TotalWidth - colWidth
861 var sb strings.Builder
862 for _, p := range pairs {
863 leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
864 rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
865 sb.WriteString(leftStr + rightStr + "\n")
871// FormatDiff creates a side-by-side formatted view of a diff
872func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
873 diffResult, err := ParseUnifiedDiff(diffText)
878 var sb strings.Builder
879 config := NewSideBySideConfig(opts...)
881 if config.Style.ShowHeader {
882 removeIcon := lipgloss.NewStyle().
883 Background(config.Style.RemovedLineBg).
884 Foreground(config.Style.RemovedFg).
886 addIcon := lipgloss.NewStyle().
887 Background(config.Style.AddedLineBg).
888 Foreground(config.Style.AddedFg).
891 fileName := lipgloss.NewStyle().
892 Background(config.Style.ContextLineBg).
893 Foreground(config.Style.FileNameFg).
894 Render(" " + diffResult.OldFile)
897 Background(config.Style.ContextLineBg).
899 Foreground(config.Style.FileNameFg).
900 BorderStyle(lipgloss.NormalBorder()).
903 BorderForeground(config.Style.FileNameFg).
904 BorderBackground(config.Style.ContextLineBg).
905 Width(config.TotalWidth).
907 lipgloss.JoinHorizontal(lipgloss.Top,
916 for _, h := range diffResult.Hunks {
917 // Render hunk header
918 if config.Style.ShowHunkHeader {
921 Background(config.Style.HunkLineBg).
922 Foreground(config.Style.HunkLineFg).
923 Width(config.TotalWidth).
924 Render(h.Header) + "\n",
927 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
930 return sb.String(), nil
933// GenerateDiff creates a unified diff from two file contents
934func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
935 // remove the cwd prefix and ensure consistent path format
936 // this prevents issues with absolute paths in different environments
937 cwd := config.WorkingDirectory()
938 fileName = strings.TrimPrefix(fileName, cwd)
939 fileName = strings.TrimPrefix(fileName, "/")
942 unified = udiff.Unified("a/"+fileName, "b/"+fileName, beforeContent, afterContent)
947 lines := strings.Split(unified, "\n")
948 for _, line := range lines {
949 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
951 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
956 return unified, additions, removals
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list