fix(completion): bound statement block scans to active SQL

Refs: #5997
This commit is contained in:
t8y2 committed 2026-09-22 15:57:23 +08:00
1 parent 591e8f4909
commit cf36425b0a
2 files changed
+50 -5

No files matched your search

@@ -0,0 +1,43 @@
import { PostgreSQL, sql } from "@codemirror/lang-sql";
import { ensureSyntaxTree } from "@codemirror/language";
import { EditorState } from "@codemirror/state";
import { describe, expect, it, vi } from "vitest";
import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
describe("SQL completion statement-local scanning", () => {
it.each(["\n", " "])("does not split unrelated statements separated by %j before the active syntax-tree window", (separator) => {
const prefix = `SELECT id FROM previous_table;${separator}`.repeat(2_000);
const activeStatement = "SELECT customer_ FROM current_table";
const suffix = ";\n" + "SELECT id FROM later_table;\n".repeat(2_000);
const document = prefix + activeStatement + suffix;
const cursor = prefix.length + activeStatement.indexOf(" FROM");
const state = EditorState.create({ doc: document, extensions: [sql({ dialect: PostgreSQL })] });
ensureSyntaxTree(state, document.length, 5_000);
const scannedLengths: number[] = [];
const originalSplit = String.prototype.split;
const splitSpy = vi.spyOn(String.prototype, "split").mockImplementation(function (this: string, separator, limit) {
if (separator instanceof RegExp && separator.source === "\\r?\\n") scannedLengths.push(this.length);
return originalSplit.call(this, separator, limit);
});
try {
const context = getSqlCompletionContext(document, cursor, { databaseType: "postgres", editorState: state });
expect(context.referencedTables.map((table) => table.name)).toEqual(["current_table"]);
expect(scannedLengths.length).toBeGreaterThan(0);
expect(Math.max(...scannedLengths)).toBeLessThanOrEqual(activeStatement.length);
} finally {
splitSpy.mockRestore();
}
});
it("preserves CRLF offsets when selecting a semicolon-free statement block", () => {
const document = "SELECT id FROM previous_table;\r\nSELECT id FROM first_table\r\nSELECT customer_ FROM current_table\r\nSELECT id FROM later_table";
const cursor = document.indexOf("customer_") + "customer_".length;
const state = EditorState.create({ doc: document, extensions: [sql({ dialect: PostgreSQL })] });
ensureSyntaxTree(state, document.length, 5_000);
const context = getSqlCompletionContext(document, cursor, { databaseType: "postgres", editorState: state });
expect(context.prefix).toBe("customer_");
expect(context.referencedTables.map((table) => table.name)).toEqual(["current_table"]);
});
});
+7 -5
View File
@@ -2211,10 +2211,10 @@ function activeSqlCompletionStatementSpan(sql: string, cursor: number, options:
function currentSqlLikeLineBlockSpan(sql: string, cursor: number, activeStatementSpan: SqlSemanticSpan): SqlSemanticSpan | null {
const safeCursor = Math.max(0, Math.min(cursor, sql.length));
const beforeCursor = sql.slice(0, safeCursor);
const beforeCursor = sql.slice(activeStatementSpan.start, safeCursor);
const lines = beforeCursor.split(/\r?\n/);
let start: number | null = null;
let offset = 0;
let offset = activeStatementSpan.start;
for (const line of lines) {
const trimmed = line.trimStart();
@@ -2223,14 +2223,16 @@ function currentSqlLikeLineBlockSpan(sql: string, cursor: number, activeStatemen
if (/^(select|with)\b/i.test(trimmed)) start = offset + indentation;
if (/^(get|post|put|delete|patch|head)\s+\//i.test(trimmed)) start = null;
}
offset += line.length + 1;
offset += line.length;
offset += sql[offset] === "\r" && sql[offset + 1] === "\n" ? 2 : 1;
}
if (start == null) return null;
if (activeStatementSpan.start > start) return null;
const blockEnd = currentLineBlockEnd(sql, safeCursor, start);
return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, blockEnd) };
const statementSql = sql.slice(activeStatementSpan.start, activeStatementSpan.end);
const blockEnd = currentLineBlockEnd(statementSql, safeCursor - activeStatementSpan.start, start - activeStatementSpan.start);
return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, activeStatementSpan.start + blockEnd) };
}
// Equal-length masking of string/comment characters, so parenthesis depth and