feat(editor): support executing all SQL from blank lines

This commit is contained in:
zipg authored and GitHub committed 2026-08-10 18:26:39 +08:00
1 parent a9736f0ffc
commit a83f32d13f
18 files changed
+82 -11

No files matched your search

@@ -273,6 +273,7 @@ const editCustomThemes = ref<CustomTheme[]>([...settingsStore.editorSettings.cus
const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThemeId);
const showThemeCustomizer = ref(false);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editExecuteAllOnBlankLine = ref(settingsStore.editorSettings.executeAllOnBlankLine);
const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs);
const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs);
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
@@ -444,6 +445,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
customThemes: editCustomThemes.value,
activeCustomThemeId: editActiveCustomThemeId.value,
executeMode: editExecuteMode.value,
executeAllOnBlankLine: editExecuteAllOnBlankLine.value,
globalConnectTimeoutSecs: editGlobalConnectTimeoutSecs.value,
globalQueryTimeoutSecs: editGlobalQueryTimeoutSecs.value,
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
@@ -714,6 +716,7 @@ function syncEditorSettingsDraftFromStore() {
editCustomThemes.value = [...settingsStore.editorSettings.customThemes];
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editExecuteAllOnBlankLine.value = settingsStore.editorSettings.executeAllOnBlankLine;
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
@@ -929,6 +932,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily;
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine;
editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
@@ -1024,6 +1028,7 @@ function resetAllDefaults() {
editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes];
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine;
editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
@@ -3621,6 +3626,16 @@ onUnmounted(() => {
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2" :class="{ 'opacity-50': editExecuteMode !== 'current' }">
<div class="space-y-1">
<Label for="editor-execute-all-on-blank-line">{{ t("settings.executeAllOnBlankLine") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.executeAllOnBlankLineDescription") }}
</p>
</div>
<Switch id="editor-execute-all-on-blank-line" v-model="editExecuteAllOnBlankLine" :disabled="editExecuteMode !== 'current'" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-global-connect-timeout">{{ t("settings.globalConnectTimeout") }}</Label>
@@ -800,12 +800,21 @@ function requestExecuteFromView(currentView: EditorViewType, cursorPos: number,
const doc = currentView.state.doc.toString();
const parameterOptions = sqlStatementParameterOptions();
const candidates = buildExecutionCandidates(doc, cursorPos, props.databaseType, parameterOptions);
if (candidates.length === 0) return true;
const executeMode = settingsStore.editorSettings.executeMode;
if (candidates.length === 0) {
if (executeMode === "current") toast(t("editor.noExecutableStatementAtCursor"), 3000);
return true;
}
const candidate = executionCandidateForMode(candidates, executeMode, {
executeAllOnBlankLine: settingsStore.editorSettings.executeAllOnBlankLine,
});
if (!candidate) {
toast(t("editor.noExecutableStatementAtCursor"), 3000);
return true;
}
// The execution shortcut keeps executing the configured target (cursor/all) directly:
// it stays keyboard-driven and never pops the picker, which is reserved for click entry points.
if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker || !hasMultipleExecutionTargets(doc, props.databaseType, parameterOptions)) {
const candidate = executionCandidateForMode(candidates, settingsStore.editorSettings.executeMode);
if (!candidate) return true;
emitExecutionRequest(sqlExecutionSnapshotForRange(currentView, candidate), options.openInNewResultTab);
return true;
}
+3
View File
@@ -1227,6 +1227,7 @@ export default {
noResults: "No results",
inSelection: "In selection",
},
noExecutableStatementAtCursor: "There is no executable statement at the cursor.",
executionPicker: {
title: "Execution Target",
currentStatement: "Current SQL",
@@ -5537,6 +5538,8 @@ export default {
executeMode: "Execute Mode (Cmd+Enter)",
executeModeAll: "Execute all SQL",
executeModeCurrent: "Execute statement at cursor",
executeAllOnBlankLine: "Execute all SQL from blank lines",
executeAllOnBlankLineDescription: "In current-statement mode, execute all SQL when there is no statement at the cursor. Disabled by default.",
showExecutionTargetPicker: "Show execution target picker",
showExecutionTargetPickerDescription: "When enabled, running without a selection lets you choose between the current statement and all SQL.",
showStatementRunButtons: "Show left-side run buttons",
+3
View File
@@ -1191,6 +1191,7 @@ export default withEnglishFallback({
noResults: "Sin resultados",
inSelection: "En la selección",
},
noExecutableStatementAtCursor: "No hay ninguna sentencia ejecutable en la posición del cursor.",
executionPicker: {
title: "Execution Target",
currentStatement: "Current SQL",
@@ -5267,6 +5268,8 @@ export default withEnglishFallback({
executeMode: "Modo de ejecución (Cmd+Enter)",
executeModeAll: "Ejecutar todo el SQL",
executeModeCurrent: "Ejecutar sentencia en el cursor",
executeAllOnBlankLine: "Ejecutar todo el SQL desde líneas en blanco",
executeAllOnBlankLineDescription: "En el modo de sentencia actual, ejecuta todo el SQL cuando no hay una sentencia en el cursor. Desactivado de forma predeterminada.",
showExecutionTargetPicker: "Mostrar selector de objetivo",
showExecutionTargetPickerDescription: "Al activarlo, ejecutar sin selección permite elegir entre la sentencia actual y todo el SQL.",
showStatementRunButtons: "Mostrar botones de ejecución laterales",
+3
View File
@@ -1189,6 +1189,7 @@ export default withEnglishFallback({
noResults: "Nessun risultato",
inSelection: "Nella selezione",
},
noExecutableStatementAtCursor: "Non è presente alcuna istruzione eseguibile in corrispondenza del cursore.",
executionPicker: {
title: "Execution Target",
currentStatement: "Current SQL",
@@ -5267,6 +5268,8 @@ export default withEnglishFallback({
executeMode: "Modalità Esecuzione (Cmd+Enter)",
executeModeAll: "Esegui tutto l'SQL",
executeModeCurrent: "Esegui istruzione al cursore",
executeAllOnBlankLine: "Esegui tutto l'SQL dalle righe vuote",
executeAllOnBlankLineDescription: "In modalità istruzione corrente, esegue tutto l'SQL quando non è presente un'istruzione al cursore. Disattivato per impostazione predefinita.",
showExecutionTargetPicker: "Mostra selettore destinazione",
showExecutionTargetPickerDescription: "Se attivo, l'esecuzione senza selezione permette di scegliere tra istruzione corrente e tutto l'SQL.",
showStatementRunButtons: "Mostra pulsanti di esecuzione laterali",
+3
View File
@@ -1209,6 +1209,7 @@ export default withEnglishFallback({
noResults: "結果なし",
inSelection: "選択範囲内",
},
noExecutableStatementAtCursor: "カーソル位置に実行可能な文がありません。",
executionPicker: {
title: "Execution Target",
currentStatement: "Current SQL",
@@ -5296,6 +5297,8 @@ export default withEnglishFallback({
executeMode: "実行モード (Cmd+Enter)",
executeModeAll: "すべてのSQLを実行",
executeModeCurrent: "カーソル位置の文を実行",
executeAllOnBlankLine: "空行ではすべてのSQLを実行",
executeAllOnBlankLineDescription: "現在の文を実行するモードで、カーソル位置に文がない場合はすべてのSQLを実行します。既定では無効です。",
showExecutionTargetPicker: "実行対象ピッカーを表示",
showExecutionTargetPickerDescription: "有効にすると、選択なしで実行するときに現在の文とすべてのSQLを一時的に選べます。",
showStatementRunButtons: "左側の実行ボタンを表示",
+3
View File
@@ -1117,6 +1117,7 @@ export default withEnglishFallback({
noResults: "결과 없음",
inSelection: "선택 영역 내",
},
noExecutableStatementAtCursor: "커서 위치에 실행 가능한 구문이 없습니다.",
executionPicker: {
title: "실행 대상",
currentStatement: "현재 SQL",
@@ -5035,6 +5036,8 @@ export default withEnglishFallback({
executeMode: "실행 모드 (Cmd+Enter)",
executeModeAll: "모든 SQL 실행",
executeModeCurrent: "커서 위치의 구문 실행",
executeAllOnBlankLine: "빈 줄에서 모든 SQL 실행",
executeAllOnBlankLineDescription: "현재 구문 실행 모드에서 커서 위치에 구문이 없으면 모든 SQL을 실행합니다. 기본적으로 비활성화됩니다.",
showExecutionTargetPicker: "실행 대상 선택기 표시",
showExecutionTargetPickerDescription: "활성화하면 선택 없이 실행할 때 현재 구문과 전체 SQL 중에서 선택할 수 있습니다.",
showStatementRunButtons: "왼쪽 실행 버튼 표시",
+3
View File
@@ -1190,6 +1190,7 @@ export default withEnglishFallback({
noResults: "Nenhum resultado",
inSelection: "Na seleção",
},
noExecutableStatementAtCursor: "Não há instrução executável na posição do cursor.",
executionPicker: {
title: "Execution Target",
currentStatement: "Current SQL",
@@ -5269,6 +5270,8 @@ export default withEnglishFallback({
executeMode: "Modo de execução (Cmd+Enter)",
executeModeAll: "Executar todo o SQL",
executeModeCurrent: "Executar instrução no cursor",
executeAllOnBlankLine: "Executar todo o SQL em linhas em branco",
executeAllOnBlankLineDescription: "No modo de instrução atual, executa todo o SQL quando não há instrução no cursor. Desativado por padrão.",
showExecutionTargetPicker: "Mostrar seletor de destino",
showExecutionTargetPickerDescription: "Quando ativado, executar sem seleção permite escolher entre a instrução atual e todo o SQL.",
showStatementRunButtons: "Mostrar botões de execução laterais",
+3
View File
@@ -1152,6 +1152,7 @@ export default withEnglishFallback({
noResults: "无结果",
inSelection: "选区内",
},
noExecutableStatementAtCursor: "光标所在位置没有可执行语句",
executionPicker: {
title: "执行目标",
currentStatement: "当前 SQL",
@@ -5458,6 +5459,8 @@ export default withEnglishFallback({
executeMode: "执行模式 (Cmd+Enter)",
executeModeAll: "执行全部 SQL",
executeModeCurrent: "执行光标所在语句",
executeAllOnBlankLine: "空白位置执行全部 SQL",
executeAllOnBlankLineDescription: "在“执行光标所在语句”模式下,光标没有对应语句时执行全部 SQL。默认关闭。",
showExecutionTargetPicker: "显示执行目标选择器",
showExecutionTargetPickerDescription: "开启后,无选区执行时可在当前语句和全部 SQL 之间临时选择。",
showStatementRunButtons: "显示左侧执行按钮",
+3
View File
@@ -1189,6 +1189,7 @@ export default withEnglishFallback({
noResults: "無結果",
inSelection: "選取區內",
},
noExecutableStatementAtCursor: "游標所在位置沒有可執行語句",
executionPicker: {
title: "執行目標",
currentStatement: "當前 SQL",
@@ -4712,6 +4713,8 @@ export default withEnglishFallback({
executeMode: "執行模式 (Cmd+Enter)",
executeModeAll: "執行全部 SQL",
executeModeCurrent: "執行指標所在語句",
executeAllOnBlankLine: "空白位置執行全部 SQL",
executeAllOnBlankLineDescription: "在「執行游標所在語句」模式下,游標沒有對應語句時執行全部 SQL。預設關閉。",
showExecutionTargetPicker: "顯示執行目標選擇器",
showExecutionTargetPickerDescription: "啟用後,無選取執行時可在目前語句與全部 SQL 之間臨時選擇。",
showStatementRunButtons: "顯示左側執行按鈕",
@@ -21,20 +21,21 @@ describe("QueryEditor execution routing", () => {
it("keeps selection priority and the configured current/all target choice", () => {
const selectionBranch = queryEditorSource.indexOf("if (!options.ignoreSelection && !selection.empty)");
const executeModeBranch = queryEditorSource.indexOf("executionCandidateForMode(candidates, settingsStore.editorSettings.executeMode)");
const executeModeBranch = queryEditorSource.indexOf("executionCandidateForMode(candidates, executeMode");
expect(selectionBranch).toBeGreaterThan(-1);
expect(executeModeBranch).toBeGreaterThan(selectionBranch);
});
it("does not fall back to all SQL when current mode has no statement at the cursor", () => {
expect(queryEditorSource).toContain("const candidate = executionCandidateForMode(candidates, settingsStore.editorSettings.executeMode)");
expect(queryEditorSource).toContain("if (!candidate) return true");
it("uses the opt-in blank-line fallback and otherwise reports the missing cursor statement", () => {
expect(queryEditorSource).toContain("executeAllOnBlankLine: settingsStore.editorSettings.executeAllOnBlankLine");
expect(queryEditorSource).toContain('toast(t("editor.noExecutableStatementAtCursor"), 3000)');
expect(queryEditorSource).not.toContain("?? candidates[0]");
});
it("consumes the execution shortcut when the editor has no executable target", () => {
expect(queryEditorSource).toContain("if (candidates.length === 0) return true");
it("consumes the execution shortcut and reports an empty current target", () => {
expect(queryEditorSource).toContain("if (candidates.length === 0)");
expect(queryEditorSource).toContain('if (executeMode === "current") toast(t("editor.noExecutableStatementAtCursor"), 3000)');
});
it("preserves the source range when executing a current/all candidate without a manual selection", () => {
@@ -23,6 +23,13 @@ describe("executionCandidateForMode", () => {
expect(executionCandidateForMode([all], "all")).toBe(all);
});
it("falls back to all SQL only when blank-line execution is enabled", () => {
const all = candidate("all", ["all"]);
expect(executionCandidateForMode([all], "current", { executeAllOnBlankLine: false })).toBeNull();
expect(executionCandidateForMode([all], "current", { executeAllOnBlankLine: true })).toBe(all);
});
it("uses the deduplicated candidate when one statement is both current and all", () => {
const currentAndAll = candidate("all", ["cursor", "all"]);
@@ -1321,6 +1321,7 @@ WHERE t2.product_name = '12345'
expect(candidateKinds(candidates)).toEqual(["all"]);
expect(candidates[0].supportedKinds).toEqual(["all"]);
expect(executionCandidateForMode(candidates, "current")).toBeNull();
expect(executionCandidateForMode(candidates, "current", { executeAllOnBlankLine: true })).toBe(candidates[0]);
expect(executionCandidateForMode(candidates, "all")).toBe(candidates[0]);
});
@@ -12,6 +12,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"customThemes",
"activeCustomThemeId",
"executeMode",
"executeAllOnBlankLine",
"globalConnectTimeoutSecs",
"globalQueryTimeoutSecs",
"showExecutionTargetPicker",
@@ -106,6 +106,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-theme", category: "editor", titleKey: "settings.theme", targetId: "editor" },
{ id: "editor-font-size", category: "editor", titleKey: "settings.fontSize", targetId: "editor" },
{ id: "editor-execute-mode", category: "editor", titleKey: "settings.executeMode", targetId: "editor" },
{ id: "editor-execute-all-on-blank-line", category: "editor", titleKey: "settings.executeAllOnBlankLine", descriptionKey: "settings.executeAllOnBlankLineDescription", targetId: "editor" },
{ id: "editor-global-connect-timeout", category: "editor", titleKey: "settings.globalConnectTimeout", descriptionKey: "settings.globalConnectTimeoutDescription", targetId: "editor" },
{ id: "editor-global-query-timeout", category: "editor", titleKey: "settings.globalQueryTimeout", descriptionKey: "settings.globalQueryTimeoutDescription", targetId: "editor" },
{ id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" },
@@ -41,9 +41,11 @@ export function isSqlExecutionSnapshot(value: SqlExecutionOverride | undefined):
return typeof value === "object" && value !== null && typeof value.fullSql === "string" && typeof value.selectedSql === "string" && typeof value.cursorPos === "number" && typeof value.selectionFrom === "number" && typeof value.selectionTo === "number";
}
export function executionCandidateForMode(candidates: SqlExecutionCandidate[], mode: ExecuteMode): SqlExecutionCandidate | null {
export function executionCandidateForMode(candidates: SqlExecutionCandidate[], mode: ExecuteMode, options: { executeAllOnBlankLine?: boolean } = {}): SqlExecutionCandidate | null {
const targetKind: SqlExecutionTargetKind = mode === "current" ? "cursor" : "all";
return candidates.find((candidate) => candidate.supportedKinds.includes(targetKind)) ?? null;
const candidate = candidates.find((item) => item.supportedKinds.includes(targetKind));
if (candidate || mode !== "current" || !options.executeAllOnBlankLine) return candidate ?? null;
return candidates.find((item) => item.supportedKinds.includes("all")) ?? null;
}
export function resolveExecutableSql(fullSql: string, selectedSql: string, options?: { mode?: ExecuteMode; cursorPos?: number }): string {
@@ -52,6 +52,12 @@ describe("normalizeEditorSettings", () => {
).toBe("all");
});
it("keeps blank-line execute-all disabled by default and preserves an explicit opt-in", () => {
expect(normalizeEditorSettings({}).executeAllOnBlankLine).toBe(false);
expect(normalizeEditorSettings({ executeAllOnBlankLine: false }).executeAllOnBlankLine).toBe(false);
expect(normalizeEditorSettings({ executeAllOnBlankLine: true }).executeAllOnBlankLine).toBe(true);
});
it("enables automatic table aliases by default", () => {
expect(normalizeEditorSettings({}).autoAliasTables).toBe(true);
});
+4
View File
@@ -478,6 +478,7 @@ export interface EditorSettings {
activeCustomThemeId: string;
executeMode: "all" | "current";
executeModeDefaultVersion: number;
executeAllOnBlankLine: boolean;
globalConnectTimeoutSecs: number;
connectTimeoutInheritConnectionIds: string[];
globalQueryTimeoutSecs: number;
@@ -664,6 +665,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
activeCustomThemeId: "default",
executeMode: "current",
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
executeAllOnBlankLine: false,
globalConnectTimeoutSecs: 10,
connectTimeoutInheritConnectionIds: [],
globalQueryTimeoutSecs: 30,
@@ -992,6 +994,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
executeMode: hasCurrentExecuteModeDefault && (settings.executeMode === "all" || settings.executeMode === "current") ? settings.executeMode : DEFAULT_EDITOR_SETTINGS.executeMode,
executeModeDefaultVersion,
executeAllOnBlankLine: settings.executeAllOnBlankLine === true,
globalConnectTimeoutSecs: normalizeGlobalConnectTimeoutSecs(settings.globalConnectTimeoutSecs),
connectTimeoutInheritConnectionIds: Array.isArray(settings.connectTimeoutInheritConnectionIds) ? [...new Set(settings.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [],
globalQueryTimeoutSecs: normalizeGlobalQueryTimeoutSecs(settings.globalQueryTimeoutSecs ?? legacyTimeoutSettings.queryTimeoutSecs),
@@ -1525,6 +1528,7 @@ export const useSettingsStore = defineStore("settings", () => {
}
}
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
if (partial.executeAllOnBlankLine !== undefined) editorSettings.value.executeAllOnBlankLine = partial.executeAllOnBlankLine === true;
if (partial.globalConnectTimeoutSecs !== undefined) editorSettings.value.globalConnectTimeoutSecs = normalizeGlobalConnectTimeoutSecs(partial.globalConnectTimeoutSecs);
if (partial.connectTimeoutInheritConnectionIds !== undefined) {
editorSettings.value.connectTimeoutInheritConnectionIds = [...new Set(partial.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];