@@ -14,12 +14,17 @@ import { DialogSelect } from "@tui/ui/dialog-select"
1414import {
1515 allExpandedFileTreeDirectories ,
1616 buildFileTree ,
17+ fileTreeFileSelection ,
1718 flattenFileTree ,
1819 moveFileTreeSelection ,
1920 moveFileTreeSelectionToFirstChild ,
2021 moveFileTreeSelectionToFile ,
2122 moveFileTreeSelectionToParent ,
23+ movePatchFileIndex ,
24+ orderedPatchFileIndexes ,
25+ relativePatchFileIndexFromViewport ,
2226 setFileTreeDirectoryExpanded ,
27+ singlePatchFileIndex ,
2328 toggleFileTreeDirectory ,
2429} from "./diff-viewer-file-tree-utils"
2530
@@ -108,6 +113,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
108113 const [ selectedFileIndex , setSelectedFileIndex ] = createSignal < number | undefined > ( )
109114 const [ reviewedFileNames , setReviewedFileNames ] = createSignal < ReadonlySet < string > > ( new Set ( ) )
110115 const fileRows = createMemo ( ( ) => flattenFileTree ( fileTree ( ) , expandedFileNodes ( ) ) )
116+ const patchFileIndexes = createMemo ( ( ) => orderedPatchFileIndexes ( flattenFileTree ( fileTree ( ) ) ) )
111117 const focusRunner = ( input : Record < DiffViewerFocus , ( ) => void > ) => ( ) => input [ focus ( ) ] ( )
112118 const switchFocusShortcut = useCommandShortcut ( "diff.switch_focus" )
113119 const nextFileShortcut = useCommandShortcut ( "diff.next_file" )
@@ -154,94 +160,158 @@ function DiffViewer(props: { api: TuiPluginApi }) {
154160 setActivePatchFileIndex ( undefined )
155161 }
156162
157- const scrollPatchNodeToTop = ( patchNode : BoxRenderable , fileIndex : number ) => {
158- if ( ! scroll ) return
159- const offset = fileIndex === 0 ? 0 : 1
160- scroll . scrollBy ( patchNode . y - scroll . viewport . y + offset )
163+ const scrollPatchNodeToTop = ( patchNode : BoxRenderable ) => {
161164 requestAnimationFrame ( ( ) => {
162- if ( scroll ) scroll . scrollBy ( patchNode . y - scroll . viewport . y + offset )
165+ if ( ! scroll ) return
166+ const scrollDelta = patchNode . y - scroll . viewport . y
167+ const contentY = scroll . scrollTop + scrollDelta
168+ const offset = contentY === 0 ? 0 : 1
169+ scroll . scrollBy ( scrollDelta + offset )
163170 } )
164171 }
165172
166173 const revealFileTreeFile = ( fileIndex : number ) => {
167- const node = fileTree ( ) . nodes . find ( ( item ) => item . kind === "file" && item . fileIndex === fileIndex )
168- if ( ! node ) return
174+ const selection = fileTreeFileSelection ( fileTree ( ) , fileIndex )
175+ if ( ! selection ) return
169176 setExpandedFileNodes ( ( expanded ) => {
170177 const next = new Set ( expanded )
171- for ( let parent = node . parent ; parent !== undefined ; parent = fileTree ( ) . nodes [ parent ] ?. parent ) {
172- next . add ( parent )
173- }
178+ selection . expandedNodes . forEach ( ( node ) => next . add ( node ) )
174179 return next
175180 } )
176- setHighlighted ( node . id )
181+ setHighlighted ( selection . highlightedNode )
177182 }
178183
179- const scrollToFileIndex = ( fileIndex : number | undefined ) => {
180- if ( fileIndex === undefined ) return
184+ const selectPatchFile = ( fileIndex : number ) => {
185+ revealFileTreeFile ( fileIndex )
181186 setActivePatchFileIndex ( fileIndex )
182187 setSelectedFileIndex ( fileIndex )
188+ }
189+
190+ const scrollToFileIndex = ( fileIndex : number | undefined ) => {
191+ if ( fileIndex === undefined ) return
192+ selectPatchFile ( fileIndex )
183193 const patchNode = patchNodeByFileIndex . get ( fileIndex )
184- if ( patchNode ) scrollPatchNodeToTop ( patchNode , fileIndex )
194+ if ( patchNode ) scrollPatchNodeToTop ( patchNode )
185195 }
186196
187197 const jumpToFileIndex = ( fileIndex : number | undefined ) => {
188198 if ( fileIndex === undefined ) return
189- revealFileTreeFile ( fileIndex )
190199 scrollToFileIndex ( fileIndex )
191200 }
192201
193202 const currentPatchFileIndex = ( ) => {
194203 if ( ! scroll ) return undefined
195- const entries = files ( )
196- . map ( ( _ , fileIndex ) => ( { fileIndex, node : patchNodeByFileIndex . get ( fileIndex ) } ) )
204+ const viewportContentY = scroll . scrollTop + 1
205+ const entries = patchFileIndexes ( )
206+ . map ( ( fileIndex ) => ( {
207+ fileIndex,
208+ node : patchNodeByFileIndex . get ( fileIndex ) ,
209+ } ) )
197210 . filter ( ( entry ) : entry is { fileIndex : number ; node : BoxRenderable } => Boolean ( entry . node ) )
198- . sort ( ( left , right ) => left . node . y - right . node . y )
199- return entries . findLast ( ( entry ) => entry . node . y <= scroll ! . viewport . y + 1 ) ?. fileIndex ?? entries [ 0 ] ?. fileIndex
211+ . map ( ( entry ) => ( {
212+ ...entry ,
213+ contentY : scroll ! . scrollTop + entry . node . y - scroll ! . viewport . y ,
214+ } ) )
215+ . sort ( ( left , right ) => left . contentY - right . contentY )
216+ return entries . findLast ( ( entry ) => entry . contentY <= viewportContentY ) ?. fileIndex ?? entries [ 0 ] ?. fileIndex
217+ }
218+
219+ const nextPatchFileIndexFromViewport = ( offset : number ) => {
220+ if ( ! scroll ) return undefined
221+ return relativePatchFileIndexFromViewport (
222+ patchFileIndexes ( )
223+ . map ( ( fileIndex ) => ( { fileIndex, node : patchNodeByFileIndex . get ( fileIndex ) } ) )
224+ . filter ( ( entry ) : entry is { fileIndex : number ; node : BoxRenderable } => Boolean ( entry . node ) )
225+ . map ( ( entry ) => {
226+ const contentY = scroll ! . scrollTop + entry . node . y - scroll ! . viewport . y
227+ return {
228+ fileIndex : entry . fileIndex ,
229+ titleContentY : contentY + ( contentY === 0 ? 0 : 1 ) ,
230+ }
231+ } ) ,
232+ scroll . scrollTop ,
233+ offset ,
234+ )
200235 }
201236
202237 const jumpRelativePatchFile = ( offset : number ) => {
238+ if ( singlePatch ( ) ) {
239+ const next = movePatchFileIndex (
240+ patchFileIndexes ( ) ,
241+ visiblePatchFiles ( ) [ 0 ] ?. fileIndex ?? selectedFileIndex ( ) ?? activePatchFileIndex ( ) ?? firstPatchFileIndex ( ) ,
242+ offset ,
243+ )
244+ if ( next === undefined ) return
245+ selectPatchFile ( next )
246+ scrollSinglePatchToTop ( )
247+ return
248+ }
249+
203250 const current = focus ( ) === "files" ? highlightedFileNode ( ) : undefined
204251 const nextFromSelection =
205252 current === undefined ? undefined : moveFileTreeSelectionToFile ( fileRows ( ) , current , offset )
206253 if ( nextFromSelection !== undefined ) {
207254 jumpToFileIndex ( fileRows ( ) . find ( ( row ) => row . id === nextFromSelection ) ?. fileIndex )
208255 return
209256 }
210- const currentFileIndex = activePatchFileIndex ( ) ?? currentPatchFileIndex ( )
211- const currentRow = fileRows ( ) . find ( ( row ) => row . fileIndex === currentFileIndex )
212257 scrollToFileIndex (
213- fileRows ( ) . find ( ( row ) => row . id === moveFileTreeSelectionToFile ( fileRows ( ) , currentRow ?. id , offset ) ) ?. fileIndex ,
258+ nextPatchFileIndexFromViewport ( offset ) ??
259+ movePatchFileIndex ( patchFileIndexes ( ) , currentPatchFileIndex ( ) ?? activePatchFileIndex ( ) , offset ) ,
214260 )
215261 }
216262
217263 const highlightedPatchFileIndex = ( ) => fileRows ( ) . find ( ( row ) => row . id === highlightedFileNode ( ) ) ?. fileIndex
218264 const firstPatchFileIndex = ( ) => fileRows ( ) . find ( ( row ) => row . fileIndex !== undefined ) ?. fileIndex
219265 const visiblePatchFiles = createMemo ( ( ) => {
220- if ( ! singlePatch ( ) ) return files ( ) . map ( ( file , fileIndex ) => ( { file, fileIndex } ) )
221- const fileIndex = activePatchFileIndex ( ) ?? currentPatchFileIndex ( ) ?? firstPatchFileIndex ( )
266+ if ( ! singlePatch ( ) ) {
267+ return patchFileIndexes ( ) . flatMap ( ( fileIndex ) => {
268+ const file = files ( ) [ fileIndex ]
269+ return file ? [ { file, fileIndex } ] : [ ]
270+ } )
271+ }
272+ const fileIndex = singlePatchFileIndex (
273+ selectedFileIndex ( ) ,
274+ activePatchFileIndex ( ) ,
275+ currentPatchFileIndex ( ) ,
276+ firstPatchFileIndex ( ) ,
277+ )
222278 const file = fileIndex === undefined ? undefined : files ( ) [ fileIndex ]
223279 return file && fileIndex !== undefined ? [ { file, fileIndex } ] : [ ]
224280 } )
225281
226282 const ensureHighlightedPatchFile = ( ) => {
227- if ( activePatchFileIndex ( ) !== undefined ) return
228- const fileIndex = currentPatchFileIndex ( ) ?? firstPatchFileIndex ( )
229- if ( fileIndex !== undefined ) setActivePatchFileIndex ( fileIndex )
283+ const fileIndex = currentPatchFileIndex ( ) ?? activePatchFileIndex ( ) ?? firstPatchFileIndex ( )
284+ if ( fileIndex === undefined ) return
285+ selectPatchFile ( fileIndex )
230286 }
231287
232- const scrollToHighlightedPatchFile = ( ) => {
233- const fileIndex = activePatchFileIndex ( )
234- if ( fileIndex === undefined ) return
288+ const scrollToPatchFileIndexAfterRender = ( fileIndex : number ) => {
235289 setPendingPatchScrollFileIndex ( fileIndex )
290+ requestAnimationFrame ( ( ) => {
291+ const patchNode = patchNodeByFileIndex . get ( fileIndex )
292+ if ( patchNode ) scrollPatchNodeToTop ( patchNode )
293+ requestAnimationFrame ( ( ) => {
294+ const patchNode = patchNodeByFileIndex . get ( fileIndex )
295+ if ( patchNode ) scrollPatchNodeToTop ( patchNode )
296+ setPendingPatchScrollFileIndex ( undefined )
297+ } )
298+ } )
299+ }
300+
301+ const scrollSinglePatchToTop = ( ) => {
302+ requestAnimationFrame ( ( ) => {
303+ scroll ?. scrollTo ( 0 )
304+ requestAnimationFrame ( ( ) => scroll ?. scrollTo ( 0 ) )
305+ } )
236306 }
237307
238308 const registerPatchNode = ( fileIndex : number , element : BoxRenderable ) => {
239309 patchNodeByFileIndex . set ( fileIndex , element )
240310 if ( pendingPatchScrollFileIndex ( ) !== fileIndex ) return
241311 requestAnimationFrame ( ( ) => {
242- scrollPatchNodeToTop ( element , fileIndex )
312+ scrollPatchNodeToTop ( element )
243313 requestAnimationFrame ( ( ) => {
244- scrollPatchNodeToTop ( element , fileIndex )
314+ scrollPatchNodeToTop ( element )
245315 setPendingPatchScrollFileIndex ( undefined )
246316 } )
247317 } )
@@ -437,12 +507,23 @@ function DiffViewer(props: { api: TuiPluginApi }) {
437507 title : "Toggle single patch view" ,
438508 category : "VCS" ,
439509 run ( ) {
440- setSinglePatch ( ( value ) => {
441- const next = ! value
442- if ( next ) ensureHighlightedPatchFile ( )
443- else scrollToHighlightedPatchFile ( )
444- return next
445- } )
510+ if ( ! singlePatch ( ) ) {
511+ ensureHighlightedPatchFile ( )
512+ setSinglePatch ( true )
513+ scrollSinglePatchToTop ( )
514+ return
515+ }
516+ const fileIndex =
517+ visiblePatchFiles ( ) [ 0 ] ?. fileIndex ??
518+ singlePatchFileIndex (
519+ selectedFileIndex ( ) ,
520+ activePatchFileIndex ( ) ,
521+ currentPatchFileIndex ( ) ,
522+ firstPatchFileIndex ( ) ,
523+ )
524+ if ( fileIndex !== undefined ) selectPatchFile ( fileIndex )
525+ setSinglePatch ( false )
526+ if ( fileIndex !== undefined ) scrollToPatchFileIndexAfterRender ( fileIndex )
446527 } ,
447528 } ,
448529 {
@@ -581,7 +662,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
581662 flexDirection = "row"
582663 gap = { 1 }
583664 flexShrink = { 0 }
584- paddingLeft = { 2 }
665+ paddingLeft = { 1 }
585666 paddingRight = { 1 }
586667 border = { [ "left" ] }
587668 borderColor = { theme ( ) . border }
0 commit comments