import type { EnvironmentId } from "@t3tools/contracts"; import { Editor } from "@pierre/diffs/editor"; import { EditorProvider, File, Virtualizer } from "@pierre/diffs/react"; import { ChevronRight, FolderTree, LoaderCircle } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { ensureEnvironmentApi } from "~/environmentApi"; import { useTheme } from "~/hooks/useTheme"; import { resolveDiffThemeName } from "~/lib/diffRendering"; import { cn } from "~/lib/utils"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Toggle } from "~/components/ui/toggle"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import FileBrowserPanel from "./FileBrowserPanel"; import { projectFileCacheKey } from "./fileContentRevision"; import { fileBreadcrumbs } from "./filePath"; import { FileSaveCoordinator } from "./fileSaveCoordinator"; import { confirmProjectFileQueryData, setProjectFileQueryData, useProjectFileQuery, } from "./projectFilesQueryState"; interface FilePreviewPanelProps { environmentId: EnvironmentId; cwd: string; projectName: string; relativePath: string | null; onOpenFile: (relativePath: string) => void; onPendingChange: (relativePath: string, pending: boolean) => void; } const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; const FILE_SAVE_DEBOUNCE_MS = 500; interface EditableFileSurfaceProps { environmentId: EnvironmentId; cwd: string; relativePath: string; contents: string; resolvedTheme: "light" | "dark"; onPendingChange: (relativePath: string, pending: boolean) => void; } function EditableFileSurface({ environmentId, cwd, relativePath, contents, resolvedTheme, onPendingChange, }: EditableFileSurfaceProps) { const saveCoordinator = useMemo( () => new FileSaveCoordinator({ debounceMs: FILE_SAVE_DEBOUNCE_MS, onPendingChange: (pending) => onPendingChange(relativePath, pending), persist: async (nextContents) => { await ensureEnvironmentApi(environmentId).projects.writeFile({ cwd, relativePath, contents: nextContents, }); }, onConfirmed: (confirmedContents) => { confirmProjectFileQueryData(environmentId, cwd, relativePath, confirmedContents); }, }), [cwd, environmentId, onPendingChange, relativePath], ); const editor = useMemo( () => new Editor({ onChange: (file) => { setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); saveCoordinator.change(file.contents); }, }), [cwd, environmentId, relativePath, saveCoordinator], ); useEffect( () => () => { editor.cleanUp(); saveCoordinator.dispose(); }, [editor, saveCoordinator], ); return ( ); } function initialExplorerOpen(): boolean { try { return window.localStorage.getItem(FILE_EXPLORER_STORAGE_KEY) !== "false"; } catch { return true; } } export default function FilePreviewPanel({ environmentId, cwd, projectName, relativePath, onOpenFile, onPendingChange, }: FilePreviewPanelProps) { const { resolvedTheme } = useTheme(); const file = useProjectFileQuery(environmentId, cwd, relativePath); const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); const breadcrumbRef = useRef(null); const breadcrumbs = useMemo( () => (relativePath ? fileBreadcrumbs(projectName, relativePath) : []), [projectName, relativePath], ); useEffect(() => { const currentCrumb = breadcrumbRef.current?.querySelector( "[data-current-file-crumb='true']", ); currentCrumb?.scrollIntoView({ block: "nearest", inline: "end" }); }, [relativePath]); const toggleExplorer = () => { setExplorerOpen((current) => { const next = !current; try { window.localStorage.setItem(FILE_EXPLORER_STORAGE_KEY, String(next)); } catch {} return next; }); }; return ( {relativePath ? ( {breadcrumbs.map((crumb, index) => ( {index > 0 ? ( ) : null} {crumb.label} ))} } /> {explorerOpen ? "Hide file explorer" : "Show file explorer"} ) : null} {relativePath && file.data?.truncated ? ( Preview limited to the first 1 MB of a {file.data.byteLength.toLocaleString()} byte file. ) : null} {relativePath && file.error && file.data === null ? ( {file.error} ) : relativePath && file.data === null ? ( ) : relativePath && file.data ? ( file.data.truncated ? ( ) : ( ) ) : null} {explorerOpen || relativePath === null ? ( ) : null} ); }