mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-18 12:54:08 +08:00
- 支持仅执行选中 SQL、光标所在语句和增量新增语句 - 持久化查询草稿,避免重启后丢失历史 SQL - 在表字段提示中展示注释信息 - 修复清空默认 SQL 后被自动回填的问题 Refs #483
202 lines
5.2 KiB
TypeScript
202 lines
5.2 KiB
TypeScript
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;
|
||
};
|