fix(web): move save logic from monaco to react with manual and auto-save
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Editor, { type OnMount, type OnValidate } from "@monaco-editor/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Editor, { type OnValidate } from "@monaco-editor/react";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { inferLanguage, useFileRead, useFileWrite } from "@/lib/api/files";
|
||||
|
||||
@@ -17,6 +18,7 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
const [savedContent, setSavedContent] = useState("");
|
||||
const dirty = buffer !== savedContent;
|
||||
const saveInProgress = write.isPending;
|
||||
const saveTimeoutRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Sync editor buffer with fetched content when the file changes or loads.
|
||||
useEffect(() => {
|
||||
@@ -42,25 +44,14 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
workspaceIdRef.current = workspaceId;
|
||||
}, [buffer, savedContent, path, workspaceId]);
|
||||
|
||||
// Suppress the browser's Save Page dialog on Cmd/Ctrl+S when a file is open.
|
||||
// Monaco's own keybinding still handles saving.
|
||||
useEffect(() => {
|
||||
if (!path) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if ((e.key === "s" || e.key === "S") && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const clearPendingSave = () => {
|
||||
if (saveTimeoutRef.current !== undefined) {
|
||||
window.clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = undefined;
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [path]);
|
||||
|
||||
// Wire Cmd/Ctrl+S to save the current buffer.
|
||||
const handleMount: OnMount = (editor, monaco) => {
|
||||
editor.addCommand(
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
|
||||
() => {
|
||||
const save = useCallback(() => {
|
||||
const currentBuffer = bufferRef.current;
|
||||
const currentSaved = savedContentRef.current;
|
||||
const currentPath = pathRef.current;
|
||||
@@ -81,10 +72,41 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [write]);
|
||||
|
||||
const handleManualSave = () => {
|
||||
clearPendingSave();
|
||||
save();
|
||||
};
|
||||
|
||||
// Suppress the browser's Save Page dialog on Cmd/Ctrl+S when a file is open
|
||||
// and trigger save from React-owned code.
|
||||
useEffect(() => {
|
||||
if (!path) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if ((e.key === "s" || e.key === "S") && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
save();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [path, save]);
|
||||
|
||||
// Auto-save 2s after the last edit when the buffer is dirty.
|
||||
useEffect(() => {
|
||||
if (!dirty) {
|
||||
clearPendingSave();
|
||||
return;
|
||||
}
|
||||
clearPendingSave();
|
||||
saveTimeoutRef.current = window.setTimeout(() => {
|
||||
save();
|
||||
}, 2000);
|
||||
return () => clearPendingSave();
|
||||
}, [buffer, dirty, save]);
|
||||
|
||||
// Resetting dirty state on file switch is acceptable for this task because
|
||||
// each file gets its own EditorPanel mount via the `key={path}` prop.
|
||||
const [, setMarkers] = useState<Parameters<OnValidate>[0]>([]);
|
||||
@@ -115,6 +137,19 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
{write.isError && (
|
||||
<span className="text-destructive">{write.error.message}</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleManualSave}
|
||||
disabled={!dirty || saveInProgress}
|
||||
className="flex items-center justify-center rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
||||
aria-label="Save"
|
||||
>
|
||||
{saveInProgress ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{readError && (
|
||||
@@ -135,7 +170,6 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
value={buffer}
|
||||
onChange={(value) => setBuffer(value ?? "")}
|
||||
onValidate={setMarkers}
|
||||
onMount={handleMount}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
|
||||
Reference in New Issue
Block a user