From 34eae35becb705bc269fc9a831f9c64deefebc4f Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Thu, 27 Nov 2025 13:23:33 +0200 Subject: [PATCH] Use VirtualList for commits and file list Replace manual iteration with VirtualList in BranchCommitList and FileList to improve performance with large lists. Create ConfigurableVirtualList wrapper that applies user's scrollbar visibility preference. Implement tree flattening for virtual scrolling in tree mode with expand/collapse support. - Add ConfigurableVirtualList component wrapping VirtualList with user settings - Add flattenTree() to convert tree structure to flat list for virtual rendering - Add max-height constraint to commit list container - Remove transition from CommitRow to prevent scroll performance issues --- .../src/components/BranchCommitList.svelte | 390 +++++++++--------- apps/desktop/src/components/CommitRow.svelte | 3 +- .../components/ConfigurableVirtualList.svelte | 63 +++ apps/desktop/src/components/FileList.svelte | 115 ++++-- apps/desktop/src/lib/files/filetreeV3.ts | 77 ++++ packages/ui/src/lib/index.ts | 1 + 6 files changed, 431 insertions(+), 218 deletions(-) create mode 100644 apps/desktop/src/components/ConfigurableVirtualList.svelte diff --git a/apps/desktop/src/components/BranchCommitList.svelte b/apps/desktop/src/components/BranchCommitList.svelte index 12df1917f9..fd2b11c285 100644 --- a/apps/desktop/src/components/BranchCommitList.svelte +++ b/apps/desktop/src/components/BranchCommitList.svelte @@ -4,6 +4,7 @@ import CommitContextMenu from '$components/CommitContextMenu.svelte'; import CommitGoesHere from '$components/CommitGoesHere.svelte'; import CommitRow from '$components/CommitRow.svelte'; + import ConfigurableVirtualList from '$components/ConfigurableVirtualList.svelte'; import Dropzone from '$components/Dropzone.svelte'; import LineOverlay from '$components/LineOverlay.svelte'; @@ -230,30 +231,36 @@ use:focusable={{ vertical: true }} > {#if hasRemoteCommits} - {#each upstreamOnlyCommits as commit, i (commit.id)} - {@const first = i === 0} - {@const lastCommit = i === upstreamOnlyCommits.length - 1} - {@const selected = commit.id === selectedCommitId && branchName === selectedBranchName} - {@const commitId = commit.id} - {#if !isCommitting} - handleCommitClick(commit.id, true)} - disableCommitActions={false} - editable={!!stackId} - /> - {/if} - {/each} + {#if !isCommitting} + + {#snippet chunkTemplate(chunk: typeof upstreamOnlyCommits)} + {#each chunk as commit} + {@const absoluteIndex = upstreamOnlyCommits.indexOf(commit)} + {@const first = absoluteIndex === 0} + {@const lastCommit = absoluteIndex === upstreamOnlyCommits.length - 1} + {@const selected = + commit.id === selectedCommitId && branchName === selectedBranchName} + {@const commitId = commit.id} + handleCommitClick(commit.id, true)} + disableCommitActions={false} + editable={!!stackId} + /> + {/each} + {/snippet} + + {/if} {#snippet action()} @@ -264,173 +271,181 @@ {/if} - {#each localAndRemoteCommits as commit, i (commit.id)} - {@const first = i === 0} - {@const last = i === localAndRemoteCommits.length - 1} - {@const commitId = commit.id} - {@const selected = commit.id === selectedCommitId && branchName === selectedBranchName} - {#if isCommitting} - - { - projectState.exclusiveAction.set({ - type: 'commit', - stackId, - branchName, - parentCommitId: commitId - }); - }} - /> - {/if} - {@const dzCommit: DzCommitData = { - id: commit.id, - isRemote: isUpstreamCommit(commit), - isIntegrated: isLocalAndRemoteCommit(commit) && commit.state.type === 'Integrated', - hasConflicts: isLocalAndRemoteCommit(commit) && commit.hasConflicts, - }} - {@const amendHandler = stackId - ? new AmendCommitWithChangeDzHandler( - projectId, - stackService, - hooksService, - stackId, - $runHooks, - dzCommit, - (newId) => { - const previewOpen = selection.current?.previewOpen ?? false; - uiState.lane(stackId).selection.set({ branchName, commitId: newId, previewOpen }); - }, - uiState - ) - : undefined} - {@const squashHandler = stackId - ? new SquashCommitDzHandler({ - stackService, - projectId, - stackId, - commit: dzCommit - }) - : undefined} - {@const hunkHandler = stackId - ? new AmendCommitWithHunkDzHandler({ - stackService, - hooksService, - projectId, - stackId, - commit: dzCommit, - runHooks: $runHooks, - // TODO: Use correct value! - okWithForce: true, - uiState - }) - : undefined} - {@const tooltip = commitStatusLabel(commit.state.type)} - - {#snippet overlay({ hovered, activated, handler })} - {@const label = - handler instanceof AmendCommitWithChangeDzHandler || - handler instanceof AmendCommitWithHunkDzHandler - ? 'Amend' - : 'Squash'} - - {/snippet} -
+ {#snippet chunkTemplate(chunk: typeof localAndRemoteCommits)} + {#each chunk as commit} + {@const absoluteIndex = localAndRemoteCommits.indexOf(commit)} + {@const first = absoluteIndex === 0} + {@const last = absoluteIndex === localAndRemoteCommits.length - 1} + {@const commitId = commit.id} + {@const selected = + commit.id === selectedCommitId && branchName === selectedBranchName} + {#if isCommitting} + + { + projectState.exclusiveAction.set({ + type: 'commit', stackId, - { - id: commitId, - isRemote: !!branchDetails.remoteTrackingBranch, - hasConflicts: isLocalAndRemoteCommit(commit) && commit.hasConflicts, - isIntegrated: - isLocalAndRemoteCommit(commit) && commit.state.type === 'Integrated' - }, - false, - branchName - ) - : undefined, - viewportId: 'board-viewport', - dropzoneRegistry, - dragStateService + branchName, + parentCommitId: commitId + }); + }} + /> + {/if} + {@const dzCommit: DzCommitData = { + id: commit.id, + isRemote: isUpstreamCommit(commit), + isIntegrated: isLocalAndRemoteCommit(commit) && commit.state.type === 'Integrated', + hasConflicts: isLocalAndRemoteCommit(commit) && commit.hasConflicts, }} - > - handleCommitClick(commit.id, false)} - disableCommitActions={false} - editable={!!stackId} - > - {#snippet menu({ rightClickTrigger })} - {@const data = { + {@const amendHandler = stackId + ? new AmendCommitWithChangeDzHandler( + projectId, + stackService, + hooksService, stackId, - commitId, - commitMessage: commit.message, - commitStatus: commit.state.type, - commitUrl: forge.current.commitUrl(commitId), - onUncommitClick: () => handleUncommit(commit.id, branchName), - onEditMessageClick: () => startEditingCommitMessage(branchName, commit.id) - }} - + $runHooks, + dzCommit, + (newId) => { + const previewOpen = selection.current?.previewOpen ?? false; + uiState + .lane(stackId) + .selection.set({ branchName, commitId: newId, previewOpen }); + }, + uiState + ) + : undefined} + {@const squashHandler = stackId + ? new SquashCommitDzHandler({ + stackService, + projectId, + stackId, + commit: dzCommit + }) + : undefined} + {@const hunkHandler = stackId + ? new AmendCommitWithHunkDzHandler({ + stackService, + hooksService, + projectId, + stackId, + commit: dzCommit, + runHooks: $runHooks, + // TODO: Use correct value! + okWithForce: true, + uiState + }) + : undefined} + {@const tooltip = commitStatusLabel(commit.state.type)} + + {#snippet overlay({ hovered, activated, handler })} + {@const label = + handler instanceof AmendCommitWithChangeDzHandler || + handler instanceof AmendCommitWithHunkDzHandler + ? 'Amend' + : 'Squash'} + {/snippet} - -
-
- {@render commitReorderDz( - stackingReorderDropzoneManager.belowCommit(branchName, commit.id) - )} - {#if isCommitting && last} - { - projectState.exclusiveAction.set({ - type: 'commit', - stackId, - branchName, - parentCommitId: branchDetails.baseCommit - }); - }} - /> - {/if} - {/each} +
+ handleCommitClick(commit.id, false)} + disableCommitActions={false} + editable={!!stackId} + > + {#snippet menu({ rightClickTrigger })} + {@const data = { + stackId, + commitId, + commitMessage: commit.message, + commitStatus: commit.state.type, + commitUrl: forge.current.commitUrl(commitId), + onUncommitClick: () => handleUncommit(commit.id, branchName), + onEditMessageClick: () => startEditingCommitMessage(branchName, commit.id) + }} + + {/snippet} + +
+ + {@render commitReorderDz( + stackingReorderDropzoneManager.belowCommit(branchName, commit.id) + )} + {#if isCommitting && last} + { + projectState.exclusiveAction.set({ + type: 'commit', + stackId, + branchName, + parentCommitId: branchDetails.baseCommit + }); + }} + /> + {/if} + {/each} + {/snippet} + {/if} {/snippet} @@ -441,6 +456,7 @@ display: flex; position: relative; flex-direction: column; + max-height: 370px; overflow: hidden; border: 1px solid var(--clr-border-2); border-radius: 0 0 var(--radius-ml) var(--radius-ml); diff --git a/apps/desktop/src/components/CommitRow.svelte b/apps/desktop/src/components/CommitRow.svelte index aafdbdd377..5ec2a29abd 100644 --- a/apps/desktop/src/components/CommitRow.svelte +++ b/apps/desktop/src/components/CommitRow.svelte @@ -201,7 +201,8 @@ overflow: hidden; outline: none; background-color: var(--clr-bg-1); - transition: background-color var(--transition-fast); + /* Incompatible with virtual scroll, scroll thumb becomes laggy. */ + /* transition: background-color var(--transition-fast); */ &:hover, &.menu-shown { diff --git a/apps/desktop/src/components/ConfigurableVirtualList.svelte b/apps/desktop/src/components/ConfigurableVirtualList.svelte new file mode 100644 index 0000000000..bbe19730de --- /dev/null +++ b/apps/desktop/src/components/ConfigurableVirtualList.svelte @@ -0,0 +1,63 @@ + + + + + diff --git a/apps/desktop/src/components/FileList.svelte b/apps/desktop/src/components/FileList.svelte index e097ae4f5d..2d849707d8 100644 --- a/apps/desktop/src/components/FileList.svelte +++ b/apps/desktop/src/components/FileList.svelte @@ -1,9 +1,10 @@