mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
feat(settings): add custom shortcut settings
This commit is contained in:
+7
-5
@@ -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]")
|
||||
) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 插件",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user