fix(sql): keep Ctrl+Enter on current statement at trailing whitespace
This commit is contained in:
parent
c32f92a902
commit
b437c470e1
|
|
@ -103,6 +103,40 @@ describe("executableStatementRangeCacheForDoc", () => {
|
|||
expect(executableStatementRangeAtCursor(cache, delimiterCursor + 1)?.sql).toBe("SELECT *\nFROM users");
|
||||
});
|
||||
|
||||
it("resolves a trailing-whitespace cursor on a multi-line statement tail to that statement", () => {
|
||||
const sql = "WITH x AS (SELECT 1)\nSELECT 2 \nSELECT 3;";
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, contentEnd)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
expect(executableStatementRangeAtCursor(cache, contentEnd + 1)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
expect(executableStatementRangeAtCursor(cache, contentEnd + 2)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
});
|
||||
|
||||
it("keeps the simple single-line trailing-whitespace case on the current statement", () => {
|
||||
const sql = "SELECT 1;\nSELECT 2 \nSELECT 3;";
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, contentEnd)?.sql).toBe("SELECT 2");
|
||||
expect(executableStatementRangeAtCursor(cache, contentEnd + 1)?.sql).toBe("SELECT 2");
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on a non-final line of a multi-line statement in the frame cache", () => {
|
||||
const sql = "SELECT * FROM `profiles`;\nSELECT * FROM `users` AS uu \nWHERE id = 2\nSELECT * FROM `orders`;";
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
const markerEnd = sql.indexOf("uu") + 2;
|
||||
const lineEnd = sql.indexOf("\n", markerEnd);
|
||||
const expected = "SELECT * FROM `users` AS uu \nWHERE id = 2";
|
||||
|
||||
for (let pos = markerEnd; pos < lineEnd; pos += 1) {
|
||||
expect(executableStatementRangeAtCursor(cache, pos)?.sql).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not attach a semicolon after a blank line to the previous statement", () => {
|
||||
const sql = "SELECT 1\n\n;";
|
||||
const doc = Text.of(sql.split("\n"));
|
||||
|
|
|
|||
|
|
@ -29,6 +29,23 @@ function candidateSummaries(candidates: Array<{ kind: string; sql: string }>): s
|
|||
return candidates.map((candidate) => `${candidate.kind}:${candidate.sql.trim()}`);
|
||||
}
|
||||
|
||||
function trailingSpacePositionsAfter(sql: string, marker: string): number[] {
|
||||
const markerEnd = sql.indexOf(marker) + marker.length;
|
||||
const lineEnd = sql.indexOf("\n", markerEnd);
|
||||
const end = lineEnd === -1 ? sql.length : lineEnd;
|
||||
const positions: number[] = [];
|
||||
for (let p = markerEnd; p < end; p += 1) {
|
||||
if (sql[p] === " " || sql[p] === "\t") positions.push(p);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
// Reported repro: trailing spaces sit after `AS uu` (a NON-final line of the
|
||||
// middle statement) with `WHERE id = 2` continuing on the next line. The cursor
|
||||
// is inside the statement body, so the middle statement must be kept.
|
||||
const screenshotSqlNoSemi = "SELECT * FROM `profiles`\nWHERE active = 1\nSELECT * FROM `users` AS uu \nWHERE id = 2\nSELECT * FROM `orders`\nWHERE status = 'paid'";
|
||||
const screenshotSqlWithSemi = "SELECT * FROM `profiles`;\nSELECT * FROM `users` AS uu \nWHERE id = 2\nSELECT * FROM `orders`;";
|
||||
|
||||
const oraclePlSqlFixture = `DECLARE
|
||||
v_order_count NUMBER;
|
||||
BEGIN
|
||||
|
|
@ -582,6 +599,70 @@ GET /_cat/indices`;
|
|||
expect(statementRangeAtCursor(sql, indexOf(sql, "tbB"))?.sql.trim()).toBe(expected);
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on a multi-line WITH statement tail", () => {
|
||||
const sql = "WITH x AS (SELECT 1)\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
for (const pos of [contentEnd, contentEnd + 1, contentEnd + 2]) {
|
||||
expect(statementRangeAtCursor(sql, pos)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on a UNION operand tail", () => {
|
||||
const sql = "SELECT 1\nUNION\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
expect(statementRangeAtCursor(sql, contentEnd + 1)?.sql).toBe("SELECT 1\nUNION\nSELECT 2");
|
||||
expect(statementRangeAtCursor(sql, contentEnd + 2)?.sql).toBe("SELECT 1\nUNION\nSELECT 2");
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on an EXPLAIN target tail", () => {
|
||||
const sql = "EXPLAIN\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
expect(statementRangeAtCursor(sql, contentEnd + 1)?.sql).toBe("EXPLAIN\nSELECT 2");
|
||||
});
|
||||
|
||||
it("does not swallow the following statement when a trailing-whitespace cursor sits on a semicolon-terminated multi-line statement tail", () => {
|
||||
const sql = "SELECT 0;\nWITH x AS (SELECT 1)\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
|
||||
expect(statementRangeAtCursor(sql, contentEnd + 1)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
expect(statementRangeAtCursor(sql, contentEnd + 2)?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on a non-final line of a multi-line middle statement (no semicolons before)", () => {
|
||||
// Reported repro: spaces after `AS uu`, `WHERE id = 2` follows on the next line.
|
||||
const expected = "SELECT * FROM `users` AS uu \nWHERE id = 2";
|
||||
const positions = trailingSpacePositionsAfter(screenshotSqlNoSemi, "uu");
|
||||
|
||||
expect(positions.length).toBeGreaterThan(0);
|
||||
for (const pos of positions) {
|
||||
expect(statementRangeAtCursor(screenshotSqlNoSemi, pos, "mysql")?.sql).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on a non-final line of a multi-line middle statement (semicolons before)", () => {
|
||||
const expected = "SELECT * FROM `users` AS uu \nWHERE id = 2";
|
||||
const positions = trailingSpacePositionsAfter(screenshotSqlWithSemi, "uu");
|
||||
|
||||
expect(positions.length).toBeGreaterThan(0);
|
||||
for (const pos of positions) {
|
||||
expect(statementRangeAtCursor(screenshotSqlWithSemi, pos, "mysql")?.sql).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a trailing-whitespace cursor on the last line of a multi-line middle statement", () => {
|
||||
const sql = "SELECT 1;\nSELECT * FROM `users` AS uu\nWHERE id = 2 \nSELECT * FROM `orders`;";
|
||||
const expected = "SELECT * FROM `users` AS uu\nWHERE id = 2";
|
||||
const positions = trailingSpacePositionsAfter(sql, "= 2");
|
||||
|
||||
expect(positions.length).toBeGreaterThan(0);
|
||||
for (const pos of positions) {
|
||||
expect(statementRangeAtCursor(sql, pos, "mysql")?.sql).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps newline set-operation operands with ALL modifiers together", () => {
|
||||
const sql = "select * from tbA\nunion all\nselect * from tbB\nSELECT * FROM logs;";
|
||||
const range = statementRangeAtCursor(sql, indexOf(sql, "tbA"));
|
||||
|
|
@ -1175,6 +1256,51 @@ WHERE t2.product_name = '12345'
|
|||
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT 2", "all:SELECT 1\nSELECT 2"]);
|
||||
});
|
||||
|
||||
it("uses the multi-line cursor statement at a trailing-whitespace tail instead of falling back to the whole script", () => {
|
||||
const sql = "WITH x AS (SELECT 1)\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
const candidates = buildExecutionCandidates(sql, contentEnd + 1, "mysql");
|
||||
|
||||
expect(candidateKinds(candidates)).toEqual(["cursor", "all"]);
|
||||
expect(candidates[0].sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
expect(executionCandidateForMode(candidates, "current")?.sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
});
|
||||
|
||||
it("uses the cursor statement at a semicolon-terminated multi-line tail instead of swallowing the following statement", () => {
|
||||
const sql = "SELECT 0;\nWITH x AS (SELECT 1)\nSELECT 2 \nSELECT 3;";
|
||||
const contentEnd = sql.indexOf("SELECT 2") + "SELECT 2".length;
|
||||
const candidates = buildExecutionCandidates(sql, contentEnd + 2, "mysql");
|
||||
|
||||
expect(candidates[0].kind).toBe("cursor");
|
||||
expect(candidates[0].sql).toBe("WITH x AS (SELECT 1)\nSELECT 2");
|
||||
});
|
||||
|
||||
it("submits only the middle statement for a trailing-whitespace cursor on its non-final line (no semicolons before)", () => {
|
||||
const expected = "SELECT * FROM `users` AS uu \nWHERE id = 2";
|
||||
const pos = trailingSpacePositionsAfter(screenshotSqlNoSemi, "uu")[0];
|
||||
const candidates = buildExecutionCandidates(screenshotSqlNoSemi, pos, "mysql");
|
||||
|
||||
expect(executionCandidateForMode(candidates, "current")?.sql).toBe(expected);
|
||||
expect(executionCandidateForMode(candidates, "all")?.sql).toBe(screenshotSqlNoSemi);
|
||||
});
|
||||
|
||||
it("submits only the middle statement for a trailing-whitespace cursor on its non-final line (semicolons before)", () => {
|
||||
const expected = "SELECT * FROM `users` AS uu \nWHERE id = 2";
|
||||
const pos = trailingSpacePositionsAfter(screenshotSqlWithSemi, "uu")[0];
|
||||
const candidates = buildExecutionCandidates(screenshotSqlWithSemi, pos, "mysql");
|
||||
|
||||
expect(executionCandidateForMode(candidates, "current")?.sql).toBe(expected);
|
||||
});
|
||||
|
||||
it("submits only the middle statement, not the merged script, for a trailing-whitespace cursor on its last line", () => {
|
||||
const sql = "SELECT 1;\nSELECT * FROM `users` AS uu\nWHERE id = 2 \nSELECT * FROM `orders`;";
|
||||
const expected = "SELECT * FROM `users` AS uu\nWHERE id = 2";
|
||||
const pos = trailingSpacePositionsAfter(sql, "= 2")[0];
|
||||
const candidates = buildExecutionCandidates(sql, pos, "mysql");
|
||||
|
||||
expect(executionCandidateForMode(candidates, "current")?.sql).toBe(expected);
|
||||
});
|
||||
|
||||
it("uses the final soft statement when the cursor is on a standalone trailing semicolon", () => {
|
||||
const sql = "SELECT * FROM `t_0001`\nSELECT * FROM `t_0001` LIMIT 1\n;";
|
||||
const candidates = buildExecutionCandidates(sql, sql.lastIndexOf(";"), "mysql");
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@ export function executableStatementRangeAtCursor(cache: ExecutableStatementRange
|
|||
}
|
||||
|
||||
const next = cache.ranges[index + 1];
|
||||
if (pos > range.to && (!next || pos < next.from) && cursorBelongsToTrailingStatementDelimiter(cache.doc, range.to, pos)) {
|
||||
return range;
|
||||
if (pos > range.to && (!next || pos < next.from)) {
|
||||
if (cursorBelongsToTrailingStatementDelimiter(cache.doc, range.to, pos)) return range;
|
||||
if (isCursorOnRangeEndLine(cache.doc, pos, range.to)) return range;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,3 +82,8 @@ function isCursorOnLeadingBlockComment(lineText: string, lineOffset: number): bo
|
|||
|
||||
return lineOffset <= commentEnd + 2;
|
||||
}
|
||||
|
||||
function isCursorOnRangeEndLine(doc: Text, pos: number, rangeTo: number): boolean {
|
||||
const line = doc.lineAt(pos);
|
||||
return rangeTo >= line.from && rangeTo <= line.to;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
|
|||
if (pos > statement.to && (!next || pos < next.hitFrom)) {
|
||||
const softRange = rangeForCursorInSoftRanges(sql, softRanges, pos);
|
||||
if (softRange) return softRange;
|
||||
if (isCursorOnStatementLine(sql, pos, statement)) return rangeFor(statement, sql);
|
||||
if (isCursorOnRangeEndLine(sql, pos, statement)) return rangeFor(statement, sql);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -639,7 +639,7 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
|
|||
}
|
||||
|
||||
const next = ranges[index + 1];
|
||||
if (pos > range.to && (!next || pos < next.hitFrom) && isCursorOnStatementLine(sql, pos, range)) {
|
||||
if (pos > range.to && (!next || pos < next.hitFrom) && isCursorOnRangeEndLine(sql, pos, range)) {
|
||||
return rangeFor(range, sql);
|
||||
}
|
||||
}
|
||||
|
|
@ -1992,6 +1992,13 @@ function isCursorOnStatementLine(sql: string, pos: number, statement: Pick<RawSt
|
|||
return statement.from >= lineStart && statement.from <= lineEnd;
|
||||
}
|
||||
|
||||
function isCursorOnRangeEndLine(sql: string, pos: number, range: Pick<RawStatement, "to">): boolean {
|
||||
const lineStart = sql.lastIndexOf("\n", pos - 1) + 1;
|
||||
let lineEnd = sql.indexOf("\n", pos);
|
||||
if (lineEnd === -1) lineEnd = sql.length;
|
||||
return range.to >= lineStart && range.to <= lineEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full document as a range, or `null` when it is empty/whitespace.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue