feat: add execute mode settings and cursor position handling in SQL editor

This commit is contained in:
t8y2
2026-05-04 21:19:07 +08:00
parent 9ced92fdc8
commit 790791f946
9 changed files with 229 additions and 6 deletions
+10 -1
View File
@@ -124,6 +124,7 @@ const showUpdateDialog = ref(false);
const dangerSql = ref("");
const pendingDangerSql = ref("");
const selectedSql = ref("");
const cursorPos = ref(0);
const formatSqlRequestId = ref(0);
const activeOutputView = ref<"result" | "explain">("result");
const showDangerDialog = ref(false);
@@ -455,7 +456,10 @@ const activeTab = computed(() =>
const executableSql = computed(() => {
const tab = activeTab.value;
return tab ? resolveExecutableSql(tab.sql, selectedSql.value) : "";
return tab ? resolveExecutableSql(tab.sql, selectedSql.value, {
mode: settingsStore.editorSettings.executeMode,
cursorPos: cursorPos.value,
}) : "";
});
watch(() => queryStore.activeTabId, () => {
@@ -615,6 +619,10 @@ function onEditorSelectionChange(val: string) {
selectedSql.value = val;
}
function onEditorCursorChange(pos: number) {
cursorPos.value = pos;
}
function formatActiveSql() {
const tab = activeTab.value;
if (!tab || tab.mode !== "query" || !tab.sql.trim()) return;
@@ -1464,6 +1472,7 @@ async function setupFileDrop() {
:format-request-id="formatSqlRequestId"
@update:model-value="onEditorUpdate"
@selection-change="onEditorSelectionChange"
@cursor-change="onEditorCursorChange"
@format-error="onFormatSqlError"
@execute="tryExecute"
/>
+25 -3
View File
@@ -32,6 +32,7 @@ const emit = defineEmits<{
const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
const editFontSize = ref(settingsStore.editorSettings.fontSize);
const editTheme = ref(settingsStore.editorSettings.theme);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
// Sync from store when dialog opens
watch(() => props.open, (open) => {
@@ -39,6 +40,7 @@ watch(() => props.open, (open) => {
editFontFamily.value = settingsStore.editorSettings.fontFamily;
editFontSize.value = settingsStore.editorSettings.fontSize;
editTheme.value = settingsStore.editorSettings.theme;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
}
});
@@ -46,7 +48,8 @@ function hasChanges(): boolean {
return (
editFontFamily.value !== settingsStore.editorSettings.fontFamily ||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
editTheme.value !== settingsStore.editorSettings.theme
editTheme.value !== settingsStore.editorSettings.theme ||
editExecuteMode.value !== settingsStore.editorSettings.executeMode
);
}
@@ -55,6 +58,7 @@ function applySettings() {
fontFamily: editFontFamily.value,
fontSize: editFontSize.value,
theme: editTheme.value,
executeMode: editExecuteMode.value,
});
emit("update:open", false);
}
@@ -63,6 +67,11 @@ function resetDefaults() {
editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily;
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
}
function onExecuteModeChange(v: any) {
if (v === "all" || v === "current") editExecuteMode.value = v;
}
function onFontFamilyChange(v: any) {
@@ -160,7 +169,7 @@ watch(() => props.open, (open) => {
<template>
<Dialog :open="open" @update:open="(v: boolean) => emit('update:open', v)">
<DialogContent class="sm:max-w-[720px] max-h-[calc(100vh-80px)] overflow-y-auto">
<DialogContent class="sm:max-w-[720px] max-h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Settings class="h-4 w-4" />
@@ -243,6 +252,19 @@ watch(() => props.open, (open) => {
</Select>
</div>
<div class="space-y-2">
<Label>{{ t('settings.executeMode') }}</Label>
<Select :model-value="editExecuteMode" @update:model-value="onExecuteModeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.executeMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{{ t('settings.executeModeAll') }}</SelectItem>
<SelectItem value="current">{{ t('settings.executeModeCurrent') }}</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<!-- Live Preview -->
@@ -252,7 +274,7 @@ watch(() => props.open, (open) => {
class="rounded-md border overflow-auto max-w-full"
:class="editTheme === 'vscode-light' || editTheme === 'duotone-light' || editTheme === 'xcode' ? 'border-border' : 'border-border/50'"
>
<div ref="previewRef" style="height: 160px; min-width: 100%" />
<div ref="previewRef" style="min-width: 100%" />
</div>
</div>
</div>
+2
View File
@@ -24,6 +24,7 @@ const props = defineProps<{
const emit = defineEmits<{
"update:modelValue": [value: string];
"selectionChange": [value: string];
"cursorChange": [pos: number];
"formatError": [message: string];
execute: [sql: string];
}>();
@@ -245,6 +246,7 @@ onMounted(async () => {
}
if (update.selectionSet || update.docChanged) {
emit("selectionChange", selectedSqlFromView(update.view));
emit("cursorChange", update.state.selection.main.head);
}
}),
fontThemeComp.of(
+3
View File
@@ -610,6 +610,9 @@ export default {
selectTheme: "Select theme...",
apply: "Apply",
resetDefaults: "Reset Defaults",
executeMode: "Execute Mode (Cmd+Enter)",
executeModeAll: "Execute all SQL",
executeModeCurrent: "Execute statement at cursor",
preview: "Live Preview",
},
};
+3
View File
@@ -608,6 +608,9 @@ export default {
selectTheme: "选择主题...",
apply: "应用",
resetDefaults: "恢复默认",
executeMode: "执行模式 (Cmd+Enter)",
executeModeAll: "执行全部 SQL",
executeModeCurrent: "执行光标所在语句",
preview: "实时预览",
},
};
+16 -2
View File
@@ -1,4 +1,18 @@
export function resolveExecutableSql(fullSql: string, selectedSql: string): string {
import { findStatementAtCursor } from "./sqlStatementSplit";
export type ExecuteMode = "all" | "current";
export function resolveExecutableSql(
fullSql: string,
selectedSql: string,
options?: { mode?: ExecuteMode; cursorPos?: number },
): string {
const trimmedSelection = selectedSql.trim();
return trimmedSelection ? trimmedSelection : fullSql;
if (trimmedSelection) return trimmedSelection;
if (options?.mode === "current" && options.cursorPos !== undefined) {
return findStatementAtCursor(fullSql, options.cursorPos);
}
return fullSql;
}
+100
View File
@@ -0,0 +1,100 @@
export function findStatementAtCursor(sql: string, cursorPos: number): string {
const statements = splitStatements(sql);
for (const stmt of statements) {
if (cursorPos >= stmt.start && cursorPos <= stmt.end) {
return stmt.text;
}
}
if (statements.length > 0) {
const last = statements[statements.length - 1];
if (cursorPos >= last.end) return last.text;
}
return sql.trim();
}
interface StatementRange {
text: string;
start: number;
end: number;
}
function splitStatements(sql: string): StatementRange[] {
const results: StatementRange[] = [];
let i = 0;
let stmtStart = 0;
while (i < sql.length) {
const ch = sql[i];
if (ch === "-" && sql[i + 1] === "-") {
i = skipLineComment(sql, i);
continue;
}
if (ch === "/" && sql[i + 1] === "*") {
i = skipBlockComment(sql, i);
continue;
}
if (ch === "'" || ch === '"') {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "`") {
i = skipQuoted(sql, i, "`");
continue;
}
if (ch === ";") {
const text = sql.slice(stmtStart, i).trim();
if (text) {
results.push({ text, start: stmtStart, end: i + 1 });
}
stmtStart = i + 1;
}
i++;
}
const trailing = sql.slice(stmtStart).trim();
if (trailing) {
results.push({ text: trailing, start: stmtStart, end: sql.length });
}
return results;
}
function skipLineComment(sql: string, pos: number): number {
let i = pos + 2;
while (i < sql.length && sql[i] !== "\n") i++;
return i + 1;
}
function skipBlockComment(sql: string, pos: number): number {
let i = pos + 2;
while (i < sql.length - 1) {
if (sql[i] === "*" && sql[i + 1] === "/") return i + 2;
i++;
}
return sql.length;
}
function skipQuoted(sql: string, pos: number, quote: string): number {
let i = pos + 1;
while (i < sql.length) {
if (sql[i] === quote) {
if (sql[i + 1] === quote) {
i += 2;
continue;
}
return i + 1;
}
if (sql[i] === "\\" && quote === "'") {
i += 2;
continue;
}
i++;
}
return sql.length;
}
+3
View File
@@ -25,6 +25,7 @@ export interface EditorSettings {
fontFamily: string;
fontSize: number;
theme: EditorTheme;
executeMode: "all" | "current";
}
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
@@ -53,6 +54,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: 13,
theme: "one-dark",
executeMode: "all",
};
export const STORAGE_KEY = "dbx-editor-settings";
@@ -68,6 +70,7 @@ function loadEditorSettings(): EditorSettings {
fontFamily: parsed.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
fontSize: parsed.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
theme: parsed.theme ?? DEFAULT_EDITOR_SETTINGS.theme,
executeMode: parsed.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
};
}
} catch { /* ignore */ }