fix(editor): restore custom snippet trigger matching

This commit is contained in:
zipg authored and GitHub committed 2026-07-30 00:08:04 +08:00
1 parent 682b9d53c5
commit 87ef514188
5 files changed
+80 -2

No files matched your search

@@ -65,6 +65,7 @@ import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme,
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
@@ -2490,12 +2491,14 @@ function shouldInsertSqlCompletionSpace(): boolean {
}
function completionOptionForItem(item: QueryCompletionItem) {
const filterText = "filterText" in item && typeof item.filterText === "string" ? item.filterText : undefined;
const labelPresentation = completionLabelPresentation(item.label, filterText);
const record = () => {
recordCompletionSelection(item.label, item.type);
};
if ((item.type === "snippet" || item.type === "function") && item.apply) {
const completion = codeMirrorSnippetCompletion(item.apply, {
label: item.label,
...labelPresentation,
type: item.type,
detail: item.detail,
info: item.info,
@@ -2520,7 +2523,7 @@ function completionOptionForItem(item: QueryCompletionItem) {
};
}
return {
label: item.label,
...labelPresentation,
type: item.type,
detail: item.detail,
info: item.info,
@@ -0,0 +1,46 @@
// @vitest-environment happy-dom
import { acceptCompletion, autocompletion, completionStatus, currentCompletions, snippetCompletion, startCompletion } from "@codemirror/autocomplete";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { describe, expect, it } from "vitest";
import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
describe("SQL completion label presentation", () => {
it("filters a custom snippet by trigger while preserving its display label", () => {
expect(completionLabelPresentation("SELECT *", "ssf")).toEqual({
label: "ssf SELECT *",
displayLabel: "SELECT *",
sortText: "SELECT *",
});
});
it("leaves ordinary completion labels unchanged", () => {
expect(completionLabelPresentation("SELECT")).toEqual({ label: "SELECT" });
expect(completionLabelPresentation("select *", "sel")).toEqual({ label: "select *" });
});
it("keeps a differently named snippet visible and applicable in CodeMirror", async () => {
const presentation = completionLabelPresentation("SELECT *", "ssf");
const view = new EditorView({
parent: document.createElement("div"),
state: EditorState.create({
doc: "ssf",
selection: { anchor: 3 },
extensions: [
autocompletion({
interactionDelay: 0,
override: [() => ({ from: 0, options: [snippetCompletion("SELECT * FROM", { ...presentation, type: "snippet" })] })],
}),
],
}),
});
startCompletion(view);
await expect.poll(() => completionStatus(view.state)).toBe("active");
expect(currentCompletions(view.state)[0]).toMatchObject({ label: "ssf SELECT *", displayLabel: "SELECT *" });
expect(acceptCompletion(view)).toBe(true);
expect(view.state.doc.toString()).toBe("SELECT * FROM");
view.destroy();
});
});
@@ -50,6 +50,18 @@ describe("buildSnippetItems", () => {
expect(items[0].apply).toBe("SELECT *\nFROM my_table;");
});
it("keeps a distinct trigger available to CodeMirror filtering", () => {
const items = buildSnippetItemsForTest("ssf", [{ id: "custom", label: "SELECT *", prefix: "ssf", body: "SELECT * FROM" }]);
expect(items).toEqual([
expect.objectContaining({
label: "SELECT *",
filterText: "ssf",
type: "snippet",
}),
]);
});
it("uses CodeMirror placeholders for the built-in select snippet", () => {
const items = buildSnippetItemsForTest("sel", [BUILTIN_SELECT]);
@@ -0,0 +1,15 @@
export interface CompletionLabelPresentation {
label: string;
displayLabel?: string;
sortText?: string;
}
export function completionLabelPresentation(label: string, filterText?: string): CompletionLabelPresentation {
const normalizedFilterText = filterText?.trim();
if (!normalizedFilterText || label.toLowerCase().startsWith(normalizedFilterText.toLowerCase())) return { label };
return {
label: `${normalizedFilterText} ${label}`,
displayLabel: label,
sortText: label,
};
}
@@ -1191,6 +1191,7 @@ export interface SqlCompletionForeignKey {
export interface SqlCompletionItem {
label: string;
filterText?: string;
type: "keyword" | "table" | "column" | "snippet" | "function" | "schema";
detail?: string;
info?: string;
@@ -3971,6 +3972,7 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet, resolvedBody), keywordCase);
return {
label: snippet.label,
filterText: snippet.prefix,
type: "snippet" as const,
detail: body,
apply,