11import { memo , useMemo , useEffect , useState } from "react"
2- import { parsePatch } from "diff "
2+ import { parseUnifiedDiff } from "@src/utils/parseUnifiedDiff "
33import { toJsxRuntime } from "hast-util-to-jsx-runtime"
44import { Fragment , jsx , jsxs } from "react/jsx-runtime"
55import { getHighlighter , normalizeLanguage } from "@src/utils/highlighter"
@@ -10,14 +10,6 @@ interface DiffViewProps {
1010 filePath ?: string
1111}
1212
13- interface DiffLine {
14- oldLineNum : number | null
15- newLineNum : number | null
16- type : "context" | "addition" | "deletion" | "gap"
17- content : string
18- hiddenCount ?: number
19- }
20-
2113/**
2214 * DiffView component renders unified diffs with side-by-side line numbers
2315 * matching VSCode's diff editor style
@@ -85,261 +77,71 @@ const DiffView = memo(({ source, filePath }: DiffViewProps) => {
8577 }
8678 }
8779
88- // Parse diff and extract line information
89- const diffLines = useMemo ( ( ) => {
90- if ( ! source ) return [ ]
91-
92- try {
93- const patches = parsePatch ( source )
94- if ( ! patches || patches . length === 0 ) return [ ]
95-
96- const lines : DiffLine [ ] = [ ]
97- const patch = filePath
98- ? ( patches . find ( ( p ) =>
99- [ p . newFileName , p . oldFileName ] . some (
100- ( n ) => typeof n === "string" && ( n === filePath || n ?. endsWith ( "/" + filePath ) ) ,
101- ) ,
102- ) ?? patches [ 0 ] )
103- : patches [ 0 ]
104-
105- if ( ! patch ) return [ ]
106-
107- let prevHunk : any = null
108- for ( const hunk of patch . hunks ) {
109- // Insert a compact "hidden lines" separator between hunks
110- if ( prevHunk ) {
111- const gapNew = hunk . newStart - ( prevHunk . newStart + prevHunk . newLines )
112- const gapOld = hunk . oldStart - ( prevHunk . oldStart + prevHunk . oldLines )
113- const hidden = Math . max ( gapNew , gapOld )
114- if ( hidden > 0 ) {
115- lines . push ( {
116- oldLineNum : null ,
117- newLineNum : null ,
118- type : "gap" ,
119- content : "" ,
120- hiddenCount : hidden ,
121- } )
122- }
123- }
124-
125- let oldLine = hunk . oldStart
126- let newLine = hunk . newStart
127-
128- for ( const line of hunk . lines ) {
129- const firstChar = line [ 0 ]
130- const content = line . slice ( 1 )
131-
132- if ( firstChar === "-" ) {
133- lines . push ( {
134- oldLineNum : oldLine ,
135- newLineNum : null ,
136- type : "deletion" ,
137- content,
138- } )
139- oldLine ++
140- } else if ( firstChar === "+" ) {
141- lines . push ( {
142- oldLineNum : null ,
143- newLineNum : newLine ,
144- type : "addition" ,
145- content,
146- } )
147- newLine ++
148- } else {
149- // Context line
150- lines . push ( {
151- oldLineNum : oldLine ,
152- newLineNum : newLine ,
153- type : "context" ,
154- content,
155- } )
156- oldLine ++
157- newLine ++
158- }
159- }
160-
161- prevHunk = hunk
162- }
163-
164- return lines
165- } catch ( error ) {
166- console . error ( "[DiffView] Failed to parse diff:" , error )
167- return [ ]
168- }
169- } , [ source , filePath ] )
80+ // Parse diff server-provided unified patch into renderable lines
81+ const diffLines = useMemo ( ( ) => parseUnifiedDiff ( source , filePath ) , [ source , filePath ] )
17082
17183 return (
172- < div
173- style = { {
174- backgroundColor : "var(--vscode-editor-background)" ,
175- borderRadius : "6px" ,
176- overflow : "hidden" ,
177- fontFamily : "var(--vscode-editor-font-family)" ,
178- fontSize : "0.95em" ,
179- } } >
180- < div style = { { overflowX : "hidden" } } >
181- < table
182- style = { {
183- width : "100%" ,
184- borderCollapse : "collapse" ,
185- tableLayout : "auto" ,
186- } } >
84+ < div className = "diff-view bg-[var(--vscode-editor-background)] rounded-md overflow-hidden text-[0.95em]" >
85+ < div className = "overflow-x-hidden" >
86+ < table className = "w-full border-collapse table-auto" >
18787 < tbody >
18888 { diffLines . map ( ( line , idx ) => {
18989 // Render compact separator between hunks
19090 if ( line . type === "gap" ) {
191- // Match the header/container background tone
192- const gapBg = "var(--vscode-editor-background)"
91+ // Compact separator between hunks
19392 return (
19493 < tr key = { idx } >
195- < td
196- style = { {
197- width : "45px" ,
198- textAlign : "right" ,
199- paddingRight : "12px" ,
200- paddingLeft : "8px" ,
201- userSelect : "none" ,
202- verticalAlign : "top" ,
203- whiteSpace : "nowrap" ,
204- backgroundColor : gapBg ,
205- } }
206- />
207- < td
208- style = { {
209- width : "45px" ,
210- textAlign : "right" ,
211- paddingRight : "12px" ,
212- userSelect : "none" ,
213- verticalAlign : "top" ,
214- whiteSpace : "nowrap" ,
215- backgroundColor : gapBg ,
216- } }
217- />
218- < td
219- style = { {
220- width : "12px" ,
221- backgroundColor : gapBg ,
222- verticalAlign : "top" ,
223- } }
224- />
94+ < td className = "w-[45px] text-right pr-3 pl-2 select-none align-top whitespace-nowrap bg-[var(--vscode-editor-background)]" />
95+ < td className = "w-[45px] text-right pr-3 select-none align-top whitespace-nowrap bg-[var(--vscode-editor-background)]" />
96+ < td className = "w-[12px] align-top bg-[var(--vscode-editor-background)]" />
22597 { /* +/- column (empty for gap) */ }
226- < td
227- style = { {
228- width : "16px" ,
229- textAlign : "center" ,
230- userSelect : "none" ,
231- backgroundColor : gapBg ,
232- } }
233- />
234- < td
235- style = { {
236- paddingRight : "12px" ,
237- whiteSpace : "pre-wrap" ,
238- overflowWrap : "anywhere" ,
239- wordBreak : "break-word" ,
240- fontFamily : "var(--vscode-editor-font-family)" ,
241- width : "100%" ,
242- textAlign : "left" ,
243- fontStyle : "italic" ,
244- backgroundColor : gapBg ,
245- } } >
98+ < td className = "w-[16px] text-center select-none bg-[var(--vscode-editor-background)]" />
99+ < td className = "pr-3 whitespace-pre-wrap break-words w-full italic bg-[var(--vscode-editor-background)]" >
246100 { `${ line . hiddenCount ?? 0 } hidden lines` }
247101 </ td >
248102 </ tr >
249103 )
250104 }
251105
252- // Use VSCode's built-in diff editor color variables with 50% opacity
253- const gutterBg =
106+ // Use VSCode's built-in diff editor color variables as classes for gutters
107+ const gutterBgClass =
254108 line . type === "addition"
255- ? "var(--vscode-diffEditor-insertedTextBackground)"
109+ ? "bg-[ var(--vscode-diffEditor-insertedTextBackground)] "
256110 : line . type === "deletion"
257- ? "var(--vscode-diffEditor-removedTextBackground)"
258- : "var(--vscode-editorGroup-border)"
111+ ? "bg-[ var(--vscode-diffEditor-removedTextBackground)] "
112+ : "bg-[ var(--vscode-editorGroup-border)] "
259113
260- const contentBgStyles =
114+ const contentBgClass =
261115 line . type === "addition"
262- ? {
263- backgroundColor :
264- "color-mix(in srgb, var(--vscode-diffEditor-insertedTextBackground) 70%, transparent)" ,
265- }
116+ ? "diff-content-inserted"
266117 : line . type === "deletion"
267- ? {
268- backgroundColor :
269- "color-mix(in srgb, var(--vscode-diffEditor-removedTextBackground) 70%, transparent)" ,
270- }
271- : {
272- backgroundColor :
273- "color-mix(in srgb, var(--vscode-editorGroup-border) 100%, transparent)" ,
274- }
118+ ? "diff-content-removed"
119+ : "diff-content-context"
275120
276121 const sign = line . type === "addition" ? "+" : line . type === "deletion" ? "-" : ""
277122
278123 return (
279124 < tr key = { idx } >
280125 { /* Old line number */ }
281126 < td
282- style = { {
283- width : "45px" ,
284- textAlign : "right" ,
285- paddingRight : "4px" ,
286- paddingLeft : "4px" ,
287- userSelect : "none" ,
288- verticalAlign : "top" ,
289- whiteSpace : "nowrap" ,
290- backgroundColor : gutterBg ,
291- } } >
127+ className = { `w-[45px] text-right pr-1 pl-1 select-none align-top whitespace-nowrap ${ gutterBgClass } ` } >
292128 { line . oldLineNum || "" }
293129 </ td >
294130 { /* New line number */ }
295131 < td
296- style = { {
297- width : "45px" ,
298- textAlign : "right" ,
299- paddingRight : "4px" ,
300- userSelect : "none" ,
301- verticalAlign : "top" ,
302- whiteSpace : "nowrap" ,
303- backgroundColor : gutterBg ,
304- } } >
132+ className = { `w-[45px] text-right pr-1 select-none align-top whitespace-nowrap ${ gutterBgClass } ` } >
305133 { line . newLineNum || "" }
306134 </ td >
307135 { /* Narrow colored gutter */ }
308- < td
309- style = { {
310- width : "12px" ,
311- backgroundColor : gutterBg ,
312- verticalAlign : "top" ,
313- } }
314- />
136+ < td className = { `w-[12px] ${ gutterBgClass } align-top` } />
315137 { /* +/- fixed column to prevent wrapping into it */ }
316138 < td
317- style = { {
318- width : "16px" ,
319- textAlign : "center" ,
320- userSelect : "none" ,
321- whiteSpace : "nowrap" ,
322- paddingLeft : "4px" ,
323- paddingRight : "4px" ,
324- backgroundColor : gutterBg ,
325- color : "var(--vscode-editor-foreground)" ,
326- fontFamily : "var(--vscode-editor-font-family)" ,
327- } } >
139+ className = { `w-[16px] text-center select-none whitespace-nowrap px-1 ${ gutterBgClass } ` } >
328140 { sign }
329141 </ td >
330142 { /* Code content (no +/- prefix here) */ }
331143 < td
332- style = { {
333- paddingLeft : "4px" ,
334- paddingRight : "12px" ,
335- whiteSpace : "pre-wrap" ,
336- overflowWrap : "anywhere" ,
337- wordBreak : "break-word" ,
338- fontFamily : "var(--vscode-editor-font-family)" ,
339- color : "var(--vscode-editor-foreground)" ,
340- width : "100%" ,
341- ...contentBgStyles ,
342- } } >
144+ className = { `pl-1 pr-3 whitespace-pre-wrap break-words w-full ${ contentBgClass } ` } >
343145 { renderHighlighted ( line . content ) }
344146 </ td >
345147 </ tr >
0 commit comments