on main help sign in
fix segment diff and add new theme
commitf6be348bf704ab3d012eec549357f5acd9c74796 authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 14 Apr 2025, 20:12 (2 years ago) parent0136948
1 file changed+348−175
internal/diff/diff.go+348−175
internal/diff/diff.go+348−175
@@ -22,89 +22,95 @@ import (
2222 "github.com/sergi/go-diff/diffmatchpatch"
2323)
2424
25+// -------------------------------------------------------------------------
26+// Core Types
27+// -------------------------------------------------------------------------
28+
2529// LineType represents the kind of line in a diff.
2630type LineType int
2731
2832const (
29− // LineContext represents a line that exists in both the old and new file.
30− LineContext LineType = iota
31− // LineAdded represents a line added in the new file.
32− LineAdded
33− // LineRemoved represents a line removed from the old file.
34− LineRemoved
33+ LineContext LineType = iota // Line exists in both files
34+ LineAdded // Line added in the new file
35+ LineRemoved // Line removed from the old file
3536)
3637
37−// DiffLine represents a single line in a diff, either from the old file,
38−// the new file, or a context line.
38+// Segment represents a portion of a line for intra-line highlighting
39+type Segment struct {
40+ Start int
41+ End int
42+ Type LineType
43+ Text string
44+}
45+
46+// DiffLine represents a single line in a diff
3947type DiffLine struct {
40− OldLineNo int // Line number in the old file (0 for added lines)
41− NewLineNo int // Line number in the new file (0 for removed lines)
42− Kind LineType // Type of line (added, removed, context)
43− Content string // Content of the line
48+ OldLineNo int // Line number in old file (0 for added lines)
49+ NewLineNo int // Line number in new file (0 for removed lines)
50+ Kind LineType // Type of line (added, removed, context)
51+ Content string // Content of the line
52+ Segments []Segment // Segments for intraline highlighting
4453}
4554
46−// Hunk represents a section of changes in a diff.
55+// Hunk represents a section of changes in a diff
4756type Hunk struct {
4857 Header string
4958 Lines []DiffLine
5059}
5160
52−// DiffResult contains the parsed result of a diff.
61+// DiffResult contains the parsed result of a diff
5362type DiffResult struct {
5463 OldFile string
5564 NewFile string
5665 Hunks []Hunk
5766}
5867
59−// HunkDelta represents the change statistics for a hunk.
60−type HunkDelta struct {
61− StartLine1 int
62− LineCount1 int
63− StartLine2 int
64− LineCount2 int
65−}
66−
67−// linePair represents a pair of lines to be displayed side by side.
68+// linePair represents a pair of lines for side-by-side display
6869type linePair struct {
6970 left *DiffLine
7071 right *DiffLine
7172}
7273
7374// -------------------------------------------------------------------------
74−// Style Configuration with Option Pattern
75+// Style Configuration
7576// -------------------------------------------------------------------------
7677
77−// StyleConfig defines styling for diff rendering.
78+// StyleConfig defines styling for diff rendering
7879type StyleConfig struct {
80+ // Background colors
7981 RemovedLineBg lipgloss.Color
8082 AddedLineBg lipgloss.Color
8183 ContextLineBg lipgloss.Color
8284 HunkLineBg lipgloss.Color
83− HunkLineFg lipgloss.Color
84− RemovedFg lipgloss.Color
85− AddedFg lipgloss.Color
86− LineNumberFg lipgloss.Color
87− HighlightStyle string
88− RemovedHighlightBg lipgloss.Color
89− AddedHighlightBg lipgloss.Color
9085 RemovedLineNumberBg lipgloss.Color
9186 AddedLineNamerBg lipgloss.Color
92− RemovedHighlightFg lipgloss.Color
93− AddedHighlightFg lipgloss.Color
87+
88+ // Foreground colors
89+ HunkLineFg lipgloss.Color
90+ RemovedFg lipgloss.Color
91+ AddedFg lipgloss.Color
92+ LineNumberFg lipgloss.Color
93+ RemovedHighlightFg lipgloss.Color
94+ AddedHighlightFg lipgloss.Color
95+
96+ // Highlight settings
97+ HighlightStyle string
98+ RemovedHighlightBg lipgloss.Color
99+ AddedHighlightBg lipgloss.Color
94100}
95101
96−// StyleOption defines a function that modifies a StyleConfig.
102+// StyleOption is a function that modifies a StyleConfig
97103type StyleOption func(*StyleConfig)
98104
99−// NewStyleConfig creates a StyleConfig with default values and applies any provided options.
105+// NewStyleConfig creates a StyleConfig with default values
100106func NewStyleConfig(opts ...StyleOption) StyleConfig {
101− // Set default values
107+ // Default color scheme
102108 config := StyleConfig{
103109 RemovedLineBg: lipgloss.Color("#3A3030"),
104110 AddedLineBg: lipgloss.Color("#303A30"),
105111 ContextLineBg: lipgloss.Color("#212121"),
106− HunkLineBg: lipgloss.Color("#2A2822"),
107− HunkLineFg: lipgloss.Color("#D4AF37"),
112+ HunkLineBg: lipgloss.Color("#23252D"),
113+ HunkLineFg: lipgloss.Color("#8CA3B4"),
108114 RemovedFg: lipgloss.Color("#7C4444"),
109115 AddedFg: lipgloss.Color("#478247"),
110116 LineNumberFg: lipgloss.Color("#888888"),
@@ -125,56 +131,35 @@ func NewStyleConfig(opts ...StyleOption) StyleConfig {
125131 return config
126132}
127133
128−// WithRemovedLineBg sets the background color for removed lines.
134+// Style option functions
129135func WithRemovedLineBg(color lipgloss.Color) StyleOption {
130− return func(s *StyleConfig) {
131− s.RemovedLineBg = color
132− }
136+ return func(s *StyleConfig) { s.RemovedLineBg = color }
133137}
134138
135−// WithAddedLineBg sets the background color for added lines.
136139func WithAddedLineBg(color lipgloss.Color) StyleOption {
137− return func(s *StyleConfig) {
138− s.AddedLineBg = color
139− }
140+ return func(s *StyleConfig) { s.AddedLineBg = color }
140141}
141142
142−// WithContextLineBg sets the background color for context lines.
143143func WithContextLineBg(color lipgloss.Color) StyleOption {
144− return func(s *StyleConfig) {
145− s.ContextLineBg = color
146− }
144+ return func(s *StyleConfig) { s.ContextLineBg = color }
147145}
148146
149−// WithRemovedFg sets the foreground color for removed line markers.
150147func WithRemovedFg(color lipgloss.Color) StyleOption {
151− return func(s *StyleConfig) {
152− s.RemovedFg = color
153− }
148+ return func(s *StyleConfig) { s.RemovedFg = color }
154149}
155150
156−// WithAddedFg sets the foreground color for added line markers.
157151func WithAddedFg(color lipgloss.Color) StyleOption {
158− return func(s *StyleConfig) {
159− s.AddedFg = color
160− }
152+ return func(s *StyleConfig) { s.AddedFg = color }
161153}
162154
163−// WithLineNumberFg sets the foreground color for line numbers.
164155func WithLineNumberFg(color lipgloss.Color) StyleOption {
165− return func(s *StyleConfig) {
166− s.LineNumberFg = color
167− }
156+ return func(s *StyleConfig) { s.LineNumberFg = color }
168157}
169158
170−// WithHighlightStyle sets the syntax highlighting style.
171159func WithHighlightStyle(style string) StyleOption {
172− return func(s *StyleConfig) {
173− s.HighlightStyle = style
174− }
160+ return func(s *StyleConfig) { s.HighlightStyle = style }
175161}
176162
177−// WithRemovedHighlightColors sets the colors for highlighted parts in removed text.
178163func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
179164 return func(s *StyleConfig) {
180165 s.RemovedHighlightBg = bg
@@ -182,7 +167,6 @@ func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
182167 }
183168}
184169
185−// WithAddedHighlightColors sets the colors for highlighted parts in added text.
186170func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
187171 return func(s *StyleConfig) {
188172 s.AddedHighlightBg = bg
@@ -190,45 +174,35 @@ func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
190174 }
191175}
192176
193−// WithRemovedLineNumberBg sets the background color for removed line numbers.
194177func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
195− return func(s *StyleConfig) {
196− s.RemovedLineNumberBg = color
197− }
178+ return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
198179}
199180
200−// WithAddedLineNumberBg sets the background color for added line numbers.
201181func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
202− return func(s *StyleConfig) {
203− s.AddedLineNamerBg = color
204− }
182+ return func(s *StyleConfig) { s.AddedLineNamerBg = color }
205183}
206184
207185func WithHunkLineBg(color lipgloss.Color) StyleOption {
208− return func(s *StyleConfig) {
209− s.HunkLineBg = color
210− }
186+ return func(s *StyleConfig) { s.HunkLineBg = color }
211187}
212188
213189func WithHunkLineFg(color lipgloss.Color) StyleOption {
214− return func(s *StyleConfig) {
215− s.HunkLineFg = color
216− }
190+ return func(s *StyleConfig) { s.HunkLineFg = color }
217191}
218192
219193// -------------------------------------------------------------------------
220−// Parse Options with Option Pattern
194+// Parse Configuration
221195// -------------------------------------------------------------------------
222196
223−// ParseConfig configures the behavior of diff parsing.
197+// ParseConfig configures the behavior of diff parsing
224198type ParseConfig struct {
225199 ContextSize int // Number of context lines to include
226200}
227201
228−// ParseOption defines a function that modifies a ParseConfig.
202+// ParseOption modifies a ParseConfig
229203type ParseOption func(*ParseConfig)
230204
231−// WithContextSize sets the number of context lines to include.
205+// WithContextSize sets the number of context lines to include
232206func WithContextSize(size int) ParseOption {
233207 return func(p *ParseConfig) {
234208 if size >= 0 {
@@ -238,27 +212,25 @@ func WithContextSize(size int) ParseOption {
238212}
239213
240214// -------------------------------------------------------------------------
241−// Side-by-Side Options with Option Pattern
215+// Side-by-Side Configuration
242216// -------------------------------------------------------------------------
243217
244−// SideBySideConfig configures the rendering of side-by-side diffs.
218+// SideBySideConfig configures the rendering of side-by-side diffs
245219type SideBySideConfig struct {
246220 TotalWidth int
247221 Style StyleConfig
248222}
249223
250−// SideBySideOption defines a function that modifies a SideBySideConfig.
224+// SideBySideOption modifies a SideBySideConfig
251225type SideBySideOption func(*SideBySideConfig)
252226
253−// NewSideBySideConfig creates a SideBySideConfig with default values and applies any provided options.
227+// NewSideBySideConfig creates a SideBySideConfig with default values
254228func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
255− // Set default values
256229 config := SideBySideConfig{
257230 TotalWidth: 160, // Default width for side-by-side view
258231 Style: NewStyleConfig(),
259232 }
260233
261− // Apply all provided options
262234 for _, opt := range opts {
263235 opt(&config)
264236 }
@@ -266,7 +238,7 @@ func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
266238 return config
267239}
268240
269−// WithTotalWidth sets the total width for side-by-side view.
241+// WithTotalWidth sets the total width for side-by-side view
270242func WithTotalWidth(width int) SideBySideOption {
271243 return func(s *SideBySideConfig) {
272244 if width > 0 {
@@ -275,14 +247,14 @@ func WithTotalWidth(width int) SideBySideOption {
275247 }
276248}
277249
278−// WithStyle sets the styling configuration.
250+// WithStyle sets the styling configuration
279251func WithStyle(style StyleConfig) SideBySideOption {
280252 return func(s *SideBySideConfig) {
281253 s.Style = style
282254 }
283255}
284256
285−// WithStyleOptions applies the specified style options.
257+// WithStyleOptions applies the specified style options
286258func WithStyleOptions(opts ...StyleOption) SideBySideOption {
287259 return func(s *SideBySideConfig) {
288260 s.Style = NewStyleConfig(opts...)
@@ -290,10 +262,10 @@ func WithStyleOptions(opts ...StyleOption) SideBySideOption {
290262}
291263
292264// -------------------------------------------------------------------------
293−// Diff Parsing and Generation
265+// Diff Parsing
294266// -------------------------------------------------------------------------
295267
296−// ParseUnifiedDiff parses a unified diff format string into structured data.
268+// ParseUnifiedDiff parses a unified diff format string into structured data
297269func ParseUnifiedDiff(diff string) (DiffResult, error) {
298270 var result DiffResult
299271 var currentHunk *Hunk
@@ -305,7 +277,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
305277 inFileHeader := true
306278
307279 for _, line := range lines {
308− // Parse the file headers
280+ // Parse file headers
309281 if inFileHeader {
310282 if strings.HasPrefix(line, "--- a/") {
311283 result.OldFile = strings.TrimPrefix(line, "--- a/")
@@ -332,27 +304,27 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
332304 newStart, _ := strconv.Atoi(matches[3])
333305 oldLine = oldStart
334306 newLine = newStart
335−
336307 continue
337308 }
338309
339− // ignore the \\ No newline at end of file
310+ // Ignore "No newline at end of file" markers
340311 if strings.HasPrefix(line, "\\ No newline at end of file") {
341312 continue
342313 }
314+
343315 if currentHunk == nil {
344316 continue
345317 }
346318
319+ // Process the line based on its prefix
347320 if len(line) > 0 {
348− // Process the line based on its prefix
349321 switch line[0] {
350322 case '+':
351323 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
352324 OldLineNo: 0,
353325 NewLineNo: newLine,
354326 Kind: LineAdded,
355− Content: line[1:], // skip '+'
327+ Content: line[1:],
356328 })
357329 newLine++
358330 case '-':
@@ -360,7 +332,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
360332 OldLineNo: oldLine,
361333 NewLineNo: 0,
362334 Kind: LineRemoved,
363− Content: line[1:], // skip '-'
335+ Content: line[1:],
364336 })
365337 oldLine++
366338 default:
@@ -394,14 +366,13 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
394366 return result, nil
395367}
396368
397−// HighlightIntralineChanges updates the content of lines in a hunk to show
398−// character-level differences within lines.
369+// HighlightIntralineChanges updates lines in a hunk to show character-level differences
399370func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
400371 var updated []DiffLine
401372 dmp := diffmatchpatch.New()
402373
403374 for i := 0; i < len(h.Lines); i++ {
404− // Look for removed line followed by added line, which might have similar content
375+ // Look for removed line followed by added line
405376 if i+1 < len(h.Lines) &&
406377 h.Lines[i].Kind == LineRemoved &&
407378 h.Lines[i+1].Kind == LineAdded {
@@ -411,12 +382,40 @@ func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
411382
412383 // Find character-level differences
413384 patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
414− patches = dmp.DiffCleanupEfficiency(patches)
415385 patches = dmp.DiffCleanupSemantic(patches)
386+ patches = dmp.DiffCleanupMerge(patches)
387+ patches = dmp.DiffCleanupEfficiency(patches)
416388
417− // Apply highlighting to the differences
418− oldLine.Content = colorizeSegments(patches, true, style)
419− newLine.Content = colorizeSegments(patches, false, style)
389+ segments := make([]Segment, 0)
390+
391+ removeStart := 0
392+ addStart := 0
393+ for _, patch := range patches {
394+ switch patch.Type {
395+ case diffmatchpatch.DiffDelete:
396+ segments = append(segments, Segment{
397+ Start: removeStart,
398+ End: removeStart + len(patch.Text),
399+ Type: LineRemoved,
400+ Text: patch.Text,
401+ })
402+ removeStart += len(patch.Text)
403+ case diffmatchpatch.DiffInsert:
404+ segments = append(segments, Segment{
405+ Start: addStart,
406+ End: addStart + len(patch.Text),
407+ Type: LineAdded,
408+ Text: patch.Text,
409+ })
410+ addStart += len(patch.Text)
411+ default:
412+ // Context text, no highlighting needed
413+ removeStart += len(patch.Text)
414+ addStart += len(patch.Text)
415+ }
416+ }
417+ oldLine.Segments = segments
418+ newLine.Segments = segments
420419
421420 updated = append(updated, oldLine, newLine)
422421 i++ // Skip the next line as we've already processed it
@@ -428,45 +427,7 @@ func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
428427 h.Lines = updated
429428}
430429
431−// colorizeSegments applies styles to the character-level diff segments.
432−func colorizeSegments(diffs []diffmatchpatch.Diff, isOld bool, style StyleConfig) string {
433− var buf strings.Builder
434−
435− removeBg := lipgloss.NewStyle().
436− Background(style.RemovedHighlightBg).
437− Foreground(style.RemovedHighlightFg)
438−
439− addBg := lipgloss.NewStyle().
440− Background(style.AddedHighlightBg).
441− Foreground(style.AddedHighlightFg)
442−
443− removedLineStyle := lipgloss.NewStyle().Background(style.RemovedLineBg)
444− addedLineStyle := lipgloss.NewStyle().Background(style.AddedLineBg)
445−
446− for _, d := range diffs {
447− switch d.Type {
448− case diffmatchpatch.DiffEqual:
449− // Handle text that's the same in both versions
450− buf.WriteString(d.Text)
451− case diffmatchpatch.DiffDelete:
452− // Handle deleted text (only show in old version)
453− if isOld {
454− buf.WriteString(removeBg.Render(d.Text))
455− buf.WriteString(removedLineStyle.Render(""))
456− }
457− case diffmatchpatch.DiffInsert:
458− // Handle inserted text (only show in new version)
459− if !isOld {
460− buf.WriteString(addBg.Render(d.Text))
461− buf.WriteString(addedLineStyle.Render(""))
462− }
463− }
464− }
465−
466− return buf.String()
467−}
468−
469−// pairLines converts a flat list of diff lines to pairs for side-by-side display.
430+// pairLines converts a flat list of diff lines to pairs for side-by-side display
470431func pairLines(lines []DiffLine) []linePair {
471432 var pairs []linePair
472433 i := 0
@@ -498,7 +459,7 @@ func pairLines(lines []DiffLine) []linePair {
498459// Syntax Highlighting
499460// -------------------------------------------------------------------------
500461
501−// SyntaxHighlight applies syntax highlighting to a string based on the file extension.
462+// SyntaxHighlight applies syntax highlighting to text based on file extension
502463func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
503464 // Determine the language lexer to use
504465 l := lexers.Match(fileName)
@@ -515,21 +476,98 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
515476 if f == nil {
516477 f = formatters.Fallback
517478 }
518−
519− // Get the style
520− s := styles.Get("dracula")
521− if s == nil {
522− s = styles.Fallback
523− }
524−
479+ theme := `
480+ <style name="vscode-dark-plus">
481+ <!-- Base colors -->
482+ <entry type="Background" style="bg:#1E1E1E"/>
483+ <entry type="Text" style="#D4D4D4"/>
484+ <entry type="Other" style="#D4D4D4"/>
485+ <entry type="Error" style="#F44747"/>
486+ <!-- Keywords - using the Control flow / Special keywords color -->
487+ <entry type="Keyword" style="#C586C0"/>
488+ <entry type="KeywordConstant" style="#4FC1FF"/>
489+ <entry type="KeywordDeclaration" style="#C586C0"/>
490+ <entry type="KeywordNamespace" style="#C586C0"/>
491+ <entry type="KeywordPseudo" style="#C586C0"/>
492+ <entry type="KeywordReserved" style="#C586C0"/>
493+ <entry type="KeywordType" style="#4EC9B0"/>
494+ <!-- Names -->
495+ <entry type="Name" style="#D4D4D4"/>
496+ <entry type="NameAttribute" style="#9CDCFE"/>
497+ <entry type="NameBuiltin" style="#4EC9B0"/>
498+ <entry type="NameBuiltinPseudo" style="#9CDCFE"/>
499+ <entry type="NameClass" style="#4EC9B0"/>
500+ <entry type="NameConstant" style="#4FC1FF"/>
501+ <entry type="NameDecorator" style="#DCDCAA"/>
502+ <entry type="NameEntity" style="#9CDCFE"/>
503+ <entry type="NameException" style="#4EC9B0"/>
504+ <entry type="NameFunction" style="#DCDCAA"/>
505+ <entry type="NameLabel" style="#C8C8C8"/>
506+ <entry type="NameNamespace" style="#4EC9B0"/>
507+ <entry type="NameOther" style="#9CDCFE"/>
508+ <entry type="NameTag" style="#569CD6"/>
509+ <entry type="NameVariable" style="#9CDCFE"/>
510+ <entry type="NameVariableClass" style="#9CDCFE"/>
511+ <entry type="NameVariableGlobal" style="#9CDCFE"/>
512+ <entry type="NameVariableInstance" style="#9CDCFE"/>
513+ <!-- Literals -->
514+ <entry type="Literal" style="#CE9178"/>
515+ <entry type="LiteralDate" style="#CE9178"/>
516+ <entry type="LiteralString" style="#CE9178"/>
517+ <entry type="LiteralStringBacktick" style="#CE9178"/>
518+ <entry type="LiteralStringChar" style="#CE9178"/>
519+ <entry type="LiteralStringDoc" style="#CE9178"/>
520+ <entry type="LiteralStringDouble" style="#CE9178"/>
521+ <entry type="LiteralStringEscape" style="#d7ba7d"/>
522+ <entry type="LiteralStringHeredoc" style="#CE9178"/>
523+ <entry type="LiteralStringInterpol" style="#CE9178"/>
524+ <entry type="LiteralStringOther" style="#CE9178"/>
525+ <entry type="LiteralStringRegex" style="#d16969"/>
526+ <entry type="LiteralStringSingle" style="#CE9178"/>
527+ <entry type="LiteralStringSymbol" style="#CE9178"/>
528+ <!-- Numbers - using the numberLiteral color -->
529+ <entry type="LiteralNumber" style="#b5cea8"/>
530+ <entry type="LiteralNumberBin" style="#b5cea8"/>
531+ <entry type="LiteralNumberFloat" style="#b5cea8"/>
532+ <entry type="LiteralNumberHex" style="#b5cea8"/>
533+ <entry type="LiteralNumberInteger" style="#b5cea8"/>
534+ <entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
535+ <entry type="LiteralNumberOct" style="#b5cea8"/>
536+ <!-- Operators -->
537+ <entry type="Operator" style="#D4D4D4"/>
538+ <entry type="OperatorWord" style="#C586C0"/>
539+ <entry type="Punctuation" style="#D4D4D4"/>
540+ <!-- Comments - standard VSCode Dark+ comment color -->
541+ <entry type="Comment" style="#6A9955"/>
542+ <entry type="CommentHashbang" style="#6A9955"/>
543+ <entry type="CommentMultiline" style="#6A9955"/>
544+ <entry type="CommentSingle" style="#6A9955"/>
545+ <entry type="CommentSpecial" style="#6A9955"/>
546+ <entry type="CommentPreproc" style="#C586C0"/>
547+ <!-- Generic styles -->
548+ <entry type="Generic" style="#D4D4D4"/>
549+ <entry type="GenericDeleted" style="#F44747"/>
550+ <entry type="GenericEmph" style="italic #D4D4D4"/>
551+ <entry type="GenericError" style="#F44747"/>
552+ <entry type="GenericHeading" style="bold #D4D4D4"/>
553+ <entry type="GenericInserted" style="#b5cea8"/>
554+ <entry type="GenericOutput" style="#808080"/>
555+ <entry type="GenericPrompt" style="#D4D4D4"/>
556+ <entry type="GenericStrong" style="bold #D4D4D4"/>
557+ <entry type="GenericSubheading" style="bold #D4D4D4"/>
558+ <entry type="GenericTraceback" style="#F44747"/>
559+ <entry type="GenericUnderline" style="underline"/>
560+ <entry type="TextWhitespace" style="#D4D4D4"/>
561+</style>
562+`
563+
564+ r := strings.NewReader(theme)
565+ style := chroma.MustNewXMLStyle(r)
525566 // Modify the style to use the provided background
526− s, err := s.Builder().Transform(
567+ s, err := style.Builder().Transform(
527568 func(t chroma.StyleEntry) chroma.StyleEntry {
528569 r, g, b, _ := bg.RGBA()
529− ru8 := uint8(r >> 8)
530− gu8 := uint8(g >> 8)
531− bu8 := uint8(b >> 8)
532− t.Background = chroma.NewColour(ru8, gu8, bu8)
570+ t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
533571 return t
534572 },
535573 ).Build()
@@ -546,7 +584,7 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
546584 return f.Format(w, s, it)
547585}
548586
549−// highlightLine applies syntax highlighting to a single line.
587+// highlightLine applies syntax highlighting to a single line
550588func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
551589 var buf bytes.Buffer
552590 err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
@@ -556,7 +594,7 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
556594 return buf.String()
557595}
558596
559−// createStyles generates the lipgloss styles needed for rendering diffs.
597+// createStyles generates the lipgloss styles needed for rendering diffs
560598func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
561599 removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
562600 addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
@@ -566,7 +604,106 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
566604 return
567605}
568606
569−// renderLeftColumn formats the left side of a side-by-side diff.
607+// -------------------------------------------------------------------------
608+// Rendering Functions
609+// -------------------------------------------------------------------------
610+
611+// applyHighlighting applies intra-line highlighting to a piece of text
612+func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
613+) string {
614+ // Find all ANSI sequences in the content
615+ ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
616+ ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
617+
618+ // Build a mapping of visible character positions to their actual indices
619+ visibleIdx := 0
620+ ansiSequences := make(map[int]string)
621+ lastAnsiSeq := "\x1b[0m" // Default reset sequence
622+
623+ for i := 0; i < len(content); {
624+ isAnsi := false
625+ for _, match := range ansiMatches {
626+ if match[0] == i {
627+ ansiSequences[visibleIdx] = content[match[0]:match[1]]
628+ lastAnsiSeq = content[match[0]:match[1]]
629+ i = match[1]
630+ isAnsi = true
631+ break
632+ }
633+ }
634+ if isAnsi {
635+ continue
636+ }
637+
638+ // For non-ANSI positions, store the last ANSI sequence
639+ if _, exists := ansiSequences[visibleIdx]; !exists {
640+ ansiSequences[visibleIdx] = lastAnsiSeq
641+ }
642+ visibleIdx++
643+ i++
644+ }
645+
646+ // Apply highlighting
647+ var sb strings.Builder
648+ inSelection := false
649+ currentPos := 0
650+
651+ for i := 0; i < len(content); {
652+ // Check if we're at an ANSI sequence
653+ isAnsi := false
654+ for _, match := range ansiMatches {
655+ if match[0] == i {
656+ sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence
657+ i = match[1]
658+ isAnsi = true
659+ break
660+ }
661+ }
662+ if isAnsi {
663+ continue
664+ }
665+
666+ // Check for segment boundaries
667+ for _, seg := range segments {
668+ if seg.Type == segmentType {
669+ if currentPos == seg.Start {
670+ inSelection = true
671+ }
672+ if currentPos == seg.End {
673+ inSelection = false
674+ }
675+ }
676+ }
677+
678+ // Get current character
679+ char := string(content[i])
680+
681+ if inSelection {
682+ // Get the current styling
683+ currentStyle := ansiSequences[currentPos]
684+
685+ // Apply background highlight
686+ sb.WriteString("\x1b[48;2;")
687+ r, g, b, _ := highlightBg.RGBA()
688+ sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
689+ sb.WriteString(char)
690+ sb.WriteString("\x1b[49m") // Reset only background
691+
692+ // Reapply the original ANSI sequence
693+ sb.WriteString(currentStyle)
694+ } else {
695+ // Not in selection, just copy the character
696+ sb.WriteString(char)
697+ }
698+
699+ currentPos++
700+ i++
701+ }
702+
703+ return sb.String()
704+}
705+
706+// renderLeftColumn formats the left side of a side-by-side diff
570707func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
571708 if dl == nil {
572709 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
@@ -575,9 +712,9 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
575712
576713 removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
577714
715+ // Determine line style based on line type
578716 var marker string
579717 var bgStyle lipgloss.Style
580−
581718 switch dl.Kind {
582719 case LineRemoved:
583720 marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
@@ -591,18 +728,29 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
591728 bgStyle = contextLineStyle
592729 }
593730
731+ // Format line number
594732 lineNum := ""
595733 if dl.OldLineNo > 0 {
596734 lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
597735 }
598736
737+ // Create the line prefix
599738 prefix := lineNumberStyle.Render(lineNum + " " + marker)
739+
740+ // Apply syntax highlighting
600741 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
601742
743+ // Apply intra-line highlighting for removed lines
744+ if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
745+ content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
746+ }
747+
748+ // Add a padding space for removed lines
602749 if dl.Kind == LineRemoved {
603750 content = bgStyle.Render(" ") + content
604751 }
605752
753+ // Create the final line and truncate if needed
606754 lineText := prefix + content
607755 return bgStyle.MaxHeight(1).Width(colWidth).Render(
608756 ansi.Truncate(
@@ -613,7 +761,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
613761 )
614762}
615763
616−// renderRightColumn formats the right side of a side-by-side diff.
764+// renderRightColumn formats the right side of a side-by-side diff
617765func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
618766 if dl == nil {
619767 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
@@ -622,9 +770,9 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
622770
623771 _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
624772
773+ // Determine line style based on line type
625774 var marker string
626775 var bgStyle lipgloss.Style
627−
628776 switch dl.Kind {
629777 case LineAdded:
630778 marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
@@ -638,18 +786,29 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
638786 bgStyle = contextLineStyle
639787 }
640788
789+ // Format line number
641790 lineNum := ""
642791 if dl.NewLineNo > 0 {
643792 lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
644793 }
645794
795+ // Create the line prefix
646796 prefix := lineNumberStyle.Render(lineNum + " " + marker)
797+
798+ // Apply syntax highlighting
647799 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
648800
801+ // Apply intra-line highlighting for added lines
802+ if dl.Kind == LineAdded && len(dl.Segments) > 0 {
803+ content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
804+ }
805+
806+ // Add a padding space for added lines
649807 if dl.Kind == LineAdded {
650808 content = bgStyle.Render(" ") + content
651809 }
652810
811+ // Create the final line and truncate if needed
653812 lineText := prefix + content
654813 return bgStyle.MaxHeight(1).Width(colWidth).Render(
655814 ansi.Truncate(
@@ -661,10 +820,10 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
661820}
662821
663822// -------------------------------------------------------------------------
664−// Public API Methods
823+// Public API
665824// -------------------------------------------------------------------------
666825
667−// RenderSideBySideHunk formats a hunk for side-by-side display.
826+// RenderSideBySideHunk formats a hunk for side-by-side display
668827func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
669828 // Apply options to create the configuration
670829 config := NewSideBySideConfig(opts...)
@@ -692,7 +851,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
692851 return sb.String()
693852}
694853
695−// FormatDiff creates a side-by-side formatted view of a diff.
854+// FormatDiff creates a side-by-side formatted view of a diff
696855func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
697856 diffResult, err := ParseUnifiedDiff(diffText)
698857 if err != nil {
@@ -700,11 +859,18 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
700859 }
701860
702861 var sb strings.Builder
703−
704862 config := NewSideBySideConfig(opts...)
863+
705864 for i, h := range diffResult.Hunks {
706865 if i > 0 {
707− sb.WriteString(lipgloss.NewStyle().Background(config.Style.HunkLineBg).Foreground(config.Style.HunkLineFg).Width(config.TotalWidth).Render(h.Header) + "\n")
866+ // Render hunk header
867+ sb.WriteString(
868+ lipgloss.NewStyle().
869+ Background(config.Style.HunkLineBg).
870+ Foreground(config.Style.HunkLineFg).
871+ Width(config.TotalWidth).
872+ Render(h.Header) + "\n",
873+ )
708874 }
709875 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
710876 }
@@ -712,14 +878,16 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
712878 return sb.String(), nil
713879}
714880
715−// GenerateDiff creates a unified diff from two file contents.
881+// GenerateDiff creates a unified diff from two file contents
716882func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
883+ // Create temporary directory for git operations
717884 tempDir, err := os.MkdirTemp("", "git-diff-temp")
718885 if err != nil {
719886 return "", 0, 0
720887 }
721888 defer os.RemoveAll(tempDir)
722889
890+ // Initialize git repo
723891 repo, err := git.PlainInit(tempDir, false)
724892 if err != nil {
725893 return "", 0, 0
@@ -730,6 +898,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
730898 return "", 0, 0
731899 }
732900
901+ // Write the "before" content and commit it
733902 fullPath := filepath.Join(tempDir, fileName)
734903 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
735904 return "", 0, 0
@@ -754,7 +923,9 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
754923 return "", 0, 0
755924 }
756925
926+ // Write the "after" content and commit it
757927 if err = os.WriteFile(fullPath, []byte(afterContent), 0o644); err != nil {
928+ return "", 0, 0
758929 }
759930
760931 _, err = wt.Add(fileName)
@@ -773,6 +944,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
773944 return "", 0, 0
774945 }
775946
947+ // Get the diff between the two commits
776948 beforeCommitObj, err := repo.CommitObject(beforeCommit)
777949 if err != nil {
778950 return "", 0, 0
@@ -788,6 +960,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
788960 return "", 0, 0
789961 }
790962
963+ // Count additions and removals
791964 additions := 0
792965 removals := 0
793966 for _, fileStat := range patch.Stats() {