fix(completion): preserve longer keyword prefixes

This commit is contained in:
zipg authored and GitHub committed 2026-07-21 17:14:01 +08:00
1 parent a12f8d09b7
commit 51f163ee6c
2 files changed
+42

No files matched your search

@@ -35,6 +35,34 @@ function semanticCompletion(markedSql: string, input: Partial<SqlCompletionProvi
}
describe("semantic SQL completion candidates", () => {
it.each([
["MySQL ORDER BY", "SELECT * FROM t LIMIT 100 or|", "mysql", "mysql", "ORDER BY"],
["PostgreSQL ON CONFLICT", "INSERT INTO t VALUES (1) on|", "postgres", "postgres", "ON CONFLICT"],
["Oracle EXECUTE IMMEDIATE", "exec|", "oracle", undefined, "EXECUTE IMMEDIATE"],
] as const)("keeps the longer %s keyword available before the current token is committed", (_label, sql, databaseType, dialect, expectedKeyword) => {
const { context, items } = semanticCompletion(sql, {}, { databaseType, dialect });
expect(context.suggestKeywords).toBe(false);
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: expectedKeyword, type: "keyword" })]));
});
it("does not offer keyword continuations for qualified column prefixes", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["t", [{ name: "order_number", table: "t" }]]]);
const { context, items } = semanticCompletion("SELECT * FROM t WHERE t.or|", { columnsByTable }, { databaseType: "mysql", dialect: "mysql" });
expect(context.qualifier).toBe("t");
expect(items.some((item) => item.label === "ORDER BY")).toBe(false);
});
it("stops offering ORDER BY as a prefix continuation after OR is committed with whitespace", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["t", [{ name: "id", table: "t" }]]]);
const { context, items } = semanticCompletion("SELECT * FROM t WHERE id = 1 OR |", { columnsByTable }, { databaseType: "mysql", dialect: "mysql" });
expect(context.prefix).toBe("");
expect(items.some((item) => item.label === "ORDER BY")).toBe(false);
expect(items.some((item) => item.label === "id" && item.type === "column")).toBe(true);
});
it("keeps matching functions available in column expressions", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
[
+14
View File
@@ -1393,6 +1393,8 @@ class SqlCompletionProvider {
if (context.suggestKeywords && !context.exclusiveRoutineSuggestions && !pendingJoinKeyword) {
this.items.push(...buildJoinModifierKeywordItems(context.prefix, this.input.keywordCase));
this.items.push(...buildKeywordItems(context.prefix, context, this.databaseType, this.input.keywordCase));
} else if (shouldOfferKeywordPrefixContinuations(context, pendingJoinKeyword)) {
this.items.push(...buildKeywordPrefixContinuationItems(context.prefix, context, this.databaseType, this.input.keywordCase));
}
if (!context.exclusiveTableSuggestions && context.suggestColumns) {
@@ -4147,6 +4149,18 @@ function buildKeywordItems(prefix: string, context: SqlCompletionContext, databa
});
}
function shouldOfferKeywordPrefixContinuations(context: SqlCompletionContext, pendingJoinKeyword: boolean): boolean {
return !!context.prefix && !pendingJoinKeyword && !context.qualifier && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions;
}
function buildKeywordPrefixContinuationItems(prefix: string, context: SqlCompletionContext, databaseType?: DatabaseType, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
const normalizedPrefix = prefix.toLowerCase();
return buildKeywordItems(prefix, context, databaseType, keywordCase).filter((item) => {
const normalizedLabel = item.label.toLowerCase();
return normalizedLabel.length > normalizedPrefix.length && normalizedLabel.startsWith(normalizedPrefix);
});
}
function matchesPrefix(candidate: string, prefix: string): boolean {
if (!prefix) return true;
return computeMatchScore(candidate, prefix) >= 0;