2026-02-23 19:53:36 +01:00
|
|
|
import { useCallback, useRef } from 'react'
|
|
|
|
|
import type { SetStateAction } from 'react'
|
|
|
|
|
import { useSaveNote } from './useSaveNote'
|
|
|
|
|
|
|
|
|
|
interface Tab {
|
|
|
|
|
entry: { path: string }
|
|
|
|
|
content: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EditorSaveConfig {
|
|
|
|
|
updateVaultContent: (path: string, content: string) => void
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tab types vary between layers
|
|
|
|
|
setTabs: (fn: SetStateAction<any[]>) => void
|
|
|
|
|
setToastMessage: (msg: string | null) => void
|
2026-02-23 21:13:25 +01:00
|
|
|
onAfterSave?: () => void
|
2026-03-11 16:31:03 +01:00
|
|
|
/** Called after content is persisted — used to clear unsaved state and live-reload themes. */
|
|
|
|
|
onNotePersisted?: (path: string, content: string) => void
|
2026-02-23 19:53:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hook that manages explicit save (Cmd+S) for editor content.
|
|
|
|
|
* Tracks pending (unsaved) content and provides save + pre-rename helpers.
|
|
|
|
|
*/
|
2026-02-24 15:54:24 +01:00
|
|
|
const noop = () => {}
|
|
|
|
|
|
2026-02-27 18:17:47 +01:00
|
|
|
export function useEditorSave({ updateVaultContent, setTabs, setToastMessage, onAfterSave = noop, onNotePersisted }: EditorSaveConfig) {
|
2026-02-23 19:53:36 +01:00
|
|
|
const pendingContentRef = useRef<{ path: string; content: string } | null>(null)
|
|
|
|
|
|
|
|
|
|
const updateTabAndContent = useCallback((path: string, content: string) => {
|
|
|
|
|
updateVaultContent(path, content)
|
|
|
|
|
setTabs((prev: Tab[]) =>
|
|
|
|
|
prev.map((t) => t.entry.path === path ? { ...t, content } : t)
|
|
|
|
|
)
|
|
|
|
|
}, [updateVaultContent, setTabs])
|
|
|
|
|
|
|
|
|
|
const { saveNote } = useSaveNote(updateTabAndContent)
|
|
|
|
|
|
2026-02-23 20:34:12 +01:00
|
|
|
/** Persist pending content matching an optional path filter; returns true if saved */
|
|
|
|
|
const flushPending = useCallback(async (pathFilter?: string): Promise<boolean> => {
|
|
|
|
|
const pending = pendingContentRef.current
|
|
|
|
|
if (!pending) return false
|
|
|
|
|
if (pathFilter && pending.path !== pathFilter) return false
|
|
|
|
|
await saveNote(pending.path, pending.content)
|
2026-03-11 16:31:03 +01:00
|
|
|
const savedContent = pending.content
|
2026-02-23 20:34:12 +01:00
|
|
|
pendingContentRef.current = null
|
2026-03-11 16:31:03 +01:00
|
|
|
onNotePersisted?.(pending.path, savedContent)
|
2026-02-23 20:34:12 +01:00
|
|
|
return true
|
2026-02-27 18:17:47 +01:00
|
|
|
}, [saveNote, onNotePersisted])
|
2026-02-23 20:34:12 +01:00
|
|
|
|
2026-02-27 18:17:47 +01:00
|
|
|
/** Called by Cmd+S — persists the current editor content to disk.
|
|
|
|
|
* Accepts optional fallback for unsaved notes with no pending edits. */
|
|
|
|
|
const handleSave = useCallback(async (unsavedFallback?: { path: string; content: string }) => {
|
2026-02-23 19:53:36 +01:00
|
|
|
try {
|
2026-02-23 20:34:12 +01:00
|
|
|
const saved = await flushPending()
|
2026-02-27 18:17:47 +01:00
|
|
|
if (!saved && unsavedFallback) {
|
|
|
|
|
await saveNote(unsavedFallback.path, unsavedFallback.content)
|
2026-03-11 16:31:03 +01:00
|
|
|
onNotePersisted?.(unsavedFallback.path, unsavedFallback.content)
|
2026-02-27 18:17:47 +01:00
|
|
|
setToastMessage('Saved')
|
|
|
|
|
onAfterSave()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-24 11:15:36 +01:00
|
|
|
setToastMessage(saved ? 'Saved' : 'Nothing to save')
|
2026-02-24 15:54:24 +01:00
|
|
|
onAfterSave()
|
2026-02-23 19:53:36 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Save failed:', err)
|
|
|
|
|
setToastMessage(`Save failed: ${err}`)
|
|
|
|
|
}
|
2026-02-27 18:17:47 +01:00
|
|
|
}, [flushPending, setToastMessage, onAfterSave, saveNote, onNotePersisted])
|
2026-02-23 19:53:36 +01:00
|
|
|
|
2026-03-08 15:05:01 +01:00
|
|
|
/** Called by Editor onChange — buffers the latest content and syncs tab state
|
|
|
|
|
* so consumers (e.g. AI panel) always see current editor content. */
|
2026-02-23 19:53:36 +01:00
|
|
|
const handleContentChange = useCallback((path: string, content: string) => {
|
|
|
|
|
pendingContentRef.current = { path, content }
|
2026-03-08 15:05:01 +01:00
|
|
|
setTabs((prev: Tab[]) =>
|
|
|
|
|
prev.map((t) => t.entry.path === path ? { ...t, content } : t)
|
|
|
|
|
)
|
|
|
|
|
}, [setTabs])
|
2026-02-23 19:53:36 +01:00
|
|
|
|
|
|
|
|
/** Save pending content for a specific path (used before rename) */
|
2026-02-23 20:34:12 +01:00
|
|
|
const savePendingForPath = useCallback(
|
|
|
|
|
(path: string): Promise<boolean> => flushPending(path),
|
|
|
|
|
[flushPending],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/** Flush any pending content to disk silently (used before git commit).
|
|
|
|
|
* Does NOT call onAfterSave — callers manage their own refresh. */
|
|
|
|
|
const savePending = useCallback((): Promise<boolean> => flushPending(), [flushPending])
|
2026-02-23 19:53:36 +01:00
|
|
|
|
2026-02-23 20:34:12 +01:00
|
|
|
return { handleSave, handleContentChange, savePendingForPath, savePending }
|
2026-02-23 19:53:36 +01:00
|
|
|
}
|