From f043e631b7ab6f7c1a8b2079d1d700f09228a33e Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:54:45 +0900 Subject: [PATCH] fix: ignore stale large-file loads --- scripts/largeFileLoadRevision.test.ts | 15 +++++++++++++++ src/lib/MarkdownViewer.svelte | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 scripts/largeFileLoadRevision.test.ts diff --git a/scripts/largeFileLoadRevision.test.ts b/scripts/largeFileLoadRevision.test.ts new file mode 100644 index 00000000..a5aeeb1d --- /dev/null +++ b/scripts/largeFileLoadRevision.test.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const viewer = readFileSync(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url), 'utf8'); + +test('large-file completion requires the current clean load revision and unchanged view mode', () => { + assert.match(viewer, /const loadRevisionByTab = new Map\(\);/); + assert.match(viewer, /const fullLoadRevision = \(loadRevisionByTab\.get\(activeId\) \?\? 0\) \+ 1;/); + assert.match(viewer, /loadRevisionByTab\.set\(activeId, fullLoadRevision\);/); + assert.match( + viewer, + /loadRevisionByTab\.get\(activeId\) === fullLoadRevision[\s\S]*!targetTab\.isDirty[\s\S]*targetTab\.isEditing === initialIsEditing[\s\S]*targetTab\.isSplit === initialIsSplit/, + ); +}); diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index df723ec7..623d66b8 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -210,6 +210,7 @@ import { t } from './utils/i18n.js'; let canGoForwardInFileHistory = $derived(tabManager.activeTabId ? tabManager.canGoForward(tabManager.activeTabId) : false); let loadingTabs = $state([]); + const loadRevisionByTab = new Map(); let isAtBottom = $state(false); let showHome = $state(false); @@ -785,6 +786,8 @@ import { t } from './utils/i18n.js'; } const activeId = tabManager.activeTabId; if (!activeId) return; + const fullLoadRevision = (loadRevisionByTab.get(activeId) ?? 0) + 1; + loadRevisionByTab.set(activeId, fullLoadRevision); const isMarkdown = hasMarkdownLinkExtension(filePath); const tab = tabManager.tabs.find((t) => t.id === activeId); @@ -794,6 +797,8 @@ import { t } from './utils/i18n.js'; if (tab && !options.preserveEditState && !existing) { tab.isEditing = settings.startInEditor; } + const initialIsEditing = tab?.isEditing ?? false; + const initialIsSplit = tab?.isSplit ?? false; const [, content, isFull] = await invoke('open_markdown_preview', { path: filePath, maxBytes: 50000 }) as [string, string, boolean]; if (pendingNavigateTabId) { tabManager.navigate(pendingNavigateTabId, filePath); @@ -803,6 +808,16 @@ import { t } from './utils/i18n.js'; tabManager.setTabRawContent(activeId, content); if (!isFull) { + const canApplyFullLoad = () => { + const targetTab = tabManager.tabs.find((t) => t.id === activeId); + return ( + targetTab?.path === filePath && + loadRevisionByTab.get(activeId) === fullLoadRevision && + !targetTab.isDirty && + targetTab.isEditing === initialIsEditing && + targetTab.isSplit === initialIsSplit + ); + }; loadingTabs = [...loadingTabs, activeId]; tick().then(() => { if (markdownBody) isAtBottom = markdownBody.scrollHeight <= markdownBody.clientHeight + 100; @@ -814,9 +829,13 @@ import { t } from './utils/i18n.js'; setTimeout(applyFull, 100); return; } - if (tabManager.tabs.find((t) => t.id === activeId)?.path === filePath) { + if (canApplyFullLoad()) { renderMarkdownPreview(fullContent, filePath) .then((fullProcessed) => { + if (!canApplyFullLoad()) { + loadingTabs = loadingTabs.filter((id) => id !== activeId); + return; + } tabManager.updateTabContent(activeId, fullProcessed); tabManager.setTabRawContent(activeId, fullContent); loadingTabs = loadingTabs.filter((id) => id !== activeId);