/** * 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 ( ); }