From 847539397c2cd4fab90b26a17e9beac793a199f5 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 13 Aug 2026 19:28:13 +0800 Subject: [PATCH] fix(editor): suppress completion after newline --- .../src/components/editor/QueryEditor.vue | 10 +++- .../queryEditorCompletionKeymap.spec.ts | 49 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index d997bd1c6e..d97537a977 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1778,7 +1778,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") codeMirrorKeymap.of([ { key: "Enter", - run: codeMirrorInsertNewlineKeepIndent ?? undefined, + run: insertNewlineWithoutCompletion, }, ...binding(shortcuts.find, openSearch), ...binding(shortcuts.replace, openReplace), @@ -1842,6 +1842,14 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") ]; } +function insertNewlineWithoutCompletion(view: EditorViewType): boolean { + codeMirrorCloseCompletion?.(view); + suppressNextSqlCompletionAutoStartUntil = Date.now() + 750; + const handled = codeMirrorInsertNewlineKeepIndent?.(view) ?? false; + if (!handled) suppressNextSqlCompletionAutoStartUntil = 0; + return handled; +} + function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean { const databaseType = props.databaseType; const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "victoriametrics" ? "text" : "sql"; diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts index 74368fc6a1..8c72204814 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts @@ -46,29 +46,52 @@ interface MockView { interface TabHarness { handleTab: (view: MockView) => boolean; + insertNewlineWithoutCompletion: (view: MockView) => boolean; acceptCompletionOrNextSnippetField: (view: MockView) => boolean; clearPendingCompletionTab: () => void; + consumeSqlCompletionAutoStartSuppression: () => boolean; } -function createHarness(options: { completionStatus: (state: MockState) => "active" | "pending" | null; acceptCompletion?: (view: MockView) => boolean; nextSnippetField?: (view: MockView) => boolean; indentMore?: (view: MockView) => boolean }): TabHarness { +function createHarness(options: { + completionStatus: (state: MockState) => "active" | "pending" | null; + acceptCompletion?: (view: MockView) => boolean; + closeCompletion?: (view: MockView) => boolean; + insertNewlineKeepIndent?: (view: MockView) => boolean; + nextSnippetField?: (view: MockView) => boolean; + indentMore?: (view: MockView) => boolean; +}): TabHarness { const source = [ extractDeclaration(/const COMPLETION_REMOTE_LATENCY_BUDGET_MS = \d+;/, "remote completion latency budget"), extractDeclaration(/const COMPLETION_DEBOUNCE_DELAY_MS = \d+;/, "completion debounce delay"), extractDeclaration(/const COMPLETION_TAB_RETRY_DELAY_MS = \d+;/, "completion retry delay"), extractDeclaration(/const COMPLETION_TAB_MAX_WAIT_MS = [^;]+;/, "completion wait timeout"), "let pendingCompletionTabTimer: ReturnType | null = null;", + "let suppressNextSqlCompletionAutoStartUntil = 0;", extractFunction("editorIndentUnit"), extractFunction("handleTab"), extractFunction("performNormalTab"), + extractFunction("insertNewlineWithoutCompletion"), extractFunction("acceptCompletionOrNextSnippetField"), extractFunction("clearPendingCompletionTab"), extractFunction("waitForCompletionTab"), + extractFunction("consumeSqlCompletionAutoStartSuppression"), ].join("\n"); const javascript = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 }, }).outputText; - const factory = new Function("codeMirrorCompletionStatus", "codeMirrorAcceptCompletion", "codeMirrorNextSnippetField", "codeMirrorIndentMore", "settingsStore", `${javascript}\nreturn { handleTab, acceptCompletionOrNextSnippetField, clearPendingCompletionTab };`); - return factory(options.completionStatus, options.acceptCompletion ?? (() => false), options.nextSnippetField ?? (() => false), options.indentMore ?? (() => false), { editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 } } }) as TabHarness; + const factory = new Function( + "codeMirrorCompletionStatus", + "codeMirrorAcceptCompletion", + "codeMirrorCloseCompletion", + "codeMirrorInsertNewlineKeepIndent", + "codeMirrorNextSnippetField", + "codeMirrorIndentMore", + "settingsStore", + `${javascript}\nreturn { handleTab, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`, + ); + return factory(options.completionStatus, options.acceptCompletion ?? (() => false), options.closeCompletion ?? (() => false), options.insertNewlineKeepIndent ?? (() => false), options.nextSnippetField ?? (() => false), options.indentMore ?? (() => false), { + editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 } }, + }) as TabHarness; } function createView(text = "SELECT", position = text.length): MockView { @@ -89,6 +112,26 @@ afterEach(() => { }); describe("QueryEditor completion Tab keymap", () => { + it("closes completion and suppresses its restart for the Enter newline", () => { + const closeCompletion = vi.fn(() => true); + let harness: TabHarness; + const insertNewlineKeepIndent = vi.fn(() => harness.consumeSqlCompletionAutoStartSuppression()); + harness = createHarness({ completionStatus: () => "active", closeCompletion, insertNewlineKeepIndent }); + const view = createView("SELECT * FROM demo WHERE id IN ()", 32); + + expect(harness.insertNewlineWithoutCompletion(view)).toBe(true); + expect(closeCompletion).toHaveBeenCalledWith(view); + expect(insertNewlineKeepIndent).toHaveBeenCalledWith(view); + expect(harness.consumeSqlCompletionAutoStartSuppression()).toBe(false); + }); + + it("does not suppress later completion when Enter cannot insert a newline", () => { + const harness = createHarness({ completionStatus: () => null, insertNewlineKeepIndent: () => false }); + + expect(harness.insertNewlineWithoutCompletion(createView())).toBe(false); + expect(harness.consumeSqlCompletionAutoStartSuppression()).toBe(false); + }); + it("keeps CodeMirror prefix filtering enabled for SQL completion results", () => { const javascript = ts.transpileModule(extractFunction("buildCompletionResult"), { compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },