From 7cf5795666ca2a9e53cf62f9f96de379485c58b7 Mon Sep 17 00:00:00 2001 From: ekesaiting <36790768+ekesaiting@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:19:10 +0800 Subject: [PATCH] fix(editor): restore quoted table keyword completion --- apps/desktop/src/App.vue | 6 +++++ .../__tests__/sql/semantic/completion.spec.ts | 16 +++++++++++ apps/desktop/src/lib/sql/semantic/model.ts | 27 +++++++++++++++---- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 652fa13b9..463675c15 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1329,6 +1329,12 @@ async function newQuery() { databaseType: effectiveDatabaseTypeForConnection(conn), }); const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql, target.catalog); + if (initialSql) { + const prefilledTab = queryStore.tabs.find((t) => t.id === tabId); + if (prefilledTab) { + prefilledTab.editorSelection = { anchor: initialSql.length, head: initialSql.length }; + } + } try { await connectionStore.ensureConnected(target.connectionId); if (target.shouldRefreshDefaultDatabase) { diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index b068b7539..d86f9c64b 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -469,4 +469,20 @@ WHERE a.id = b.fk_kpi_set_score_id`, expect(context.suggestTables).toBe(true); expect(items.filter((item) => item.type === "table").map((item) => item.label)).toEqual(["orders"]); }); + + it.each([ + ["PostgreSQL quoted table", 'SELECT * FROM "users" wh|', "postgres", "postgres"], + ["PostgreSQL quoted schema.table", 'SELECT * FROM "public"."users" wh|', "postgres", "postgres"], + ["PostgreSQL quoted keyword table", 'SELECT * FROM "from" wh|', "postgres", "postgres"], + ["PostgreSQL quoted schema.keyword table", 'SELECT * FROM "public"."from" wh|', "postgres", "postgres"], + ["MySQL backtick table", "SELECT * FROM `users` wh|", "mysql", "mysql"], + ["MySQL backtick keyword table", "SELECT * FROM `join` wh|", "mysql", "mysql"], + ["SQL Server bracket table", "SELECT * FROM [users] wh|", "sqlserver", "sqlserver"], + ["SQL Server bracket keyword table", "SELECT * FROM [update] wh|", "sqlserver", "sqlserver"], + ] as const)("offers WHERE keyword completion after a quoted prefilled table (%s)", (_label, markedSql, databaseType, dialect) => { + const { context, items } = semanticCompletion(markedSql, {}, { databaseType, dialect }); + + expect(context.suggestKeywords).toBe(true); + expect(items.filter((item) => item.type === "keyword").map((item) => item.label)).toEqual(expect.arrayContaining(["WHERE", "WHEN", "WITH"])); + }); }); diff --git a/apps/desktop/src/lib/sql/semantic/model.ts b/apps/desktop/src/lib/sql/semantic/model.ts index a1ed6be99..f21d2b6e7 100644 --- a/apps/desktop/src/lib/sql/semantic/model.ts +++ b/apps/desktop/src/lib/sql/semantic/model.ts @@ -600,13 +600,13 @@ function trailingIdentifier(tokens: readonly SqlSemanticToken[], cursor: number, } function previousWord(tokens: readonly SqlSemanticToken[], cursor: number): string { - const before = tokens.filter((item) => item.span.end <= cursor && item.kind === "word"); - return before[before.length - 1]?.normalized ?? ""; + const before = tokens.filter((item) => item.span.end <= cursor && item.kind !== "comment"); + return nearestPreviousSyntaxWord(before); } function wordBeforePosition(tokens: readonly SqlSemanticToken[], position: number): string { - const before = tokens.filter((item) => item.span.end <= position && item.kind === "word"); - return before[before.length - 1]?.normalized ?? ""; + const before = tokens.filter((item) => item.span.end <= position && item.kind !== "comment"); + return nearestPreviousSyntaxWord(before); } function wordBeforeTrailingIdentifier(tokens: readonly SqlSemanticToken[], cursor: number, trailing: TrailingIdentifier): string { @@ -622,7 +622,24 @@ function wordBeforeTrailingIdentifier(tokens: readonly SqlSemanticToken[], curso identifiersToSkip -= 1; index -= 1; } - return before[index]?.kind === "word" ? before[index].normalized : ""; + return nearestPreviousSyntaxWord(before.slice(0, index + 1)); +} + +/** + * Scans backward from the end of `before` for the nearest unquoted syntax word. + * A quoted identifier is a barrier: its object name must not participate in + * keyword comparisons even when it is named `from`, `join`, or `update`. + */ +function nearestPreviousSyntaxWord(tokens: readonly SqlSemanticToken[]): string { + for (let index = tokens.length - 1; index >= 0; index -= 1) { + const item = tokens[index]; + if (!item) continue; + if (item.kind === "word") return item.normalized; + if (item.kind === "quoted_identifier") return ""; + if (item.text === "." || item.kind === "comment") continue; + break; + } + return ""; } function isTableListContinuation(tokens: readonly SqlSemanticToken[], position: number): boolean {