@@ -19,274 +19,6 @@ func stripAnsi(str string) string {
1919 return ansiRegex .ReplaceAllString (str , "" )
2020}
2121
22- // diffLineType represents the type of a diff line.
23- type diffLineType int
24-
25- const (
26- lineTypeContext diffLineType = iota
27- lineTypeAdded
28- lineTypeRemoved
29- lineTypeHeader
30- lineTypeFileHeader
31- lineTypeHunkHeader
32- )
33-
34- // diffRow represents a single row in the structured diff.
35- type diffRow struct {
36- lineType diffLineType
37- oldLine string
38- newLine string
39- rawLine string // Original line for fallback
40- }
41-
42- // parseDiffStructure transforms a unified diff into structured rows suitable for split-view rendering.
43- func parseDiffStructure (content string ) []diffRow {
44- lines := strings .Split (content , "\n " )
45- var rows []diffRow
46-
47- for _ , line := range lines {
48- if len (line ) == 0 {
49- rows = append (rows , diffRow {lineType : lineTypeContext , oldLine : "" , newLine : "" , rawLine : "" })
50- continue
51- }
52-
53- // Always strip ANSI codes first before any parsing logic
54- cleanedLine := stripAnsi (line )
55-
56- if len (cleanedLine ) == 0 {
57- rows = append (rows , diffRow {lineType : lineTypeContext , oldLine : "" , newLine : "" , rawLine : line })
58- continue
59- }
60-
61- firstChar := cleanedLine [0 ]
62-
63- // File headers
64- if strings .HasPrefix (cleanedLine , "diff --git" ) ||
65- strings .HasPrefix (cleanedLine , "index " ) {
66- rows = append (rows , diffRow {lineType : lineTypeFileHeader , rawLine : line })
67- continue
68- }
69-
70- // --- and +++ headers (but not as content)
71- if strings .HasPrefix (cleanedLine , "---" ) && len (cleanedLine ) > 3 && cleanedLine [3 ] == ' ' {
72- rows = append (rows , diffRow {lineType : lineTypeFileHeader , rawLine : line })
73- continue
74- }
75- if strings .HasPrefix (cleanedLine , "+++" ) && len (cleanedLine ) > 3 && cleanedLine [3 ] == ' ' {
76- rows = append (rows , diffRow {lineType : lineTypeFileHeader , rawLine : line })
77- continue
78- }
79-
80- // Hunk headers
81- if strings .HasPrefix (cleanedLine , "@@" ) {
82- rows = append (rows , diffRow {lineType : lineTypeHunkHeader , rawLine : line })
83- continue
84- }
85-
86- // Newline marker
87- if strings .HasPrefix (cleanedLine , "\\ No newline" ) {
88- rows = append (rows , diffRow {lineType : lineTypeFileHeader , rawLine : line })
89- continue
90- }
91-
92- // Added lines (but not +++ headers)
93- if firstChar == '+' {
94- // Store content without the "+" prefix for rendering
95- contentWithoutPrefix := cleanedLine [1 :]
96- rows = append (rows , diffRow {lineType : lineTypeAdded , newLine : contentWithoutPrefix , rawLine : line })
97- continue
98- }
99-
100- // Removed lines (but not --- headers)
101- if firstChar == '-' {
102- // Store content without the "-" prefix for rendering
103- contentWithoutPrefix := cleanedLine [1 :]
104- rows = append (rows , diffRow {lineType : lineTypeRemoved , oldLine : contentWithoutPrefix , rawLine : line })
105- continue
106- }
107-
108- // Context lines (start with space)
109- if firstChar == ' ' {
110- // Store content without the space prefix
111- contentWithoutSpace := cleanedLine [1 :]
112- rows = append (rows , diffRow {lineType : lineTypeContext , oldLine : contentWithoutSpace , newLine : contentWithoutSpace , rawLine : line })
113- continue
114- }
115-
116- // Fallback for any other lines
117- rows = append (rows , diffRow {lineType : lineTypeContext , oldLine : cleanedLine , newLine : cleanedLine , rawLine : line })
118- }
119-
120- return rows
121- }
122-
123- // renderSplitDiffView renders a GitHub-style split-view diff.
124- // columnWidth should be roughly half the viewport width minus some padding.
125- func renderSplitDiffView (rows []diffRow , columnWidth int , theme Theme ) string {
126- if columnWidth < 20 {
127- // Column too narrow for split view
128- return ""
129- }
130-
131- // Create themed styles
132- headerStyle := lipgloss .NewStyle ().Bold (true )
133-
134- // Use theme's added/removed colors for backgrounds
135- addedStyle := theme .GitStaged .
136- Width (columnWidth ).
137- Padding (0 , 1 )
138-
139- removedStyle := theme .GitUnstaged .
140- Width (columnWidth ).
141- Padding (0 , 1 )
142-
143- contextStyle := lipgloss .NewStyle ().
144- Width (columnWidth ).
145- Padding (0 , 1 )
146-
147- emptyStyle := lipgloss .NewStyle ().
148- Width (columnWidth ).
149- Padding (0 , 1 )
150-
151- var renderedRows []string
152-
153- for _ , row := range rows {
154- var left , right string
155-
156- switch row .lineType {
157- case lineTypeFileHeader :
158- // File headers span full width
159- fullLine := headerStyle .Width (columnWidth * 2 ).Render (row .rawLine )
160- renderedRows = append (renderedRows , fullLine )
161-
162- case lineTypeHunkHeader :
163- // Hunk headers span full width
164- fullLine := headerStyle .Width (columnWidth * 2 ).Render (row .rawLine )
165- renderedRows = append (renderedRows , fullLine )
166-
167- case lineTypeRemoved :
168- // Removed line on left (oldLine is already without "-" prefix), empty on right
169- left = removedStyle .Render (row .oldLine )
170- right = emptyStyle .Render ("" )
171- renderedRows = append (renderedRows , lipgloss .JoinHorizontal (lipgloss .Top , left , right ))
172-
173- case lineTypeAdded :
174- // Empty on left, added line on right (newLine is already without "+" prefix)
175- left = emptyStyle .Render ("" )
176- right = addedStyle .Render (row .newLine )
177- renderedRows = append (renderedRows , lipgloss .JoinHorizontal (lipgloss .Top , left , right ))
178-
179- case lineTypeContext :
180- // Context lines on both sides (oldLine and newLine are already without space prefix and identical)
181- left = contextStyle .Render (row .oldLine )
182- right = contextStyle .Render (row .newLine )
183- renderedRows = append (renderedRows , lipgloss .JoinHorizontal (lipgloss .Top , left , right ))
184-
185- default :
186- // Fallback: show on both sides
187- left = contextStyle .Render (row .oldLine )
188- right = contextStyle .Render (row .newLine )
189- renderedRows = append (renderedRows , lipgloss .JoinHorizontal (lipgloss .Top , left , right ))
190- }
191- }
192-
193- return strings .Join (renderedRows , "\n " )
194- }
195-
196- // renderAdaptiveDiffView returns the appropriately formatted diff based on viewport width.
197- // If width >= 80, uses split-view; otherwise falls back to unified diff.
198- func renderAdaptiveDiffView (content string , width int , theme Theme ) string {
199- // Quick check: if content doesn't look like a diff, return as-is
200- if ! strings .Contains (content , "diff --git" ) && ! strings .Contains (content , "@@" ) {
201- return content
202- }
203-
204- // Threshold for split-view: 80 chars available
205- const splitViewThreshold = 80
206-
207- if width >= splitViewThreshold && width > 60 {
208- // Use split-view mode
209- columnWidth := (width - 1 ) / 2
210- rows := parseDiffStructure (content )
211- splitView := renderSplitDiffView (rows , columnWidth , theme )
212- if splitView != "" {
213- return splitView
214- }
215- }
216-
217- // Fallback to unified diff styling
218- return styleDiffContent (content , theme )
219- }
220-
221- // styleDiffContent applies visual highlighting to diff lines for better readability.
222- // It detects and styles:
223- // - Diff headers (diff --git, index, ---, +++, @@) with bold magenta
224- // - Added lines (+) with green
225- // - Removed lines (-) with red
226- // - Context lines with normal or dimmed styling
227- // It also adds visual separation before hunk markers for improved scanability.
228- func styleDiffContent (content string , theme Theme ) string {
229- // Quick check: if content doesn't look like a diff, return as-is
230- if ! strings .Contains (content , "diff --git" ) && ! strings .Contains (content , "@@" ) {
231- return content
232- }
233-
234- lines := strings .Split (content , "\n " )
235-
236- // Create styles for different diff elements
237- headerStyle := lipgloss .NewStyle ().Bold (true )
238-
239- // Use theme colors for added/removed lines (aligns with git status colors)
240- addedStyle := theme .GitStaged // Green
241- removedStyle := theme .GitUnstaged // Red
242-
243- var result []string
244- for i , line := range lines {
245- if len (line ) == 0 {
246- result = append (result , line )
247- continue
248- }
249-
250- // Strip ANSI codes before checking prefixes
251- cleanedLine := stripAnsi (line )
252-
253- if len (cleanedLine ) == 0 {
254- result = append (result , line )
255- continue
256- }
257-
258- firstChar := cleanedLine [0 ]
259-
260- // Add spacing before hunk markers (visual separation of hunks)
261- if strings .HasPrefix (cleanedLine , "@@" ) && i > 0 && result [len (result )- 1 ] != "" {
262- result = append (result , "" ) // Add blank line for visual separation
263- }
264-
265- // Handle diff headers
266- if strings .HasPrefix (cleanedLine , "diff --git" ) ||
267- strings .HasPrefix (cleanedLine , "index " ) ||
268- strings .HasPrefix (cleanedLine , "---" ) ||
269- strings .HasPrefix (cleanedLine , "+++" ) ||
270- strings .HasPrefix (cleanedLine , "@@" ) {
271- result = append (result , headerStyle .Render (line ))
272- } else if firstChar == '+' && ! strings .HasPrefix (cleanedLine , "+++" ) {
273- // Added line: apply green styling
274- result = append (result , addedStyle .Render (line ))
275- } else if firstChar == '-' && ! strings .HasPrefix (cleanedLine , "---" ) {
276- // Removed line: apply red styling
277- result = append (result , removedStyle .Render (line ))
278- } else if firstChar == '\\' {
279- // "\ No newline at end of file" marker - treat as metadata
280- result = append (result , headerStyle .Render (line ))
281- } else {
282- // Context line (starts with space) - keep as-is
283- result = append (result , line )
284- }
285- }
286-
287- return strings .Join (result , "\n " )
288- }
289-
29022// View is the main render function for the application.
29123func (m Model ) View () string {
29224
0 commit comments