From 15631731730c7bfeac425015dcf63596d30d8702 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 15 May 2026 14:56:26 +0800 Subject: [PATCH] fix(editor): suppress completion after punctuation --- src/components/editor/QueryEditor.vue | 18 +++++++++++++++--- src/lib/sqlCompletion.ts | 6 ++++++ tests/sqlCompletion.test.ts | 13 +++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/editor/QueryEditor.vue b/src/components/editor/QueryEditor.vue index 12707cff3..b06b0df65 100644 --- a/src/components/editor/QueryEditor.vue +++ b/src/components/editor/QueryEditor.vue @@ -6,7 +6,11 @@ import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; -import { buildSqlCompletionItemsFromContext, getSqlCompletionContext } from "@/lib/sqlCompletion"; +import { + buildSqlCompletionItemsFromContext, + getSqlCompletionContext, + shouldAutoOpenSqlCompletion, +} from "@/lib/sqlCompletion"; import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigation"; import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes"; import type { SqlCompletionColumn } from "@/lib/sqlCompletion"; @@ -122,11 +126,17 @@ async function formatCurrentSql() { } } -async function provideSqlCompletions(currentState: import("@codemirror/state").EditorState, position: number) { +async function provideSqlCompletions( + currentState: import("@codemirror/state").EditorState, + position: number, + explicit: boolean, +) { if (!props.connectionId || !props.database) return null; try { const fullDoc = currentState.doc.toString(); + if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position)) return null; + const completionContext = getSqlCompletionContext(fullDoc, position); const shouldLoadTables = completionContext.suggestTables || !!completionContext.qualifier; let tables = shouldLoadTables @@ -338,7 +348,9 @@ onMounted(async () => { sql({ dialect }), autocompletion({ activateOnTyping: true, - override: [async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos)], + override: [ + async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos, context.explicit), + ], }), codeMirrorTheme.of(theme), closeBrackets(), diff --git a/src/lib/sqlCompletion.ts b/src/lib/sqlCompletion.ts index 37cae77cf..5f8abef22 100644 --- a/src/lib/sqlCompletion.ts +++ b/src/lib/sqlCompletion.ts @@ -333,6 +333,12 @@ export function buildSqlCompletionItemsFromContext( return dedupeAndSort(items); } +export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolean { + const previousChar = sql[cursor - 1]; + if (!previousChar) return false; + return /[\w$.]/.test(previousChar); +} + /** * Find the start position of the SQL statement containing the cursor. * Respects semicolons and string literals. diff --git a/tests/sqlCompletion.test.ts b/tests/sqlCompletion.test.ts index a3e84ff47..01a4027e5 100644 --- a/tests/sqlCompletion.test.ts +++ b/tests/sqlCompletion.test.ts @@ -2,6 +2,7 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { buildSqlCompletionItems, + shouldAutoOpenSqlCompletion, type SqlCompletionColumn, type SqlCompletionTable, } from "../src/lib/sqlCompletion.ts"; @@ -159,6 +160,18 @@ test("suggests SQL Server data types in CREATE TABLE column definitions", () => assert.ok(items.some((item) => item.type === "keyword" && item.label === "NVARCHAR")); }); +test("does not auto-open completion after structural punctuation", () => { + for (const sql of ["select count(*)", "select * from users;", "select * from users,"]) { + assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), false, sql); + } +}); + +test("auto-opens completion after word characters and explicit dot qualifiers", () => { + for (const sql of ["sel", "select * from us", "select u."]) { + assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true, sql); + } +}); + test("limits table suggestions for large schemas after filtering by prefix", () => { const largeTables: SqlCompletionTable[] = Array.from({ length: 500 }, (_, index) => ({ name: `erp_invoice_${String(index).padStart(4, "0")}`,