* fix(explorer): prevent persisted trees from crashing startup Persisted expansion restored every directory and recursively flattened the resulting tree during render. Limit automatic restoration to five rendered directory levels and flatten iteratively so deep or broad workspaces cannot exhaust the JavaScript stack. * fix(explorer): preserve expansion changes during restore Reconcile completed restoration against the current expansion set atomically so folder interactions made while directory requests are in flight are not overwritten. * fix(explorer): show hidden files before restoration * fix(explorer): apply directory toggles atomically
204 lines
5.6 KiB
TypeScript
204 lines
5.6 KiB
TypeScript
import type { ExplorerDirectory, ExplorerEntry } from "@/stores/session-store";
|
|
import type { SortOption } from "@/stores/panel-store/state";
|
|
import { filterVisibleExplorerEntries } from "./visibility";
|
|
|
|
export const MAX_AUTO_EXPANDED_DIRECTORY_DEPTH = 5;
|
|
|
|
export interface ExplorerTreeRow {
|
|
entry: ExplorerEntry;
|
|
depth: number;
|
|
}
|
|
|
|
interface FlattenExplorerTreeInput {
|
|
directories: ReadonlyMap<string, ExplorerDirectory>;
|
|
expandedPaths: ReadonlySet<string>;
|
|
sortOption: SortOption;
|
|
showHiddenFiles: boolean;
|
|
}
|
|
|
|
interface RestoreExpandedDirectoriesInput {
|
|
rootDirectory: ExplorerDirectory;
|
|
persistedExpandedPaths: ReadonlySet<string>;
|
|
showHiddenFiles: boolean;
|
|
requestDirectoryListing: (path: string) => Promise<ExplorerDirectory | null>;
|
|
}
|
|
|
|
interface ShowHiddenFilesAndRestoreExpandedDirectoriesInput extends Omit<
|
|
RestoreExpandedDirectoriesInput,
|
|
"showHiddenFiles"
|
|
> {
|
|
showHiddenFiles: () => void;
|
|
}
|
|
|
|
interface ReconcileRestoredExpandedPathsInput {
|
|
persistedExpandedPaths: ReadonlySet<string>;
|
|
currentExpandedPaths: ReadonlySet<string>;
|
|
restoredExpandedPaths: string[];
|
|
}
|
|
|
|
interface SetExpandedDirectoryPathInput {
|
|
currentExpandedPaths: readonly string[];
|
|
directoryPath: string;
|
|
expanded: boolean;
|
|
}
|
|
|
|
export function flattenExplorerTree({
|
|
directories,
|
|
expandedPaths,
|
|
sortOption,
|
|
showHiddenFiles,
|
|
}: FlattenExplorerTreeInput): ExplorerTreeRow[] {
|
|
const root = directories.get(".");
|
|
if (!root) {
|
|
return [];
|
|
}
|
|
|
|
const rows: ExplorerTreeRow[] = [];
|
|
const pending = rowsForDirectory(root, 0, sortOption, showHiddenFiles).toReversed();
|
|
|
|
while (pending.length > 0) {
|
|
const row = pending.pop();
|
|
if (!row) {
|
|
break;
|
|
}
|
|
rows.push(row);
|
|
|
|
const entry = row.entry;
|
|
if (entry.kind !== "directory" || !expandedPaths.has(entry.path)) {
|
|
continue;
|
|
}
|
|
const childDirectory = directories.get(entry.path);
|
|
if (!childDirectory) {
|
|
continue;
|
|
}
|
|
const childRows = rowsForDirectory(childDirectory, row.depth + 1, sortOption, showHiddenFiles);
|
|
for (let index = childRows.length - 1; index >= 0; index -= 1) {
|
|
pending.push(childRows[index]);
|
|
}
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
export async function restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles,
|
|
requestDirectoryListing,
|
|
}: RestoreExpandedDirectoriesInput): Promise<string[]> {
|
|
const restoredPaths = ["."];
|
|
const restoredPathSet = new Set(restoredPaths);
|
|
let parentDirectories = [rootDirectory];
|
|
|
|
for (let depth = 1; depth <= MAX_AUTO_EXPANDED_DIRECTORY_DEPTH; depth += 1) {
|
|
const pathsToRequest: string[] = [];
|
|
for (const directory of parentDirectories) {
|
|
const entries = filterVisibleExplorerEntries(directory.entries, showHiddenFiles);
|
|
for (const entry of entries) {
|
|
const isPersistedExpandedDirectory =
|
|
entry.kind === "directory" && persistedExpandedPaths.has(entry.path);
|
|
if (isPersistedExpandedDirectory && !restoredPathSet.has(entry.path)) {
|
|
pathsToRequest.push(entry.path);
|
|
restoredPathSet.add(entry.path);
|
|
}
|
|
}
|
|
}
|
|
if (pathsToRequest.length === 0) {
|
|
break;
|
|
}
|
|
|
|
const requestedDirectories = await Promise.all(
|
|
pathsToRequest.map((path) => requestDirectoryListing(path)),
|
|
);
|
|
parentDirectories = [];
|
|
for (const directory of requestedDirectories) {
|
|
if (!directory) {
|
|
continue;
|
|
}
|
|
restoredPaths.push(directory.path);
|
|
parentDirectories.push(directory);
|
|
}
|
|
}
|
|
|
|
return restoredPaths;
|
|
}
|
|
|
|
export function showHiddenFilesAndRestoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles,
|
|
requestDirectoryListing,
|
|
}: ShowHiddenFilesAndRestoreExpandedDirectoriesInput): Promise<string[]> {
|
|
showHiddenFiles();
|
|
return restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles: true,
|
|
requestDirectoryListing,
|
|
});
|
|
}
|
|
|
|
export function reconcileRestoredExpandedPaths({
|
|
persistedExpandedPaths,
|
|
currentExpandedPaths,
|
|
restoredExpandedPaths,
|
|
}: ReconcileRestoredExpandedPathsInput): string[] {
|
|
const reconciledPaths = new Set(restoredExpandedPaths);
|
|
|
|
for (const path of persistedExpandedPaths) {
|
|
if (!currentExpandedPaths.has(path)) {
|
|
reconciledPaths.delete(path);
|
|
}
|
|
}
|
|
for (const path of currentExpandedPaths) {
|
|
if (!persistedExpandedPaths.has(path)) {
|
|
reconciledPaths.add(path);
|
|
}
|
|
}
|
|
|
|
return Array.from(reconciledPaths);
|
|
}
|
|
|
|
export function setExpandedDirectoryPath({
|
|
currentExpandedPaths,
|
|
directoryPath,
|
|
expanded,
|
|
}: SetExpandedDirectoryPathInput): string[] {
|
|
const nextPaths = new Set(currentExpandedPaths);
|
|
if (expanded) {
|
|
nextPaths.add(directoryPath);
|
|
} else {
|
|
nextPaths.delete(directoryPath);
|
|
}
|
|
return Array.from(nextPaths);
|
|
}
|
|
|
|
function rowsForDirectory(
|
|
directory: ExplorerDirectory,
|
|
depth: number,
|
|
sortOption: SortOption,
|
|
showHiddenFiles: boolean,
|
|
): ExplorerTreeRow[] {
|
|
const visibleEntries = filterVisibleExplorerEntries(directory.entries, showHiddenFiles);
|
|
const sortedEntries = sortExplorerEntries(visibleEntries, sortOption);
|
|
return sortedEntries.map((entry) => ({ entry, depth }));
|
|
}
|
|
|
|
function sortExplorerEntries(entries: ExplorerEntry[], sortOption: SortOption): ExplorerEntry[] {
|
|
const sorted = [...entries];
|
|
sorted.sort((a, b) => {
|
|
if (a.kind !== b.kind) {
|
|
return a.kind === "directory" ? -1 : 1;
|
|
}
|
|
switch (sortOption) {
|
|
case "name":
|
|
return a.name.localeCompare(b.name);
|
|
case "modified":
|
|
return new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
|
case "size":
|
|
return b.size - a.size;
|
|
}
|
|
});
|
|
return sorted;
|
|
}
|