on main help sign in
fix segment diff and add new theme
f6be348Kujtim Hoxha
internal/diff/diff.go+348−175
@@ -22,89 +22,95 @@ import (
22 "github.com/sergi/go-diff/diffmatchpatch"
22 "github.com/sergi/go-diff/diffmatchpatch"
23)
23)
24
24
25+// -------------------------------------------------------------------------
26+// Core Types
27+// -------------------------------------------------------------------------
28+
25// LineType represents the kind of line in a diff.
29// LineType represents the kind of line in a diff.
26type LineType int
30type LineType int
27
31
28const (
32const (
29− // LineContext represents a line that exists in both the old and new file.
30− LineContext LineType = iota
33+ LineContext LineType = iota // Line exists in both files
31− // LineAdded represents a line added in the new file.
34+ LineAdded // Line added in the new file
32− LineAdded
33− // LineRemoved represents a line removed from the old file.
35+ LineRemoved // Line removed from the old file
34− LineRemoved
35)
36)
36
37
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+
37−// DiffLine represents a single line in a diff, either from the old file,
46+// DiffLine represents a single line in a diff
38−// the new file, or a context line.
39type DiffLine struct {
47type DiffLine struct {
40− OldLineNo int // Line number in the old file (0 for added lines)
48+ OldLineNo int // Line number in old file (0 for added lines)
41− NewLineNo int // Line number in the new file (0 for removed lines)
49+ NewLineNo int // Line number in new file (0 for removed lines)
42− Kind LineType // Type of line (added, removed, context)
50+ Kind LineType // Type of line (added, removed, context)
43− Content string // Content of the line
51+ Content string // Content of the line
52+ Segments []Segment // Segments for intraline highlighting
44}
53}
45
54
46−// Hunk represents a section of changes in a diff.
55+// Hunk represents a section of changes in a diff
47type Hunk struct {
56type Hunk struct {
48 Header string
57 Header string
49 Lines []DiffLine
58 Lines []DiffLine
50}
59}
51
60
52−// DiffResult contains the parsed result of a diff.
61+// DiffResult contains the parsed result of a diff
53type DiffResult struct {
62type DiffResult struct {
54 OldFile string
63 OldFile string
55 NewFile string
64 NewFile string
56 Hunks []Hunk
65 Hunks []Hunk
57}
66}
58
67
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
68type linePair struct {
69type linePair struct {
69 left *DiffLine
70 left *DiffLine
70 right *DiffLine
71 right *DiffLine
71}
72}
72
73
73// -------------------------------------------------------------------------
74// -------------------------------------------------------------------------
74−// Style Configuration with Option Pattern
75+// Style Configuration
75// -------------------------------------------------------------------------
76// -------------------------------------------------------------------------
76
77
77−// StyleConfig defines styling for diff rendering.
78+// StyleConfig defines styling for diff rendering
78type StyleConfig struct {
79type StyleConfig struct {
80+ // Background colors
79 RemovedLineBg lipgloss.Color
81 RemovedLineBg lipgloss.Color
80 AddedLineBg lipgloss.Color
82 AddedLineBg lipgloss.Color
81 ContextLineBg lipgloss.Color
83 ContextLineBg lipgloss.Color
82 HunkLineBg lipgloss.Color
84 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
90 RemovedLineNumberBg lipgloss.Color
85 RemovedLineNumberBg lipgloss.Color
91 AddedLineNamerBg lipgloss.Color
86 AddedLineNamerBg lipgloss.Color
87+
88+ // Foreground colors
89+ HunkLineFg lipgloss.Color
90+ RemovedFg lipgloss.Color
91+ AddedFg lipgloss.Color
92+ LineNumberFg lipgloss.Color
92− RemovedHighlightFg lipgloss.Color
93+ RemovedHighlightFg lipgloss.Color
93− AddedHighlightFg lipgloss.Color
94+ AddedHighlightFg lipgloss.Color
95+
96+ // Highlight settings
97+ HighlightStyle string
98+ RemovedHighlightBg lipgloss.Color
99+ AddedHighlightBg lipgloss.Color
94}
100}
95
101
96−// StyleOption defines a function that modifies a StyleConfig.
102+// StyleOption is a function that modifies a StyleConfig
97type StyleOption func(*StyleConfig)
103type StyleOption func(*StyleConfig)
98
104
99−// NewStyleConfig creates a StyleConfig with default values and applies any provided options.
105+// NewStyleConfig creates a StyleConfig with default values
100func NewStyleConfig(opts ...StyleOption) StyleConfig {
106func NewStyleConfig(opts ...StyleOption) StyleConfig {
101− // Set default values
107+ // Default color scheme
102 config := StyleConfig{
108 config := StyleConfig{
103 RemovedLineBg: lipgloss.Color("#3A3030"),
109 RemovedLineBg: lipgloss.Color("#3A3030"),
104 AddedLineBg: lipgloss.Color("#303A30"),
110 AddedLineBg: lipgloss.Color("#303A30"),
105 ContextLineBg: lipgloss.Color("#212121"),
111 ContextLineBg: lipgloss.Color("#212121"),
106− HunkLineBg: lipgloss.Color("#2A2822"),
112+ HunkLineBg: lipgloss.Color("#23252D"),
107− HunkLineFg: lipgloss.Color("#D4AF37"),
113+ HunkLineFg: lipgloss.Color("#8CA3B4"),
108 RemovedFg: lipgloss.Color("#7C4444"),
114 RemovedFg: lipgloss.Color("#7C4444"),
109 AddedFg: lipgloss.Color("#478247"),
115 AddedFg: lipgloss.Color("#478247"),
110 LineNumberFg: lipgloss.Color("#888888"),
116 LineNumberFg: lipgloss.Color("#888888"),
@@ -125,56 +131,35 @@ func NewStyleConfig(opts ...StyleOption) StyleConfig {
125 return config
131 return config
126}
132}
127
133
128−// WithRemovedLineBg sets the background color for removed lines.
134+// Style option functions
129func WithRemovedLineBg(color lipgloss.Color) StyleOption {
135func WithRemovedLineBg(color lipgloss.Color) StyleOption {
130− return func(s *StyleConfig) {
136+ return func(s *StyleConfig) { s.RemovedLineBg = color }
131− s.RemovedLineBg = color
132− }
133}
137}
134
138
135−// WithAddedLineBg sets the background color for added lines.
136func WithAddedLineBg(color lipgloss.Color) StyleOption {
139func WithAddedLineBg(color lipgloss.Color) StyleOption {
137− return func(s *StyleConfig) {
140+ return func(s *StyleConfig) { s.AddedLineBg = color }
138− s.AddedLineBg = color
139− }
140}
141}
141
142
142−// WithContextLineBg sets the background color for context lines.
143func WithContextLineBg(color lipgloss.Color) StyleOption {
143func WithContextLineBg(color lipgloss.Color) StyleOption {
144− return func(s *StyleConfig) {
144+ return func(s *StyleConfig) { s.ContextLineBg = color }
145− s.ContextLineBg = color
146− }
147}
145}
148
146
149−// WithRemovedFg sets the foreground color for removed line markers.
150func WithRemovedFg(color lipgloss.Color) StyleOption {
147func WithRemovedFg(color lipgloss.Color) StyleOption {
151− return func(s *StyleConfig) {
148+ return func(s *StyleConfig) { s.RemovedFg = color }
152− s.RemovedFg = color
153− }
154}
149}
155
150
156−// WithAddedFg sets the foreground color for added line markers.
157func WithAddedFg(color lipgloss.Color) StyleOption {
151func WithAddedFg(color lipgloss.Color) StyleOption {
158− return func(s *StyleConfig) {
152+ return func(s *StyleConfig) { s.AddedFg = color }
159− s.AddedFg = color
160− }
161}
153}
162
154
163−// WithLineNumberFg sets the foreground color for line numbers.
164func WithLineNumberFg(color lipgloss.Color) StyleOption {
155func WithLineNumberFg(color lipgloss.Color) StyleOption {
165− return func(s *StyleConfig) {
156+ return func(s *StyleConfig) { s.LineNumberFg = color }
166− s.LineNumberFg = color
167− }
168}
157}
169
158
170−// WithHighlightStyle sets the syntax highlighting style.
171func WithHighlightStyle(style string) StyleOption {
159func WithHighlightStyle(style string) StyleOption {
172− return func(s *StyleConfig) {
160+ return func(s *StyleConfig) { s.HighlightStyle = style }
173− s.HighlightStyle = style
174− }
175}
161}
176
162
177−// WithRemovedHighlightColors sets the colors for highlighted parts in removed text.
178func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
163func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
179 return func(s *StyleConfig) {
164 return func(s *StyleConfig) {
180 s.RemovedHighlightBg = bg
165 s.RemovedHighlightBg = bg
@@ -182,7 +167,6 @@ func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
182 }
167 }
183}
168}
184
169
185−// WithAddedHighlightColors sets the colors for highlighted parts in added text.
186func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
170func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
187 return func(s *StyleConfig) {
171 return func(s *StyleConfig) {
188 s.AddedHighlightBg = bg
172 s.AddedHighlightBg = bg
@@ -190,45 +174,35 @@ func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
190 }
174 }
191}
175}
192
176
193−// WithRemovedLineNumberBg sets the background color for removed line numbers.
194func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
177func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
195− return func(s *StyleConfig) {
196− s.RemovedLineNumberBg = color
178+ return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
197− }
198}
179}
199
180
200−// WithAddedLineNumberBg sets the background color for added line numbers.
201func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
181func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
202− return func(s *StyleConfig) {
182+ return func(s *StyleConfig) { s.AddedLineNamerBg = color }
203− s.AddedLineNamerBg = color
204− }
205}
183}
206
184
207func WithHunkLineBg(color lipgloss.Color) StyleOption {
185func WithHunkLineBg(color lipgloss.Color) StyleOption {
208− return func(s *StyleConfig) {
186+ return func(s *StyleConfig) { s.HunkLineBg = color }
209− s.HunkLineBg = color
210− }
211}
187}
212
188
213func WithHunkLineFg(color lipgloss.Color) StyleOption {
189func WithHunkLineFg(color lipgloss.Color) StyleOption {
214− return func(s *StyleConfig) {
190+ return func(s *StyleConfig) { s.HunkLineFg = color }
215− s.HunkLineFg = color
216− }
217}
191}
218
192
219// -------------------------------------------------------------------------
193// -------------------------------------------------------------------------
220−// Parse Options with Option Pattern
194+// Parse Configuration
221// -------------------------------------------------------------------------
195// -------------------------------------------------------------------------
222
196
223−// ParseConfig configures the behavior of diff parsing.
197+// ParseConfig configures the behavior of diff parsing
224type ParseConfig struct {
198type ParseConfig struct {
225 ContextSize int // Number of context lines to include
199 ContextSize int // Number of context lines to include
226}
200}
227
201
228−// ParseOption defines a function that modifies a ParseConfig.
202+// ParseOption modifies a ParseConfig
229type ParseOption func(*ParseConfig)
203type ParseOption func(*ParseConfig)
230
204
231−// WithContextSize sets the number of context lines to include.
205+// WithContextSize sets the number of context lines to include
232func WithContextSize(size int) ParseOption {
206func WithContextSize(size int) ParseOption {
233 return func(p *ParseConfig) {
207 return func(p *ParseConfig) {
234 if size >= 0 {
208 if size >= 0 {
@@ -238,27 +212,25 @@ func WithContextSize(size int) ParseOption {
238}
212}
239
213
240// -------------------------------------------------------------------------
214// -------------------------------------------------------------------------
241−// Side-by-Side Options with Option Pattern
215+// Side-by-Side Configuration
242// -------------------------------------------------------------------------
216// -------------------------------------------------------------------------
243
217
244−// SideBySideConfig configures the rendering of side-by-side diffs.
218+// SideBySideConfig configures the rendering of side-by-side diffs
245type SideBySideConfig struct {
219type SideBySideConfig struct {
246 TotalWidth int
220 TotalWidth int
247 Style StyleConfig
221 Style StyleConfig
248}
222}
249
223
250−// SideBySideOption defines a function that modifies a SideBySideConfig.
224+// SideBySideOption modifies a SideBySideConfig
251type SideBySideOption func(*SideBySideConfig)
225type SideBySideOption func(*SideBySideConfig)
252
226
253−// NewSideBySideConfig creates a SideBySideConfig with default values and applies any provided options.
227+// NewSideBySideConfig creates a SideBySideConfig with default values
254func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
228func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
255− // Set default values
256 config := SideBySideConfig{
229 config := SideBySideConfig{
257 TotalWidth: 160, // Default width for side-by-side view
230 TotalWidth: 160, // Default width for side-by-side view
258 Style: NewStyleConfig(),
231 Style: NewStyleConfig(),
259 }
232 }
260
233
261− // Apply all provided options
262 for _, opt := range opts {
234 for _, opt := range opts {
263 opt(&config)
235 opt(&config)
264 }
236 }
@@ -266,7 +238,7 @@ func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
266 return config
238 return config
267}
239}
268
240
269−// WithTotalWidth sets the total width for side-by-side view.
241+// WithTotalWidth sets the total width for side-by-side view
270func WithTotalWidth(width int) SideBySideOption {
242func WithTotalWidth(width int) SideBySideOption {
271 return func(s *SideBySideConfig) {
243 return func(s *SideBySideConfig) {
272 if width > 0 {
244 if width > 0 {
@@ -275,14 +247,14 @@ func WithTotalWidth(width int) SideBySideOption {
275 }
247 }
276}
248}
277
249
278−// WithStyle sets the styling configuration.
250+// WithStyle sets the styling configuration
279func WithStyle(style StyleConfig) SideBySideOption {
251func WithStyle(style StyleConfig) SideBySideOption {
280 return func(s *SideBySideConfig) {
252 return func(s *SideBySideConfig) {
281 s.Style = style
253 s.Style = style
282 }
254 }
283}
255}
284
256
285−// WithStyleOptions applies the specified style options.
257+// WithStyleOptions applies the specified style options
286func WithStyleOptions(opts ...StyleOption) SideBySideOption {
258func WithStyleOptions(opts ...StyleOption) SideBySideOption {
287 return func(s *SideBySideConfig) {
259 return func(s *SideBySideConfig) {
288 s.Style = NewStyleConfig(opts...)
260 s.Style = NewStyleConfig(opts...)
@@ -290,10 +262,10 @@ func WithStyleOptions(opts ...StyleOption) SideBySideOption {
290}
262}
291
263
292// -------------------------------------------------------------------------
264// -------------------------------------------------------------------------
293−// Diff Parsing and Generation
265+// Diff Parsing
294// -------------------------------------------------------------------------
266// -------------------------------------------------------------------------
295
267
296−// ParseUnifiedDiff parses a unified diff format string into structured data.
268+// ParseUnifiedDiff parses a unified diff format string into structured data
297func ParseUnifiedDiff(diff string) (DiffResult, error) {
269func ParseUnifiedDiff(diff string) (DiffResult, error) {
298 var result DiffResult
270 var result DiffResult
299 var currentHunk *Hunk
271 var currentHunk *Hunk
@@ -305,7 +277,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
305 inFileHeader := true
277 inFileHeader := true
306
278
307 for _, line := range lines {
279 for _, line := range lines {
308− // Parse the file headers
280+ // Parse file headers
309 if inFileHeader {
281 if inFileHeader {
310 if strings.HasPrefix(line, "--- a/") {
282 if strings.HasPrefix(line, "--- a/") {
311 result.OldFile = strings.TrimPrefix(line, "--- a/")
283 result.OldFile = strings.TrimPrefix(line, "--- a/")
@@ -332,27 +304,27 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
332 newStart, _ := strconv.Atoi(matches[3])
304 newStart, _ := strconv.Atoi(matches[3])
333 oldLine = oldStart
305 oldLine = oldStart
334 newLine = newStart
306 newLine = newStart
335−
336 continue
307 continue
337 }
308 }
338
309
339− // ignore the \\ No newline at end of file
310+ // Ignore "No newline at end of file" markers
340 if strings.HasPrefix(line, "\\ No newline at end of file") {
311 if strings.HasPrefix(line, "\\ No newline at end of file") {
341 continue
312 continue
342 }
313 }
314+
343 if currentHunk == nil {
315 if currentHunk == nil {
344 continue
316 continue
345 }
317 }
346
318
319+ // Process the line based on its prefix
347 if len(line) > 0 {
320 if len(line) > 0 {
348− // Process the line based on its prefix
349 switch line[0] {
321 switch line[0] {
350 case '+':
322 case '+':
351 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
323 currentHunk.Lines = append(currentHunk.Lines, DiffLine{
352 OldLineNo: 0,
324 OldLineNo: 0,
353 NewLineNo: newLine,
325 NewLineNo: newLine,
354 Kind: LineAdded,
326 Kind: LineAdded,
355− Content: line[1:], // skip '+'
327+ Content: line[1:],
356 })
328 })
357 newLine++
329 newLine++
358 case '-':
330 case '-':
@@ -360,7 +332,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
360 OldLineNo: oldLine,
332 OldLineNo: oldLine,
361 NewLineNo: 0,
333 NewLineNo: 0,
362 Kind: LineRemoved,
334 Kind: LineRemoved,
363− Content: line[1:], // skip '-'
335+ Content: line[1:],
364 })
336 })
365 oldLine++
337 oldLine++
366 default:
338 default:
@@ -394,14 +366,13 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
394 return result, nil
366 return result, nil
395}
367}
396
368
397−// HighlightIntralineChanges updates the content of lines in a hunk to show
369+// HighlightIntralineChanges updates lines in a hunk to show character-level differences
398−// character-level differences within lines.
399func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
370func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
400 var updated []DiffLine
371 var updated []DiffLine
401 dmp := diffmatchpatch.New()
372 dmp := diffmatchpatch.New()
402
373
403 for i := 0; i < len(h.Lines); i++ {
374 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
405 if i+1 < len(h.Lines) &&
376 if i+1 < len(h.Lines) &&
406 h.Lines[i].Kind == LineRemoved &&
377 h.Lines[i].Kind == LineRemoved &&
407 h.Lines[i+1].Kind == LineAdded {
378 h.Lines[i+1].Kind == LineAdded {
@@ -411,12 +382,40 @@ func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
411
382
412 // Find character-level differences
383 // Find character-level differences
413 patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
384 patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
414− patches = dmp.DiffCleanupEfficiency(patches)
415 patches = dmp.DiffCleanupSemantic(patches)
385 patches = dmp.DiffCleanupSemantic(patches)
386+ patches = dmp.DiffCleanupMerge(patches)
387+ patches = dmp.DiffCleanupEfficiency(patches)
416
388
417− // Apply highlighting to the differences
389+ segments := make([]Segment, 0)
418− oldLine.Content = colorizeSegments(patches, true, style)
390+
419− newLine.Content = colorizeSegments(patches, false, style)
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
420
419
421 updated = append(updated, oldLine, newLine)
420 updated = append(updated, oldLine, newLine)
422 i++ // Skip the next line as we've already processed it
421 i++ // Skip the next line as we've already processed it
@@ -428,45 +427,7 @@ func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
428 h.Lines = updated
427 h.Lines = updated
429}
428}
430
429
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
470func pairLines(lines []DiffLine) []linePair {
431func pairLines(lines []DiffLine) []linePair {
471 var pairs []linePair
432 var pairs []linePair
472 i := 0
433 i := 0
@@ -498,7 +459,7 @@ func pairLines(lines []DiffLine) []linePair {
498// Syntax Highlighting
459// Syntax Highlighting
499// -------------------------------------------------------------------------
460// -------------------------------------------------------------------------
500
461
501−// SyntaxHighlight applies syntax highlighting to a string based on the file extension.
462+// SyntaxHighlight applies syntax highlighting to text based on file extension
502func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
463func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
503 // Determine the language lexer to use
464 // Determine the language lexer to use
504 l := lexers.Match(fileName)
465 l := lexers.Match(fileName)
@@ -515,21 +476,98 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
515 if f == nil {
476 if f == nil {
516 f = formatters.Fallback
477 f = formatters.Fallback
517 }
478 }
518−
479+ theme := `
519− // Get the style
480+ <style name="vscode-dark-plus">
520− s := styles.Get("dracula")
481+ <!-- Base colors -->
521− if s == nil {
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"/>
522− s = styles.Fallback
547+ <!-- Generic styles -->
523− }
548+ <entry type="Generic" style="#D4D4D4"/>
524−
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)
525 // Modify the style to use the provided background
566 // Modify the style to use the provided background
526− s, err := s.Builder().Transform(
567+ s, err := style.Builder().Transform(
527 func(t chroma.StyleEntry) chroma.StyleEntry {
568 func(t chroma.StyleEntry) chroma.StyleEntry {
528 r, g, b, _ := bg.RGBA()
569 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))
533 return t
571 return t
534 },
572 },
535 ).Build()
573 ).Build()
@@ -546,7 +584,7 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
546 return f.Format(w, s, it)
584 return f.Format(w, s, it)
547}
585}
548
586
549−// highlightLine applies syntax highlighting to a single line.
587+// highlightLine applies syntax highlighting to a single line
550func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
588func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
551 var buf bytes.Buffer
589 var buf bytes.Buffer
552 err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
590 err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
@@ -556,7 +594,7 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
556 return buf.String()
594 return buf.String()
557}
595}
558
596
559−// createStyles generates the lipgloss styles needed for rendering diffs.
597+// createStyles generates the lipgloss styles needed for rendering diffs
560func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
598func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
561 removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
599 removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
562 addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
600 addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
@@ -566,7 +604,106 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
566 return
604 return
567}
605}
568
606
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+
569−// renderLeftColumn formats the left side of a side-by-side diff.
706+// renderLeftColumn formats the left side of a side-by-side diff
570func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
707func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
571 if dl == nil {
708 if dl == nil {
572 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
709 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
@@ -575,9 +712,9 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
575
712
576 removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
713 removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
577
714
715+ // Determine line style based on line type
578 var marker string
716 var marker string
579 var bgStyle lipgloss.Style
717 var bgStyle lipgloss.Style
580−
581 switch dl.Kind {
718 switch dl.Kind {
582 case LineRemoved:
719 case LineRemoved:
583 marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
720 marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
@@ -591,18 +728,29 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
591 bgStyle = contextLineStyle
728 bgStyle = contextLineStyle
592 }
729 }
593
730
731+ // Format line number
594 lineNum := ""
732 lineNum := ""
595 if dl.OldLineNo > 0 {
733 if dl.OldLineNo > 0 {
596 lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
734 lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
597 }
735 }
598
736
737+ // Create the line prefix
599 prefix := lineNumberStyle.Render(lineNum + " " + marker)
738 prefix := lineNumberStyle.Render(lineNum + " " + marker)
739+
740+ // Apply syntax highlighting
600 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
741 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
601
742
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
602 if dl.Kind == LineRemoved {
749 if dl.Kind == LineRemoved {
603 content = bgStyle.Render(" ") + content
750 content = bgStyle.Render(" ") + content
604 }
751 }
605
752
753+ // Create the final line and truncate if needed
606 lineText := prefix + content
754 lineText := prefix + content
607 return bgStyle.MaxHeight(1).Width(colWidth).Render(
755 return bgStyle.MaxHeight(1).Width(colWidth).Render(
608 ansi.Truncate(
756 ansi.Truncate(
@@ -613,7 +761,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
613 )
761 )
614}
762}
615
763
616−// renderRightColumn formats the right side of a side-by-side diff.
764+// renderRightColumn formats the right side of a side-by-side diff
617func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
765func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
618 if dl == nil {
766 if dl == nil {
619 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
767 contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
@@ -622,9 +770,9 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
622
770
623 _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
771 _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
624
772
773+ // Determine line style based on line type
625 var marker string
774 var marker string
626 var bgStyle lipgloss.Style
775 var bgStyle lipgloss.Style
627−
628 switch dl.Kind {
776 switch dl.Kind {
629 case LineAdded:
777 case LineAdded:
630 marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
778 marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
@@ -638,18 +786,29 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
638 bgStyle = contextLineStyle
786 bgStyle = contextLineStyle
639 }
787 }
640
788
789+ // Format line number
641 lineNum := ""
790 lineNum := ""
642 if dl.NewLineNo > 0 {
791 if dl.NewLineNo > 0 {
643 lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
792 lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
644 }
793 }
645
794
795+ // Create the line prefix
646 prefix := lineNumberStyle.Render(lineNum + " " + marker)
796 prefix := lineNumberStyle.Render(lineNum + " " + marker)
797+
798+ // Apply syntax highlighting
647 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
799 content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
648
800
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
649 if dl.Kind == LineAdded {
807 if dl.Kind == LineAdded {
650 content = bgStyle.Render(" ") + content
808 content = bgStyle.Render(" ") + content
651 }
809 }
652
810
811+ // Create the final line and truncate if needed
653 lineText := prefix + content
812 lineText := prefix + content
654 return bgStyle.MaxHeight(1).Width(colWidth).Render(
813 return bgStyle.MaxHeight(1).Width(colWidth).Render(
655 ansi.Truncate(
814 ansi.Truncate(
@@ -661,10 +820,10 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
661}
820}
662
821
663// -------------------------------------------------------------------------
822// -------------------------------------------------------------------------
664−// Public API Methods
823+// Public API
665// -------------------------------------------------------------------------
824// -------------------------------------------------------------------------
666
825
667−// RenderSideBySideHunk formats a hunk for side-by-side display.
826+// RenderSideBySideHunk formats a hunk for side-by-side display
668func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
827func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
669 // Apply options to create the configuration
828 // Apply options to create the configuration
670 config := NewSideBySideConfig(opts...)
829 config := NewSideBySideConfig(opts...)
@@ -692,7 +851,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
692 return sb.String()
851 return sb.String()
693}
852}
694
853
695−// FormatDiff creates a side-by-side formatted view of a diff.
854+// FormatDiff creates a side-by-side formatted view of a diff
696func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
855func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
697 diffResult, err := ParseUnifiedDiff(diffText)
856 diffResult, err := ParseUnifiedDiff(diffText)
698 if err != nil {
857 if err != nil {
@@ -700,11 +859,18 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
700 }
859 }
701
860
702 var sb strings.Builder
861 var sb strings.Builder
703−
704 config := NewSideBySideConfig(opts...)
862 config := NewSideBySideConfig(opts...)
863+
705 for i, h := range diffResult.Hunks {
864 for i, h := range diffResult.Hunks {
706 if i > 0 {
865 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+ )
708 }
874 }
709 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
875 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
710 }
876 }
@@ -712,14 +878,16 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
712 return sb.String(), nil
878 return sb.String(), nil
713}
879}
714
880
715−// GenerateDiff creates a unified diff from two file contents.
881+// GenerateDiff creates a unified diff from two file contents
716func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
882func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
883+ // Create temporary directory for git operations
717 tempDir, err := os.MkdirTemp("", "git-diff-temp")
884 tempDir, err := os.MkdirTemp("", "git-diff-temp")
718 if err != nil {
885 if err != nil {
719 return "", 0, 0
886 return "", 0, 0
720 }
887 }
721 defer os.RemoveAll(tempDir)
888 defer os.RemoveAll(tempDir)
722
889
890+ // Initialize git repo
723 repo, err := git.PlainInit(tempDir, false)
891 repo, err := git.PlainInit(tempDir, false)
724 if err != nil {
892 if err != nil {
725 return "", 0, 0
893 return "", 0, 0
@@ -730,6 +898,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
730 return "", 0, 0
898 return "", 0, 0
731 }
899 }
732
900
901+ // Write the "before" content and commit it
733 fullPath := filepath.Join(tempDir, fileName)
902 fullPath := filepath.Join(tempDir, fileName)
734 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
903 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
735 return "", 0, 0
904 return "", 0, 0
@@ -754,7 +923,9 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
754 return "", 0, 0
923 return "", 0, 0
755 }
924 }
756
925
926+ // Write the "after" content and commit it
757 if err = os.WriteFile(fullPath, []byte(afterContent), 0o644); err != nil {
927 if err = os.WriteFile(fullPath, []byte(afterContent), 0o644); err != nil {
928+ return "", 0, 0
758 }
929 }
759
930
760 _, err = wt.Add(fileName)
931 _, err = wt.Add(fileName)
@@ -773,6 +944,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
773 return "", 0, 0
944 return "", 0, 0
774 }
945 }
775
946
947+ // Get the diff between the two commits
776 beforeCommitObj, err := repo.CommitObject(beforeCommit)
948 beforeCommitObj, err := repo.CommitObject(beforeCommit)
777 if err != nil {
949 if err != nil {
778 return "", 0, 0
950 return "", 0, 0
@@ -788,6 +960,7 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
788 return "", 0, 0
960 return "", 0, 0
789 }
961 }
790
962
963+ // Count additions and removals
791 additions := 0
964 additions := 0
792 removals := 0
965 removals := 0
793 for _, fileStat := range patch.Stats() {
966 for _, fileStat := range patch.Stats() {