Files
MyGoNavi/frontend/src/utils/sqlStatementSelection.ts
Syngnat b9c743d67e feat(query-editor): 增强 SQL 编辑器执行与历史体验
- 支持仅执行选中 SQL、光标所在语句和增量新增语句

- 持久化查询草稿,避免重启后丢失历史 SQL

- 在表字段提示中展示注释信息

- 修复清空默认 SQL 后被自动回填的问题

Refs #483
2026-05-23 17:07:47 +08:00

202 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface SqlStatementRange {
start: number;
end: number;
text: string;
}
export type SqlExecutionSelectionSource = 'selection' | 'statement' | 'line';
export interface SqlExecutionSelection {
sql: string;
source: SqlExecutionSelectionSource;
}
const isWhitespace = (ch: string): boolean => (
ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r'
);
const trimStatementRange = (sql: string, start: number, end: number): SqlStatementRange | null => {
let nextStart = Math.max(0, start);
let nextEnd = Math.min(sql.length, Math.max(start, end));
while (nextStart < nextEnd && isWhitespace(sql[nextStart])) {
nextStart++;
}
while (nextEnd > nextStart && isWhitespace(sql[nextEnd - 1])) {
nextEnd--;
}
if (nextStart >= nextEnd) {
return null;
}
return {
start: nextStart,
end: nextEnd,
text: sql.slice(nextStart, nextEnd),
};
};
export const findSqlStatementRanges = (sql: string): SqlStatementRange[] => {
const text = String(sql || '').replace(/\r\n/g, '\n');
const ranges: SqlStatementRange[] = [];
let statementStart = 0;
let inSingle = false;
let inDouble = false;
let inBacktick = false;
let escaped = false;
let inLineComment = false;
let inBlockComment = false;
let dollarTag: string | null = null;
const push = (end: number) => {
const range = trimStatementRange(text, statementStart, end);
if (range) {
ranges.push(range);
}
};
for (let index = 0; index < text.length; index++) {
const ch = text[index];
const next = index + 1 < text.length ? text[index + 1] : '';
const prev = index > 0 ? text[index - 1] : '';
const next2 = index + 2 < text.length ? text[index + 2] : '';
if (dollarTag) {
if (text.startsWith(dollarTag, index)) {
index += dollarTag.length - 1;
dollarTag = null;
}
continue;
}
if (inLineComment) {
if (ch === '\n') {
inLineComment = false;
}
continue;
}
if (inBlockComment) {
if (ch === '*' && next === '/') {
index++;
inBlockComment = false;
}
continue;
}
if (!inSingle && !inDouble && !inBacktick) {
if (ch === '/' && next === '*') {
index++;
inBlockComment = true;
continue;
}
if (ch === '#') {
inLineComment = true;
continue;
}
if (ch === '-' && next === '-' && (index === 0 || isWhitespace(prev)) && (next2 === '' || isWhitespace(next2))) {
index++;
inLineComment = true;
continue;
}
if (ch === '$') {
const match = text.slice(index).match(/^\$[A-Za-z0-9_]*\$/);
if (match?.[0]) {
dollarTag = match[0];
index += dollarTag.length - 1;
continue;
}
}
}
if (escaped) {
escaped = false;
continue;
}
if ((inSingle || inDouble) && ch === '\\') {
escaped = true;
continue;
}
if (!inDouble && !inBacktick && ch === "'") {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inBacktick && ch === '"') {
inDouble = !inDouble;
continue;
}
if (!inSingle && !inDouble && ch === '`') {
inBacktick = !inBacktick;
continue;
}
if (!inSingle && !inDouble && !inBacktick && (ch === ';' || ch === '')) {
push(index);
statementStart = index + 1;
}
}
push(text.length);
return ranges;
};
export const resolveCurrentSqlStatementRange = (sql: string, cursorOffset: number): SqlStatementRange | null => {
const text = String(sql || '').replace(/\r\n/g, '\n');
const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
const ranges = findSqlStatementRanges(text);
if (ranges.length === 0) {
return null;
}
const containingRange = ranges.find((range) => offset >= range.start && offset <= range.end);
if (containingRange) {
return containingRange;
}
const nextRange = ranges.find((range) => offset < range.start);
if (nextRange) {
return nextRange;
}
return ranges[ranges.length - 1];
};
export const resolveExecutableSql = (
sql: string,
cursorOffset: number,
selectedSql = '',
): SqlExecutionSelection | null => {
const selected = String(selectedSql || '').trim();
if (selected) {
return { sql: selectedSql, source: 'selection' };
}
const text = String(sql || '').replace(/\r\n/g, '\n');
const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0));
const ranges = findSqlStatementRanges(text);
const statement = ranges.find((range) => offset >= range.start && offset <= range.end);
if (statement?.text.trim()) {
return { sql: statement.text, source: 'statement' };
}
const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
const nextLineBreak = text.indexOf('\n', offset);
const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak;
const line = text.slice(lineStart, lineEnd).trim();
if (line) {
const lineStatement = [...ranges].reverse().find((range) => range.start < lineEnd && range.end >= lineStart);
if (lineStatement?.text.trim()) {
return { sql: lineStatement.text, source: 'statement' };
}
}
if (line) {
return { sql: line, source: 'line' };
}
return null;
};