From fcc7dfb89699faa3b09543fee9b2430863f31687 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 7 Sep 2026 19:21:42 +0800 Subject: [PATCH] fix(editor): restore viewport on uncached tab activations Closes #8374 --- .../src/components/editor/QueryEditor.vue | 6 +++ .../editorTabSwitchViewportRestore.test.ts | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/app-tests/editorTabSwitchViewportRestore.test.ts diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index cce624676f..ee6909c88d 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -6701,6 +6701,12 @@ function activateTabDocument(prevTabId: string | undefined, tabId: string | unde const cached = tabId === undefined ? undefined : tabStateCache.get(tabId); if (!cached) { swapEditorDocument(doc); + // First activation in this editor instance (or a cache-evicted tab, e.g. + // beyond MAX_CACHED_TAB_STATES): restore the tab's saved cursor and scroll + // position exactly like the cached-state branch, otherwise the swapped-in + // document keeps whatever scroll offset the dispatch left behind (#8374). + restoreEditorSelection(); + restoreEditorViewport(); return; } // setState swaps doc, selection, undo history and all fields at once, but it diff --git a/packages/app-tests/editorTabSwitchViewportRestore.test.ts b/packages/app-tests/editorTabSwitchViewportRestore.test.ts new file mode 100644 index 0000000000..1261e3c695 --- /dev/null +++ b/packages/app-tests/editorTabSwitchViewportRestore.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { test } from "vitest"; + +/** + * The shared editor swaps documents on tab switches (per-tab EditorState cache + * since the undo-isolation fix). Both branches of that swap — the cached + * `setState` path AND the uncached `swapEditorDocument` path (first activation + * in this editor instance, or a tab evicted from the state cache) — must + * restore the incoming tab's saved cursor and scroll position. Skipping the + * restore on the uncached branch is what made switching back to a tab land on + * line 1 instead of the previous viewport (#8374). + */ +function activateTabDocumentSource(): string { + const source = readFileSync(path.resolve("apps/desktop/src/components/editor/QueryEditor.vue"), "utf8"); + const start = source.indexOf("function activateTabDocument"); + assert.notEqual(start, -1, "expected activateTabDocument in QueryEditor.vue"); + const end = source.indexOf("\n}", start); + assert.notEqual(end, -1, "expected activateTabDocument closing brace"); + return source.slice(start, end); +} + +test("uncached tab activation restores the saved cursor and scroll position", () => { + const source = activateTabDocumentSource(); + assert.match(source, /if \(!cached\) \{[\s\S]*?swapEditorDocument\(doc\);[\s\S]*?restoreEditorSelection\(\);[\s\S]*?restoreEditorViewport\(\);[\s\S]*?return;/, "the swapEditorDocument branch must restore selection and viewport before returning"); +}); + +test("cached tab activation keeps restoring selection and viewport", () => { + const source = activateTabDocumentSource(); + assert.match(source, /currentView\.setState\(cached\);/); + assert.match(source, /currentView\.setState\(cached\);[\s\S]*?restoreEditorSelection\(\);\s*restoreEditorViewport\(\);/); +}); + +test("viewport restore prefers the per-tab saved viewport", () => { + const source = readFileSync(path.resolve("apps/desktop/src/components/editor/QueryEditor.vue"), "utf8"); + assert.match(source, /function restoreEditorViewport\(\) \{[\s\S]*?const viewport = props\.initialViewport \?\? latestViewport;/); +});