feat(query-editor): enhance current statement framing
This commit is contained in:
18 files changed
+480
-10
No files matched your search
@@ -0,0 +1,23 @@
|
||||
import type { SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
export function currentStatementFrameRangeTo(nextChar: string, range: SqlTextRange): number {
|
||||
return nextChar === ";" ? range.to + 1 : range.to;
|
||||
}
|
||||
|
||||
export function visualSqlColumns(text: string): number {
|
||||
let columns = 0;
|
||||
for (const ch of text) {
|
||||
if (ch === "\t") {
|
||||
columns += 4;
|
||||
} else if (isWideSqlChar(ch)) {
|
||||
columns += 2;
|
||||
} else {
|
||||
columns += 1;
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function isWideSqlChar(ch: string): boolean {
|
||||
return /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/u.test(ch);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface ExecutableStatementRangeCache {
|
||||
doc: Text;
|
||||
databaseType?: DatabaseType;
|
||||
byStart: Map<number, SqlTextRange>;
|
||||
ranges: SqlTextRange[];
|
||||
}
|
||||
|
||||
export type ExecutableStatementRangeParser = (sql: string, databaseType?: DatabaseType) => SqlTextRange[];
|
||||
@@ -14,12 +15,59 @@ export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRa
|
||||
if (cache?.doc === doc && cache.databaseType === databaseType) return cache;
|
||||
|
||||
const byStart = new Map<number, SqlTextRange>();
|
||||
for (const range of parse(doc.toString(), databaseType)) {
|
||||
const ranges = parse(doc.toString(), databaseType);
|
||||
for (const range of ranges) {
|
||||
byStart.set(range.from, range);
|
||||
}
|
||||
return { doc, databaseType, byStart };
|
||||
return { doc, databaseType, byStart, ranges };
|
||||
}
|
||||
|
||||
export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null {
|
||||
return cache.byStart.get(lineFrom) ?? null;
|
||||
}
|
||||
|
||||
export function executableStatementRangeAtCursor(cache: ExecutableStatementRangeCache, cursorPos: number): SqlTextRange | null {
|
||||
const pos = Math.max(0, Math.min(cursorPos, cache.doc.length));
|
||||
const line = cache.doc.lineAt(pos);
|
||||
const lineText = line.text.trim();
|
||||
if (!lineText || lineText.startsWith("--") || lineText.startsWith("#") || isCursorOnLeadingBlockComment(line.text, pos - line.from)) return null;
|
||||
|
||||
for (let index = 0; index < cache.ranges.length; index += 1) {
|
||||
const range = cache.ranges[index];
|
||||
if (pos >= range.from && pos <= range.to) return range;
|
||||
|
||||
if (pos < range.from && range.from <= line.to && cache.doc.sliceString(pos, range.from).trim() === "") {
|
||||
return range;
|
||||
}
|
||||
|
||||
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)) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCursorOnLeadingBlockComment(lineText: string, lineOffset: number): boolean {
|
||||
const commentStart = lineText.search(/\S/);
|
||||
if (commentStart < 0 || !lineText.startsWith("/*", commentStart)) return false;
|
||||
|
||||
const commentEnd = lineText.indexOf("*/", commentStart + 2);
|
||||
if (commentEnd < 0) return true;
|
||||
const afterComment = lineText.slice(commentEnd + 2);
|
||||
if (!afterComment.trim()) return true;
|
||||
|
||||
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() === "";
|
||||
}
|
||||
@@ -271,6 +271,14 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
continue;
|
||||
}
|
||||
|
||||
if (supportsSqlServerGoCommands(databaseType) && isAtLineStart(sql, i) && isSqlServerGoLine(sql, i)) {
|
||||
const lineEnd = findLineEnd(sql, i);
|
||||
flush(i);
|
||||
i = nextLineStart(sql, lineEnd);
|
||||
statementHitStart = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
markContent(i);
|
||||
state = "single";
|
||||
@@ -296,7 +304,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
||||
continue;
|
||||
}
|
||||
// Postgres dollar quoting: $tag$ ... $tag$ (tag may be empty, i.e. $$)
|
||||
if (ch === "$") {
|
||||
if (!customDelimiter && ch === "$") {
|
||||
const tagMatch = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
|
||||
if (tagMatch) {
|
||||
markContent(i);
|
||||
@@ -1386,6 +1394,15 @@ function isSlashLine(sql: string, pos: number): boolean {
|
||||
return sql.slice(pos, lineEnd).trim() === "/";
|
||||
}
|
||||
|
||||
function supportsSqlServerGoCommands(databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "sqlserver";
|
||||
}
|
||||
|
||||
function isSqlServerGoLine(sql: string, pos: number): boolean {
|
||||
const lineEnd = findLineEnd(sql, pos);
|
||||
return /^go(?:\s+\d+)?$/i.test(sql.slice(pos, lineEnd).trim());
|
||||
}
|
||||
|
||||
function startsDelimiterCommand(sql: string, pos: number): boolean {
|
||||
const prefix = sql.slice(pos, pos + 9);
|
||||
return prefix.toLowerCase() === "delimiter" && (sql[pos + 9] === " " || sql[pos + 9] === "\t");
|
||||
@@ -1468,9 +1485,15 @@ export function executableStatementRanges(sql: string, databaseType?: DatabaseTy
|
||||
return splitSqlStatementRanges(sql, databaseType).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType).map((range) => rangeFor(range, sql)));
|
||||
}
|
||||
|
||||
export function currentExecutableStatementRange(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlTextRange | null {
|
||||
if (databaseType === "redis") return redisCommandRangeAtCursor(sql, cursorPos);
|
||||
if (databaseType === "mongodb") return null;
|
||||
return statementRangeAtCursor(sql, cursorPos, databaseType);
|
||||
}
|
||||
|
||||
export function buildExecutionCandidates(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlExecutionCandidate[] {
|
||||
const full = fullSqlRange(sql);
|
||||
const cursorStatement = databaseType === "redis" ? redisCommandRangeAtCursor(sql, cursorPos) : statementRangeAtCursor(sql, cursorPos, databaseType);
|
||||
const cursorStatement = currentExecutableStatementRange(sql, cursorPos, databaseType);
|
||||
|
||||
if (!full && !cursorStatement) return [];
|
||||
if (!full) {
|
||||
|
||||
Reference in new issue
Block a user