fix(editor): preserve current SQL execution boundaries
This commit is contained in:
13 files changed
+232
-44
No files matched your search
@@ -1,7 +1,9 @@
|
||||
import type { SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { trailingStatementDelimiterPosition, type StatementDelimiterDocument } from "@/lib/sql/statementDelimiter";
|
||||
|
||||
export function currentStatementFrameRangeTo(nextChar: string, range: SqlTextRange): number {
|
||||
return nextChar === ";" ? range.to + 1 : range.to;
|
||||
export function currentStatementFrameRangeTo(doc: StatementDelimiterDocument, range: SqlTextRange): number {
|
||||
const delimiterPos = trailingStatementDelimiterPosition(doc, range.to);
|
||||
return delimiterPos === null ? range.to : delimiterPos + 1;
|
||||
}
|
||||
|
||||
export function shouldRebuildCurrentStatementFrame(update: { docChanged: boolean; selectionSet: boolean; configurationChanged: boolean }): boolean {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Text } from "@codemirror/state";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { executableStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { cursorBelongsToTrailingStatementDelimiter } from "@/lib/sql/statementDelimiter";
|
||||
|
||||
export interface ExecutableStatementRangeCache {
|
||||
doc: Text;
|
||||
@@ -61,7 +62,7 @@ export function executableStatementRangeAtCursor(cache: ExecutableStatementRange
|
||||
}
|
||||
|
||||
const next = cache.ranges[index + 1];
|
||||
if (pos > range.to && (!next || pos < next.from) && range.to >= line.from && range.to <= line.to && cursorRemainsOnRangeLine(cache.doc, range.to, pos)) {
|
||||
if (pos > range.to && (!next || pos < next.from) && cursorBelongsToTrailingStatementDelimiter(cache.doc, range.to, pos)) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
@@ -80,14 +81,3 @@ function isCursorOnLeadingBlockComment(lineText: string, lineOffset: number): bo
|
||||
|
||||
return lineOffset <= commentEnd + 2;
|
||||
}
|
||||
|
||||
function cursorRemainsOnRangeLine(doc: Text, rangeTo: number, cursorPos: number): boolean {
|
||||
const between = doc.sliceString(rangeTo, cursorPos);
|
||||
if (between.includes("\n")) return false;
|
||||
const delimiterIndex = between.lastIndexOf(";");
|
||||
if (delimiterIndex === -1) return between.trim() === "";
|
||||
|
||||
const beforeDelimiter = between.slice(0, delimiterIndex);
|
||||
const afterDelimiter = between.slice(delimiterIndex + 1);
|
||||
return beforeDelimiter.trim() === "" && afterDelimiter.trim() === "";
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export type SqlExecutionTargetKind = "cursor" | "all";
|
||||
*/
|
||||
export interface SqlExecutionCandidate {
|
||||
kind: SqlExecutionTargetKind;
|
||||
supportedKinds: SqlExecutionTargetKind[];
|
||||
label: string;
|
||||
sql: string;
|
||||
from: number;
|
||||
@@ -40,6 +41,11 @@ export function isSqlExecutionSnapshot(value: SqlExecutionOverride | undefined):
|
||||
return typeof value === "object" && value !== null && typeof value.fullSql === "string" && typeof value.selectedSql === "string" && typeof value.cursorPos === "number" && typeof value.selectionFrom === "number" && typeof value.selectionTo === "number";
|
||||
}
|
||||
|
||||
export function executionCandidateForMode(candidates: SqlExecutionCandidate[], mode: ExecuteMode): SqlExecutionCandidate | null {
|
||||
const targetKind: SqlExecutionTargetKind = mode === "current" ? "cursor" : "all";
|
||||
return candidates.find((candidate) => candidate.supportedKinds.includes(targetKind)) ?? null;
|
||||
}
|
||||
|
||||
export function resolveExecutableSql(fullSql: string, selectedSql: string, options?: { mode?: ExecuteMode; cursorPos?: number }): string {
|
||||
const trimmedSelection = selectedSql.trim();
|
||||
if (trimmedSelection) return trimmedSelection;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { cursorBelongsToTrailingStatementDelimiter } from "@/lib/sql/statementDelimiter";
|
||||
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
@@ -576,8 +577,8 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
|
||||
const next = statements[index + 1];
|
||||
// A caret after a statement's semicolon still belongs to that statement
|
||||
// until the next statement's text begins.
|
||||
if (pos > statement.to && (!next || pos < next.from) && isCursorInSameLineDelimiterGap(sql, statement.to, pos)) {
|
||||
return rangeForCursorInSoftRanges(sql, softRanges, pos) ?? rangeFor(statement, sql);
|
||||
if (pos > statement.to && (!next || pos < next.from) && isCursorInTrailingDelimiterGap(sql, statement.to, pos)) {
|
||||
return rangeForCursorInSoftRanges(sql, softRanges, pos) ?? rangeFor(softRanges[softRanges.length - 1] ?? statement, sql);
|
||||
}
|
||||
|
||||
// Cursor in indentation or inter-statement whitespace immediately before
|
||||
@@ -585,9 +586,9 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
|
||||
// execution range remains tight around the SQL text itself.
|
||||
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (isElasticsearchCompatibleDatabaseType(databaseType) && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
|
||||
const previous = statements[index - 1];
|
||||
if (previous && isCursorInSameLineDelimiterGap(sql, previous.to, pos)) {
|
||||
if (previous && isCursorInTrailingDelimiterGap(sql, previous.to, pos)) {
|
||||
const previousSoftRanges = splitStatementRangeAtSoftStarts(sql, previous, databaseType, parameterOptions);
|
||||
return rangeForCursorInSoftRanges(sql, previousSoftRanges, pos) ?? rangeFor(previous, sql);
|
||||
return rangeForCursorInSoftRanges(sql, previousSoftRanges, pos) ?? rangeFor(previousSoftRanges[previousSoftRanges.length - 1] ?? previous, sql);
|
||||
}
|
||||
return rangeForCursorInSoftRanges(sql, softRanges, pos) ?? rangeFor(statement, sql);
|
||||
}
|
||||
@@ -614,7 +615,7 @@ export function mongoCommandRangeAtCursor(sql: string, cursorPos: number): SqlTe
|
||||
if (pos >= command.from && pos <= command.to) return range;
|
||||
|
||||
const next = commands[index + 1];
|
||||
if (pos > command.to && (!next || pos < next.from) && isCursorInSameLineDelimiterGap(sql, command.to, pos)) return range;
|
||||
if (pos > command.to && (!next || pos < next.from) && isCursorInTrailingDelimiterGap(sql, command.to, pos)) return range;
|
||||
|
||||
if (pos < command.from && sql.slice(pos, command.from).trim() === "" && isCursorOnStatementLine(sql, pos, command)) return range;
|
||||
}
|
||||
@@ -622,13 +623,8 @@ export function mongoCommandRangeAtCursor(sql: string, cursorPos: number): SqlTe
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCursorInSameLineDelimiterGap(sql: string, previousStatementEnd: number, cursorPos: number): boolean {
|
||||
if (cursorPos <= previousStatementEnd) return false;
|
||||
const between = sql.slice(previousStatementEnd, cursorPos);
|
||||
const delimiterIndex = between.lastIndexOf(";");
|
||||
if (delimiterIndex === -1) return false;
|
||||
const afterDelimiter = between.slice(delimiterIndex + 1);
|
||||
return !afterDelimiter.includes("\n") && between.slice(0, delimiterIndex).trim() === "" && afterDelimiter.trim() === "";
|
||||
function isCursorInTrailingDelimiterGap(sql: string, previousStatementEnd: number, cursorPos: number): boolean {
|
||||
return cursorBelongsToTrailingStatementDelimiter(sql, previousStatementEnd, cursorPos);
|
||||
}
|
||||
|
||||
function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: number): SqlTextRange | null {
|
||||
@@ -2043,16 +2039,17 @@ export function buildExecutionCandidates(sql: string, cursorPos: number, databas
|
||||
|
||||
const sameContent = normalizeSql(cursorStatement.sql) === normalizeSql(full.sql);
|
||||
if (sameContent) {
|
||||
return [candidateFromRange(full, "all", databaseType)];
|
||||
return [candidateFromRange(full, "all", databaseType, ["cursor", "all"])];
|
||||
}
|
||||
|
||||
return [candidateFromRange(cursorStatement, "cursor", databaseType), candidateFromRange(full, "all", databaseType)];
|
||||
}
|
||||
|
||||
function candidateFromRange(range: SqlTextRange, kind: SqlExecutionCandidate["kind"], databaseType?: DatabaseType): SqlExecutionCandidate {
|
||||
function candidateFromRange(range: SqlTextRange, kind: SqlExecutionCandidate["kind"], databaseType?: DatabaseType, supportedKinds: SqlExecutionCandidate["supportedKinds"] = [kind]): SqlExecutionCandidate {
|
||||
const isRedis = databaseType === "redis";
|
||||
return {
|
||||
kind,
|
||||
supportedKinds,
|
||||
label: kind === "cursor" ? (isRedis ? "currentCommand" : "currentStatement") : isRedis ? "allCommands" : "allStatements",
|
||||
sql: range.sql,
|
||||
from: range.from,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface StatementDelimiterDocument {
|
||||
readonly length: number;
|
||||
sliceString(from: number, to: number): string;
|
||||
}
|
||||
|
||||
type StatementDelimiterSource = string | StatementDelimiterDocument;
|
||||
|
||||
export function trailingStatementDelimiterPosition(source: StatementDelimiterSource, rangeTo: number): number | null {
|
||||
let delimiterPos = rangeTo;
|
||||
let lineBreakCount = 0;
|
||||
while (delimiterPos < source.length) {
|
||||
const char = sliceSource(source, delimiterPos, delimiterPos + 1);
|
||||
if (!/\s/u.test(char)) break;
|
||||
if (char === "\n" && ++lineBreakCount > 1) return null;
|
||||
delimiterPos += 1;
|
||||
}
|
||||
return sliceSource(source, delimiterPos, delimiterPos + 1) === ";" ? delimiterPos : null;
|
||||
}
|
||||
|
||||
export function cursorBelongsToTrailingStatementDelimiter(source: StatementDelimiterSource, rangeTo: number, cursorPos: number): boolean {
|
||||
if (cursorPos < rangeTo) return false;
|
||||
const delimiterPos = trailingStatementDelimiterPosition(source, rangeTo);
|
||||
if (delimiterPos === null) return false;
|
||||
if (cursorPos <= delimiterPos + 1) return true;
|
||||
|
||||
const afterDelimiter = sliceSource(source, delimiterPos + 1, cursorPos);
|
||||
return !afterDelimiter.includes("\n") && afterDelimiter.trim() === "";
|
||||
}
|
||||
|
||||
function sliceSource(source: StatementDelimiterSource, from: number, to: number): string {
|
||||
return typeof source === "string" ? source.slice(from, to) : source.sliceString(from, to);
|
||||
}
|
||||
Reference in new issue
Block a user