feat(settings): add custom shortcut settings

This commit is contained in:
t8y2
2026-05-15 17:53:51 +08:00
parent 8d73e6e00d
commit 6db2430994
12 changed files with 417 additions and 78 deletions
+7 -5
View File
@@ -450,7 +450,9 @@ function onAiRequestAutoExecuteSql(sql: string) {
}
function handleKeydown(e: KeyboardEvent) {
if (isFocusSearchShortcut(e)) {
const shortcuts = settingsStore.editorSettings.shortcuts;
if (isFocusSearchShortcut(e, shortcuts)) {
const focused = contentAreaRef.value?.focusSearch() || appSidebarRef.value?.focusSearch();
if (focused) {
e.preventDefault();
@@ -458,7 +460,7 @@ function handleKeydown(e: KeyboardEvent) {
}
return;
}
if (isCloseTabShortcut(e)) {
if (isCloseTabShortcut(e, shortcuts)) {
e.preventDefault();
if (showDriverStore.value) {
showDriverStore.value = false;
@@ -467,10 +469,10 @@ function handleKeydown(e: KeyboardEvent) {
}
return;
}
if (isSaveShortcut(e) && e.target instanceof Element && isObjectSourceSaveShortcutTarget(e.target)) {
if (isSaveShortcut(e, shortcuts) && e.target instanceof Element && isObjectSourceSaveShortcutTarget(e.target)) {
return;
}
if (activeTab.value?.mode === "query" && !showSaveSqlDialog.value && isSaveShortcut(e)) {
if (activeTab.value?.mode === "query" && !showSaveSqlDialog.value && isSaveShortcut(e, shortcuts)) {
e.preventDefault();
e.stopPropagation();
void openSaveSqlDialog();
@@ -478,7 +480,7 @@ function handleKeydown(e: KeyboardEvent) {
}
if (
activeTab.value?.mode === "query" &&
isExecuteSqlShortcut(e) &&
isExecuteSqlShortcut(e, shortcuts) &&
e.target instanceof Element &&
e.target.closest("[data-query-editor-root]")
) {
+97 -5
View File
@@ -22,6 +22,14 @@ import {
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { aiTestConnection } from "@/lib/api";
import { eventToShortcut } from "@/lib/keyboardShortcuts";
import {
SHORTCUT_DEFINITIONS,
findShortcutConflict,
formatShortcut,
normalizeShortcutSettings,
type ShortcutActionId,
} from "@/lib/shortcutRegistry";
const { t } = useI18n();
const settingsStore = useSettingsStore();
@@ -44,6 +52,7 @@ const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
// Sync from store when dialog opens
@@ -58,10 +67,19 @@ watch(
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editAppLayout.value = settingsStore.editorSettings.appLayout;
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
}
},
);
const shortcutConflicts = computed(() =>
SHORTCUT_DEFINITIONS.flatMap((definition) => {
const conflict = findShortcutConflict(definition.id, editShortcuts.value[definition.id], editShortcuts.value);
return conflict ? [definition.id] : [];
}),
);
const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0);
function hasChanges(): boolean {
return (
editFontFamily.value !== settingsStore.editorSettings.fontFamily ||
@@ -70,11 +88,13 @@ function hasChanges(): boolean {
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts)
);
}
function applySettings() {
if (hasShortcutConflicts.value) return;
settingsStore.updateEditorSettings({
fontFamily: editFontFamily.value,
fontSize: editFontSize.value,
@@ -83,6 +103,7 @@ function applySettings() {
wordWrap: editWordWrap.value,
appLayout: editAppLayout.value,
redisScanPageSize: editRedisScanPageSize.value,
shortcuts: editShortcuts.value,
});
emit("update:open", false);
}
@@ -95,6 +116,7 @@ function resetDefaults() {
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
}
function onExecuteModeChange(v: any) {
@@ -114,6 +136,27 @@ function onRedisScanPageSizeChange(v: any) {
if (redisScanPageSizeOptions.includes(value)) editRedisScanPageSize.value = value;
}
function onShortcutChange(actionId: ShortcutActionId, value: any) {
if (typeof value !== "string") return;
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return;
editShortcuts.value = { ...editShortcuts.value, [actionId]: value };
}
function onShortcutKeydown(actionId: ShortcutActionId, event: KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
const shortcut = eventToShortcut(event);
if (!shortcut) return;
onShortcutChange(actionId, shortcut);
}
function resetShortcut(actionId: ShortcutActionId) {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return;
editShortcuts.value = { ...editShortcuts.value, [actionId]: definition.defaultShortcut };
}
function setAppLayout(value: "separated" | "classic") {
editAppLayout.value = value;
}
@@ -121,11 +164,12 @@ function setAppLayout(value: "separated" | "classic") {
const activeSettingsTab = ref("editor");
const isWeb = !isTauriRuntime();
const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : ""));
type SettingsCategory = "editor" | "appearance" | "redis" | "ai" | "security" | "about";
type SettingsCategory = "editor" | "appearance" | "redis" | "shortcuts" | "ai" | "security" | "about";
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
{ value: "editor", label: t("settings.editorTab") },
{ value: "appearance", label: t("settings.appearanceTab") },
{ value: "redis", label: t("settings.redisTab") },
{ value: "shortcuts", label: t("settings.shortcutsTab") },
{ value: "ai", label: t("settings.aiTab") },
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
{ value: "about", label: t("settings.aboutTab") },
@@ -535,7 +579,7 @@ watch(
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
<Button :disabled="!hasChanges()" @click="applySettings">
<Button :disabled="!hasChanges() || hasShortcutConflicts" @click="applySettings">
{{ t("settings.apply") }}
</Button>
</DialogFooter>
@@ -580,7 +624,7 @@ watch(
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
<Button :disabled="!hasChanges()" @click="applySettings">
<Button :disabled="!hasChanges() || hasShortcutConflicts" @click="applySettings">
{{ t("settings.apply") }}
</Button>
</DialogFooter>
@@ -610,7 +654,55 @@ watch(
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
<Button :disabled="!hasChanges()" @click="applySettings">
<Button :disabled="!hasChanges() || hasShortcutConflicts" @click="applySettings">
{{ t("settings.apply") }}
</Button>
</DialogFooter>
</section>
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex min-h-full flex-col gap-5 py-2">
<div class="space-y-3">
<div
v-for="definition in SHORTCUT_DEFINITIONS"
:key="definition.id"
class="grid gap-2 rounded-md border bg-background p-3 sm:grid-cols-[minmax(0,1fr)_220px] sm:items-center"
>
<div class="min-w-0 space-y-1">
<Label>{{ t(definition.labelKey) }}</Label>
<p class="text-xs text-muted-foreground">
{{ t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`) }}
</p>
</div>
<div class="space-y-1.5">
<div class="flex gap-2">
<Input
:model-value="formatShortcut(editShortcuts[definition.id])"
readonly
:aria-invalid="shortcutConflicts.includes(definition.id)"
:placeholder="t('settings.shortcutPressShortcut')"
class="font-mono"
@keydown="(event: KeyboardEvent) => onShortcutKeydown(definition.id, event)"
/>
<Button type="button" variant="outline" class="shrink-0" @click="resetShortcut(definition.id)">
{{ t("settings.reset") }}
</Button>
</div>
<p v-if="shortcutConflicts.includes(definition.id)" class="text-xs text-destructive">
{{ t("settings.shortcutConflict") }}
</p>
</div>
</div>
</div>
<DialogFooter class="mt-auto border-t-0 bg-transparent gap-3 sm:gap-3">
<Button variant="outline" @click="resetDefaults">
{{ t("settings.resetDefaults") }}
</Button>
<div class="flex-1" />
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
<Button :disabled="!hasChanges() || hasShortcutConflicts" @click="applySettings">
{{ t("settings.apply") }}
</Button>
</DialogFooter>
+56 -47
View File
@@ -13,6 +13,7 @@ import {
} from "@/lib/sqlCompletion";
import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigation";
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
import { shortcutToCodeMirrorKey } from "@/lib/shortcutRegistry";
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
const props = defineProps<{
@@ -50,6 +51,7 @@ let fontThemeComp: import("@codemirror/state").Compartment | null = null;
let codeMirrorTheme: import("@codemirror/state").Compartment | null = null;
let wordWrapComp: import("@codemirror/state").Compartment | null = null;
let readOnlyComp: import("@codemirror/state").Compartment | null = null;
let runKeymapComp: import("@codemirror/state").Compartment | null = null;
// Completion cache
let cachedTables: Array<{ name: string; schema?: string; type?: "table" | "view" }> = [];
@@ -87,6 +89,54 @@ function resetZoom() {
setFontSize(13);
}
function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view"))["keymap"]) {
const shortcuts = settingsStore.editorSettings.shortcuts;
return codeMirrorKeymap.of([
{
key: "Mod-=",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod-+",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod--",
run: () => {
zoomOut();
return true;
},
},
{
key: "Mod-0",
run: () => {
resetZoom();
return true;
},
},
{
key: shortcutToCodeMirrorKey(shortcuts.executeSql),
run: () => {
if (view.value) emit("execute", executableSqlFromView(view.value));
return true;
},
},
{
key: shortcutToCodeMirrorKey(shortcuts.saveSql),
run: () => {
emit("save");
return true;
},
},
]);
}
function wordWrapExtension() {
if (!editorViewModule) return [];
return props.forceWordWrap || settingsStore.editorSettings.wordWrap ? editorViewModule.EditorView.lineWrapping : [];
@@ -283,6 +333,7 @@ onMounted(async () => {
codeMirrorTheme = new Compartment();
wordWrapComp = new Compartment();
readOnlyComp = new Compartment();
runKeymapComp = new Compartment();
const ss = settingsStore.editorSettings;
@@ -294,51 +345,6 @@ onMounted(async () => {
keywords: (baseDialect.spec.keywords || "") + " " + extraKeywords,
});
const runKeymap = keymap.of([
{
key: "Mod-=",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod-+",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod--",
run: () => {
zoomOut();
return true;
},
},
{
key: "Mod-0",
run: () => {
resetZoom();
return true;
},
},
{
key: "Mod-Enter",
run: () => {
if (view.value) emit("execute", executableSqlFromView(view.value));
return true;
},
},
{
key: "Mod-s",
run: () => {
emit("save");
return true;
},
},
]);
const theme = await loadEditorTheme(ss.theme);
const state = EditorState.create({
@@ -356,7 +362,7 @@ onMounted(async () => {
closeBrackets(),
bracketMatching(),
Prec.highest(keymap.of([...closeBracketsKeymap, indentWithTab])),
runKeymap,
runKeymapComp.of(runKeymapExtension(keymap)),
wordWrapComp.of(props.forceWordWrap || ss.wordWrap ? EditorView.lineWrapping : []),
readOnlyComp.of([EditorState.readOnly.of(!!props.readOnly), EditorView.editable.of(!props.readOnly)]),
rectangularSelection({ eventFilter: (e: MouseEvent) => e.altKey || e.button === 1 }),
@@ -575,12 +581,15 @@ watch(
watch(
() => settingsStore.editorSettings,
async (ss) => {
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !editorViewModule) return;
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !runKeymapComp || !editorViewModule) {
return;
}
const themeExt = await loadEditorTheme(ss.theme);
view.value.dispatch({
effects: [
codeMirrorTheme.reconfigure(themeExt),
wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []),
runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap)),
fontThemeComp.reconfigure(
editorFontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily, {
fixedHeight: true,
+12
View File
@@ -1006,6 +1006,7 @@ export default {
editorTab: "Editor",
appearanceTab: "Appearance",
redisTab: "Redis",
shortcutsTab: "Shortcuts",
aiTab: "AI",
jdbcTab: "JDBC Drivers",
securityTab: "Security",
@@ -1021,6 +1022,7 @@ export default {
appLayoutClassic: "Classic compact",
appLayoutClassicDescription: "Continuous tab bar with compact panel headers.",
apply: "Apply",
reset: "Reset",
resetDefaults: "Reset Defaults",
executeMode: "Execute Mode (Cmd+Enter)",
executeModeAll: "Execute all SQL",
@@ -1030,6 +1032,16 @@ export default {
redisScanPageSize: "Redis scan count",
redisScanPageSizeDescription: "Keys requested per Redis SCAN page when browsing keys.",
redisScanPageSizeOption: "{count} keys",
shortcutExecuteSql: "Execute SQL",
shortcutSaveSql: "Save SQL",
shortcutCloseTab: "Close tab",
shortcutFocusSearch: "Focus search",
shortcutCancelSearch: "Cancel search",
shortcutScopeGlobal: "Global",
shortcutScopeEditor: "SQL editor",
shortcutScopeSearch: "Search fields",
shortcutPressShortcut: "Press shortcut",
shortcutConflict: "This shortcut conflicts with another action in the same scope.",
preview: "Live Preview",
jdbcPlugin: "DBX JDBC plugin",
jdbcPluginInstall: "Install JDBC plugin",
+12
View File
@@ -984,6 +984,7 @@ export default {
editorTab: "Editor",
appearanceTab: "Apariencia",
redisTab: "Redis",
shortcutsTab: "Atajos",
aiTab: "IA",
jdbcTab: "Drivers JDBC",
securityTab: "Seguridad",
@@ -999,6 +1000,7 @@ export default {
appLayoutClassic: "Clásico compacto",
appLayoutClassicDescription: "Barra de pestañas continua con encabezados de panel compactos.",
apply: "Aplicar",
reset: "Restablecer",
resetDefaults: "Restablecer valores por defecto",
executeMode: "Modo de ejecución (Cmd+Enter)",
executeModeAll: "Ejecutar todo el SQL",
@@ -1008,6 +1010,16 @@ export default {
redisScanPageSize: "Cantidad de escaneo Redis",
redisScanPageSizeDescription: "Claves solicitadas por página SCAN al explorar claves Redis.",
redisScanPageSizeOption: "{count} claves",
shortcutExecuteSql: "Ejecutar SQL",
shortcutSaveSql: "Guardar SQL",
shortcutCloseTab: "Cerrar pestaña",
shortcutFocusSearch: "Enfocar búsqueda",
shortcutCancelSearch: "Cancelar búsqueda",
shortcutScopeGlobal: "Global",
shortcutScopeEditor: "Editor SQL",
shortcutScopeSearch: "Campos de búsqueda",
shortcutPressShortcut: "Presiona un atajo",
shortcutConflict: "Este atajo entra en conflicto con otra acción del mismo ámbito.",
preview: "Vista previa en tiempo real",
jdbcPlugin: "Plugin JDBC de DBX",
jdbcPluginInstall: "Instalar plugin JDBC",
+12
View File
@@ -988,6 +988,7 @@ export default {
editorTab: "编辑器",
appearanceTab: "外观",
redisTab: "Redis",
shortcutsTab: "快捷键",
aiTab: "AI",
jdbcTab: "JDBC 驱动",
securityTab: "安全",
@@ -1003,6 +1004,7 @@ export default {
appLayoutClassic: "经典紧凑",
appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。",
apply: "应用",
reset: "重置",
resetDefaults: "恢复默认",
executeMode: "执行模式 (Cmd+Enter)",
executeModeAll: "执行全部 SQL",
@@ -1012,6 +1014,16 @@ export default {
redisScanPageSize: "Redis 扫描数量",
redisScanPageSizeDescription: "浏览 Redis Key 时每次 SCAN 请求的 Key 数量。",
redisScanPageSizeOption: "{count} 个 Key",
shortcutExecuteSql: "执行 SQL",
shortcutSaveSql: "保存 SQL",
shortcutCloseTab: "关闭标签页",
shortcutFocusSearch: "聚焦搜索",
shortcutCancelSearch: "取消搜索",
shortcutScopeGlobal: "全局",
shortcutScopeEditor: "SQL 编辑器",
shortcutScopeSearch: "搜索框",
shortcutPressShortcut: "按下快捷键",
shortcutConflict: "这个快捷键与同一作用域内的其他操作冲突。",
preview: "实时预览",
jdbcPlugin: "DBX JDBC 插件",
jdbcPluginInstall: "安装 JDBC 插件",
+71 -21
View File
@@ -1,36 +1,87 @@
import {
DEFAULT_SHORTCUT_SETTINGS,
normalizeShortcutSettings,
type ShortcutActionId,
type ShortcutSettings,
} from "@/lib/shortcutRegistry";
export interface ShortcutLikeEvent {
key: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
isComposing?: boolean;
}
export function isExecuteSqlShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key === "Enter";
function normalizeKey(key: string): string {
if (key === " ") return "Space";
return key.length === 1 ? key.toLowerCase() : key;
}
export function isCloseTabShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (!event.metaKey) return false;
return event.key.toLowerCase() === "w";
function shortcutKeyName(key: string): string | null {
if (key === " ") return "Space";
if (["Control", "Meta", "Shift", "Alt"].includes(key)) return null;
if (key.length === 1) return key.toUpperCase();
return key;
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === "f";
export function eventToShortcut(event: ShortcutLikeEvent): string | null {
if (event.isComposing) return null;
const key = shortcutKeyName(event.key);
if (!key) return null;
const hasModifier = !!event.metaKey || !!event.ctrlKey || !!event.altKey || !!event.shiftKey;
if (!hasModifier && key.length === 1) return null;
const parts: string[] = [];
if (event.shiftKey) parts.push("Shift");
if (event.metaKey || event.ctrlKey) parts.push("Mod");
if (event.altKey) parts.push("Alt");
parts.push(key);
return parts.join("+");
}
export function isSaveShortcut(event: ShortcutLikeEvent): boolean {
export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === "s";
const parts = shortcut.split("+");
const key = parts[parts.length - 1] ?? "";
const modifiers = new Set(parts.slice(0, -1));
const usesMod = modifiers.has("Mod");
const usesMeta = modifiers.has("Meta");
const usesCtrl = modifiers.has("Ctrl");
if (usesMod) {
if (!event.metaKey && !event.ctrlKey) return false;
} else {
if (!!event.metaKey !== usesMeta) return false;
if (!!event.ctrlKey !== usesCtrl) return false;
}
if (!!event.altKey !== modifiers.has("Alt")) return false;
if (!!event.shiftKey !== modifiers.has("Shift")) return false;
return normalizeKey(event.key) === normalizeKey(key);
}
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>): string {
return normalizeShortcutSettings(shortcuts)[actionId] || DEFAULT_SHORTCUT_SETTINGS[actionId];
}
export function isExecuteSqlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("executeSql", shortcuts));
}
export function isCloseTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("closeTab", shortcuts));
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("focusSearch", shortcuts));
}
export function isSaveShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("saveSql", shortcuts));
}
export function isObjectSourceSaveShortcutTarget(
@@ -39,7 +90,6 @@ export function isObjectSourceSaveShortcutTarget(
return !!target?.closest("[data-object-source-editor], [data-object-source-preview]");
}
export function isCancelSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
return event.key === "Escape";
export function isCancelSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("cancelSearch", shortcuts));
}
+93
View File
@@ -0,0 +1,93 @@
export type ShortcutActionId = "executeSql" | "saveSql" | "closeTab" | "focusSearch" | "cancelSearch";
export type ShortcutScope = "global" | "editor" | "search";
export interface ShortcutDefinition {
id: ShortcutActionId;
labelKey: string;
scope: ShortcutScope;
defaultShortcut: string;
}
export type ShortcutSettings = Record<ShortcutActionId, string>;
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
id: "executeSql",
labelKey: "settings.shortcutExecuteSql",
scope: "editor",
defaultShortcut: "Mod+Enter",
},
{
id: "saveSql",
labelKey: "settings.shortcutSaveSql",
scope: "editor",
defaultShortcut: "Mod+S",
},
{
id: "closeTab",
labelKey: "settings.shortcutCloseTab",
scope: "global",
defaultShortcut: "Meta+W",
},
{
id: "focusSearch",
labelKey: "settings.shortcutFocusSearch",
scope: "global",
defaultShortcut: "Mod+F",
},
{
id: "cancelSearch",
labelKey: "settings.shortcutCancelSearch",
scope: "search",
defaultShortcut: "Escape",
},
];
export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => [definition.id, definition.defaultShortcut]),
) as ShortcutSettings;
export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>): ShortcutSettings {
return Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => [
definition.id,
typeof settings?.[definition.id] === "string" && settings[definition.id]?.trim()
? settings[definition.id]
: definition.defaultShortcut,
]),
) as ShortcutSettings;
}
export function shortcutToCodeMirrorKey(shortcut: string): string {
return shortcut
.split("+")
.map((part) => (part.length === 1 ? part.toLowerCase() : part))
.join("-");
}
export function formatShortcut(shortcut: string, platform = globalThis.navigator?.platform || ""): string {
const isMac = platform.toLowerCase().includes("mac");
return shortcut
.split("+")
.map((part) => {
if (part === "Mod") return isMac ? "Cmd" : "Ctrl";
if (part === "Meta") return isMac ? "Cmd" : "Meta";
return part;
})
.join("+");
}
export function findShortcutConflict(
actionId: ShortcutActionId,
shortcut: string,
shortcuts: ShortcutSettings,
): ShortcutActionId | null {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return null;
const conflict = SHORTCUT_DEFINITIONS.find(
(item) => item.id !== actionId && item.scope === definition.scope && shortcuts[item.id] === shortcut,
);
return conflict?.id ?? null;
}
+4
View File
@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import * as api from "@/lib/api";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/shortcutRegistry";
export type AiProvider = "claude" | "openai" | "custom";
export type AiApiStyle = "completions" | "responses";
@@ -52,6 +53,7 @@ export interface EditorSettings {
appLayout: "separated" | "classic";
pageSize: number;
redisScanPageSize: number;
shortcuts: ShortcutSettings;
}
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
@@ -85,6 +87,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
appLayout: "classic",
pageSize: 100,
redisScanPageSize: 1000,
shortcuts: normalizeShortcutSettings(),
};
export const STORAGE_KEY = "dbx-editor-settings";
@@ -100,6 +103,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>): Edit
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: settings.pageSize ?? DEFAULT_EDITOR_SETTINGS.pageSize,
redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize,
shortcuts: normalizeShortcutSettings(settings.shortcuts),
};
}