Virtualize the file tree's root entries

The file tree was the one list left un-virtualized, so wide directories
mounted every entry and re-rendered all rows on selection. Render
entries through VirtualList like the other lists.
This commit is contained in:
Rūdolfs Ošiņš
2026-07-03 11:29:40 +02:00
parent 5b121b131a
commit bc1e636fbf
2 changed files with 42 additions and 6 deletions
+10 -2
View File
@@ -11,6 +11,10 @@
currentPath: string;
name: string;
prefix: string;
// Expansion state lives in Tree.svelte so it survives the virtualizer
// unmounting off-screen rows.
isExpanded: (prefix: string) => boolean;
toggleExpanded: (prefix: string) => void;
indent?: number;
}
@@ -19,10 +23,12 @@
fetchBlob,
currentPath,
prefix,
isExpanded,
toggleExpanded,
fetchTree,
indent = 0.5,
}: Props = $props();
let expanded = $derived(currentPath.indexOf(prefix) === 0);
const expanded = $derived(isExpanded(prefix));
const treePromise = $derived(
expanded ? fetchTree(prefix) : Promise.resolve(undefined),
@@ -46,7 +52,7 @@
class="folder"
style:padding-left="{indent}rem"
style:padding-right="0.5rem"
onclick={() => (expanded = !expanded)}>
onclick={() => toggleExpanded(prefix)}>
<div class="global-flex txt-body-m-regular" style:padding="0.25rem 0">
<div class:txt-missing={!expanded}>
<Icon name={expanded ? "folder-open" : "folder"} />
@@ -68,6 +74,8 @@
{fetchBlob}
name={entry.name}
{currentPath}
{isExpanded}
{toggleExpanded}
prefix={`${entry.path}/`}
indent={indent + 1.5} />
{:else if entry.kind === "blob"}
+32 -4
View File
@@ -1,8 +1,11 @@
<script lang="ts">
import type { Tree } from "@bindings/source/Tree";
import { SvelteMap } from "svelte/reactivity";
import FileTreeFile from "@app/components/FileTreeFile.svelte";
import FileTreeFolder from "@app/components/FileTreeFolder.svelte";
import VirtualList from "@app/components/VirtualList.svelte";
interface Props {
tree: Tree;
@@ -12,15 +15,40 @@
}
const { currentPath, tree, fetchTree, fetchBlob }: Props = $props();
// Manual expand/collapse overrides, keyed by folder prefix. Held here
// rather than in FileTreeFolder so the state survives the virtualizer
// unmounting off-screen rows; without an override a folder is expanded
// when it contains the open file.
const expandedOverrides = new SvelteMap<string, boolean>();
$effect(() => {
// Navigating to another file resets every folder to its default, the
// same behaviour the per-folder writable $derived had before the state
// was hoisted.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
currentPath;
expandedOverrides.clear();
});
function isExpanded(prefix: string): boolean {
return expandedOverrides.get(prefix) ?? currentPath.indexOf(prefix) === 0;
}
function toggleExpanded(prefix: string) {
expandedOverrides.set(prefix, !isExpanded(prefix));
}
</script>
<div style:display="flex" style:flex-direction="column" style:gap="0.25rem">
{#each tree.entries as entry (entry.name)}
<VirtualList
items={tree.entries}
getKey={entry => entry.path}
estimatedItemSize={28}>
{#snippet row(entry)}
{#if entry.kind === "tree"}
<FileTreeFolder
name={entry.name}
prefix={`${entry.path}/`}
{currentPath}
{isExpanded}
{toggleExpanded}
{fetchTree}
{fetchBlob} />
{:else}
@@ -29,5 +57,5 @@
fetchBlob={() => fetchBlob(entry.path)}
active={entry.path === currentPath} />
{/if}
{/each}
</div>
{/snippet}
</VirtualList>