mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(editor): rank exact completion matches first
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user