Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/documentWatcherSession.test.ts
Original file line number Diff line number Diff line change
@@ -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;/);
});
10 changes: 2 additions & 8 deletions src/lib/MarkdownViewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu
const lastContentRefByTab = new Map<string, string>();
// 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<string, number>();
const SELF_WRITE_GRACE_MS = 400;
const AUTO_SAVE_DEBOUNCE_MS = 1500;

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}),
);
Expand Down
34 changes: 25 additions & 9 deletions src/lib/sessions/documentSession.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();
const loadingTabs = new Set<string>();
const selfWriteUntilByPath = new Map<string, number>();

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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -207,5 +223,5 @@ export function createDocumentSession(options: DocumentSessionOptions) {
return true;
}

return { loadMarkdown, saveContent, saveContentAs, toggleTaskCheckbox };
return { loadMarkdown, saveContent, saveContentAs, toggleTaskCheckbox, shouldReloadExternalChange };
}
Loading