mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-18 21:04:16 +08:00
✨ feat(query-editor): 新增选择当前语句快捷键
- 快捷键配置:新增选择当前语句动作,默认绑定 Ctrl+E - 编辑器接入:在 Monaco 查询编辑器中注册选择当前语句 action - 语句识别:新增 SQL 语句范围解析,支持按光标定位当前语句 - 兼容处理:忽略字符串、注释和 dollar quote 内的分号 - 测试覆盖:补充快捷键默认配置和语句选择解析单元测试 Refs #404
This commit is contained in:
159
frontend/src/utils/sqlStatementSelection.ts
Normal file
159
frontend/src/utils/sqlStatementSelection.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
export interface SqlStatementRange {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
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];
|
||||
};
|
||||
Reference in New Issue
Block a user