feat(sql): add placeholders to built-in snippets

This commit is contained in:
hb 2026-06-24 23:10:43 +08:00 committed by GitHub
parent c8464bf842
commit fa29d4bbde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 9 deletions

View File

@ -7,14 +7,74 @@ const TEST_SNIPPETS: SqlSnippet[] = [
{ id: "2", label: "insert row", prefix: "ins", body: "INSERT INTO my_table VALUES (1);" },
];
const BUILTIN_SELECT: SqlSnippet = { id: "builtin-sel", label: "select *", prefix: "sel", body: "SELECT *\nFROM table\nLIMIT 100;" };
const BUILTIN_CREATE_TABLE: SqlSnippet = { id: "builtin-ct", label: "create table", prefix: "ct", body: "CREATE TABLE table (\n column type\n);" };
const BUILTIN_UPDATE: SqlSnippet = { id: "builtin-upd", label: "update set", prefix: "upd", body: "UPDATE table\nSET column = value\nWHERE condition;" };
const BUILTIN_CTE: SqlSnippet = { id: "builtin-cte", label: "common table expression", prefix: "cte", body: "WITH name AS (\n SELECT columns\n FROM table\n)\nSELECT *\nFROM name;" };
const BUILTIN_INSERT: SqlSnippet = { id: "builtin-ins", label: "insert into", prefix: "ins", body: "INSERT INTO table (columns)\nVALUES (values);" };
const BUILTIN_JOIN: SqlSnippet = { id: "builtin-join", label: "join", prefix: "join", body: "JOIN table ON left_column = right_column" };
const BUILTIN_CASE: SqlSnippet = { id: "builtin-case", label: "case when", prefix: "case", body: "CASE\n WHEN condition THEN value\n ELSE default\nEND" };
const BUILTIN_CREATE_INDEX: SqlSnippet = { id: "builtin-ci", label: "create index", prefix: "ci", body: "CREATE INDEX idx_name\nON table (column);" };
describe("buildSnippetItems", () => {
it("returns matching snippet by prefix", () => {
const items = buildSnippetItemsForTest("sel", TEST_SNIPPETS);
expect(items).toHaveLength(1);
expect(items[0].label).toBe("select all");
expect(items[0].detail).toBe("SELECT *\nFROM my_table;");
expect(items[0].apply).toBe("SELECT *\nFROM my_table;");
});
it("uses CodeMirror placeholders for the built-in select snippet", () => {
const items = buildSnippetItemsForTest("sel", [BUILTIN_SELECT]);
expect(items[0].detail).toBe("SELECT *\nFROM table\nLIMIT 100;");
expect(items[0].apply).toBe("SELECT *\nFROM ${table}\nLIMIT 100;");
});
it("replaces placeholder words but preserves uppercase SQL keywords", () => {
const items = buildSnippetItemsForTest("ct", [BUILTIN_CREATE_TABLE]);
expect(items[0].apply).toBe("CREATE TABLE ${table} (\n ${column} ${type}\n);");
});
it("adds placeholders across built-in update snippets", () => {
const items = buildSnippetItemsForTest("upd", [BUILTIN_UPDATE]);
expect(items[0].apply).toBe("UPDATE ${table}\nSET ${column} = ${value}\nWHERE ${condition};");
});
it("keeps keyword casing separate from editable placeholders", () => {
const items = buildSnippetItemsForTest("sel", [BUILTIN_SELECT], "lower");
expect(items[0].detail).toBe("select *\nfrom table\nlimit 100;");
expect(items[0].apply).toBe("select *\nfrom ${table}\nlimit 100;");
});
it("determines placeholders from original casing even when keywords are lowercased", () => {
const items = buildSnippetItemsForTest("ct", [BUILTIN_CREATE_TABLE], "lower");
expect(items[0].detail).toBe("create table table (\n column type\n);");
expect(items[0].apply).toBe("create table ${table} (\n ${column} ${type}\n);");
});
it("wraps repeated placeholder words in CTE snippets", () => {
const items = buildSnippetItemsForTest("cte", [BUILTIN_CTE]);
expect(items[0].apply).toBe("WITH ${name} AS (\n SELECT ${columns}\n FROM ${table}\n)\nSELECT *\nFROM ${name};");
});
it.each([
[BUILTIN_INSERT, "INSERT INTO ${table} (${columns})\nVALUES (${values});"],
[BUILTIN_JOIN, "JOIN ${table} ON ${left_column} = ${right_column}"],
[BUILTIN_CASE, "CASE\n WHEN ${condition} THEN ${value}\n ELSE ${default}\nEND"],
[BUILTIN_CREATE_INDEX, "CREATE INDEX ${idx_name}\nON ${table} (${column});"],
])("adds placeholders for built-in %s snippet", (snippet, expectedApply) => {
const items = buildSnippetItemsForTest(snippet.prefix, [snippet]);
expect(items[0].apply).toBe(expectedApply);
});
it("returns matching snippet by label substring", () => {
const items = buildSnippetItemsForTest("select", TEST_SNIPPETS);
expect(items).toHaveLength(1);

View File

@ -2523,10 +2523,17 @@ function shouldFormatBuiltinSnippet(snippet: SqlSnippet): boolean {
return snippet.id.startsWith("builtin-");
}
function applyBuiltinSnippetKeywordCase(snippet: SqlSnippet, keywordCase?: SqlKeywordCase): string {
function applyBuiltinSnippetKeywordCase(snippet: SqlSnippet, text: string, keywordCase?: SqlKeywordCase): string {
if (!shouldFormatBuiltinSnippet(snippet)) return text;
if (keywordCase === "lower") return text.toLowerCase();
return text;
}
const BUILTIN_SNIPPET_PLACEHOLDER_RE = /\b(idx_name|left_column|right_column|columns|values|condition|column|default|value|name|type|table)\b/g;
function applyBuiltinSnippetPlaceholders(snippet: SqlSnippet): string {
if (!shouldFormatBuiltinSnippet(snippet)) return snippet.body;
if (keywordCase === "lower") return snippet.body.toLowerCase();
return snippet.body;
return snippet.body.replace(BUILTIN_SNIPPET_PLACEHOLDER_RE, (match) => `\${${match}}`);
}
function buildPreferredKeywordItems(prefix: string, keywords: string[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
@ -3209,8 +3216,8 @@ function singularTableName(name: string): string {
return lower;
}
export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[]): SqlCompletionItem[] {
return buildSnippetItems(prefix, snippets);
export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
return buildSnippetItems(prefix, snippets, keywordCase);
}
function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
@ -3229,12 +3236,15 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
// they are likely typing the actual keyword — reduce the base boost so
// the real keyword can rank higher.
const baseBoost = matchesByPrefix ? 4000 : 0;
const body = applyBuiltinSnippetKeywordCase(snippet, keywordCase);
// Placeholder replacement runs on the original (UPPER-case) body first,
// then keyword casing is applied to both variants uniformly.
const body = applyBuiltinSnippetKeywordCase(snippet, snippet.body, keywordCase);
const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet), keywordCase);
return {
label: snippet.label,
type: "snippet" as const,
detail: body,
apply: body,
apply,
boost: Math.max(boostByPrefix, boostByLabel) + baseBoost,
};
});

View File

@ -788,7 +788,8 @@ test("suggests SQL snippets for common abbreviations", () => {
const snippet = items.find((item) => item.type === "snippet" && item.label === "select *");
assert.ok(snippet);
assert.equal(snippet.apply, "SELECT *\nFROM table\nLIMIT 100;");
assert.equal(snippet.detail, "SELECT *\nFROM table\nLIMIT 100;");
assert.equal(snippet.apply, "SELECT *\nFROM ${table}\nLIMIT 100;");
});
test("applies keyword case to built-in SQL snippets", () => {
@ -800,7 +801,8 @@ test("applies keyword case to built-in SQL snippets", () => {
const snippet = items.find((item) => item.type === "snippet" && item.label === "select *");
assert.ok(snippet);
assert.equal(snippet.apply, "select *\nfrom table\nlimit 100;");
assert.equal(snippet.detail, "select *\nfrom table\nlimit 100;");
assert.equal(snippet.apply, "select *\nfrom ${table}\nlimit 100;");
});
test("suggests DATE_FORMAT as parameter snippet", () => {