fix(editor): bound SQL completion to active statement

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-18 11:51:07 +08:00
1 parent 8271ba2a71
commit 41ffc969a1
12 files changed
+917 -210

No files matched your search

@@ -194,3 +194,15 @@ export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; di
return SQL_SEMANTIC_DIALECTS.generic;
}
}
/**
* Resolves the dialect id ("mysql", "postgres", ...) used to keep lexical/statement-boundary
* scanning in sync with tokenizeSqlSemantic's own dialect-aware tokenization -- most importantly,
* whether '#' starts a line comment (MySQL) or is an operator (PostgreSQL: #, #>, #>>, #-).
* Defaults to "mysql" when no dialect info is available (rather than sqlSemanticDialectFor's own
* "generic" default), matching tokenizeSqlSemantic's default and preserving the behavior callers
* had before dialect-aware scanning existed.
*/
export function resolveSqlDialectId(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" }): string {
return options.databaseType || options.dialect ? sqlSemanticDialectFor(options).id : "mysql";
}
+11 -2
View File
@@ -1,5 +1,6 @@
import { sqlSemanticDialectFor, type SqlSemanticDialectAdapter } from "@/lib/sql/semantic/dialect";
import { findActiveSqlStatementSpan, isSuppressedSqlSemanticContext, tokenIsIdentifier, tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
import { resolveSqlStatementWindow } from "@/lib/sql/sqlSyntaxTreeWindow";
import type {
SqlSemanticBuildOptions,
SqlSemanticClauseSpans,
@@ -785,8 +786,16 @@ function buildScope(statement: SqlSemanticStatement, rowSources: SqlSemanticRowS
export function buildSqlSemanticModel(sql: string, cursor: number, options: SqlSemanticBuildOptions = {}): SqlSemanticModel {
const safeCursor = Math.max(0, Math.min(cursor, sql.length));
const dialect = sqlSemanticDialectFor(options);
const allTokens = tokenizeSqlSemantic(sql, dialect.id);
const statementSpan = findActiveSqlStatementSpan(sql, allTokens, safeCursor);
const window = resolveSqlStatementWindow(sql, safeCursor, options.editorState, dialect.id);
const windowSql = sql.slice(window.from, window.to);
const windowCursor = safeCursor - window.from;
const localTokens = tokenizeSqlSemantic(windowSql, dialect.id);
const localStatementSpan = findActiveSqlStatementSpan(windowSql, localTokens, windowCursor);
const allTokens = localTokens.map((token) => ({
...token,
span: { start: token.span.start + window.from, end: token.span.end + window.from },
}));
const statementSpan = { start: localStatementSpan.start + window.from, end: localStatementSpan.end + window.from };
const tokens = significantTokens(allTokens.filter((item) => item.span.end > statementSpan.start && item.span.start < statementSpan.end));
const kind = statementKind(tokens);
const statement: SqlSemanticStatement = {
+14 -1
View File
@@ -29,6 +29,19 @@ function readQuoted(input: string, start: number, open: string, close: string):
return input.length;
}
const DOLLAR_QUOTE_TAG_PATTERN = /\$[A-Za-z_0-9]*\$/y;
/**
* Matches a PostgreSQL dollar-quote tag (`$$` or `$tag$`) starting exactly at `index`, without
* allocating a substring: a sticky ("y") regex anchors its match to `lastIndex` and scans the
* original string in place, unlike `pattern.exec(input.slice(index))` which copies everything
* from `index` to the end of `input` on every call.
*/
export function matchDollarQuoteTag(input: string, index: number): string | undefined {
DOLLAR_QUOTE_TAG_PATTERN.lastIndex = index;
return DOLLAR_QUOTE_TAG_PATTERN.exec(input)?.[0];
}
export function tokenizeSqlSemantic(input: string, dialectId = "mysql"): SqlSemanticToken[] {
const tokens: SqlSemanticToken[] = [];
let index = 0;
@@ -79,7 +92,7 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql"): SqlSema
}
if (ch === "$") {
const marker = /^\$[A-Za-z_0-9]*\$/.exec(input.slice(start))?.[0];
const marker = matchDollarQuoteTag(input, start);
if (marker) {
const closing = input.indexOf(marker, start + marker.length);
index = closing < 0 ? input.length : closing + marker.length;
@@ -1,3 +1,4 @@
import type { EditorState } from "@codemirror/state";
import type { DatabaseType } from "@/types/database";
export interface SqlSemanticSpan {
@@ -128,4 +129,12 @@ export interface SqlSemanticModel {
export interface SqlSemanticBuildOptions {
databaseType?: DatabaseType;
dialect?: "mysql" | "postgres" | "sqlserver";
/**
* Live CodeMirror state for the document being edited, when the caller has one (i.e. this is a
* real editor completion request, not a pure-string test/utility call). When present and its
* doc length matches the `sql` string being scanned, boundary-sensitive lookups prefer resolving
* against its already-incrementally-parsed syntax tree (see sqlSyntaxTreeWindow.ts) instead of
* the bounded heuristic scanner in insertValueHints.ts, which remains the fallback.
*/
editorState?: EditorState;
}