fix(editor): refresh SQL snippet completion after settings changes
This commit is contained in:
parent
aaa6cb177c
commit
915728ebec
|
|
@ -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<SqlSemanticDiagnostic[]> | 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<string, SqlCompletionColumn[]>();
|
||||
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<SqlSemanticDiagnostic[]>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
|
|||
|
|
@ -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<str
|
|||
const paramStr = parameters.length > 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<str
|
|||
if (!matchesPrefix(name, prefix)) continue;
|
||||
items.push({
|
||||
label: name,
|
||||
type: "snippet" as const,
|
||||
type: "function" as const,
|
||||
detail: "window function",
|
||||
apply: `${name}() OVER (PARTITION BY \${col} ORDER BY \${col})`,
|
||||
boost: computeBoost(name, prefix) + 250,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync("apps/desktop/src/components/editor/QueryEditor.vue", "utf8");
|
||||
|
||||
test("query editor reconfigures autocompletion when snippets change", () => {
|
||||
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);
|
||||
});
|
||||
Loading…
Reference in New Issue