fix(editor): preserve current SQL execution boundaries

This commit is contained in:
zipg authored and GitHub committed 2026-08-04 00:37:42 +08:00
1 parent c9764be9a0
commit a6f1a9a871
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;
+11 -14
View File
@@ -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);
}