From 3f203f8c49cace8010d88d9dcf812852196fae16 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:29:32 +0530 Subject: [PATCH] feat(editor): implement auto-save functionality and manual save command in SourceCodeEditor --- .../components/editor-panel/editor-panel.tsx | 12 ++-- .../components/editor/source-code-editor.tsx | 55 +++++++++++++++++++ .../layout/ui/right-panel/RightPanel.tsx | 2 +- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/surfsense_web/components/editor-panel/editor-panel.tsx b/surfsense_web/components/editor-panel/editor-panel.tsx index 137ece5e2..739428df3 100644 --- a/surfsense_web/components/editor-panel/editor-panel.tsx +++ b/surfsense_web/components/editor-panel/editor-panel.tsx @@ -187,7 +187,7 @@ export function EditorPanelContent({ setEditedMarkdown(md); }, []); - const handleSave = useCallback(async () => { + const handleSave = useCallback(async (options?: { silent?: boolean }) => { setSaving(true); try { if (isLocalFileMode) { @@ -197,18 +197,18 @@ export function EditorPanelContent({ if (!electronAPI?.writeAgentLocalFileText) { throw new Error("Local file editor is available only in desktop mode."); } + const contentToSave = markdownRef.current; const writeResult = await electronAPI.writeAgentLocalFileText( localFilePath, - markdownRef.current + contentToSave ); if (!writeResult.ok) { throw new Error(writeResult.error || "Failed to save local file"); } setEditorDoc((prev) => - prev ? { ...prev, source_markdown: markdownRef.current } : prev + prev ? { ...prev, source_markdown: contentToSave } : prev ); - setEditedMarkdown(null); - toast.success("File saved"); + setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current); return; } if (!searchSpaceId || !documentId) { @@ -363,6 +363,8 @@ export function EditorPanelContent({ path={localFilePath ?? "local-file.txt"} language={localFileLanguage} value={localFileContent} + onSave={() => handleSave({ silent: true })} + saveMode="auto" onChange={(next) => { markdownRef.current = next; setLocalFileContent(next); diff --git a/surfsense_web/components/editor/source-code-editor.tsx b/surfsense_web/components/editor/source-code-editor.tsx index 7bb7bee35..bd3728721 100644 --- a/surfsense_web/components/editor/source-code-editor.tsx +++ b/surfsense_web/components/editor/source-code-editor.tsx @@ -1,6 +1,7 @@ "use client"; import dynamic from "next/dynamic"; +import { useEffect, useRef } from "react"; import { useTheme } from "next-themes"; import { Spinner } from "@/components/ui/spinner"; @@ -15,6 +16,9 @@ interface SourceCodeEditorProps { language?: string; readOnly?: boolean; fontSize?: number; + onSave?: () => Promise | void; + saveMode?: "manual" | "auto" | "both"; + autoSaveDelayMs?: number; } export function SourceCodeEditor({ @@ -24,8 +28,50 @@ export function SourceCodeEditor({ language = "plaintext", readOnly = false, fontSize = 12, + onSave, + saveMode = "manual", + autoSaveDelayMs = 800, }: SourceCodeEditorProps) { const { resolvedTheme } = useTheme(); + const saveTimerRef = useRef | null>(null); + const onSaveRef = useRef(onSave); + const skipNextAutoSaveRef = useRef(true); + + useEffect(() => { + onSaveRef.current = onSave; + }, [onSave]); + + useEffect(() => { + skipNextAutoSaveRef.current = true; + }, [path]); + + useEffect(() => { + if (readOnly || !onSaveRef.current) return; + if (saveMode !== "auto" && saveMode !== "both") return; + + if (skipNextAutoSaveRef.current) { + skipNextAutoSaveRef.current = false; + return; + } + + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + } + + saveTimerRef.current = setTimeout(() => { + void onSaveRef.current?.(); + saveTimerRef.current = null; + }, autoSaveDelayMs); + + return () => { + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + saveTimerRef.current = null; + } + }; + }, [autoSaveDelayMs, readOnly, saveMode, value]); + + const isManualSaveEnabled = !!onSave && !readOnly && (saveMode === "manual" || saveMode === "both"); return (
@@ -40,6 +86,12 @@ export function SourceCodeEditor({
} + onMount={(editor, monaco) => { + if (!isManualSaveEnabled) return; + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { + void onSaveRef.current?.(); + }); + }} options={{ automaticLayout: true, minimap: { enabled: false }, @@ -51,6 +103,9 @@ export function SourceCodeEditor({ overviewRulerLanes: 0, hideCursorInOverviewRuler: true, scrollBeyondLastLine: false, + renderLineHighlight: "none", + selectionHighlight: false, + occurrencesHighlight: "off", wordWrap: "off", scrollbar: { vertical: "hidden", diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx index f6debed34..2394480b2 100644 --- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx +++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx @@ -53,7 +53,7 @@ function CollapseButton({ onClick }: { onClick: () => void }) { Collapse panel - Collapse panel + Collapse panel ); }