fix(editor): preserve sql tab scroll position

This commit is contained in:
t8y2
2026-06-09 18:04:03 +08:00
parent 2050bee75c
commit e153d88091
6 changed files with 58 additions and 0 deletions
+4
View File
@@ -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="
@@ -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<HTMLDivElement>();
const view = shallowRef<EditorViewType | null>(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;
}
@@ -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')"
@@ -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,
+7
View File
@@ -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,
+4
View File
@@ -396,6 +396,10 @@ export interface QueryTab {
isExecuting: boolean;
isCancelling?: boolean;
queryExecutionStartedAt?: number;
editorViewport?: {
scrollTop: number;
scrollLeft: number;
};
executionId?: string;
isExplaining?: boolean;
explainExecutionId?: string;