fix(editor): preserve sql tab cursor state

This commit is contained in:
t8y2
2026-06-09 18:18:18 +08:00
parent 80531c8765
commit 426b7911bc
6 changed files with 78 additions and 2 deletions
+4
View File
@@ -1128,6 +1128,10 @@ onUnmounted(() => {
(tabId: string, viewport: { scrollTop: number; scrollLeft: number }) =>
queryStore.updateEditorViewport(tabId, viewport)
"
@editor-selection-state-change="
(tabId: string, selection: { anchor: number; head: number }) =>
queryStore.updateEditorSelection(tabId, selection)
"
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="void openSaveSqlDialog()"
@reload="
@@ -105,6 +105,7 @@ const props = defineProps<{
readOnly?: boolean;
forceWordWrap?: boolean;
initialViewport?: { scrollTop: number; scrollLeft: number };
initialSelection?: { anchor: number; head: number };
}>();
const emit = defineEmits<{
@@ -118,6 +119,7 @@ const emit = defineEmits<{
clickColumn: [columns: Array<{ name: string; table: string; schema?: string }>, error?: string | undefined];
closeColumnPanel: [];
viewportChange: [viewport: { scrollTop: number; scrollLeft: number }];
selectionStateChange: [selection: { anchor: number; head: number }];
}>();
const editorRef = ref<HTMLDivElement>();
@@ -125,6 +127,7 @@ const view = shallowRef<EditorViewType | null>(null);
let viewportEmitFrame: number | null = null;
let viewportRestoreFrame: number | null = null;
let latestViewport: { scrollTop: number; scrollLeft: number } | undefined = props.initialViewport;
let latestSelection: { anchor: number; head: number } | undefined = props.initialSelection;
const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const { isDark } = useTheme();
@@ -1728,6 +1731,7 @@ onMounted(async () => {
const state = EditorState.create({
doc: props.modelValue,
selection: normalizedEditorSelection(props.initialSelection, props.modelValue.length),
extensions: [
cmSearch({
top: true,
@@ -1798,6 +1802,8 @@ onMounted(async () => {
syncContextMenuState(update.view);
emit("selectionChange", selectedSqlFromView(update.view));
emit("cursorChange", update.state.selection.main.head);
latestSelection = readEditorSelection(update.view);
if (editorIsActive) emitEditorSelection(latestSelection);
}
}),
fontThemeComp.of(
@@ -1816,6 +1822,11 @@ onMounted(async () => {
drop(event, currentView) {
return insertDroppedTableReference(currentView, event);
},
blur(_event, currentView) {
latestSelection = readEditorSelection(currentView);
if (editorIsActive) emitEditorSelection(latestSelection);
return false;
},
wheel(event) {
if (!event.metaKey && !event.ctrlKey) return false;
event.preventDefault();
@@ -2095,6 +2106,7 @@ watch(
function pauseQueryEditorBackgroundWork() {
flushEditorViewport();
flushEditorSelection();
editorIsActive = false;
semanticDiagnosticRunId++;
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
@@ -2107,6 +2119,8 @@ function resumeQueryEditorBackgroundWork() {
editorIsActive = true;
registerTableReferenceDropListener();
scheduleSemanticDiagnostics();
restoreEditorSelection();
restoreEditorFocus();
restoreEditorViewport();
}
@@ -2136,12 +2150,55 @@ function readEditorViewport(currentView: EditorViewType) {
};
}
function normalizedEditorSelection(selection: { anchor: number; head: number } | undefined, docLength: number) {
if (!selection) return undefined;
return {
anchor: Math.min(Math.max(0, selection.anchor), docLength),
head: Math.min(Math.max(0, selection.head), docLength),
};
}
function readEditorSelection(currentView: EditorViewType) {
const selection = currentView.state.selection.main;
return {
anchor: selection.anchor,
head: selection.head,
};
}
function emitEditorSelection(selection: { anchor: number; head: number }) {
emit("selectionStateChange", selection);
}
function flushEditorSelection() {
if (view.value) latestSelection = readEditorSelection(view.value);
if (latestSelection) emitEditorSelection(latestSelection);
}
function restoreEditorSelection() {
const selection = normalizedEditorSelection(props.initialSelection ?? latestSelection, props.modelValue.length);
if (!view.value || !selection) return;
view.value.dispatch({ selection });
}
function restoreEditorFocus() {
const focusEditorAcrossFrames = () => {
if (!view.value || view.value.hasFocus) return;
view.value.focus();
};
focusEditorAcrossFrames();
nextTick(() => {
focusEditorAcrossFrames();
requestAnimationFrame(focusEditorAcrossFrames);
});
}
function emitEditorViewport(viewport: { scrollTop: number; scrollLeft: number }) {
emit("viewportChange", viewport);
}
function scheduleEditorViewportEmit() {
if (!view.value) return;
if (!view.value || !editorIsActive) return;
latestViewport = readEditorViewport(view.value);
if (viewportEmitFrame !== null) return;
viewportEmitFrame = requestAnimationFrame(() => {
@@ -2198,7 +2255,7 @@ function openReplace(): boolean {
}
function scrollCursorIntoView() {
if (!view.value || !editorViewModule) return;
if (!view.value || !editorViewModule || !editorIsActive) return;
const pos = view.value.state.selection.main.head;
view.value.dispatch({
effects: editorViewModule.EditorView.scrollIntoView(pos, { y: "nearest" }),
@@ -112,6 +112,7 @@ const emit = defineEmits<{
editorSelectionChange: [value: string];
editorCursorChange: [pos: number];
editorViewportChange: [tabId: string, viewport: { scrollTop: number; scrollLeft: number }];
editorSelectionStateChange: [tabId: string, selection: { anchor: number; head: number }];
formatError: [];
reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number];
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string];
@@ -447,10 +448,12 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
:format-request-id="formatSqlRequest?.tabId === activeTab.id ? formatSqlRequest.id : undefined"
:execution-error="activeQueryError"
:initial-viewport="activeTab.editorViewport"
:initial-selection="activeTab.editorSelection"
@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)"
@selection-state-change="emit('editorSelectionStateChange', activeTab.id, $event)"
@format-error="emit('formatError')"
@execute="emit('execute')"
@save="emit('saveSql')"
@@ -101,6 +101,7 @@ export function restoreOpenTabsState(
isCancelling: false,
queryExecutionStartedAt: undefined,
editorViewport: undefined,
editorSelection: undefined,
isExplaining: false,
resultEvicted: mode === "data" ? undefined : tab.resultEvicted,
resultCacheKey: mode === "data" ? undefined : tab.resultCacheKey,
+7
View File
@@ -529,6 +529,12 @@ export const useQueryStore = defineStore("query", () => {
tab.editorViewport = viewport;
}
function updateEditorSelection(id: string, selection: { anchor: number; head: number }) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return;
tab.editorSelection = selection;
}
function renameTab(id: string, title: string) {
const trimmed = title.trim();
if (!trimmed) return false;
@@ -1753,6 +1759,7 @@ export const useQueryStore = defineStore("query", () => {
releaseDatabaseTabs,
updateSql,
updateEditorViewport,
updateEditorSelection,
renameTab,
openObjectBrowser,
openUserAdmin,
+4
View File
@@ -400,6 +400,10 @@ export interface QueryTab {
scrollTop: number;
scrollLeft: number;
};
editorSelection?: {
anchor: number;
head: number;
};
executionId?: string;
isExplaining?: boolean;
explainExecutionId?: string;