fix(sidebar): keep file tree expanded while refreshing root (#2092)
Replace the destructive refreshRoot() with an incremental refresh that re-fetches the root and every expanded directory while preserving childrenByDir. This keeps the file tree expanded instead of collapsing it to the root on every manual refresh. Closes #2036 Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
bashrusakh
parent
743d1c90c0
commit
cd1ffa8b66
@@ -437,6 +437,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
const [loadErrorsByDir, setLoadErrorsByDir] = React.useState<Record<string, string>>({});
|
||||
const loadedDirsRef = React.useRef<Set<string>>(new Set());
|
||||
const inFlightDirsRef = React.useRef<Set<string>>(new Set());
|
||||
const refreshAbortRef = React.useRef<AbortController | null>(null);
|
||||
|
||||
// Hydrate the per-root cache on mount or root change. The cache is
|
||||
// module-scoped so it survives close-and-reopen of the right sidebar;
|
||||
@@ -623,12 +624,62 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
const refreshRoot = React.useCallback(async () => {
|
||||
if (!root) return;
|
||||
|
||||
loadedDirsRef.current = new Set();
|
||||
inFlightDirsRef.current = new Set();
|
||||
setLoadErrorsByDir({});
|
||||
setChildrenByDir((prev) => (Object.keys(prev).length === 0 ? prev : {}));
|
||||
// Cancel any previous refresh so stale results for the old root don't
|
||||
// land after the user switches projects.
|
||||
refreshAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
refreshAbortRef.current = controller;
|
||||
|
||||
await loadDirectory(root);
|
||||
try {
|
||||
// Refresh root and every expanded directory under it, but keep the
|
||||
// cached children visible while re-fetching so the tree stays expanded
|
||||
// and does not flash/collapse. Read expanded paths from the store at
|
||||
// call time so this callback stays stable when directories are toggled.
|
||||
const currentExpanded = useFilesViewTabsStore.getState().byRoot[root]?.expandedPaths ?? [];
|
||||
const normalizedExpanded = currentExpanded
|
||||
.map((p) => normalizePath(p))
|
||||
.filter((normalized): normalized is string =>
|
||||
Boolean(normalized) && normalized !== root && normalized.startsWith(`${root}/`),
|
||||
);
|
||||
const pathsToRefresh = [root, ...normalizedExpanded];
|
||||
|
||||
loadedDirsRef.current = new Set(loadedDirsRef.current);
|
||||
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||
for (const dirPath of pathsToRefresh) {
|
||||
loadedDirsRef.current.delete(dirPath);
|
||||
inFlightDirsRef.current.delete(dirPath);
|
||||
}
|
||||
|
||||
setLoadErrorsByDir((prev) => {
|
||||
if (Object.keys(prev).length === 0) return prev;
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const dirPath of pathsToRefresh) {
|
||||
if (dirPath in next) {
|
||||
delete next[dirPath];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
|
||||
const isCancelled = () => controller.signal.aborted;
|
||||
|
||||
// Load root first, then expanded children with the same 3-at-a-time
|
||||
// concurrency limit used on startup to avoid API stampede.
|
||||
await loadDirectory(root, isCancelled);
|
||||
for (let i = 0; i < normalizedExpanded.length && !controller.signal.aborted; i += 3) {
|
||||
const batch = normalizedExpanded.slice(i, i + 3);
|
||||
await Promise.all(batch.map((dirPath) => loadDirectory(dirPath, isCancelled)));
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
console.error('Failed to refresh sidebar tree:', error);
|
||||
} finally {
|
||||
if (refreshAbortRef.current === controller) {
|
||||
refreshAbortRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [loadDirectory, root]);
|
||||
|
||||
/**
|
||||
@@ -652,6 +703,9 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
React.useEffect(() => {
|
||||
if (!root) return;
|
||||
|
||||
// Cancel any pending refresh so stale directory listings don't land after
|
||||
// the user switches projects or toggles showHidden / showGitignored.
|
||||
refreshAbortRef.current?.abort();
|
||||
loadedDirsRef.current = new Set();
|
||||
inFlightDirsRef.current = new Set();
|
||||
setLoadErrorsByDir({});
|
||||
|
||||
Reference in New Issue
Block a user