From b80135dbbdbfd94e7b05ce9e98864a5f23f31003 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 9 Sep 2026 11:19:48 +0800 Subject: [PATCH] fix(editor): reduce SQL editor typing latency --- .../src/components/editor/QueryEditor.vue | 58 +++++++++++++++++-- ...EditorSemanticHighlightPerformance.spec.ts | 6 ++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index dae590cbcb..002ae7cf44 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -219,6 +219,7 @@ function queryEditorSelectionLanguage(): "sql" | "text" { const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120; const COMPLETION_DEBOUNCE_DELAY_MS = 150; +const COMPLETION_TRIGGER_DEFER_DELAY_MS = 50; const COMPLETION_TAB_RETRY_DELAY_MS = 16; const COMPLETION_TAB_MAX_WAIT_MS = COMPLETION_DEBOUNCE_DELAY_MS + COMPLETION_REMOTE_LATENCY_BUDGET_MS + 100; const COMPLETION_ENTER_MAX_WAIT_MS = 125; @@ -630,6 +631,7 @@ let semanticDiagnostics: SqlSemanticDiagnostic[] = []; let semanticDiagnosticTimer: ReturnType | null = null; let semanticDiagnosticRunId = 0; let pendingSemanticDiagnosticPreserveOutsideRanges = false; +let deferredCompletionTriggerTimer: ReturnType | null = null; let editorIsActive = true; let tableReferenceDropListenerRegistered = false; let imeCompositionActive = false; @@ -4750,6 +4752,25 @@ function scheduleSqlCompletionStart(currentView: EditorViewType, delayMs = 0) { }, delayMs); } +function clearDeferredCompletionTrigger() { + if (deferredCompletionTriggerTimer === null) return; + clearTimeout(deferredCompletionTriggerTimer); + deferredCompletionTriggerTimer = null; +} + +function scheduleDeferredCompletionTrigger(currentView: EditorViewType, insertedText: string, removedText: string) { + clearDeferredCompletionTrigger(); + const expectedDoc = currentView.state.doc; + const expectedPosition = currentView.state.selection.main.head; + deferredCompletionTriggerTimer = setTimeout(() => { + deferredCompletionTriggerTimer = null; + if (view.value !== currentView || currentView.state.doc !== expectedDoc || currentView.state.selection.main.head !== expectedPosition || isEditorComposing(currentView)) return; + if (shouldStartSqlCompletionAfterInput(insertedText, removedText, currentView)) { + scheduleSqlCompletionStart(currentView); + } + }, COMPLETION_TRIGGER_DEFER_DELAY_MS); +} + function flushImeComposition() { const currentView = view.value; if (!currentView || !pendingImeModelEmit) return; @@ -5934,10 +5955,13 @@ onMounted(async () => { queryEditorLineCommentToken(props.databaseType) === "//" ? shellLineCommentHighlightPlugin : [], ]; const MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS = 32; + const SQL_SEMANTIC_HIGHLIGHT_DEBOUNCE_MS = 100; + const refreshSqlSemanticHighlightEffect = StateEffect.define(); buildSqlSemanticHighlightExtension = () => [ ViewPlugin.fromClass( class { decorations: import("@codemirror/view").DecorationSet; + private refreshTimer: ReturnType | null = null; private cachedDoc: import("@codemirror/state").Text | null = null; private cachedSql = ""; private cachedDialectId = ""; @@ -5953,7 +5977,34 @@ onMounted(async () => { } update(update: import("@codemirror/view").ViewUpdate) { - if (update.docChanged || update.viewportChanged) this.decorations = this.buildDecorations(update.view); + const refreshRequested = update.transactions.some((transaction) => transaction.effects.some((effect) => effect.is(refreshSqlSemanticHighlightEffect))); + if (update.docChanged) { + this.decorations = this.decorations.map(update.changes); + this.scheduleRefresh(update.view); + return; + } + if (refreshRequested || update.viewportChanged) { + this.cancelRefresh(); + this.decorations = this.buildDecorations(update.view); + } + } + + scheduleRefresh(currentView: import("@codemirror/view").EditorView) { + if (this.refreshTimer !== null) return; + this.refreshTimer = setTimeout(() => { + this.refreshTimer = null; + if (currentView.dom.isConnected) currentView.dispatch({ effects: refreshSqlSemanticHighlightEffect.of(null) }); + }, SQL_SEMANTIC_HIGHLIGHT_DEBOUNCE_MS); + } + + cancelRefresh() { + if (this.refreshTimer === null) return; + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + destroy() { + this.cancelRefresh(); } buildDecorations(currentView: import("@codemirror/view").EditorView) { @@ -6255,9 +6306,7 @@ onMounted(async () => { removedText += update.startState.doc.sliceString(fromA, toA); }); const suppressCompletionAutoStart = consumeSqlCompletionAutoStartSuppression(); - if (!suppressCompletionAutoStart && shouldStartSqlCompletionAfterInput(insertedText, removedText, update.view)) { - scheduleSqlCompletionStart(update.view); - } + if (!suppressCompletionAutoStart) scheduleDeferredCompletionTrigger(update.view, insertedText, removedText); } if (update.transactions.some((tr) => tr.isUserEvent("input.paste"))) { resyncCaretAfterPaste(update.view); @@ -6990,6 +7039,7 @@ function pauseQueryEditorBackgroundWork() { clearTableNavigationHover(); clearPendingCompletionEnter(); clearPendingCompletionTab(); + clearDeferredCompletionTrigger(); executionViewportOwnership.reset(); editorIsActive = false; clearScheduledSemanticDiagnostics(); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorSemanticHighlightPerformance.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorSemanticHighlightPerformance.spec.ts index 66ed23824d..c707d77bc3 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorSemanticHighlightPerformance.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorSemanticHighlightPerformance.spec.ts @@ -22,4 +22,10 @@ describe("QueryEditor semantic highlighting while scrolling", () => { expect(queryEditorSource).toContain("MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS = 32"); expect(queryEditorSource).toContain("this.cachedWindows.splice(0, this.cachedWindows.length - MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS)"); }); + + it("defers semantic highlighting while the document is changing", () => { + expect(queryEditorSource).toContain("SQL_SEMANTIC_HIGHLIGHT_DEBOUNCE_MS = 100"); + expect(queryEditorSource).toContain("this.decorations = this.decorations.map(update.changes)"); + expect(queryEditorSource).toContain("refreshSqlSemanticHighlightEffect.of(null)"); + }); });