@@ -31,9 +31,8 @@ interface InlineDiffViewProps {
3131 * when the patch is too large or the worker build fails.
3232 */
3333export function InlineDiffView ( { diffText, filePath, oldText, newText } : InlineDiffViewProps ) {
34- const displayPath = normalizeDiffFilePath ( filePath ) ;
3534 const theme = useDiffTheme ( ) ;
36- const [ diffFile , setDiffFile ] = useState < DiffFile | null > ( null ) ;
35+ const [ diffFiles , setDiffFiles ] = useState < InlineDiffFile [ ] > ( [ ] ) ;
3736 const [ state , setState ] = useState < "building" | "ready" | "fallback" > (
3837 diffText . length > MAX_DIFF_LENGTH ? "fallback" : "building" ,
3938 ) ;
@@ -45,33 +44,31 @@ export function InlineDiffView({ diffText, filePath, oldText, newText }: InlineD
4544 }
4645 let cancelled = false ;
4746 setState ( "building" ) ;
48- setDiffFile ( null ) ;
49-
50- const parsedNames = extractDiffNames ( diffText ) ;
51- const oldName = parsedNames . oldName || ( oldText === "" ? "/dev/null" : `a/${ displayPath } ` ) ;
52- const newName = parsedNames . newName || `b/${ displayPath } ` ;
53- const lang = getLang ( newName || displayPath ) ;
54-
55- void buildInWorker (
56- [
57- {
58- key : displayPath ,
59- diff : diffText ,
60- oldName,
61- newName,
62- fileLang : lang ,
63- ...( oldText !== undefined && newText !== undefined
64- ? { oldContent : oldText , newContent : newText }
65- : { } ) ,
66- } ,
67- ] ,
68- theme ,
69- )
47+ setDiffFiles ( [ ] ) ;
48+
49+ const parts = prepareInlineDiffParts ( diffText , filePath ) ;
50+ const includeContent = parts . length === 1 && oldText !== undefined && newText !== undefined ;
51+ const items = parts . map ( ( part , index ) => {
52+ const oldName = part . oldName || ( oldText === "" ? "/dev/null" : `a/${ part . displayPath } ` ) ;
53+ const newName = part . newName || `b/${ part . displayPath } ` ;
54+ return {
55+ key : `${ index } :${ part . displayPath } ` ,
56+ diff : part . diff ,
57+ oldName,
58+ newName,
59+ fileLang : getLang ( part . displayPath ) ,
60+ ...( includeContent ? { oldContent : oldText , newContent : newText } : { } ) ,
61+ } ;
62+ } ) ;
63+
64+ void buildInWorker ( items , theme )
7065 . then ( ( results ) => {
7166 if ( cancelled ) return ;
72- const r = results [ 0 ] ;
73- if ( r ?. bundle ) {
74- setDiffFile ( diffFileFromBundle ( r . data , r . bundle ) ) ;
67+ const built = results . flatMap ( ( r ) =>
68+ r . bundle ? [ { key : r . key , diffFile : diffFileFromBundle ( r . data , r . bundle ) } ] : [ ] ,
69+ ) ;
70+ if ( built . length === results . length && built . length > 0 ) {
71+ setDiffFiles ( built ) ;
7572 setState ( "ready" ) ;
7673 } else {
7774 setState ( "fallback" ) ;
@@ -84,33 +81,150 @@ export function InlineDiffView({ diffText, filePath, oldText, newText }: InlineD
8481 return ( ) => {
8582 cancelled = true ;
8683 } ;
87- } , [ diffText , displayPath , oldText , newText , theme ] ) ;
84+ } , [ diffText , filePath , oldText , newText , theme ] ) ;
8885
8986 if ( state === "fallback" ) {
9087 return < CommandOutputViewport text = { diffText } language = "diff" /> ;
9188 }
9289
93- if ( state === "building" || ! diffFile ) {
90+ if ( state === "building" || diffFiles . length === 0 ) {
9491 return < div className = "py-2 text-xs text-[color:var(--muted)]" > Building diff…</ div > ;
9592 }
9693
9794 return (
9895 < DiffViewErrorBoundary fallback = { < CommandOutputViewport text = { diffText } language = "diff" /> } >
99- < div className = "max-h-[min(24rem,50vh)] overflow-auto [scrollbar-gutter:stable]" >
100- < DiffView
101- diffFile = { diffFile }
102- diffViewMode = { UNIFIED_MODE }
103- diffViewTheme = { theme }
104- diffViewFontSize = { 12 }
105- registerHighlighter = { highlighter }
106- diffViewHighlight = { true }
107- diffViewWrap = { false }
108- />
96+ < div className = "flex max-h-[min(24rem,50vh)] flex-col gap-2 overflow-auto [scrollbar-gutter:stable]" >
97+ { diffFiles . map ( ( { key, diffFile } ) => (
98+ < DiffView
99+ key = { key }
100+ diffFile = { diffFile }
101+ diffViewMode = { UNIFIED_MODE }
102+ diffViewTheme = { theme }
103+ diffViewFontSize = { 12 }
104+ registerHighlighter = { highlighter }
105+ diffViewHighlight = { true }
106+ diffViewWrap = { false }
107+ />
108+ ) ) }
109109 </ div >
110110 </ DiffViewErrorBoundary >
111111 ) ;
112112}
113113
114+ interface InlineDiffFile {
115+ key : string ;
116+ diffFile : DiffFile ;
117+ }
118+
119+ interface DiffNames {
120+ oldName : string ;
121+ newName : string ;
122+ }
123+
124+ interface InlineDiffPart {
125+ diff : string ;
126+ displayPath : string ;
127+ oldName : string ;
128+ newName : string ;
129+ }
130+
131+ export function prepareInlineDiffParts ( diffText : string , fallbackPath : string ) : InlineDiffPart [ ] {
132+ const parts = splitUnifiedDiffFiles ( diffText ) . map ( ( part ) =>
133+ normalizeInlineDiffPart ( part , fallbackPath ) ,
134+ ) ;
135+ const merged : InlineDiffPart [ ] = [ ] ;
136+ const byNames = new Map < string , InlineDiffPart > ( ) ;
137+
138+ for ( const part of parts ) {
139+ const body = extractDiffBody ( part . diff ) ;
140+ if ( ! body ) {
141+ merged . push ( part ) ;
142+ continue ;
143+ }
144+ const key = `${ part . oldName } \0${ part . newName } ` ;
145+ const existing = byNames . get ( key ) ;
146+ if ( ! existing ) {
147+ byNames . set ( key , part ) ;
148+ merged . push ( part ) ;
149+ continue ;
150+ }
151+ existing . diff = buildUnifiedDiffText ( existing . displayPath , existing . oldName , existing . newName , [
152+ extractDiffBody ( existing . diff ) ?? "" ,
153+ body ,
154+ ] ) ;
155+ }
156+
157+ return merged ;
158+ }
159+
160+ export function splitUnifiedDiffFiles ( diffText : string ) : string [ ] {
161+ const lines = diffText . split ( / \r ? \n / ) ;
162+ const chunks : string [ ] [ ] = [ ] ;
163+ let current : string [ ] | null = null ;
164+
165+ for ( const line of lines ) {
166+ if ( line . startsWith ( "diff --git " ) ) {
167+ if ( current && current . length > 0 ) chunks . push ( current ) ;
168+ current = [ line ] ;
169+ } else if ( current ) {
170+ current . push ( line ) ;
171+ }
172+ }
173+
174+ if ( current && current . length > 0 ) chunks . push ( current ) ;
175+ if ( chunks . length === 0 ) return [ diffText ] ;
176+ return chunks . map ( ( chunk ) => chunk . join ( "\n" ) ) ;
177+ }
178+
179+ function resolveDisplayPath ( filePath : string , names : DiffNames ) : string {
180+ const candidate = names . newName && names . newName !== "/dev/null" ? names . newName : names . oldName ;
181+ return normalizeDiffFilePath ( candidate && candidate !== "/dev/null" ? candidate : filePath ) ;
182+ }
183+
184+ function normalizeInlineDiffPart ( diff : string , fallbackPath : string ) : InlineDiffPart {
185+ const names = extractDiffNames ( diff ) ;
186+ const displayPath = resolveDisplayPath ( fallbackPath , names ) ;
187+ const oldName = names . oldName === "/dev/null" ? "/dev/null" : `a/${ displayPath } ` ;
188+ const newName = names . newName === "/dev/null" ? "/dev/null" : `b/${ displayPath } ` ;
189+ const body = extractDiffBody ( diff ) ;
190+ if ( ! body ) {
191+ return {
192+ diff,
193+ displayPath,
194+ oldName : names . oldName || oldName ,
195+ newName : names . newName || newName ,
196+ } ;
197+ }
198+ return {
199+ diff : buildUnifiedDiffText ( displayPath , oldName , newName , [ body ] ) ,
200+ displayPath,
201+ oldName,
202+ newName,
203+ } ;
204+ }
205+
206+ function extractDiffBody ( diff : string ) : string | undefined {
207+ const lines = diff . split ( / \r ? \n / ) ;
208+ const start = lines . findIndex ( ( line ) => line . startsWith ( "@@" ) ) ;
209+ if ( start < 0 ) return undefined ;
210+ return lines . slice ( start ) . join ( "\n" ) . trimEnd ( ) ;
211+ }
212+
213+ function buildUnifiedDiffText (
214+ displayPath : string ,
215+ oldName : string ,
216+ newName : string ,
217+ bodies : readonly string [ ] ,
218+ ) : string {
219+ return [
220+ `diff --git a/${ displayPath } b/${ displayPath } ` ,
221+ `--- ${ oldName } ` ,
222+ `+++ ${ newName } ` ,
223+ ...bodies . map ( ( body ) => body . trimEnd ( ) ) . filter ( ( body ) => body . length > 0 ) ,
224+ "" ,
225+ ] . join ( "\n" ) ;
226+ }
227+
114228/** Catches render errors from DiffView (e.g. missing canvas in test envs). */
115229class DiffViewErrorBoundary extends Component <
116230 { children : ReactNode ; fallback : ReactNode } ,
0 commit comments