feat(completion): expand INSERT all-columns with VALUES tab stops

This commit is contained in:
zipg 2026-08-09 23:24:36 +08:00 committed by GitHub
parent 295c260a72
commit 5966aea43b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 26 additions and 12 deletions

View File

@ -417,7 +417,17 @@ WHERE a.id = b.fk_kpi_set_score_id`,
const { context, items } = semanticCompletion("INSERT INTO users (|", { columnsByTable });
expect(context.insertTable).toBe("users");
expect(items.find((item) => item.type === "snippet" && item.label === "users.*")?.apply).toBe("id, name, email");
const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*");
expect(allColumns?.apply).toBe("id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})");
expect(allColumns?.detail).toBe("3 columns: id, name, email) VALUES (value, value, value)");
});
it("uses the configured keyword case for INSERT all-column snippets", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["users", ["id", "name"].map((name) => ({ name, table: "users" }))]]);
const { items } = semanticCompletion("insert into users (|", { columnsByTable, keywordCase: "lower" });
expect(items.find((item) => item.type === "snippet" && item.label === "users.*")?.apply).toBe("id, name) values (${1:value}, ${2:value})");
});
it("keeps partial INSERT INTO targets in table completion context", () => {

View File

@ -1412,7 +1412,7 @@ class SqlCompletionProvider {
if (!context.exclusiveTableSuggestions && context.suggestColumns) {
this.items.push(...buildColumnItems(context, this.input.columnsByTable, this.dialect));
this.items.push(...buildSelectAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect));
this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect));
this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.input.keywordCase));
}
const emptyTableNameCompletion = !context.prefix && (context.suggestTables || context.exclusiveTableSuggestions);
@ -3308,7 +3308,7 @@ function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable
return items;
}
function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] {
function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
if (!context.insertTable) return [];
const columns = uniqueColumnsByName(columnsForInsertTarget(context, columnsByTable));
if (columns.length === 0) return [];
@ -3316,13 +3316,17 @@ function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable
const label = `${context.insertTable}.*`;
if (!selectAllColumnItemMatchesPrefix(label, { name: context.insertTable, schema: context.insertSchema }, columns, context.prefix)) return [];
const expansion = columns.map((column) => quoteSqlIdentifier(column.name, dialect)).join(", ");
const columnList = columns.map((column) => quoteSqlIdentifier(column.name, dialect)).join(", ");
const valuesKeyword = applySqlKeywordCase("VALUES", keywordCase);
const valueList = columns.map((_, index) => `\${${index + 1}:value}`).join(", ");
const expansion = `${columnList}) ${valuesKeyword} (${valueList})`;
const preview = `${columnList}) ${valuesKeyword} (${columns.map(() => "value").join(", ")})`;
const countText = (t?.starExpansionColumns ?? "{count} columns").replace("{count}", String(columns.length));
return [
{
label,
type: "snippet" as const,
detail: `${countText}: ${expansion.length > 60 ? expansion.slice(0, 57) + "..." : expansion}`,
detail: `${countText}: ${preview.length > 60 ? preview.slice(0, 57) + "..." : preview}`,
apply: expansion,
boost: 2450 + selectAllColumnItemPrefixBoost(label, { name: context.insertTable, schema: context.insertSchema }, columns, context.prefix),
},

View File

@ -2291,7 +2291,7 @@ test("suggests INSERT columns for a SQL Server three-part target", () => {
dialect: "sqlserver",
});
assert.equal(items.find((item) => item.type === "snippet" && item.label === "orders.*")?.apply, "target_marker");
assert.equal(items.find((item) => item.type === "snippet" && item.label === "orders.*")?.apply, "target_marker) VALUES (${1:value})");
});
test("detects MySQL backtick-qualified INSERT INTO column list context", () => {
@ -2339,7 +2339,7 @@ test("suggests all target columns for INSERT INTO column list", () => {
const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, "id, name, email");
assert.equal(allColumns.apply, "id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})");
});
test("keeps INSERT INTO all-column expansion available after a column prefix", () => {
@ -2350,7 +2350,7 @@ test("keeps INSERT INTO all-column expansion available after a column prefix", (
const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, "id, name, email");
assert.equal(allColumns.apply, "id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})");
});
test("quotes PostgreSQL identifiers in INSERT INTO all-column expansion", () => {
@ -2364,7 +2364,7 @@ test("quotes PostgreSQL identifiers in INSERT INTO all-column expansion", () =>
const allColumns = items.find((item) => item.type === "snippet" && item.label === "OrderLines.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, 'article, "OrderId", "User", "has""quote"');
assert.equal(allColumns.apply, 'article, "OrderId", "User", "has""quote") VALUES (${1:value}, ${2:value}, ${3:value}, ${4:value})');
});
test("suggests all target columns for schema-qualified INSERT INTO column lists", () => {
@ -2386,7 +2386,7 @@ test("suggests all target columns for schema-qualified INSERT INTO column lists"
const allColumns = items.find((item) => item.type === "snippet" && item.label === "Users.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, "Id, DisplayName");
assert.equal(allColumns.apply, "Id, DisplayName) VALUES (${1:value}, ${2:value})");
});
test("scopes INSERT INTO all-column expansion to the database-qualified MySQL target", () => {
@ -2400,7 +2400,7 @@ test("scopes INSERT INTO all-column expansion to the database-qualified MySQL ta
const allColumns = items.find((item) => item.type === "snippet" && item.label === "orders.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, "id, number, status");
assert.equal(allColumns.apply, "id, number, status) VALUES (${1:value}, ${2:value}, ${3:value})");
});
test("suggests all target columns for MySQL backtick-qualified INSERT INTO", () => {
@ -2414,7 +2414,7 @@ test("suggests all target columns for MySQL backtick-qualified INSERT INTO", ()
const allColumns = items.find((item) => item.type === "snippet" && item.label === "orders.*");
assert.ok(allColumns);
assert.equal(allColumns.apply, "id, number, status");
assert.equal(allColumns.apply, "id, number, status) VALUES (${1:value}, ${2:value}, ${3:value})");
});
// --- Column data type in detail ---