@@ -5,10 +5,13 @@ import {
55 canRenderImagePreview ,
66 getDiffLineCount ,
77 getMarkdownPreviewContents ,
8+ getSectionForFileDiff ,
89 getTotalDiffLineCount ,
910 getVisibleDiffSections ,
1011 fileHasVisibleDiff ,
12+ loadSectionContents ,
1113 shouldLoadDiffSectionContents ,
14+ shouldPreloadSectionContentsForSearch ,
1215} from '../lib/diff.ts' ;
1316import { isDiffSearchShortcut } from '../lib/keyboard.ts' ;
1417import { renderInlineMarkdown , sanitizeMarkdownImages } from '../lib/markdown.tsx' ;
@@ -70,17 +73,37 @@ test('pure renames are visible without content hunks', () => {
7073 expect ( fileHasVisibleDiff ( file , false ) ) . toBe ( true ) ;
7174} ) ;
7275
73- test ( 'patch-only text sections are loadable for full context expansion' , ( ) => {
76+ test ( 'patch-only text sections hydrate lazily instead of eager loading' , ( ) => {
77+ const patchOnlySection = {
78+ binary : false ,
79+ id : 'src/app.ts:unstaged' ,
80+ kind : 'unstaged' ,
81+ loadState : 'ready' ,
82+ patch : 'diff --git a/src/app.ts b/src/app.ts\n@@ -1 +1 @@\n-old\n+new\n' ,
83+ } as const ;
84+
85+ // Patch-only sections expand via `loadDiffFiles` hydration, not the eager
86+ // Load flow, but diff search still preloads their full contents.
87+ expect ( shouldLoadDiffSectionContents ( patchOnlySection ) ) . toBe ( false ) ;
88+ expect ( shouldPreloadSectionContentsForSearch ( patchOnlySection ) ) . toBe ( true ) ;
89+
7490 expect (
7591 shouldLoadDiffSectionContents ( {
76- binary : false ,
77- id : 'src/app.ts:unstaged' ,
78- kind : 'unstaged' ,
79- loadState : 'ready' ,
80- patch : 'diff --git a/src/app.ts b/src/app.ts\n@@ -1 +1 @@\n-old\n+new\n' ,
92+ ...patchOnlySection ,
93+ loadState : 'deferred' ,
8194 } ) ,
8295 ) . toBe ( true ) ;
8396
97+ expect (
98+ shouldPreloadSectionContentsForSearch ( {
99+ ...patchOnlySection ,
100+ summary : {
101+ canLoad : false ,
102+ reason : 'Codiff could not load full file context.' ,
103+ } ,
104+ } ) ,
105+ ) . toBe ( false ) ;
106+
84107 expect (
85108 shouldLoadDiffSectionContents ( {
86109 binary : false ,
@@ -114,6 +137,103 @@ test('patch-only text sections are loadable for full context expansion', () => {
114137 ) . toBe ( false ) ;
115138} ) ;
116139
140+ test ( 'patch-only diffs are registered for lazy hydration and side-cached contents upgrade re-parses' , async ( ) => {
141+ const section = {
142+ binary : false ,
143+ id : 'src/lazy.ts:unstaged' ,
144+ kind : 'unstaged' ,
145+ loadState : 'ready' ,
146+ patch :
147+ 'diff --git a/src/lazy.ts b/src/lazy.ts\n--- a/src/lazy.ts\n+++ b/src/lazy.ts\n@@ -2,3 +2,3 @@\n b\n-c\n+C\n d\n' ,
148+ } as const ;
149+ const file = {
150+ fingerprint : 'lazy-hydration' ,
151+ path : 'src/lazy.ts' ,
152+ sections : [ section ] ,
153+ status : 'modified' ,
154+ } satisfies ChangedFile ;
155+
156+ const { fileDiff } = getVisibleDiffSections ( file , false ) [ 0 ] ;
157+ expect ( fileDiff . isPartial ) . toBe ( true ) ;
158+ // Stable identity across re-parses: hydration mutates this object in place.
159+ expect ( getVisibleDiffSections ( file , false ) [ 0 ] . fileDiff ) . toBe ( fileDiff ) ;
160+
161+ const target = getSectionForFileDiff ( fileDiff ) ;
162+ expect ( target ?. file ) . toBe ( file ) ;
163+ expect ( target ?. section ) . toBe ( section ) ;
164+
165+ let loadCount = 0 ;
166+ const load = async ( ) => {
167+ loadCount += 1 ;
168+ return {
169+ newFile : { contents : 'a\nb\nC\nd\ne\n' , name : 'src/lazy.ts' } ,
170+ oldFile : { contents : 'a\nb\nc\nd\ne\n' , name : 'src/lazy.ts' } ,
171+ } ;
172+ } ;
173+
174+ // Concurrent loads dedupe; later calls resolve from the cache.
175+ const [ first , second ] = await Promise . all ( [
176+ loadSectionContents ( file , section , load ) ,
177+ loadSectionContents ( file , section , load ) ,
178+ ] ) ;
179+ const third = await loadSectionContents ( file , section , load ) ;
180+ expect ( loadCount ) . toBe ( 1 ) ;
181+ expect ( second ) . toBe ( first ) ;
182+ expect ( third ) . toBe ( first ) ;
183+
184+ // CodeView hydrates the cached object in place (as of 1.3.0-beta.9), so a
185+ // re-parse under the same cache key keeps returning the same object. A
186+ // re-parse under a different key (e.g. the whitespace toggle) starts from a
187+ // fresh patch parse and is hydrated from the cached contents instead of
188+ // resetting to a partial diff.
189+ expect ( getVisibleDiffSections ( file , false ) [ 0 ] . fileDiff ) . toBe ( fileDiff ) ;
190+ const reparsedFlippedFlag = getVisibleDiffSections ( file , true ) [ 0 ] . fileDiff ;
191+ expect ( reparsedFlippedFlag . isPartial ) . toBe ( false ) ;
192+ expect ( reparsedFlippedFlag ) . not . toBe ( fileDiff ) ;
193+ } ) ;
194+
195+ test ( 'non-loadable and placeholder diffs are not registered for hydration' , ( ) => {
196+ const nonLoadableFile = {
197+ fingerprint : 'non-loadable' ,
198+ path : 'src/locked.ts' ,
199+ sections : [
200+ {
201+ binary : false ,
202+ id : 'src/locked.ts:unstaged' ,
203+ kind : 'unstaged' ,
204+ loadState : 'ready' ,
205+ patch :
206+ 'diff --git a/src/locked.ts b/src/locked.ts\n--- a/src/locked.ts\n+++ b/src/locked.ts\n@@ -1 +1 @@\n-old\n+new\n' ,
207+ summary : {
208+ canLoad : false ,
209+ reason : 'Codiff could not load full file context.' ,
210+ } ,
211+ } ,
212+ ] ,
213+ status : 'modified' ,
214+ } satisfies ChangedFile ;
215+
216+ const binaryFile = {
217+ fingerprint : 'binary-placeholder' ,
218+ path : 'assets/logo.bin' ,
219+ sections : [
220+ {
221+ binary : true ,
222+ id : 'assets/logo.bin:unstaged' ,
223+ kind : 'unstaged' ,
224+ patch : '' ,
225+ } ,
226+ ] ,
227+ status : 'modified' ,
228+ } satisfies ChangedFile ;
229+
230+ for ( const file of [ nonLoadableFile , binaryFile ] ) {
231+ const { fileDiff } = getVisibleDiffSections ( file , false ) [ 0 ] ;
232+ expect ( fileDiff . isPartial ) . toBe ( true ) ;
233+ expect ( getSectionForFileDiff ( fileDiff ) ) . toBeUndefined ( ) ;
234+ }
235+ } ) ;
236+
117237test ( 'empty patch-only sections are not visible or countable' , ( ) => {
118238 const file = {
119239 fingerprint : 'empty-patch-only' ,
0 commit comments