fix(sql): stop completion at the next top-level statement
This commit is contained in:
1 parent
7b164d0084
commit
4cdf18d652
2 files changed
+137
-3
No files matched your search
@@ -1484,3 +1484,62 @@ describe("select alias visibility", () => {
|
||||
expect(untyped.selectAliases).toContain("cnt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("line block statement boundary", () => {
|
||||
it.each(["-- explanatory comment", "/* explanatory comment */", "/*\n explanatory comment\n*/"])("keeps column sources after %s", (comment) => {
|
||||
const sql = `select na\n${comment}\nfrom users`;
|
||||
const cursor = "select na".length;
|
||||
const context = getSqlCompletionContext(sql, cursor, { databaseType: "iris" });
|
||||
|
||||
expect(context.referencedTables.map((table) => table.name)).toEqual(["users"]);
|
||||
const items = buildSqlCompletionItems(sql, cursor, {
|
||||
databaseType: "iris",
|
||||
tables: [{ name: "users", type: "table" }],
|
||||
columnsByTable: new Map([["users", [{ name: "name", table: "users", key: "name" } as never]]]),
|
||||
});
|
||||
expect(items.some((item) => item.label === "name")).toBe(true);
|
||||
});
|
||||
|
||||
it("still ends the block at a real blank line", () => {
|
||||
const sql = "select na\n\nfrom users";
|
||||
const context = getSqlCompletionContext(sql, "select na".length, { databaseType: "iris" });
|
||||
expect(context.referencedTables).toEqual([]);
|
||||
});
|
||||
|
||||
it("stops the active block at a top-level statement line without a semicolon", () => {
|
||||
// t8y2/dbx#9370: two semicolon-free SELECT lines — completion at the first
|
||||
// statement's WHERE must not pull the second statement's table in.
|
||||
const sql = "select * from CT_Nation where\nselect * from CT_CityArea";
|
||||
const cursor = sql.indexOf("where") + "where".length;
|
||||
const context = getSqlCompletionContext(sql, cursor, { databaseType: "iris" });
|
||||
|
||||
expect(context.referencedTables.map((table) => table.name)).toEqual(["CT_Nation"]);
|
||||
});
|
||||
|
||||
it("keeps column completion unqualified once the second statement is excluded", () => {
|
||||
const sql = "select * from CT_Nation where c\nselect * from CT_CityArea";
|
||||
const cursor = sql.indexOf("where") + "where c".length;
|
||||
const items = buildSqlCompletionItems(sql, cursor, {
|
||||
databaseType: "iris",
|
||||
tables: [
|
||||
{ name: "CT_Nation", type: "table" },
|
||||
{ name: "CT_CityArea", type: "table" },
|
||||
],
|
||||
columnsByTable: new Map([
|
||||
["CT_Nation", [{ name: "CTNAT_Code", table: "CT_Nation", key: "k1" } as never]],
|
||||
["CT_CityArea", [{ name: "CITAREA_Code", table: "CT_CityArea", key: "k2" } as never]],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(items.some((item) => item.label === "CT_CityArea.CITAREA_Code")).toBe(false);
|
||||
expect(items.some((item) => item.label === "CTNAT_Code")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not end the block at a SELECT line inside parentheses", () => {
|
||||
const sql = "select *\nfrom (\n select id from users\n) x\nwhere |";
|
||||
const cursor = sql.length;
|
||||
const context = getSqlCompletionContext(sql, cursor, { databaseType: "mysql" });
|
||||
|
||||
expect(context.referencedTables.map((table) => table.name)).toEqual(expect.arrayContaining(["users"]));
|
||||
});
|
||||
});
|
||||
@@ -2134,18 +2134,93 @@ function currentSqlLikeLineBlockSpan(sql: string, cursor: number, activeStatemen
|
||||
return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, blockEnd) };
|
||||
}
|
||||
|
||||
// Equal-length masking of string/comment characters, so parenthesis depth and
|
||||
// line-start keywords can be scanned on the masked copy while offsets stay
|
||||
// valid against the original text.
|
||||
function maskSqlForStructureScan(sql: string): string {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while (i < sql.length) {
|
||||
const ch = sql[i]!;
|
||||
if (ch === "'" || ch === '"' || ch === "`") {
|
||||
const quote = ch;
|
||||
out += ch;
|
||||
i += 1;
|
||||
while (i < sql.length) {
|
||||
if (sql[i] === "\\" && i + 1 < sql.length) {
|
||||
out += " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const same = sql[i] === quote;
|
||||
out += same ? quote : " ";
|
||||
i += 1;
|
||||
if (same) break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "-" && sql[i + 1] === "-") {
|
||||
while (i < sql.length && sql[i] !== "\n") {
|
||||
out += " ";
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && sql[i + 1] === "*") {
|
||||
out += " ";
|
||||
i += 2;
|
||||
while (i < sql.length) {
|
||||
if (sql[i] === "*" && sql[i + 1] === "/") {
|
||||
out += " ";
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
out += sql[i] === "\n" ? "\n" : " ";
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out += ch;
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Statements users type without a trailing semicolon: a line starting one of
|
||||
// these at top level begins a new statement block (t8y2/dbx#9370).
|
||||
const STATEMENT_START_LINE_PATTERN = /^(?:select|with|insert|update|delete|create|drop|alter)\b/i;
|
||||
|
||||
function currentLineBlockEnd(sql: string, cursor: number, start: number): number | null {
|
||||
const masked = maskSqlForStructureScan(sql);
|
||||
let lineStart = sql.lastIndexOf("\n", cursor - 1) + 1;
|
||||
let depth = 0;
|
||||
for (let i = start; i < lineStart; i += 1) {
|
||||
const ch = masked[i];
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") depth = Math.max(0, depth - 1);
|
||||
}
|
||||
let atCursorLine = true;
|
||||
while (lineStart < sql.length) {
|
||||
const lineEnd = sql.indexOf("\n", lineStart);
|
||||
const boundedLineEnd = lineEnd >= 0 ? lineEnd : sql.length;
|
||||
const line = sql.slice(lineStart, boundedLineEnd);
|
||||
const trimmed = line.trimStart();
|
||||
if (lineStart > start && (!trimmed || /^(get|post|put|delete|patch|head)\s+\//i.test(trimmed))) {
|
||||
const originalTrimmed = sql.slice(lineStart, boundedLineEnd).trimStart();
|
||||
const trimmed = masked.slice(lineStart, boundedLineEnd).trimStart();
|
||||
if (lineStart > start && (!originalTrimmed || /^(get|post|put|delete|patch|head)\s+\//i.test(originalTrimmed))) {
|
||||
return lineStart;
|
||||
}
|
||||
// The cursor's own line always belongs to the block; only a following
|
||||
// top-level statement line ends it.
|
||||
if (!atCursorLine && lineStart > start && depth === 0 && STATEMENT_START_LINE_PATTERN.test(trimmed)) {
|
||||
return lineStart;
|
||||
}
|
||||
if (lineEnd < 0) break;
|
||||
for (let i = lineStart; i < boundedLineEnd; i += 1) {
|
||||
const ch = masked[i];
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") depth = Math.max(0, depth - 1);
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
atCursorLine = false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user