From 915728ebece1b38bebfbc24f3c2463f409e2ee1f Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 27 May 2026 17:43:05 +0800 Subject: [PATCH] fix(editor): refresh SQL snippet completion after settings changes --- .../src/components/editor/QueryEditor.vue | 72 ++++++++++++++++--- .../__tests__/sqlCompletion.snippet.spec.ts | 15 ++++ apps/desktop/src/lib/sqlCompletion.ts | 16 +++-- .../queryEditorSnippetRefresh.test.ts | 22 ++++++ 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 packages/app-tests/queryEditorSnippetRefresh.test.ts diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index f317a2415..b79e0089c 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -145,12 +145,15 @@ let codeMirrorTheme: import("@codemirror/state").Compartment | null = null; let wordWrapComp: import("@codemirror/state").Compartment | null = null; let readOnlyComp: import("@codemirror/state").Compartment | null = null; let runKeymapComp: import("@codemirror/state").Compartment | null = null; +let completionComp: import("@codemirror/state").Compartment | null = null; let diagnosticComp: import("@codemirror/state").Compartment | null = null; let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | null = null; let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null; +let buildSqlCompletionExtension: (() => import("@codemirror/state").Extension) | null = null; let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion; let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null; let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null; +let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null; let codeMirrorIndentMore: typeof import("@codemirror/commands").indentMore | null = null; let codeMirrorInsertNewlineKeepIndent: typeof import("@codemirror/commands").insertNewlineKeepIndent | null = null; let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType | null = null; @@ -698,7 +701,8 @@ async function provideSqlCompletions( position: number, explicit: boolean, ) { - if (!props.connectionId || props.database == null) return null; + if (!props.connectionId) return null; + const hasDatabase = props.database != null; const epoch = ++completionEpoch; @@ -708,6 +712,37 @@ async function provideSqlCompletions( const completionContext = getSqlCompletionContext(fullDoc, position); + if (!hasDatabase) { + const items = buildSqlCompletionItemsFromContext(completionContext, { + tables: [], + columnsByTable: new Map(), + schemas: [], + translations: completionTranslations.value, + snippets: settingsStore.editorSettings.snippets, + }); + if (items.length === 0) return null; + return { + from: position - completionContext.prefix.length, + filter: false, + options: items.map((item) => + (item.type === "snippet" || item.type === "function") && item.apply + ? codeMirrorSnippetCompletion(item.apply, { + label: item.label, + type: item.type, + detail: item.detail, + boost: item.boost, + }) + : { + label: item.label, + type: item.type, + detail: item.detail, + boost: item.boost, + }, + ), + validFor: getSqlCompletionResultValidFor(fullDoc, position), + }; + } + // Handle INSERT column list: fetch columns for the target table let insertColumnsByTable = new Map(); if (completionContext.insertTable) { @@ -889,8 +924,9 @@ async function provideSqlCompletions( return { from: position - completionContext.prefix.length, + filter: false, options: items.map((item) => - item.type === "snippet" && item.apply + (item.type === "snippet" || item.type === "function") && item.apply ? codeMirrorSnippetCompletion(item.apply, { label: item.label, type: item.type, @@ -968,10 +1004,12 @@ onMounted(async () => { wordWrapComp = new Compartment(); readOnlyComp = new Compartment(); runKeymapComp = new Compartment(); + completionComp = new Compartment(); diagnosticComp = new Compartment(); setSqlDiagnosticsEffect = StateEffect.define(); codeMirrorCompletionStatus = completionStatus; codeMirrorAcceptCompletion = acceptCompletion; + codeMirrorStartCompletion = startCompletion; codeMirrorIndentMore = indentMore; codeMirrorInsertNewlineKeepIndent = insertNewlineKeepIndent; @@ -1029,6 +1067,14 @@ onMounted(async () => { }; }); + buildSqlCompletionExtension = () => + autocompletion({ + activateOnTyping: true, + override: [ + async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos, context.explicit), + ], + }); + const ss = settingsStore.editorSettings; const baseDialect = props.dialect === "postgres" ? PostgreSQL : props.dialect === "sqlserver" ? MSSQL : MySQL; @@ -1095,12 +1141,7 @@ onMounted(async () => { keymap.of([...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap]), sql({ dialect }), tooltips({ parent: document.body }), - autocompletion({ - activateOnTyping: true, - override: [ - async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos, context.explicit), - ], - }), + completionComp.of(buildSqlCompletionExtension()), sqlCompletionTheme(EditorView), codeMirrorTheme.of(theme), closeBrackets(), @@ -1373,6 +1414,21 @@ watch( { deep: true }, ); +watch( + () => settingsStore.editorSettings.snippets, + () => { + completionEpoch++; + if (!view.value || !completionComp || !buildSqlCompletionExtension) return; + view.value.dispatch({ + effects: completionComp.reconfigure(buildSqlCompletionExtension()), + }); + if (codeMirrorCompletionStatus?.(view.value.state) === "active") { + codeMirrorStartCompletion?.(view.value); + } + }, + { deep: true }, +); + onBeforeUnmount(() => { zoomCommitScheduler.dispose(); if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer); diff --git a/apps/desktop/src/lib/__tests__/sqlCompletion.snippet.spec.ts b/apps/desktop/src/lib/__tests__/sqlCompletion.snippet.spec.ts index 1e0a53cfa..a949fcdfb 100644 --- a/apps/desktop/src/lib/__tests__/sqlCompletion.snippet.spec.ts +++ b/apps/desktop/src/lib/__tests__/sqlCompletion.snippet.spec.ts @@ -21,6 +21,21 @@ describe("buildSnippetItems", () => { expect(items[0].label).toBe("select all"); }); + it("does not keep matching a renamed snippet by its old short label prefix", () => { + const items = buildSnippetItemsForTest("sel", [ + { id: "1", label: "select all", prefix: "fff", body: "SELECT *\nFROM my_table;" }, + ]); + expect(items).toEqual([]); + }); + + it("still matches a renamed snippet by label when typing a longer descriptive query", () => { + const items = buildSnippetItemsForTest("select", [ + { id: "1", label: "select all", prefix: "fff", body: "SELECT *\nFROM my_table;" }, + ]); + expect(items).toHaveLength(1); + expect(items[0].label).toBe("select all"); + }); + it("returns empty for no match", () => { const items = buildSnippetItemsForTest("zzz", TEST_SNIPPETS); expect(items).toEqual([]); diff --git a/apps/desktop/src/lib/sqlCompletion.ts b/apps/desktop/src/lib/sqlCompletion.ts index 990decb12..ae8c237f6 100644 --- a/apps/desktop/src/lib/sqlCompletion.ts +++ b/apps/desktop/src/lib/sqlCompletion.ts @@ -610,7 +610,7 @@ export interface SqlCompletionColumn { export interface SqlCompletionItem { label: string; - type: "keyword" | "table" | "column" | "snippet" | "schema"; + type: "keyword" | "table" | "column" | "snippet" | "function" | "schema"; detail?: string; apply?: string; boost: number; @@ -1934,16 +1934,20 @@ export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[]) function buildSnippetItems(prefix: string, snippets: SqlSnippet[]): SqlCompletionItem[] { if (!prefix) return []; return snippets - .filter((snippet) => matchesPrefix(snippet.prefix, prefix) || matchesPrefix(snippet.label, prefix)) + .filter((snippet) => { + const matchesSnippetPrefix = matchesPrefix(snippet.prefix, prefix); + const matchesSnippetLabel = prefix.length > snippet.prefix.length && matchesPrefix(snippet.label, prefix); + return matchesSnippetPrefix || matchesSnippetLabel; + }) .map((snippet) => { const boostByPrefix = computeBoost(snippet.prefix, prefix); const boostByLabel = computeBoost(snippet.label, prefix); return { label: snippet.label, type: "snippet" as const, - detail: snippet.label, + detail: snippet.body, apply: snippet.body, - boost: Math.max(boostByPrefix, boostByLabel) - 1100, + boost: Math.max(boostByPrefix, boostByLabel) + 4000, }; }); } @@ -1956,7 +1960,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map 0 ? parameters.map((p) => `\${${p}}`).join(", ") : ""; items.push({ label: name, - type: "snippet" as const, + type: "function" as const, detail: functionDescriptions.get(name) ?? "function", apply: `${name}(${paramStr})`, boost: computeBoost(name, prefix) + 300, @@ -1968,7 +1972,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map { + assert.match(source, /let completionComp: import\("@codemirror\/state"\)\.Compartment \| null = null;/); + assert.match( + source, + /let buildSqlCompletionExtension: \(\(\) => import\("@codemirror\/state"\)\.Extension\) \| null = null;/, + ); + assert.match(source, /completionComp = new Compartment\(\);/); + assert.match(source, /completionComp\.of\(buildSqlCompletionExtension\(\)\)/); + assert.match(source, /\(\) => settingsStore\.editorSettings\.snippets/); + assert.match(source, /completionComp\.reconfigure\(buildSqlCompletionExtension\(\)\)/); + assert.match(source, /codeMirrorStartCompletion\?\.\(view\.value\)/); +}); + +test("query editor disables CodeMirror label re-filtering for custom SQL completions", () => { + assert.match(source, /from: position - completionContext\.prefix\.length,\s*filter: false,\s*options:/s); +});