diff --git a/src/App.vue b/src/App.vue index c9445c4a7f..6f753a6212 100644 --- a/src/App.vue +++ b/src/App.vue @@ -35,7 +35,12 @@ import * as api from "@/lib/api"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; import { isTauriRuntime } from "@/lib/tauriRuntime"; -import { isCloseTabShortcut, isExecuteSqlShortcut, isFocusSearchShortcut } from "@/lib/keyboardShortcuts"; +import { + isCloseTabShortcut, + isExecuteSqlShortcut, + isFocusSearchShortcut, + isSaveShortcut, +} from "@/lib/keyboardShortcuts"; import { isPreviewTab } from "@/lib/tabPresentation"; import { supportsSqlFileExecution } from "@/lib/databaseCapabilities"; import { classifyAiSqlExecution } from "@/lib/aiSqlExecutionPolicy"; @@ -432,14 +437,7 @@ function handleKeydown(e: KeyboardEvent) { } return; } - if ( - activeTab.value?.mode === "query" && - !showSaveSqlDialog.value && - !e.isComposing && - !e.altKey && - (e.metaKey || e.ctrlKey) && - e.key.toLowerCase() === "s" - ) { + if (activeTab.value?.mode === "query" && !showSaveSqlDialog.value && isSaveShortcut(e)) { e.preventDefault(); e.stopPropagation(); void openSaveSqlDialog(); @@ -669,6 +667,7 @@ onUnmounted(() => { @editor-selection-change="(v: string) => (selectedSql = v)" @editor-cursor-change="(p: number) => (cursorPos = p)" @format-error="toast(t('toolbar.formatSqlFailed'))" + @save-sql="void openSaveSqlDialog()" @reload=" ( sql?: string, diff --git a/src/components/editor/QueryEditor.vue b/src/components/editor/QueryEditor.vue index 970a3853bc..12707cff3a 100644 --- a/src/components/editor/QueryEditor.vue +++ b/src/components/editor/QueryEditor.vue @@ -28,6 +28,7 @@ const emit = defineEmits<{ cursorChange: [pos: number]; formatError: [message: string]; execute: [sql: string]; + save: []; clickTable: [tableName: string]; clickColumn: [columns: Array<{ name: string; table: string; schema?: string }>, error?: string | undefined]; closeColumnPanel: []; @@ -319,6 +320,13 @@ onMounted(async () => { return true; }, }, + { + key: "Mod-s", + run: () => { + emit("save"); + return true; + }, + }, ]); const theme = await loadEditorTheme(ss.theme); diff --git a/src/components/layout/ContentArea.vue b/src/components/layout/ContentArea.vue index 44b5cf70ff..0803e3babb 100644 --- a/src/components/layout/ContentArea.vue +++ b/src/components/layout/ContentArea.vue @@ -35,6 +35,7 @@ const emit = defineEmits<{ "update:activeOutputView": [value: "result" | "explain" | "chart"]; fixWithAi: [errorMessage: string]; execute: [sqlOverride?: string]; + saveSql: []; cancel: []; explain: []; editorUpdate: [value: string]; @@ -194,6 +195,7 @@ defineExpose({ focusSearch }); @cursor-change="emit('editorCursorChange', $event)" @format-error="emit('formatError')" @execute="emit('execute')" + @save="emit('saveSql')" @click-table="onHandleClickTable" @click-column="onHandleClickColumn" @close-column-panel="onHandleCloseColumnPanel" diff --git a/src/components/objects/ObjectBrowser.vue b/src/components/objects/ObjectBrowser.vue index 9e6a0ac2fe..10385f11c0 100644 --- a/src/components/objects/ObjectBrowser.vue +++ b/src/components/objects/ObjectBrowser.vue @@ -648,7 +648,7 @@ watch(
{{ sourceError }}
-
+
{{ sourceSaveError }} @@ -674,6 +674,7 @@ watch( :format-dialect="sourceFormatDialect" force-word-wrap read-only + data-object-source-preview />
diff --git a/src/lib/keyboardShortcuts.ts b/src/lib/keyboardShortcuts.ts index b9956be281..afe4fa409b 100644 --- a/src/lib/keyboardShortcuts.ts +++ b/src/lib/keyboardShortcuts.ts @@ -26,6 +26,13 @@ export function isFocusSearchShortcut(event: ShortcutLikeEvent): boolean { return event.key.toLowerCase() === "f"; } +export function isSaveShortcut(event: ShortcutLikeEvent): boolean { + if (event.isComposing) return false; + if (event.altKey) return false; + if (!event.metaKey && !event.ctrlKey) return false; + return event.key.toLowerCase() === "s"; +} + export function isCancelSearchShortcut(event: ShortcutLikeEvent): boolean { if (event.isComposing) return false; return event.key === "Escape"; diff --git a/tests/keyboardShortcuts.test.ts b/tests/keyboardShortcuts.test.ts index 51f392ec92..fbe570095d 100644 --- a/tests/keyboardShortcuts.test.ts +++ b/tests/keyboardShortcuts.test.ts @@ -5,6 +5,7 @@ import { isCloseTabShortcut, isExecuteSqlShortcut, isFocusSearchShortcut, + isSaveShortcut, } from "../src/lib/keyboardShortcuts.ts"; test("matches Cmd+Enter for SQL execution", () => { @@ -47,6 +48,22 @@ test("ignores Alt+F for focusing search", () => { assert.equal(isFocusSearchShortcut({ key: "f", altKey: true }), false); }); +test("matches Cmd+S for saving", () => { + assert.equal(isSaveShortcut({ key: "s", metaKey: true }), true); +}); + +test("matches Ctrl+S for saving", () => { + assert.equal(isSaveShortcut({ key: "S", ctrlKey: true }), true); +}); + +test("ignores save shortcut while composing", () => { + assert.equal(isSaveShortcut({ key: "s", metaKey: true, isComposing: true }), false); +}); + +test("ignores Alt+S for saving", () => { + assert.equal(isSaveShortcut({ key: "s", altKey: true }), false); +}); + test("matches Escape for cancelling search", () => { assert.equal(isCancelSearchShortcut({ key: "Escape" }), true); });