14 "github.com/alecthomas/chroma/v2"
15 "github.com/alecthomas/chroma/v2/formatters"
16 "github.com/alecthomas/chroma/v2/lexers"
17 "github.com/alecthomas/chroma/v2/styles"
18 "github.com/charmbracelet/lipgloss"
19 "github.com/charmbracelet/x/ansi"
20 "github.com/go-git/go-git/v5"
21 "github.com/go-git/go-git/v5/plumbing/object"
22 "github.com/kujtimiihoxha/opencode/internal/config"
23 "github.com/kujtimiihoxha/opencode/internal/logging"
24 "github.com/sergi/go-diff/diffmatchpatch"
27// -------------------------------------------------------------------------
29// -------------------------------------------------------------------------
31// LineType represents the kind of line in a diff.
35 LineContext LineType = iota // Line exists in both files
36 LineAdded // Line added in the new file
37 LineRemoved // Line removed from the old file
40// Segment represents a portion of a line for intra-line highlighting
48// DiffLine represents a single line in a diff
50 OldLineNo int // Line number in old file (0 for added lines)
51 NewLineNo int // Line number in new file (0 for removed lines)
52 Kind LineType // Type of line (added, removed, context)
53 Content string // Content of the line
54 Segments []Segment // Segments for intraline highlighting
57// Hunk represents a section of changes in a diff
63// DiffResult contains the parsed result of a diff
64type DiffResult struct {
70// linePair represents a pair of lines for side-by-side display
76// -------------------------------------------------------------------------
78// -------------------------------------------------------------------------
80// StyleConfig defines styling for diff rendering
81type StyleConfig struct {
83 FileNameFg lipgloss.Color
85 RemovedLineBg lipgloss.Color
86 AddedLineBg lipgloss.Color
87 ContextLineBg lipgloss.Color
88 HunkLineBg lipgloss.Color
89 RemovedLineNumberBg lipgloss.Color
90 AddedLineNamerBg lipgloss.Color
93 HunkLineFg lipgloss.Color
94 RemovedFg lipgloss.Color
95 AddedFg lipgloss.Color
96 LineNumberFg lipgloss.Color
97 RemovedHighlightFg lipgloss.Color
98 AddedHighlightFg lipgloss.Color
100 // Highlight settings
101 HighlightStyle string
102 RemovedHighlightBg lipgloss.Color
103 AddedHighlightBg lipgloss.Color
106// StyleOption is a function that modifies a StyleConfig
107type StyleOption func(*StyleConfig)
109// NewStyleConfig creates a StyleConfig with default values
110func NewStyleConfig(opts ...StyleOption) StyleConfig {
111 // Default color scheme
112 config := StyleConfig{
114 FileNameFg: lipgloss.Color("#fab283"),
115 RemovedLineBg: lipgloss.Color("#3A3030"),
116 AddedLineBg: lipgloss.Color("#303A30"),
117 ContextLineBg: lipgloss.Color("#212121"),
118 HunkLineBg: lipgloss.Color("#212121"),
119 HunkLineFg: lipgloss.Color("#a0a0a0"),
120 RemovedFg: lipgloss.Color("#7C4444"),
121 AddedFg: lipgloss.Color("#478247"),
122 LineNumberFg: lipgloss.Color("#888888"),
123 HighlightStyle: "dracula",
124 RemovedHighlightBg: lipgloss.Color("#612726"),
125 AddedHighlightBg: lipgloss.Color("#256125"),
126 RemovedLineNumberBg: lipgloss.Color("#332929"),
127 AddedLineNamerBg: lipgloss.Color("#293229"),
128 RemovedHighlightFg: lipgloss.Color("#FADADD"),
129 AddedHighlightFg: lipgloss.Color("#DAFADA"),
132 // Apply all provided options
133 for _, opt := range opts {
140// Style option functions
141func WithFileNameFg(color lipgloss.Color) StyleOption {
142 return func(s *StyleConfig) { s.FileNameFg = color }
145func WithRemovedLineBg(color lipgloss.Color) StyleOption {
146 return func(s *StyleConfig) { s.RemovedLineBg = color }
149func WithAddedLineBg(color lipgloss.Color) StyleOption {
150 return func(s *StyleConfig) { s.AddedLineBg = color }
153func WithContextLineBg(color lipgloss.Color) StyleOption {
154 return func(s *StyleConfig) { s.ContextLineBg = color }
157func WithRemovedFg(color lipgloss.Color) StyleOption {
158 return func(s *StyleConfig) { s.RemovedFg = color }
161func WithAddedFg(color lipgloss.Color) StyleOption {
162 return func(s *StyleConfig) { s.AddedFg = color }
165func WithLineNumberFg(color lipgloss.Color) StyleOption {
166 return func(s *StyleConfig) { s.LineNumberFg = color }
169func WithHighlightStyle(style string) StyleOption {
170 return func(s *StyleConfig) { s.HighlightStyle = style }
173func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
174 return func(s *StyleConfig) {
175 s.RemovedHighlightBg = bg
176 s.RemovedHighlightFg = fg
180func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
181 return func(s *StyleConfig) {
182 s.AddedHighlightBg = bg
183 s.AddedHighlightFg = fg
187func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
188 return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
191func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
192 return func(s *StyleConfig) { s.AddedLineNamerBg = color }
195func WithHunkLineBg(color lipgloss.Color) StyleOption {
196 return func(s *StyleConfig) { s.HunkLineBg = color }
199func WithHunkLineFg(color lipgloss.Color) StyleOption {
200 return func(s *StyleConfig) { s.HunkLineFg = color }
203func WithShowHeader(show bool) StyleOption {
204 return func(s *StyleConfig) { s.ShowHeader = show }
207// -------------------------------------------------------------------------
208// Parse Configuration
209// -------------------------------------------------------------------------
211// ParseConfig configures the behavior of diff parsing
212type ParseConfig struct {
213 ContextSize int // Number of context lines to include
216// ParseOption modifies a ParseConfig
217type ParseOption func(*ParseConfig)
219// WithContextSize sets the number of context lines to include
220func WithContextSize(size int) ParseOption {
221 return func(p *ParseConfig) {
228// -------------------------------------------------------------------------
229// Side-by-Side Configuration
230// -------------------------------------------------------------------------
232// SideBySideConfig configures the rendering of side-by-side diffs
233type SideBySideConfig struct {
238// SideBySideOption modifies a SideBySideConfig
239type SideBySideOption func(*SideBySideConfig)
241// NewSideBySideConfig creates a SideBySideConfig with default values
242func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
243 config := SideBySideConfig{
244 TotalWidth: 160, // Default width for side-by-side view
245 Style: NewStyleConfig(),
248 for _, opt := range opts {
255// WithTotalWidth sets the total width for side-by-side view
256func WithTotalWidth(width int) SideBySideOption {
257 return func(s *SideBySideConfig) {
264// WithStyle sets the styling configuration
265func WithStyle(style StyleConfig) SideBySideOption {
266 return func(s *SideBySideConfig) {
271// WithStyleOptions applies the specified style options
272func WithStyleOptions(opts ...StyleOption) SideBySideOption {
273 return func(s *SideBySideConfig) {
274 s.Style = NewStyleConfig(opts...)
278// -------------------------------------------------------------------------
280// -------------------------------------------------------------------------
282// ParseUnifiedDiff parses a unified diff format string into structured data
283func ParseUnifiedDiff(diff string) (DiffResult, error) {
284 var result DiffResult
285 var currentHunk *Hunk
287 hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
288 lines := strings.Split(diff, "\n")
290 var oldLine, newLine int
293 for _, line := range lines {
294 // Parse file headers
296 if strings.HasPrefix(line, "--- a/") {
297 result.OldFile = strings.TrimPrefix(line, "--- a/")
300 if strings.HasPrefix(line, "+++ b/") {
301 result.NewFile = strings.TrimPrefix(line, "+++ b/")
307 // Parse hunk headers
308 if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
309 if currentHunk != nil {
310 result.Hunks = append(result.Hunks, *currentHunk)
317 oldStart, _ := strconv.Atoi(matches[1])
318 newStart, _ := strconv.Atoi(matches[3])
324 // Ignore "No newline at end of file" markers
325 if strings.HasPrefix(line, "\\ No newline at end of file") {
329 if currentHunk == nil {
333 // Process the line based on its prefix
337 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
345 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
353 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
363 // Handle empty lines
364 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
375 // Add the last hunk if there is one
376 if currentHunk != nil {
377 result.Hunks = append(result.Hunks, *currentHunk)
383// HighlightIntralineChanges updates lines in a hunk to show character-level differences
384func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
385 var updated []DiffLine
386 dmp := diffmatchpatch.New()
388 for i := 0; i < len(h.Lines); i++ {
389 // Look for removed line followed by added line
390 if i+1 < len(h.Lines) &&
391 h.Lines[i].Kind == LineRemoved &&
392 h.Lines[i+1].Kind == LineAdded {
394 oldLine := h.Lines[i]
395 newLine := h.Lines[i+1]
397 // Find character-level differences
398 patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
399 patches = dmp.DiffCleanupSemantic(patches)
400 patches = dmp.DiffCleanupMerge(patches)
401 patches = dmp.DiffCleanupEfficiency(patches)
403 segments := make([]Segment, 0)
407 for _, patch := range patches {
409 case diffmatchpatch.DiffDelete:
410 segments = append(segments, Segment{
412 End: removeStart + len(patch.Text),
416 removeStart += len(patch.Text)
417 case diffmatchpatch.DiffInsert:
418 segments = append(segments, Segment{
420 End: addStart + len(patch.Text),
424 addStart += len(patch.Text)
426 // Context text, no highlighting needed
427 removeStart += len(patch.Text)
428 addStart += len(patch.Text)
431 oldLine.Segments = segments
432 newLine.Segments = segments
434 updated = append(updated, oldLine, newLine)
435 i++ // Skip the next line as we've already processed it
437 updated = append(updated, h.Lines[i])
444// pairLines converts a flat list of diff lines to pairs for side-by-side display
445func pairLines(lines []DiffLine) []linePair {
450 switch lines[i].Kind {
452 // Check if the next line is an addition, if so pair them
453 if i+1 < len(lines) && lines[i+1].Kind == LineAdded {
454 pairs = append(pairs, linePair{left: &lines[i], right: &lines[i+1]})
457 pairs = append(pairs, linePair{left: &lines[i], right: nil})
461 pairs = append(pairs, linePair{left: nil, right: &lines[i]})
464 pairs = append(pairs, linePair{left: &lines[i], right: &lines[i]})
472// -------------------------------------------------------------------------
473// Syntax Highlighting
474// -------------------------------------------------------------------------
476// SyntaxHighlight applies syntax highlighting to text based on file extension
477func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
478 // Determine the language lexer to use
479 l := lexers.Match(fileName)
481 l = lexers.Analyse(source)
486 l = chroma.Coalesce(l)
489 f := formatters.Get(formatter)
491 f = formatters.Fallback
494 <style name="vscode-dark-plus">
496 <entry type="Background" style="bg:#1E1E1E"/>
497 <entry type="Text" style="#D4D4D4"/>
498 <entry type="Other" style="#D4D4D4"/>
499 <entry type="Error" style="#F44747"/>
500 <!-- Keywords - using the Control flow / Special keywords color -->
501 <entry type="Keyword" style="#C586C0"/>
502 <entry type="KeywordConstant" style="#4FC1FF"/>
503 <entry type="KeywordDeclaration" style="#C586C0"/>
504 <entry type="KeywordNamespace" style="#C586C0"/>
505 <entry type="KeywordPseudo" style="#C586C0"/>
506 <entry type="KeywordReserved" style="#C586C0"/>
507 <entry type="KeywordType" style="#4EC9B0"/>
509 <entry type="Name" style="#D4D4D4"/>
510 <entry type="NameAttribute" style="#9CDCFE"/>
511 <entry type="NameBuiltin" style="#4EC9B0"/>
512 <entry type="NameBuiltinPseudo" style="#9CDCFE"/>
513 <entry type="NameClass" style="#4EC9B0"/>
514 <entry type="NameConstant" style="#4FC1FF"/>
515 <entry type="NameDecorator" style="#DCDCAA"/>
516 <entry type="NameEntity" style="#9CDCFE"/>
517 <entry type="NameException" style="#4EC9B0"/>
518 <entry type="NameFunction" style="#DCDCAA"/>
519 <entry type="NameLabel" style="#C8C8C8"/>
520 <entry type="NameNamespace" style="#4EC9B0"/>
521 <entry type="NameOther" style="#9CDCFE"/>
522 <entry type="NameTag" style="#569CD6"/>
523 <entry type="NameVariable" style="#9CDCFE"/>
524 <entry type="NameVariableClass" style="#9CDCFE"/>
525 <entry type="NameVariableGlobal" style="#9CDCFE"/>
526 <entry type="NameVariableInstance" style="#9CDCFE"/>
528 <entry type="Literal" style="#CE9178"/>
529 <entry type="LiteralDate" style="#CE9178"/>
530 <entry type="LiteralString" style="#CE9178"/>
531 <entry type="LiteralStringBacktick" style="#CE9178"/>
532 <entry type="LiteralStringChar" style="#CE9178"/>
533 <entry type="LiteralStringDoc" style="#CE9178"/>
534 <entry type="LiteralStringDouble" style="#CE9178"/>
535 <entry type="LiteralStringEscape" style="#d7ba7d"/>
536 <entry type="LiteralStringHeredoc" style="#CE9178"/>
537 <entry type="LiteralStringInterpol" style="#CE9178"/>
538 <entry type="LiteralStringOther" style="#CE9178"/>
539 <entry type="LiteralStringRegex" style="#d16969"/>
540 <entry type="LiteralStringSingle" style="#CE9178"/>
541 <entry type="LiteralStringSymbol" style="#CE9178"/>
542 <!-- Numbers - using the numberLiteral color -->
543 <entry type="LiteralNumber" style="#b5cea8"/>
544 <entry type="LiteralNumberBin" style="#b5cea8"/>
545 <entry type="LiteralNumberFloat" style="#b5cea8"/>
546 <entry type="LiteralNumberHex" style="#b5cea8"/>
547 <entry type="LiteralNumberInteger" style="#b5cea8"/>
548 <entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
549 <entry type="LiteralNumberOct" style="#b5cea8"/>
551 <entry type="Operator" style="#D4D4D4"/>
552 <entry type="OperatorWord" style="#C586C0"/>
553 <entry type="Punctuation" style="#D4D4D4"/>
554 <!-- Comments - standard VSCode Dark+ comment color -->
555 <entry type="Comment" style="#6A9955"/>
556 <entry type="CommentHashbang" style="#6A9955"/>
557 <entry type="CommentMultiline" style="#6A9955"/>
558 <entry type="CommentSingle" style="#6A9955"/>
559 <entry type="CommentSpecial" style="#6A9955"/>
560 <entry type="CommentPreproc" style="#C586C0"/>
561 <!-- Generic styles -->
562 <entry type="Generic" style="#D4D4D4"/>
563 <entry type="GenericDeleted" style="#F44747"/>
564 <entry type="GenericEmph" style="italic #D4D4D4"/>
565 <entry type="GenericError" style="#F44747"/>
566 <entry type="GenericHeading" style="bold #D4D4D4"/>
567 <entry type="GenericInserted" style="#b5cea8"/>
568 <entry type="GenericOutput" style="#808080"/>
569 <entry type="GenericPrompt" style="#D4D4D4"/>
570 <entry type="GenericStrong" style="bold #D4D4D4"/>
571 <entry type="GenericSubheading" style="bold #D4D4D4"/>
572 <entry type="GenericTraceback" style="#F44747"/>
573 <entry type="GenericUnderline" style="underline"/>
574 <entry type="TextWhitespace" style="#D4D4D4"/>
578 r := strings.NewReader(theme)
579 style := chroma.MustNewXMLStyle(r)
580 // Modify the style to use the provided background
581 s, err := style.Builder().Transform(
582 func(t chroma.StyleEntry) chroma.StyleEntry {
583 r, g, b, _ := bg.RGBA()
584 t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
592 // Tokenize and format
593 it, err := l.Tokenise(nil, source)
598 return f.Format(w, s, it)
601// highlightLine applies syntax highlighting to a single line
602func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
604 err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
611// createStyles generates the lipgloss styles needed for rendering diffs
612func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
613 removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
614 addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
615 contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
616 lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
621// -------------------------------------------------------------------------
622// Rendering Functions
623// -------------------------------------------------------------------------
625// applyHighlighting applies intra-line highlighting to a piece of text
626func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
628 // Find all ANSI sequences in the content
629 ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
630 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
632 // Build a mapping of visible character positions to their actual indices
634 ansiSequences := make(map[int]string)
635 lastAnsiSeq := "\x1b[0m" // Default reset sequence
637 for i := 0; i < len(content); {
639 for _, match := range ansiMatches {
641 ansiSequences[visibleIdx] = content[match[0]:match[1]]
642 lastAnsiSeq = content[match[0]:match[1]]
652 // For non-ANSI positions, store the last ANSI sequence
653 if _, exists := ansiSequences[visibleIdx]; !exists {
654 ansiSequences[visibleIdx] = lastAnsiSeq
660 // Apply highlighting
661 var sb strings.Builder
665 for i := 0; i < len(content); {
666 // Check if we're at an ANSI sequence
668 for _, match := range ansiMatches {
670 sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence
680 // Check for segment boundaries
681 for _, seg := range segments {
682 if seg.Type == segmentType {
683 if currentPos == seg.Start {
686 if currentPos == seg.End {
692 // Get current character
693 char := string(content[i])
696 // Get the current styling
697 currentStyle := ansiSequences[currentPos]
699 // Apply background highlight
700 sb.WriteString("\x1b[48;2;")
701 r, g, b, _ := highlightBg.RGBA()
702 sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
704 sb.WriteString("\x1b[49m") // Reset only background
706 // Reapply the original ANSI sequence
707 sb.WriteString(currentStyle)
709 // Not in selection, just copy the character
720// renderLeftColumn formats the left side of a side-by-side diff
721func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
723 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
724 return contextLineStyle.Width(colWidth).Render("")
727 removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
729 // Determine line style based on line type
731 var bgStyle lipgloss.Style
734 marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
735 bgStyle = removedLineStyle
736 lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
739 bgStyle = contextLineStyle
741 marker = contextLineStyle.Render(" ")
742 bgStyle = contextLineStyle
745 // Format line number
747 if dl.OldLineNo > 0 {
748 lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
751 // Create the line prefix
752 prefix := lineNumberStyle.Render(lineNum + " " + marker)
754 // Apply syntax highlighting
755 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
757 // Apply intra-line highlighting for removed lines
758 if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
759 content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
762 // Add a padding space for removed lines
763 if dl.Kind == LineRemoved {
764 content = bgStyle.Render(" ") + content
767 // Create the final line and truncate if needed
768 lineText := prefix + content
769 return bgStyle.MaxHeight(1).Width(colWidth).Render(
773 lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
778// renderRightColumn formats the right side of a side-by-side diff
779func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
781 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
782 return contextLineStyle.Width(colWidth).Render("")
785 _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
787 // Determine line style based on line type
789 var bgStyle lipgloss.Style
792 marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
793 bgStyle = addedLineStyle
794 lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
797 bgStyle = contextLineStyle
799 marker = contextLineStyle.Render(" ")
800 bgStyle = contextLineStyle
803 // Format line number
805 if dl.NewLineNo > 0 {
806 lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
809 // Create the line prefix
810 prefix := lineNumberStyle.Render(lineNum + " " + marker)
812 // Apply syntax highlighting
813 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
815 // Apply intra-line highlighting for added lines
816 if dl.Kind == LineAdded && len(dl.Segments) > 0 {
817 content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
820 // Add a padding space for added lines
821 if dl.Kind == LineAdded {
822 content = bgStyle.Render(" ") + content
825 // Create the final line and truncate if needed
826 lineText := prefix + content
827 return bgStyle.MaxHeight(1).Width(colWidth).Render(
831 lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
836// -------------------------------------------------------------------------
838// -------------------------------------------------------------------------
840// RenderSideBySideHunk formats a hunk for side-by-side display
841func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
842 // Apply options to create the configuration
843 config := NewSideBySideConfig(opts...)
845 // Make a copy of the hunk so we don't modify the original
846 hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
847 copy(hunkCopy.Lines, h.Lines)
849 // Highlight changes within lines
850 HighlightIntralineChanges(&hunkCopy, config.Style)
852 // Pair lines for side-by-side display
853 pairs := pairLines(hunkCopy.Lines)
855 // Calculate column width
856 colWidth := config.TotalWidth / 2
858 leftWidth := colWidth
859 rightWidth := config.TotalWidth - colWidth
860 var sb strings.Builder
861 for _, p := range pairs {
862 leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
863 rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
864 sb.WriteString(leftStr + rightStr + "\n")
870// FormatDiff creates a side-by-side formatted view of a diff
871func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
872 diffResult, err := ParseUnifiedDiff(diffText)
877 var sb strings.Builder
878 config := NewSideBySideConfig(opts...)
880 if config.Style.ShowHeader {
881 removeIcon := lipgloss.NewStyle().
882 Background(config.Style.RemovedLineBg).
883 Foreground(config.Style.RemovedFg).
885 addIcon := lipgloss.NewStyle().
886 Background(config.Style.AddedLineBg).
887 Foreground(config.Style.AddedFg).
890 fileName := lipgloss.NewStyle().
891 Background(config.Style.ContextLineBg).
892 Foreground(config.Style.FileNameFg).
893 Render(" " + diffResult.OldFile)
896 Background(config.Style.ContextLineBg).
898 Foreground(config.Style.FileNameFg).
899 BorderStyle(lipgloss.NormalBorder()).
902 BorderForeground(config.Style.FileNameFg).
903 BorderBackground(config.Style.ContextLineBg).
904 Width(config.TotalWidth).
906 lipgloss.JoinHorizontal(lipgloss.Top,
915 for _, h := range diffResult.Hunks {
916 // Render hunk header
919 Background(config.Style.HunkLineBg).
920 Foreground(config.Style.HunkLineFg).
921 Width(config.TotalWidth).
922 Render(h.Header) + "\n",
924 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
927 return sb.String(), nil
930// GenerateDiff creates a unified diff from two file contents
931func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
932 // remove the cwd prefix and ensure consistent path format
933 // this prevents issues with absolute paths in different environments
934 cwd := config.WorkingDirectory()
935 fileName = strings.TrimPrefix(fileName, cwd)
936 fileName = strings.TrimPrefix(fileName, "/")
937 // Create temporary directory for git operations
938 tempDir, err := os.MkdirTemp("", fmt.Sprintf("git-diff-%d", time.Now().UnixNano()))
940 logging.Error("Failed to create temp directory for git diff", "error", err)
943 defer os.RemoveAll(tempDir)
945 // Initialize git repo
946 repo, err := git.PlainInit(tempDir, false)
948 logging.Error("Failed to initialize git repository", "error", err)
952 wt, err := repo.Worktree()
954 logging.Error("Failed to get git worktree", "error", err)
958 // Write the "before" content and commit it
959 fullPath := filepath.Join(tempDir, fileName)
960 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
961 logging.Error("Failed to create directory for file", "error", err)
964 if err = os.WriteFile(fullPath, []byte(beforeContent), 0o644); err != nil {
965 logging.Error("Failed to write before content to file", "error", err)
969 _, err = wt.Add(fileName)
971 logging.Error("Failed to add file to git", "error", err)
975 beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
976 Author: &object.Signature{
978 Email: "coder@opencode.ai",
983 logging.Error("Failed to commit before content", "error", err)
987 // Write the "after" content and commit it
988 if err = os.WriteFile(fullPath, []byte(afterContent), 0o644); err != nil {
989 logging.Error("Failed to write after content to file", "error", err)
993 _, err = wt.Add(fileName)
995 logging.Error("Failed to add file to git", "error", err)
999 afterCommit, err := wt.Commit("After", &git.CommitOptions{
1000 Author: &object.Signature{
1002 Email: "coder@opencode.ai",
1007 logging.Error("Failed to commit after content", "error", err)
1011 // Get the diff between the two commits
1012 beforeCommitObj, err := repo.CommitObject(beforeCommit)
1014 logging.Error("Failed to get before commit object", "error", err)
1018 afterCommitObj, err := repo.CommitObject(afterCommit)
1020 logging.Error("Failed to get after commit object", "error", err)
1024 patch, err := beforeCommitObj.Patch(afterCommitObj)
1026 logging.Error("Failed to create git diff patch", "error", err)
1030 // Count additions and removals
1033 for _, fileStat := range patch.Stats() {
1034 additions += fileStat.Addition
1035 removals += fileStat.Deletion
1038 return patch.String(), 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