Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ interface FileExplorerTreeProps {
activePath?: string;
updateActivePath?: (path: string) => void;
rootFolderPath?: string;
onFileSelect: (path: string, isDir: boolean) => void | Promise<void>;
onFileSelect: (path: string, isDir: boolean) => void | string | Promise<void | string>;
onFileOpen?: (path: string, isDir: boolean) => void | Promise<void>;
onCreateNewFileInDirectory: (
directoryPath: string,
Expand Down Expand Up @@ -958,7 +958,8 @@ function FileExplorerTreeComponent({

const toggleDirectory = useCallback(
async (path: string) => {
await Promise.resolve(onFileSelect(path, true));
const resolvedPath = await Promise.resolve(onFileSelect(path, true));
return resolvedPath ?? path;
},
[onFileSelect],
);
Expand All @@ -980,9 +981,12 @@ function FileExplorerTreeComponent({
fileOpenBenchmark.mark(t.path, "explorer-click");
}
if (t.isDir) {
void toggleDirectory(t.path);
setFocusedPath(t.path);
updateActivePath?.(t.path);
void toggleDirectory(t.path).then((resolvedPath) => {
setFocusedPath(resolvedPath);
updateActivePath?.(resolvedPath);
});
} else {
setFocusedPath(t.path);
void Promise.resolve(onFileSelect(t.path, false));
Expand Down Expand Up @@ -1201,7 +1205,7 @@ function FileExplorerTreeComponent({
if (isDir) {
const expanded = useFileTreeStore.getState().actions.isExpanded(current.path);
if (!expanded) {
void toggleDirectory(current.path);
void toggleDirectory(current.path).then(setFocusedPath);
} else {
const child = visibleRows[curIndex + 1];
if (child && child.depth === visibleRows[curIndex].depth + 1) {
Expand Down Expand Up @@ -1232,7 +1236,7 @@ function FileExplorerTreeComponent({
if (!current) break;
e.preventDefault();
if (isDir) {
void toggleDirectory(current.path);
void toggleDirectory(current.path).then(setFocusedPath);
} else {
void Promise.resolve(onFileOpen?.(current.path, false));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { getBaseName, getDirName, getRelativePath, joinPath } from "@/utils/path

interface UseFileExplorerContextMenuOptions {
rootFolderPath?: string;
onFileSelect: (path: string, isDir: boolean) => void | Promise<void>;
onFileSelect: (path: string, isDir: boolean) => void | string | Promise<void | string>;
onCreateNewFileInDirectory?: (
directoryPath: string,
fileName: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FileEntry } from "@/features/file-system/types/app.types";
import { getCompactFolderChild } from "@/features/file-system/controllers/file-tree-utils";
import type { FileTreeSortOrder } from "@/features/settings/types/settings.types";
import { getBaseName, getRelativePath, joinPath, pathStartsWithRoot } from "@/utils/path-helpers";

Expand Down Expand Up @@ -116,23 +117,6 @@ export function collectFileTreeSearchHits(
return hits;
}

function getCompactFolderChild(item: FileEntry): FileEntry | null {
if (!item.isDir || item.isEditing || item.isRenaming || item.isNewItem || !item.children) {
return null;
}

if (item.children.length !== 1) {
return null;
}

const child = item.children[0];
if (!child.isDir || child.isEditing || child.isRenaming || child.isNewItem) {
return null;
}

return child;
}

function sortFileTreeEntriesForDisplay(
entries: readonly FileEntry[],
sortOrder: FileTreeSortOrder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test";
import type { FileEntry } from "../types/app.types";
import { loadFolderExpansion } from "./file-tree-utils";

const directory = (name: string, path: string, children?: FileEntry[]): FileEntry => ({
name,
path,
isDir: true,
children,
});

describe("compact folder expansion", () => {
test("loads and expands a single-child directory chain in one action", async () => {
const entries = new Map<string, FileEntry[]>([
["/a", [directory("b", "/a/b")]],
["/a/b", [directory("c", "/a/b/c")]],
["/a/b/c", [{ name: "file.ts", path: "/a/b/c/file.ts", isDir: false }]],
]);

const result = await loadFolderExpansion(
[directory("a", "/a")],
"/a",
true,
async (path) => entries.get(path) ?? [],
);

expect(result.expandedPaths).toEqual(["/a", "/a/b", "/a/b/c"]);
expect(result.finalPath).toBe("/a/b/c");
});

test("stops at a branch and keeps non-compact expansion to one level", async () => {
const entries = new Map<string, FileEntry[]>([
["/a", [directory("b", "/a/b")]],
["/a/b", [directory("c", "/a/b/c"), directory("d", "/a/b/d")]],
]);
const readChildren = async (path: string) => entries.get(path) ?? [];

const compact = await loadFolderExpansion([directory("a", "/a")], "/a", true, readChildren);
const regular = await loadFolderExpansion([directory("a", "/a")], "/a", false, readChildren);

expect(compact.expandedPaths).toEqual(["/a", "/a/b"]);
expect(regular.expandedPaths).toEqual(["/a"]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,57 @@ export function updateFileInTree(
return changed ? updatedFiles : files;
}

export function getCompactFolderChild(item: FileEntry): FileEntry | null {
if (!item.isDir || item.isEditing || item.isRenaming || item.isNewItem || !item.children) {
return null;
}

if (item.children.length !== 1) {
return null;
}

const child = item.children[0];
return child.isDir && !child.isEditing && !child.isRenaming && !child.isNewItem ? child : null;
}

export async function loadFolderExpansion(
files: FileEntry[],
startPath: string,
compactFolders: boolean,
readChildren: (path: string) => Promise<FileEntry[]>,
) {
const expandedPaths: string[] = [];
const loadedChildren = new Map<string, FileEntry[]>();
const visitedPaths = new Set<string>();
let nextFiles = files;
let currentPath = startPath;

while (true) {
if (visitedPaths.has(currentPath)) break;
visitedPaths.add(currentPath);
const folder = findFileInTree(nextFiles, currentPath);
if (!folder?.isDir) break;

let children = folder.children;
if (!children || children.length === 0) {
children = await readChildren(currentPath);
loadedChildren.set(currentPath, children);
nextFiles = updateFileInTree(nextFiles, currentPath, (item) => ({ ...item, children }));
}

expandedPaths.push(currentPath);
const child = compactFolders ? getCompactFolderChild({ ...folder, children }) : null;
if (!child) break;
currentPath = child.path;
}

return {
expandedPaths,
finalPath: expandedPaths[expandedPaths.length - 1] ?? startPath,
loadedChildren,
};
}

export function removeFileFromTree(files: FileEntry[], targetPath: string): FileEntry[] {
let changed = false;
const nextFiles: FileEntry[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
import {
addFileToTree,
findFileInTree,
loadFolderExpansion,
removeFileFromTree,
sortFileEntries,
updateFileInTree,
Expand Down Expand Up @@ -1482,8 +1483,7 @@ const createFileSystemStore = (workspaceId: string): StoreApi<ScopedFileSystemSt
isPreview = false,
) => {
if (isDir) {
await get().toggleFolder(path);
return;
return get().toggleFolder(path);
}

const selectedWslInfo = parseWslPath(path);
Expand Down Expand Up @@ -1919,31 +1919,42 @@ const createFileSystemStore = (workspaceId: string): StoreApi<ScopedFileSystemSt
const isCurrentlyExpanded = uiActions.isExpanded(path);

if (!isCurrentlyExpanded) {
// Expand: load children if not present
if (!folder.children || folder.children.length === 0) {
const childEntries = await readProviderDirectoryEntries(
folder.path,
get().rootFolderPath ?? folder.path,
);

const updatedFiles = updateFileInTree(get().files, path, (item) => ({
...item,
children: childEntries,
}));
const expansion = await loadFolderExpansion(
get().files,
path,
useSettingsStore.getState().settings.compactFoldersInFileTree,
(directoryPath) =>
readProviderDirectoryEntries(directoryPath, get().rootFolderPath ?? directoryPath),
);

if (expansion.loadedChildren.size > 0) {
set((state) => {
state.files = updatedFiles;
state.filesVersion++;
let updatedFiles = state.files;
for (const [directoryPath, children] of expansion.loadedChildren) {
updatedFiles = updateFileInTree(updatedFiles, directoryPath, (item) => ({
...item,
children,
}));
}
if (updatedFiles !== state.files) {
state.files = updatedFiles;
state.filesVersion++;
}
});
}
uiActions.toggleFolder(path);

const expandedPaths = new Set(uiActions.getExpandedPaths());
expansion.expandedPaths.forEach((expandedPath) => expandedPaths.add(expandedPath));
uiActions.setExpandedPaths(expandedPaths);
// Preload deeper children in background for snappier navigation
get()
.preloadSubtree(path, 2, 80)
.preloadSubtree(expansion.finalPath, 2, 80)
.catch(() => {});
return expansion.finalPath;
} else {
// Collapse: only toggle UI state; keep children cached
uiActions.toggleFolder(path);
return path;
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export interface FsActions {
column?: number,
codeEditorRef?: React.RefObject<CodeEditorRef | null>,
isPreview?: boolean,
) => Promise<void>;
) => Promise<string | undefined>;
handleFileOpen: (path: string, isDir: boolean) => Promise<void>;
toggleFolder: (path: string) => Promise<void>;
toggleFolder: (path: string) => Promise<string | undefined>;
revealPathInTree: (targetPath: string) => Promise<void>;
handleCreateNewFile: () => Promise<void>;
handleCreateNewFileInDirectory: (
Expand Down
Loading