From ed810d6c92cf19046a81b1118316c60a564363aa Mon Sep 17 00:00:00 2001 From: Eve <92513238+Eve-z1@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:12:42 +0800 Subject: [PATCH] fix(completion): always offer custom sql snippets --- .../sql/sqlCompletion.snippet.spec.ts | 56 +++++++++++++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 18 ++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.snippet.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.snippet.spec.ts index 267c1bce44..9fc700dc59 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.snippet.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.snippet.spec.ts @@ -128,6 +128,62 @@ describe("buildSnippetItems", () => { expect(items.findIndex((item) => item.label === "SELECT" && item.type === "keyword")).toBeGreaterThan(0); }); + it("keeps an exact custom snippet first when matching columns are preferred", () => { + const sql = "SELECT * FROM users WHERE cc"; + const items = buildSqlCompletionItems(sql, sql.length, { + snippets: [{ id: "custom-count", label: "count zero", prefix: "cc", body: "count(0)" }], + tables: [{ name: "users", type: "table" }], + columnsByTable: new Map([["users", [{ name: "country_code", table: "users" }]]]), + databaseType: "mysql", + }); + + expect(items[0]).toEqual( + expect.objectContaining({ + label: "count zero", + filterText: "cc", + type: "snippet", + apply: "count(0)", + exactMatch: true, + }), + ); + }); + + it("keeps an exact custom snippet ahead of an exact table alias", () => { + const sql = "SELECT * FROM country_codes cc WHERE cc"; + const items = buildSqlCompletionItems(sql, sql.length, { + snippets: [{ id: "custom-count", label: "count zero", prefix: "cc", body: "count(0)" }], + tables: [{ name: "country_codes", type: "table" }], + columnsByTable: new Map(), + databaseType: "mysql", + }); + + expect(items[0]).toEqual(expect.objectContaining({ label: "count zero", type: "snippet", exactMatch: true })); + }); + + it("offers custom snippets in preferred SQL value contexts", () => { + const sql = "SELECT DATEADD(cc"; + const items = buildSqlCompletionItems(sql, sql.length, { + snippets: [{ id: "custom-count", label: "count zero", prefix: "cc", body: "count(0)" }], + tables: [], + columnsByTable: new Map(), + databaseType: "sqlserver", + }); + + expect(items[0]).toEqual(expect.objectContaining({ label: "count zero", type: "snippet", apply: "count(0)", exactMatch: true })); + }); + + it("keeps an exact database routine ahead of its generic function", () => { + const sql = "SELECT COUNT"; + const items = buildSqlCompletionItems(sql, sql.length, { + objects: [{ name: "COUNT", type: "function", signature: "value" }], + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + + expect(items.find((item) => item.label === "COUNT")?.apply).toBe("`COUNT`(${1:value})"); + }); + it("keeps the real keyword first after typing past a snippet prefix", () => { const items = buildSqlCompletionItems("sele", 4, { snippets: [{ id: "custom-sel", label: "select all rows", prefix: "sel", body: "SELECT *\nFROM table;" }], diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 23d046466e..2af17f2ca0 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -645,6 +645,7 @@ const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "ful 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; +const EXACT_CUSTOM_SNIPPET_TRIGGER_BOOST = 40000; function isTableTriggerKeyword(keyword: string, options: SqlSemanticBuildOptions): boolean { return TABLE_TRIGGER_KEYWORDS.has(keyword) || (keyword === "desc" && resolveSqlDialectId(options) === "mysql"); @@ -1540,15 +1541,22 @@ class SqlCompletionProvider { return dedupeAndSort(buildMongoCompletionItemsFromContext({ mode: "root", prefix: context.prefix, from: 0 }).map(mongoCompletionItemToSqlCompletionItem)); } + const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS); + const customSnippetItems = buildSnippetItems( + context.prefix, + snippets.filter((snippet) => !shouldFormatBuiltinSnippet(snippet)), + this.input.keywordCase, + this.databaseType, + ); if (context.preferredValueKeywords?.length) { - return dedupeAndSort(buildPreferredKeywordItems(context.prefix, context.preferredValueKeywords, this.input.keywordCase)); + return dedupeAndSort([...customSnippetItems, ...buildPreferredKeywordItems(context.prefix, context.preferredValueKeywords, this.input.keywordCase)]); } const preferReferencedColumns = hasMatchingReferencedColumnPrefix(context, this.input.columnsByTable); + this.items.push(...customSnippetItems); if (!pendingJoinKeyword && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) { - const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS); if (!preferReferencedColumns) { - this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase, this.databaseType)); + this.items.push(...buildSnippetItems(context.prefix, snippets.filter(shouldFormatBuiltinSnippet), this.input.keywordCase, this.databaseType)); } if (!preferReferencedColumns || context.suggestRoutines) { const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType, context.openingParenAfterCursor, this.input.keywordCase, this.input.functionCase); @@ -4999,6 +5007,7 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: const boostByPrefix = computeBoost(snippet.prefix, prefix); const boostByLabel = computeBoost(snippet.label, prefix); const matchesByPrefix = matchesPrefix(snippet.prefix, prefix); + const isExactCustomTrigger = !shouldFormatBuiltinSnippet(snippet) && snippet.prefix.toLowerCase() === prefix.toLowerCase(); // When the user types past the snippet prefix (e.g. "sele" vs prefix "sel"), // they are likely typing the actual keyword — reduce the base boost so // the real keyword can rank higher. @@ -5014,7 +5023,8 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: type: "snippet" as const, detail: body, apply, - boost: Math.max(boostByPrefix, boostByLabel) + baseBoost, + exactMatch: isExactCustomTrigger || undefined, + boost: Math.max(boostByPrefix, boostByLabel) + baseBoost + (isExactCustomTrigger ? EXACT_CUSTOM_SNIPPET_TRIGGER_BOOST : 0), }; }); }