From e153d88091be02f7cb0356642fc1a5e52d320ca0 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 9 Jun 2026 18:04:03 +0800 Subject: [PATCH] fix(editor): preserve sql tab scroll position --- apps/desktop/src/App.vue | 4 ++ .../src/components/editor/QueryEditor.vue | 39 +++++++++++++++++++ .../src/components/layout/ContentArea.vue | 3 ++ apps/desktop/src/lib/openTabsPersistence.ts | 1 + apps/desktop/src/stores/queryStore.ts | 7 ++++ apps/desktop/src/types/database.ts | 4 ++ 6 files changed, 58 insertions(+) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 87c68655d8..8319b3b0f2 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1124,6 +1124,10 @@ onUnmounted(() => { @editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)" @editor-selection-change="(v: string) => (selectedSql = v)" @editor-cursor-change="(p: number) => (cursorPos = p)" + @editor-viewport-change=" + (tabId: string, viewport: { scrollTop: number; scrollLeft: number }) => + queryStore.updateEditorViewport(tabId, viewport) + " @format-error="toast(t('toolbar.formatSqlFailed'))" @save-sql="void openSaveSqlDialog()" @reload=" diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index aa31d33ad6..0577a5ae86 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -104,6 +104,7 @@ const props = defineProps<{ executionError?: string; readOnly?: boolean; forceWordWrap?: boolean; + initialViewport?: { scrollTop: number; scrollLeft: number }; }>(); const emit = defineEmits<{ @@ -116,10 +117,12 @@ const emit = defineEmits<{ clickTable: [tableName: string]; clickColumn: [columns: Array<{ name: string; table: string; schema?: string }>, error?: string | undefined]; closeColumnPanel: []; + viewportChange: [viewport: { scrollTop: number; scrollLeft: number }]; }>(); const editorRef = ref(); const view = shallowRef(null); +let viewportEmitFrame: number | null = null; const connectionStore = useConnectionStore(); const settingsStore = useSettingsStore(); const { isDark } = useTheme(); @@ -1955,6 +1958,8 @@ onMounted(async () => { }); view.value = new EditorView({ state, parent: editorRef.value }); + view.value.scrollDOM.addEventListener("scroll", scheduleEditorViewportEmit, { passive: true }); + restoreEditorViewport(); syncContextMenuState(view.value); syncEditorFontCssVars(liveFontSize.value, initialSettings.fontFamily); registerTableReferenceDropListener(); @@ -2087,6 +2092,7 @@ watch( ); function pauseQueryEditorBackgroundWork() { + if (view.value) emitEditorViewport(view.value); editorIsActive = false; semanticDiagnosticRunId++; if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer); @@ -2099,6 +2105,7 @@ function resumeQueryEditorBackgroundWork() { editorIsActive = true; registerTableReferenceDropListener(); scheduleSemanticDiagnostics(); + restoreEditorViewport(); } onActivated(resumeQueryEditorBackgroundWork); @@ -2107,10 +2114,42 @@ onDeactivated(pauseQueryEditorBackgroundWork); onBeforeUnmount(() => { pauseQueryEditorBackgroundWork(); + if (viewportEmitFrame !== null) { + cancelAnimationFrame(viewportEmitFrame); + viewportEmitFrame = null; + } + view.value?.scrollDOM.removeEventListener("scroll", scheduleEditorViewportEmit); zoomCommitScheduler.dispose(); view.value?.destroy(); }); +function emitEditorViewport(currentView: EditorViewType) { + emit("viewportChange", { + scrollTop: currentView.scrollDOM.scrollTop, + scrollLeft: currentView.scrollDOM.scrollLeft, + }); +} + +function scheduleEditorViewportEmit() { + if (viewportEmitFrame !== null) return; + viewportEmitFrame = requestAnimationFrame(() => { + viewportEmitFrame = null; + if (view.value) emitEditorViewport(view.value); + }); +} + +function restoreEditorViewport() { + const viewport = props.initialViewport; + if (!view.value || !viewport) return; + requestAnimationFrame(() => { + if (!view.value) return; + view.value.scrollDOM.scrollTo({ + top: viewport.scrollTop, + left: viewport.scrollLeft, + }); + }); +} + function openSearch(): boolean { return searchPanelRef.value?.openSearch() ?? false; } diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 2bb8b8afdb..5be2f820ee 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -111,6 +111,7 @@ const emit = defineEmits<{ editorUpdate: [tabId: string, value: string]; editorSelectionChange: [value: string]; editorCursorChange: [pos: number]; + editorViewportChange: [tabId: string, viewport: { scrollTop: number; scrollLeft: number }]; formatError: []; reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number]; paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string]; @@ -445,9 +446,11 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); :format-dialect="activeSqlFormatDialect" :format-request-id="formatSqlRequest?.tabId === activeTab.id ? formatSqlRequest.id : undefined" :execution-error="activeQueryError" + :initial-viewport="activeTab.editorViewport" @update:model-value="emit('editorUpdate', activeTab.id, $event)" @selection-change="emit('editorSelectionChange', $event)" @cursor-change="emit('editorCursorChange', $event)" + @viewport-change="emit('editorViewportChange', activeTab.id, $event)" @format-error="emit('formatError')" @execute="emit('execute')" @save="emit('saveSql')" diff --git a/apps/desktop/src/lib/openTabsPersistence.ts b/apps/desktop/src/lib/openTabsPersistence.ts index aee4d8230e..2739796f3b 100644 --- a/apps/desktop/src/lib/openTabsPersistence.ts +++ b/apps/desktop/src/lib/openTabsPersistence.ts @@ -100,6 +100,7 @@ export function restoreOpenTabsState( isExecuting: false, isCancelling: false, queryExecutionStartedAt: undefined, + editorViewport: undefined, isExplaining: false, resultEvicted: mode === "data" ? undefined : tab.resultEvicted, resultCacheKey: mode === "data" ? undefined : tab.resultCacheKey, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 17f33572ad..3e96eefbea 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -523,6 +523,12 @@ export const useQueryStore = defineStore("query", () => { } } + function updateEditorViewport(id: string, viewport: { scrollTop: number; scrollLeft: number }) { + const tab = tabs.value.find((t) => t.id === id); + if (!tab) return; + tab.editorViewport = viewport; + } + function renameTab(id: string, title: string) { const trimmed = title.trim(); if (!trimmed) return false; @@ -1746,6 +1752,7 @@ export const useQueryStore = defineStore("query", () => { releaseConnectionTabs, releaseDatabaseTabs, updateSql, + updateEditorViewport, renameTab, openObjectBrowser, openUserAdmin, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 75040a8513..fefa2af6fb 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -396,6 +396,10 @@ export interface QueryTab { isExecuting: boolean; isCancelling?: boolean; queryExecutionStartedAt?: number; + editorViewport?: { + scrollTop: number; + scrollLeft: number; + }; executionId?: string; isExplaining?: boolean; explainExecutionId?: string;