/**
* FileTree lists the workspace files VS Code style: Material icons, chevrons
* on directories, indent guides, and the previewed file highlighted.
* Directories load lazily. Clicking a file opens it in the preview tab;
* double-clicking opens it in the editor.
*
* Icons are the Material Icon Theme SVGs, served from /material-icons
* (copied into public/ by ui/scripts/copy-icons.mjs).
*
* @example
*
*/
import { useCallback, useEffect, useState } from "react";
import {
getIconForDirectoryPath,
getIconForFilePath,
getIconUrlByName,
isMaterialIconName,
} from "vscode-material-icons";
import { type FileEntry, listFiles } from "../api";
import { openEditor, openPreview, useWorkspace } from "../workspace";
const ICONS_URL = "/material-icons";
export function FileTree() {
const [entries, setEntries] = useState(null);
const [error, setError] = useState(null);
const refresh = useCallback(() => {
listFiles()
.then((result) => {
setEntries(result.entries);
setError(null);
})
.catch((cause: Error) => setError(cause.message));
}, []);
useEffect(() => {
refresh();
}, [refresh]);
return (
);
}
function EntryList({
entries,
depth,
}: { entries: FileEntry[]; depth: number }) {
return (
0 ? " filetree-list-nested" : ""}`}>
{entries.map((entry) => (
-
{entry.isDir ? (
) : (
)}
))}
);
}
/** materialIconUrl resolves the Material icon for an entry (open-folder aware). */
function materialIconUrl(entry: FileEntry, expanded: boolean): string {
let name = entry.isDir
? getIconForDirectoryPath(entry.name)
: getIconForFilePath(entry.name);
if (entry.isDir && expanded) {
const openName = `${name}-open`;
if (isMaterialIconName(openName)) {
name = openName;
}
}
return getIconUrlByName(name, ICONS_URL);
}
function DirectoryNode({ entry, depth }: { entry: FileEntry; depth: number }) {
const [children, setChildren] = useState(null);
const [expanded, setExpanded] = useState(false);
const toggle = () => {
const next = !expanded;
setExpanded(next);
if (next && children === null) {
listFiles(entry.path)
.then((result) => setChildren(result.entries))
.catch(() => setChildren([]));
}
};
return (
<>
{expanded && children && (
)}
>
);
}
function FileNode({ entry, depth }: { entry: FileEntry; depth: number }) {
const active = useWorkspace(
(workspace) =>
workspace.previewPath === entry.path ||
workspace.editorPath === entry.path,
);
return (
);
}