fix(editor): rank exact completion matches first

This commit is contained in:
vrustx
2026-07-10 18:12:35 +08:00
committed by GitHub
parent 024a4aedd6
commit 34cf69132d
2 changed files with 112 additions and 0 deletions
+16
View File
@@ -524,6 +524,7 @@ const EXCLUSIVE_TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "int
const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "full", "natural"]);
const JOIN_MODIFIER_KEYWORD_PHRASES = ["LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "FULL JOIN", "CROSS JOIN", "NATURAL JOIN", "LEFT OUTER JOIN", "RIGHT OUTER JOIN", "FULL OUTER JOIN"];
const MAX_TABLE_COMPLETION_ITEMS = 200;
const EXACT_LABEL_MATCH_BOOST = 10000;
// Keywords that only make sense in DDL / statement-start contexts (not inside SELECT/INSERT/UPDATE/DELETE)
const DDL_ONLY_KEYWORDS = new Set([
@@ -1112,6 +1113,7 @@ export interface SqlCompletionItem {
info?: string;
apply?: string;
boost: number;
exactMatch?: boolean;
}
export type SqlKeywordCase = "preserve" | "upper" | "lower";
@@ -1312,6 +1314,17 @@ class SqlCompletionProvider {
if (starItem) this.items.push(starItem);
}
if (context.prefix) {
for (const item of this.items) {
// Alias snippets reuse the prefix as a label while applying alias SQL, so they are not exact name matches.
const isAliasSnippet = item.type === "snippet" && item.apply === formatAliasCompletionApply(item.label, this.databaseType);
if (!isAliasSnippet && item.label.toLowerCase() === context.prefix.toLowerCase()) {
item.exactMatch = true;
item.boost += EXACT_LABEL_MATCH_BOOST;
}
}
}
return dedupeAndSort(this.items);
}
}
@@ -2714,6 +2727,8 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
detail,
apply: object.type === "trigger" || object.type === "package" ? applyName : `${applyName}()`,
boost: computeBoost(object.name, context.prefix) + (object.type === "procedure" ? 1800 : object.type === "package" ? 1600 : 900),
// Preserve exact routine matches before the capped candidate list is truncated.
exactMatch: !!context.prefix && object.name.toLowerCase() === context.prefix.toLowerCase(),
};
})
.sort(compareCompletionItems)
@@ -3921,6 +3936,7 @@ function dedupeAndSort(items: SqlCompletionItem[]): SqlCompletionItem[] {
}
function compareCompletionItems(left: SqlCompletionItem, right: SqlCompletionItem): number {
if (!left.exactMatch !== !right.exactMatch) return left.exactMatch ? -1 : 1;
const leftBonus = getHistoryBoost(left.label, left.type);
const rightBonus = getHistoryBoost(right.label, right.type);
return right.boost + rightBonus + getTypePriorityBoost(right.type) - (left.boost + leftBonus + getTypePriorityBoost(left.type));
+96
View File
@@ -1065,6 +1065,102 @@ test("ranks exact table matches above prefix and fuzzy matches", () => {
);
});
test("ranks exact keyword matches above prefix-matching routines (#3002)", () => {
const sql = "create table t (id int";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [
{ name: "int_proc_sync", type: "procedure" },
{ name: "sp_interface", type: "procedure" },
{ name: "fn_to_int", type: "function" },
],
columnsByTable: new Map(),
databaseType: "sqlserver",
});
assert.equal(items[0]?.label, "INT");
assert.equal(items[0]?.type, "keyword");
});
test("preserves an exact routine match before truncating candidates", () => {
const sql = "select aaa";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [
...Array.from({ length: 200 }, (_, index) => ({ name: `a_a_a_${index}`, type: "procedure" as const })),
{ name: "aaa", type: "function" },
],
columnsByTable: new Map(),
});
assert.equal(items[0]?.label, "aaa");
assert.equal(items[0]?.type, "function");
});
test("ranks an exact table label match first", () => {
const sql = "select * from orders";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [
{ name: "orders", type: "table" },
{ name: "orders_archive", type: "table" },
],
columnsByTable: new Map(),
});
assert.equal(items[0]?.label, "orders");
assert.equal(items[0]?.type, "table");
});
test("ranks an exact column match above an exact keyword match", () => {
const sql = "select limit from t";
const items = buildSqlCompletionItems(sql, "select limit".length, {
tables: [{ name: "t", type: "table" }],
columnsByTable: new Map([["t", [{ name: "limit", table: "t", dataType: "text" }]]]),
});
const columnIndex = items.findIndex((item) => item.type === "column" && item.label === "limit");
const keywordIndex = items.findIndex((item) => item.type === "keyword" && item.label === "LIMIT");
assert.ok(columnIndex >= 0);
assert.ok(keywordIndex >= 0);
assert.ok(columnIndex < keywordIndex);
});
test("ranks an exact table match above foreign-key related prefix matches", () => {
const sql = "select * from orders join customers";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [
{ name: "orders", type: "table" },
{ name: "customers", type: "table" },
{ name: "customers_ext", type: "table" },
],
columnsByTable: new Map(),
foreignKeysByTable: new Map([["orders", [{ column: "customer_id", ref_table: "customers_ext", ref_column: "id" }]]]),
});
assert.equal(items[0]?.label, "customers");
assert.equal(items[0]?.type, "table");
});
test("ranks an exact match first even when a fuzzy candidate outscores the numeric boost", () => {
// A long tight-fuzzy foreign-key candidate accumulates boundary bonuses beyond
// EXACT_LABEL_MATCH_BOOST; only the exactMatch comparator key keeps the exact item first.
const exactName = "ab".repeat(300);
const fuzzyName = Array.from({ length: 300 }, () => "ab").join("_");
const sql = `select * from orders join ${exactName}`;
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [
{ name: "orders", type: "table" },
{ name: exactName, type: "table" },
{ name: fuzzyName, type: "table" },
],
columnsByTable: new Map(),
foreignKeysByTable: new Map([["orders", [{ column: "x_id", ref_table: fuzzyName, ref_column: "id" }]]]),
});
assert.equal(items[0]?.label, exactName);
assert.equal(items[0]?.type, "table");
});
test("does not reuse table completion results across typed prefixes", () => {
const validFor = getSqlCompletionResultValidFor("select * from ", "select * from ".length);