fix(editor): bound SQL completion to active statement
This commit is contained in:
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";
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user