/** * EditorPane edits a workspace file with Monaco: dirty indicator, Save * button and Ctrl/Cmd+S, saving through the backend file API. * * @example * */ import { lazy, Suspense, useCallback, useEffect, useState } from "react"; import { readFile, writeFile } from "../api"; import { languageForPath } from "../lang"; const MonacoViewer = lazy(() => import("./MonacoViewer")); type SaveStatus = "idle" | "saving" | "saved"; export function EditorPane({ path }: { path: string | null }) { const [original, setOriginal] = useState(null); const [text, setText] = useState(""); const [error, setError] = useState(null); const [status, setStatus] = useState("idle"); useEffect(() => { if (!path) { return; } setOriginal(null); setError(null); setStatus("idle"); readFile(path) .then((file) => { setOriginal(file.content); setText(file.content); }) .catch((cause: Error) => setError(cause.message)); }, [path]); const dirty = original !== null && text !== original; const save = useCallback(async () => { if (!path || original === null || text === original) { return; } setStatus("saving"); setError(null); try { await writeFile(path, text); setOriginal(text); setStatus("saved"); } catch (cause) { setError((cause as Error).message); setStatus("idle"); } }, [path, text, original]); const placeholder = editorPlaceholder(path, original, error); if (placeholder) { return placeholder; } return ( // The shortcut is handled on the wrapper so it works wherever the // focus sits inside the pane, Monaco included.
{ if ((event.ctrlKey || event.metaKey) && event.key === "s") { event.preventDefault(); void save(); } }} >
loading editor…

}> setText(value ?? "")} />
); } /** editorPlaceholder covers the no-file / error / loading states. */ function editorPlaceholder( path: string | null, original: string | null, error: string | null, ) { if (!path) { return (

Double-click a file in the tree to edit it.

); } if (error && original === null) { return

{error}

; } if (original === null) { return

loading…

; } return null; } /** EditorHeader shows the file name, dirty marker, save state and button. */ function EditorHeader({ path, dirty, status, error, onSave, }: { path: string; dirty: boolean; status: SaveStatus; error: string | null; onSave: () => Promise; }) { return (
{path.split("/").pop()} {dirty ? " ●" : ""} {error && {error}} {status === "saved" && !dirty && ( saved )}
); }