diff --git a/scripts/documentWatcherSession.test.ts b/scripts/documentWatcherSession.test.ts new file mode 100644 index 0000000..f7498e1 --- /dev/null +++ b/scripts/documentWatcherSession.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'); + +test('self writes suppress watcher reloads only during their grace period', () => { + const handler = session.slice(session.indexOf('function shouldReloadExternalChange')); + assert.match(handler, /if \(Date\.now\(\) < until\) return false;/); + assert.match(handler, /selfWriteUntilByPath\.delete\(path\);/); + assert.match(handler, /return true;/); +}); diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 98172c6..f34c578 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -155,7 +155,6 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu const lastContentRefByTab = new Map(); // Suppress the file-watcher reload that fires when we ourselves write the file. // Maps absolute path -> wall-clock ms after which an event for that path is real again. - const selfWriteUntilByPath = new Map(); const SELF_WRITE_GRACE_MS = 400; const AUTO_SAVE_DEBOUNCE_MS = 1500; @@ -492,8 +491,7 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu console.error(message, error); addToast(`${message}: ${String(error)}`, 'error'); }, - markSelfWrite: (path) => selfWriteUntilByPath.set(path, Date.now() + SELF_WRITE_GRACE_MS), - clearSelfWrite: (path) => selfWriteUntilByPath.delete(path), + selfWriteGraceMs: SELF_WRITE_GRACE_MS, }); async function discardPersistedWindowState() { @@ -2588,11 +2586,7 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu // Skip events caused by our own auto-save / save invocations, // otherwise the reload would clobber any keystrokes that landed // between fs::write and this listener firing. - const until = selfWriteUntilByPath.get(currentFile); - if (until !== undefined) { - if (Date.now() < until) return; - selfWriteUntilByPath.delete(currentFile); - } + if (!documentSession.shouldReloadExternalChange(currentFile)) return; loadMarkdown(currentFile); }), ); diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index fe453ec..b89172b 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -25,13 +25,29 @@ type DocumentSessionOptions = { isScrolling: () => boolean; renderRichContent: () => void; onError: (message: string, error: unknown) => void; - markSelfWrite: (path: string) => void; - clearSelfWrite: (path: string) => void; + selfWriteGraceMs: number; }; export function createDocumentSession(options: DocumentSessionOptions) { const loadRevisionByTab = new Map(); const loadingTabs = new Set(); + const selfWriteUntilByPath = new Map(); + + function markSelfWrite(path: string) { + selfWriteUntilByPath.set(path, Date.now() + options.selfWriteGraceMs); + } + + function clearSelfWrite(path: string) { + selfWriteUntilByPath.delete(path); + } + + function shouldReloadExternalChange(path: string) { + const until = selfWriteUntilByPath.get(path); + if (until === undefined) return true; + if (Date.now() < until) return false; + selfWriteUntilByPath.delete(path); + return true; + } function updateLoading(tabId: string, loading: boolean) { if (loading) loadingTabs.add(tabId); @@ -146,10 +162,10 @@ export function createDocumentSession(options: DocumentSessionOptions) { targetPath = selected; } const snapshot = tab.rawContent; - options.markSelfWrite(targetPath); + markSelfWrite(targetPath); try { await invoke('save_file_content', { path: targetPath, content: snapshot }); - options.markSelfWrite(targetPath); + markSelfWrite(targetPath); if (tab.path === '') { tabManager.updateTabPath(tab.id, targetPath); options.saveRecentFile(targetPath); @@ -158,7 +174,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { tab.isDirty = tab.rawContent !== snapshot; return true; } catch (error) { - options.clearSelfWrite(targetPath); + clearSelfWrite(targetPath); options.onError('Failed to save file', error); return false; } @@ -176,17 +192,17 @@ export function createDocumentSession(options: DocumentSessionOptions) { }); if (!selected) return false; const snapshot = tab.rawContent; - options.markSelfWrite(selected); + markSelfWrite(selected); try { await invoke('save_file_content', { path: selected, content: snapshot }); - options.markSelfWrite(selected); + markSelfWrite(selected); tabManager.updateTabPath(tab.id, selected); options.saveRecentFile(selected); tab.originalContent = snapshot; tab.isDirty = tab.rawContent !== snapshot; return true; } catch (error) { - options.clearSelfWrite(selected); + clearSelfWrite(selected); options.onError('Failed to save file as', error); return false; } @@ -207,5 +223,5 @@ export function createDocumentSession(options: DocumentSessionOptions) { return true; } - return { loadMarkdown, saveContent, saveContentAs, toggleTaskCheckbox }; + return { loadMarkdown, saveContent, saveContentAs, toggleTaskCheckbox, shouldReloadExternalChange }; }