@@ -238,6 +238,51 @@ function createTwoFileHunkBootstrap(): AppBootstrap {
238238 } ) ;
239239}
240240
241+ /** Build the cross-file hunk-navigation shape that used to flash the previous pinned header. */
242+ function createCrossFileHunkNavigationBootstrap ( ) : AppBootstrap {
243+ const longBeforeLines = Array . from (
244+ { length : 342 } ,
245+ ( _ , index ) => `line ${ String ( index + 1 ) . padStart ( 3 , "0" ) } ` ,
246+ ) ;
247+ const longAfterLines = [ ...longBeforeLines ] ;
248+ for ( const lineNumber of [
249+ 2 , 21 , 41 , 61 , 81 , 101 , 121 , 141 , 161 , 181 , 201 , 221 , 241 , 261 , 281 , 301 , 321 , 341 ,
250+ ] ) {
251+ longAfterLines [ lineNumber - 1 ] = `line ${ String ( lineNumber ) . padStart ( 3 , "0" ) } changed` ;
252+ }
253+
254+ const shortBeforeLines = [
255+ "// hunk 0 - at the very top of the file" ,
256+ "export const top = 1;" ,
257+ "" ,
258+ "" ,
259+ ...Array . from ( { length : 25 } , ( _ , index ) => `// filler ${ index + 1 } ` ) ,
260+ "// hunk 1 - mid-file" ,
261+ "export const mid = 3;" ,
262+ ] ;
263+ const shortAfterLines = [ ...shortBeforeLines ] ;
264+ shortAfterLines [ 1 ] = "export const top = 2;" ;
265+ shortAfterLines [ 30 ] = "export const mid = 4;" ;
266+
267+ return createTestGitAppBootstrap ( {
268+ changesetId : "changeset:cross-file-hunk-navigation" ,
269+ files : [
270+ createTestDiffFile (
271+ "long-file" ,
272+ "long-file.txt" ,
273+ lines ( ...longBeforeLines ) ,
274+ lines ( ...longAfterLines ) ,
275+ ) ,
276+ createTestDiffFile (
277+ "short-file" ,
278+ "short-file.ts" ,
279+ lines ( ...shortBeforeLines ) ,
280+ lines ( ...shortAfterLines ) ,
281+ ) ,
282+ ] ,
283+ } ) ;
284+ }
285+
241286function createMouseScrollSelectionBootstrap ( ) : AppBootstrap {
242287 const firstBeforeLines = createNumberedAssignmentLines ( 1 , 12 ) ;
243288 const secondBeforeLines = Array . from (
@@ -339,6 +384,28 @@ async function waitForFrame(
339384 return frame ;
340385}
341386
387+ async function pressHunkNavigationKey (
388+ setup : Awaited < ReturnType < typeof testRender > > ,
389+ key : "]" | "[" ,
390+ count : number ,
391+ ) {
392+ for ( let index = 0 ; index < count ; index += 1 ) {
393+ await act ( async ( ) => {
394+ await setup . mockInput . typeText ( key ) ;
395+ } ) ;
396+ await flush ( setup ) ;
397+ }
398+ }
399+
400+ function firstCrossFileHunkNavigationHeader ( frame : string ) {
401+ return (
402+ frame
403+ . split ( "\n" )
404+ . map ( ( line ) => line . trim ( ) )
405+ . find ( ( line ) => line . startsWith ( "long-file.txt" ) || line . startsWith ( "short-file.ts" ) ) ?? ""
406+ ) ;
407+ }
408+
342409async function waitForSnapshot (
343410 setup : Awaited < ReturnType < typeof testRender > > ,
344411 getSnapshot : ( ) => HunkSessionSnapshot [ "state" ] | null ,
@@ -1837,6 +1904,74 @@ describe("App interactions", () => {
18371904 }
18381905 } ) ;
18391906
1907+ test ( "forward cross-file hunk navigation keeps the destination file owning the review pane" , async ( ) => {
1908+ const setup = await testRender (
1909+ < AppHost bootstrap = { createCrossFileHunkNavigationBootstrap ( ) } /> ,
1910+ {
1911+ width : 120 ,
1912+ height : 16 ,
1913+ } ,
1914+ ) ;
1915+
1916+ try {
1917+ await flush ( setup ) ;
1918+ await pressHunkNavigationKey ( setup , "]" , 18 ) ;
1919+
1920+ let frame = await waitForFrame (
1921+ setup ,
1922+ ( nextFrame ) =>
1923+ nextFrame . includes ( "short-file.ts" ) && nextFrame . includes ( "export const top = 2;" ) ,
1924+ 24 ,
1925+ ) ;
1926+ expect ( firstCrossFileHunkNavigationHeader ( frame ) ) . toContain ( "short-file.ts" ) ;
1927+
1928+ await pressHunkNavigationKey ( setup , "]" , 1 ) ;
1929+ frame = await waitForFrame (
1930+ setup ,
1931+ ( nextFrame ) => nextFrame . includes ( "export const mid = 4;" ) ,
1932+ 24 ,
1933+ ) ;
1934+
1935+ expect ( firstCrossFileHunkNavigationHeader ( frame ) ) . toContain ( "short-file.ts" ) ;
1936+ expect ( frame ) . not . toContain ( "line 341 changed" ) ;
1937+ } finally {
1938+ await act ( async ( ) => {
1939+ setup . renderer . destroy ( ) ;
1940+ } ) ;
1941+ }
1942+ } ) ;
1943+
1944+ test ( "backward cross-file hunk navigation reveals the target hunk instead of the file top" , async ( ) => {
1945+ const setup = await testRender (
1946+ < AppHost bootstrap = { createCrossFileHunkNavigationBootstrap ( ) } /> ,
1947+ {
1948+ width : 120 ,
1949+ height : 16 ,
1950+ } ,
1951+ ) ;
1952+
1953+ try {
1954+ await flush ( setup ) ;
1955+ await pressHunkNavigationKey ( setup , "]" , 19 ) ;
1956+ await waitForFrame ( setup , ( nextFrame ) => nextFrame . includes ( "export const mid = 4;" ) , 24 ) ;
1957+
1958+ await pressHunkNavigationKey ( setup , "[" , 2 ) ;
1959+ const frame = await waitForFrame (
1960+ setup ,
1961+ ( nextFrame ) =>
1962+ nextFrame . includes ( "line 341 changed" ) || nextFrame . includes ( "line 002 changed" ) ,
1963+ 24 ,
1964+ ) ;
1965+
1966+ expect ( frame ) . toContain ( "line 341 changed" ) ;
1967+ expect ( frame ) . not . toContain ( "line 002 changed" ) ;
1968+ } finally {
1969+ await act ( async ( ) => {
1970+ setup . renderer . destroy ( ) ;
1971+ } ) ;
1972+ }
1973+ } ) ;
1974+
18401975 test ( "mouse wheel scrolling updates the active file and hunk to the viewport center" , async ( ) => {
18411976 const { getLatestSnapshot, hostClient } = createMockHostClient ( ) ;
18421977 const setup = await testRender (
0 commit comments