nandi/oripublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

EditorPane.tsx · 147 lines · 3.4 KBTypeScript Blame HistoryRaw
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday1/**
2 * EditorPane edits a workspace file with Monaco: dirty indicator, Save
3 * button and Ctrl/Cmd+S, saving through the backend file API.
4 *
5 * @example
6 * <EditorPane path="src/main.go" />
7 */
8
9import { lazy, Suspense, useCallback, useEffect, useState } from "react";
10import { readFile, writeFile } from "../api";
11import { languageForPath } from "../lang";
12
13const MonacoViewer = lazy(() => import("./MonacoViewer"));
14
15type SaveStatus = "idle" | "saving" | "saved";
16
17export function EditorPane({ path }: { path: string | null }) {
18 const [original, setOriginal] = useState<string | null>(null);
19 const [text, setText] = useState("");
20 const [error, setError] = useState<string | null>(null);
21 const [status, setStatus] = useState<SaveStatus>("idle");
22
23 useEffect(() => {
24 if (!path) {
25 return;
26 }
27 setOriginal(null);
28 setError(null);
29 setStatus("idle");
30 readFile(path)
31 .then((file) => {
32 setOriginal(file.content);
33 setText(file.content);
34 })
35 .catch((cause: Error) => setError(cause.message));
36 }, [path]);
37
38 const dirty = original !== null && text !== original;
39
40 const save = useCallback(async () => {
41 if (!path || original === null || text === original) {
42 return;
43 }
44 setStatus("saving");
45 setError(null);
46 try {
47 await writeFile(path, text);
48 setOriginal(text);
49 setStatus("saved");
50 } catch (cause) {
51 setError((cause as Error).message);
52 setStatus("idle");
53 }
54 }, [path, text, original]);
55
56 const placeholder = editorPlaceholder(path, original, error);
57 if (placeholder) {
58 return placeholder;
59 }
60
61 return (
62 // The shortcut is handled on the wrapper so it works wherever the
63 // focus sits inside the pane, Monaco included.
64 <div
65 className="editor"
66 onKeyDown={(event) => {
67 if ((event.ctrlKey || event.metaKey) && event.key === "s") {
68 event.preventDefault();
69 void save();
70 }
71 }}
72 >
73 <EditorHeader
74 path={path as string}
75 dirty={dirty}
76 status={status}
77 error={error}
78 onSave={save}
79 />
80 <div className="editor-body">
81 <Suspense fallback={<p className="pane-empty">loading editor</p>}>
82 <MonacoViewer
83 content={text}
84 language={languageForPath(path as string)}
85 onChange={(value) => setText(value ?? "")}
86 />
87 </Suspense>
88 </div>
89 </div>
90 );
91}
92
93/** editorPlaceholder covers the no-file / error / loading states. */
94function editorPlaceholder(
95 path: string | null,
96 original: string | null,
97 error: string | null,
98) {
99 if (!path) {
100 return (
101 <p className="pane-empty">Double-click a file in the tree to edit it.</p>
102 );
103 }
104 if (error && original === null) {
105 return <p className="pane-error">{error}</p>;
106 }
107 if (original === null) {
108 return <p className="pane-empty">loading</p>;
109 }
110 return null;
111}
112
113/** EditorHeader shows the file name, dirty marker, save state and button. */
114function EditorHeader({
115 path,
116 dirty,
117 status,
118 error,
119 onSave,
120}: {
121 path: string;
122 dirty: boolean;
123 status: SaveStatus;
124 error: string | null;
125 onSave: () => Promise<void>;
126}) {
127 return (
128 <div className="pane-header">
129 <span className="pane-path" title={path}>
130 {path.split("/").pop()}
131 {dirty ? " ●" : ""}
132 </span>
133 {error && <span className="pane-error-inline">{error}</span>}
134 {status === "saved" && !dirty && (
135 <span className="pane-saved">saved</span>
136 )}
137 <button
138 type="button"
139 className="pane-action"
140 disabled={!dirty || status === "saving"}
141 onClick={() => void onSave()}
142 >
143 {status === "saving" ? "Saving…" : "Save"}
144 </button>
145 </div>
146 );
147}