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 Editor, { type OnValidate } from "@monaco-editor/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { Loader2, Save } from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { inferLanguage, useFileRead, useFileWrite } from "@/lib/api/files";
|
import { inferLanguage, useFileRead, useFileWrite } from "@/lib/api/files";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
|||||||
const [savedContent, setSavedContent] = useState("");
|
const [savedContent, setSavedContent] = useState("");
|
||||||
const dirty = buffer !== savedContent;
|
const dirty = buffer !== savedContent;
|
||||||
const saveInProgress = write.isPending;
|
const saveInProgress = write.isPending;
|
||||||
|
const saveTimeoutRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
// Sync editor buffer with fetched content when the file changes or loads.
|
// Sync editor buffer with fetched content when the file changes or loads.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,25 +44,14 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
|||||||
workspaceIdRef.current = workspaceId;
|
workspaceIdRef.current = workspaceId;
|
||||||
}, [buffer, savedContent, path, workspaceId]);
|
}, [buffer, savedContent, path, workspaceId]);
|
||||||
|
|
||||||
// Suppress the browser's Save Page dialog on Cmd/Ctrl+S when a file is open.
|
const clearPendingSave = () => {
|
||||||
// Monaco's own keybinding still handles saving.
|
if (saveTimeoutRef.current !== undefined) {
|
||||||
useEffect(() => {
|
window.clearTimeout(saveTimeoutRef.current);
|
||||||
if (!path) return;
|
saveTimeoutRef.current = undefined;
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if ((e.key === "s" || e.key === "S") && (e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", handler);
|
|
||||||
return () => window.removeEventListener("keydown", handler);
|
|
||||||
}, [path]);
|
|
||||||
|
|
||||||
// Wire Cmd/Ctrl+S to save the current buffer.
|
const save = useCallback(() => {
|
||||||
const handleMount: OnMount = (editor, monaco) => {
|
|
||||||
editor.addCommand(
|
|
||||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
|
|
||||||
() => {
|
|
||||||
const currentBuffer = bufferRef.current;
|
const currentBuffer = bufferRef.current;
|
||||||
const currentSaved = savedContentRef.current;
|
const currentSaved = savedContentRef.current;
|
||||||
const currentPath = pathRef.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
|
// Resetting dirty state on file switch is acceptable for this task because
|
||||||
// each file gets its own EditorPanel mount via the `key={path}` prop.
|
// each file gets its own EditorPanel mount via the `key={path}` prop.
|
||||||
const [, setMarkers] = useState<Parameters<OnValidate>[0]>([]);
|
const [, setMarkers] = useState<Parameters<OnValidate>[0]>([]);
|
||||||
@@ -115,6 +137,19 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
|||||||
{write.isError && (
|
{write.isError && (
|
||||||
<span className="text-destructive">{write.error.message}</span>
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{readError && (
|
{readError && (
|
||||||
@@ -135,7 +170,6 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
|||||||
value={buffer}
|
value={buffer}
|
||||||
onChange={(value) => setBuffer(value ?? "")}
|
onChange={(value) => setBuffer(value ?? "")}
|
||||||
onValidate={setMarkers}
|
onValidate={setMarkers}
|
||||||
onMount={handleMount}
|
|
||||||
options={{
|
options={{
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user