feat(editor): add SQL editor context menu shortcuts

* feat(editor): add SQL editor context menu shortcuts

* chore(i18n): autofill new translations

---------

Co-authored-by: faithererer <faithererer@users.noreply.github.com>
Co-authored-by: dbx-i18n-bot <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
faithererer
2026-07-05 19:38:51 +08:00
committed by GitHub
co-authored by faithererer dbx-i18n-bot
parent d502b4ce6c
commit 8b035d0989
17 changed files with 207 additions and 108 deletions
@@ -68,6 +68,7 @@ import {
} from "@/lib/backend/api";
import { eventToShortcut } from "@/lib/editor/keyboardShortcuts";
import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates";
@@ -590,7 +591,26 @@ const shortcutConflicts = computed(() =>
return conflict ? [definition.id] : [];
}),
);
const formatterEditorShortcutIds: ShortcutActionId[] = ["formatSql", "find", "replace", "saveSql", "acceptCompletion", "indentMore", "indentLess", "duplicateLine", "deleteLine", "moveLineUp", "moveLineDown", "copyLineUp", "copyLineDown", "undo", "redo", "selectAll"];
const formatterEditorShortcutIds: ShortcutActionId[] = [
"formatSql",
"find",
"replace",
"saveSql",
"acceptCompletion",
"indentMore",
"indentLess",
"duplicateLine",
"deleteLine",
"moveLineUp",
"moveLineDown",
"copyLineUp",
"copyLineDown",
"undo",
"redo",
"selectAll",
"uppercaseSelection",
"lowercaseSelection",
];
const formatterEditorShortcutDefinitions = computed(() => formatterEditorShortcutIds.map((id) => SHORTCUT_DEFINITIONS.find((definition) => definition.id === id)).filter((definition): definition is (typeof SHORTCUT_DEFINITIONS)[number] => !!definition));
const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0);
const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts));
@@ -1082,29 +1102,7 @@ function onShortcutKeydown(actionId: ShortcutActionId, event: KeyboardEvent) {
}
function formatShortcutPill(shortcut: string): string {
if (!shortcut) return "—";
const isMac = globalThis.navigator?.platform?.toLowerCase().includes("mac") ?? false;
return shortcut
.split("+")
.filter(Boolean)
.map((part) => {
if (part === "Mod") return isMac ? "⌘" : "Ctrl";
if (part === "Meta") return isMac ? "⌘" : "Meta";
if (part === "Shift") return isMac ? "⇧" : "Shift";
if (part === "Alt") return isMac ? "⌥" : "Alt";
if (part === "Control" || part === "Ctrl") return isMac ? "⌃" : "Ctrl";
if (part === "Enter") return "↵";
if (part === "Backspace") return "⌫";
if (part === "Delete") return isMac ? "⌦" : "Del";
if (part === "Escape") return "Esc";
if (part === "ArrowUp") return "↑";
if (part === "ArrowDown") return "↓";
if (part === "ArrowLeft") return "←";
if (part === "ArrowRight") return "→";
if (part === " ") return "Space";
return part.length === 1 ? part.toUpperCase() : part;
})
.join(isMac ? " " : " + ");
return formatShortcutDisplay(shortcut);
}
const shortcutPressShortcutLabel = computed(() => t("settings.shortcutPressShortcut"));
@@ -566,10 +566,10 @@ function selectAllSqlFromContextMenu() {
focusEditor();
}
function convertSelectedSqlCaseFromContextMenu(mode: SelectionCaseMode) {
function convertSelectedSqlCase(mode: SelectionCaseMode): boolean {
const currentView = view.value;
const EditorSelection = codeMirrorEditorSelection;
if (!currentView || !EditorSelection || !canCopySelectedSql.value) return;
if (!currentView || !EditorSelection) return false;
const state = currentView.state;
const transaction = state.changeByRange((range) => {
@@ -585,8 +585,10 @@ function convertSelectedSqlCaseFromContextMenu(mode: SelectionCaseMode) {
if (!transaction.changes.empty) {
currentView.dispatch({ ...transaction, scrollIntoView: true, userEvent: "input" });
focusEditor();
return true;
}
focusEditor();
return false;
}
function openTableFromContextMenu() {
@@ -637,52 +639,59 @@ function selectSqlLineFromGutter(currentView: EditorViewType, line: { from: numb
return true;
}
const contextMenuItems = computed<ContextMenuItem[]>(() => [
{
label: executeContextMenuLabel.value,
action: executeFromContextMenu,
disabled: !canExecuteContextSql.value,
icon: Play,
},
{
label: t("contextMenu.viewData"),
action: openTableFromContextMenu,
disabled: !contextTableName.value,
icon: Table2,
},
{
label: t("contextMenu.editStructure"),
action: editTableStructureFromContextMenu,
disabled: !contextTableName.value,
icon: PencilRuler,
},
{
label: t("contextMenu.viewDdl"),
action: openTableDdlFromContextMenu,
disabled: !contextTableName.value,
icon: FileCode,
},
{ label: "", separator: true },
{
label: t("editor.contextMenu.copySelection"),
action: copySelectedSqlFromContextMenu,
disabled: !canCopySelectedSql.value,
icon: Copy,
},
{
label: t("editor.contextMenu.uppercaseSelection"),
action: () => convertSelectedSqlCaseFromContextMenu("upper"),
disabled: !canCopySelectedSql.value,
icon: CaseUpper,
},
{
label: t("editor.contextMenu.lowercaseSelection"),
action: () => convertSelectedSqlCaseFromContextMenu("lower"),
disabled: !canCopySelectedSql.value,
icon: CaseLower,
},
{ label: t("editor.contextMenu.selectAll"), action: selectAllSqlFromContextMenu, icon: TextSelect },
]);
const contextMenuItems = computed<ContextMenuItem[]>(() => {
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
return [
{
label: executeContextMenuLabel.value,
action: executeFromContextMenu,
disabled: !canExecuteContextSql.value,
icon: Play,
shortcut: shortcuts.executeSql,
},
{
label: t("contextMenu.viewData"),
action: openTableFromContextMenu,
disabled: !contextTableName.value,
icon: Table2,
},
{
label: t("contextMenu.editStructure"),
action: editTableStructureFromContextMenu,
disabled: !contextTableName.value,
icon: PencilRuler,
},
{
label: t("contextMenu.viewDdl"),
action: openTableDdlFromContextMenu,
disabled: !contextTableName.value,
icon: FileCode,
},
{ label: "", separator: true },
{
label: t("editor.contextMenu.copySelection"),
action: copySelectedSqlFromContextMenu,
disabled: !canCopySelectedSql.value,
icon: Copy,
shortcut: "Mod+C",
},
{
label: t("editor.contextMenu.uppercaseSelection"),
action: () => convertSelectedSqlCase("upper"),
disabled: !canCopySelectedSql.value,
icon: CaseUpper,
shortcut: shortcuts.uppercaseSelection,
},
{
label: t("editor.contextMenu.lowercaseSelection"),
action: () => convertSelectedSqlCase("lower"),
disabled: !canCopySelectedSql.value,
icon: CaseLower,
shortcut: shortcuts.lowercaseSelection,
},
{ label: t("editor.contextMenu.selectAll"), action: selectAllSqlFromContextMenu, icon: TextSelect, shortcut: shortcuts.selectAll },
];
});
function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view"))["keymap"]) {
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
@@ -718,6 +727,8 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
...binding(shortcuts.undo, (view) => codeMirrorUndo?.(view) ?? false),
...binding(shortcuts.redo, (view) => codeMirrorRedo?.(view) ?? false),
...binding(shortcuts.selectAll, (view) => codeMirrorSelectAll?.(view) ?? false),
...binding(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")),
...binding(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")),
]),
) ?? [],
codeMirrorKeymap.of(
@@ -131,7 +131,6 @@ import { useExportTracker, type ExportTask } from "@/composables/useExportTracke
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { copyToClipboard } from "@/lib/common/clipboard";
import { hasEnabledTransportLayers } from "@/lib/backend/connectionTransport";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
import { isWindows } from "@/lib/backend/platform";
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSql/savedSqlHistory";
import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers";
@@ -4038,8 +4037,8 @@ onBeforeUnmount(() => {
// ---- CustomContextMenu ----
const shortcutCopyName = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.copySidebarSelection));
const shortcutEditConnection = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.editSidebarConnection));
const shortcutCopyName = computed(() => settingsStore.editorSettings.shortcuts.copySidebarSelection);
const shortcutEditConnection = computed(() => settingsStore.editorSettings.shortcuts.editSidebarConnection);
const shortcutRename = "F2";
const shortcutRefresh = "F5";
const shortcutDelete = "Delete";
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, watch, onBeforeUnmount, nextTick, type Component } from "vue";
import { ChevronRight } from "@lucide/vue";
import { shortcutDisplayKeys } from "@/lib/editor/shortcutDisplay";
export interface ContextMenuItem {
label: string;
@@ -9,6 +10,7 @@ export interface ContextMenuItem {
separator?: boolean;
icon?: Component;
iconClass?: string;
// Raw shortcut syntax such as `Mod+C` or `Shift+Alt+U`; display formatting stays in this component.
shortcut?: string;
variant?: "default" | "destructive";
visible?: boolean;
@@ -239,20 +241,8 @@ function itemButtonClass(variant?: "default" | "destructive") {
];
}
function shortcutKeyLabel(part: string): string {
if (part === "Cmd") return "⌘";
if (part === "Meta") return "⌘";
if (part === "Alt") return "⌥";
if (part === "Shift") return "⇧";
if (part === "Delete") return "Del";
if (part === "Backspace") return "⌫";
if (part === "Enter") return "↵";
if (part === "Escape") return "Esc";
return part;
}
function shortcutKeys(shortcut?: string): string[] {
return shortcut?.split("+").filter(Boolean).map(shortcutKeyLabel) || [];
return shortcutDisplayKeys(shortcut);
}
onBeforeUnmount(() => {
+2
View File
@@ -3026,6 +3026,8 @@ export default {
project: "Project",
openSource: "Open-source repository",
officialDocs: "Official docs",
shortcutUppercaseSelection: "Convert selection to uppercase",
shortcutLowercaseSelection: "Convert selection to lowercase",
},
driverStore: {
progressJreExtract: "Extracting JRE...",
+2
View File
@@ -2982,6 +2982,8 @@ export default withEnglishFallback({
project: "Proyecto",
openSource: "Repositorio de código abierto",
officialDocs: "Documentación oficial",
shortcutUppercaseSelection: "Convertir selección a mayúsculas",
shortcutLowercaseSelection: "Convertir selección a minúsculas",
},
driverStore: {
progressJreExtract: "Extrayendo JRE...",
+2
View File
@@ -2980,6 +2980,8 @@ export default withEnglishFallback({
project: "Progetto",
openSource: "Repository open-source",
officialDocs: "Documenti ufficiali",
shortcutUppercaseSelection: "Converti selezione in maiuscolo",
shortcutLowercaseSelection: "Converti selezione in minuscolo",
},
driverStore: {
progressJreExtract: "Estrazione JRE...",
+2
View File
@@ -2980,6 +2980,8 @@ export default withEnglishFallback({
project: "プロジェクト",
openSource: "オープンソースリポジトリ",
officialDocs: "公式ドキュメント",
shortcutUppercaseSelection: "選択範囲を大文字に変換",
shortcutLowercaseSelection: "選択範囲を小文字に変換",
},
driverStore: {
progressJreExtract: "JREを展開中...",
+2
View File
@@ -2981,6 +2981,8 @@ export default withEnglishFallback({
project: "Projeto",
openSource: "Repositório de código aberto",
officialDocs: "Documentação oficial",
shortcutUppercaseSelection: "Converter seleção em maiúsculas",
shortcutLowercaseSelection: "Converter seleção em minúsculas",
},
driverStore: {
progressJreExtract: "Extraindo JRE...",
+2
View File
@@ -2894,6 +2894,8 @@ export default withEnglishFallback({
shortcutUndo: "撤销",
shortcutRedo: "重做",
shortcutSelectAll: "全选",
shortcutUppercaseSelection: "选中内容转为大写",
shortcutLowercaseSelection: "选中内容转为小写",
shortcutCopyCurrentRow: "复制当前数据行",
shortcutDeleteCurrentRow: "删除当前数据行",
shortcutNewQuery: "新建查询",
+2
View File
@@ -2884,6 +2884,8 @@ export default withEnglishFallback({
exportRowLimitUnlimited: "無限制",
queryExportKeysetOptimizationEnabled: "嘗試 keyset 最佳化",
queryExportKeysetOptimizationEnabledDescription: "僅適用於可安全識別的單表查詢;複雜查詢會自動回退。",
shortcutUppercaseSelection: "選取內容轉為大寫",
shortcutLowercaseSelection: "選取內容轉為小寫",
},
driverStore: {
progressJreExtract: "解壓縮 JRE……",
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { formatShortcutDisplay, shortcutDisplayKeys } from "@/lib/editor/shortcutDisplay";
describe("shortcut display", () => {
it("uses readable modifier labels on Windows", () => {
expect(shortcutDisplayKeys("Shift+Alt+U", "Win32")).toEqual(["Shift", "Alt", "U"]);
});
it("uses readable modifier labels on Linux", () => {
expect(shortcutDisplayKeys("Ctrl+Alt+Delete", "Linux x86_64")).toEqual(["Ctrl", "Alt", "Del"]);
});
it("uses Apple platform glyphs on macOS", () => {
expect(shortcutDisplayKeys("Mod+Alt+Enter", "MacIntel")).toEqual(["⌘", "⌥", "↵"]);
});
it("formats shortcut pills with platform separators", () => {
expect(formatShortcutDisplay("Shift+Alt+ArrowUp", "Win32")).toBe("Shift + Alt + ↑");
expect(formatShortcutDisplay("Shift+Alt+ArrowUp", "MacIntel")).toBe("⇧ ⌥ ↑");
expect(formatShortcutDisplay("Mod+Delete", "MacIntel")).toBe("⌘ ⌦");
});
});
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
describe("shortcutRegistry editor actions", () => {
const formatterEditorActionIds: ShortcutActionId[] = ["formatSql", "indentMore", "indentLess", "duplicateLine", "deleteLine", "moveLineUp", "moveLineDown", "copyLineUp", "copyLineDown", "undo", "redo", "selectAll"];
const formatterEditorActionIds: ShortcutActionId[] = ["formatSql", "indentMore", "indentLess", "duplicateLine", "deleteLine", "moveLineUp", "moveLineDown", "copyLineUp", "copyLineDown", "undo", "redo", "selectAll", "uppercaseSelection", "lowercaseSelection"];
const sidebarShortcutActionIds: ShortcutActionId[] = ["copySidebarSelection", "pasteSidebarSelection", "editSidebarConnection"];
it("registers formatter editor shortcuts in the generic editor scope", () => {
@@ -30,6 +30,8 @@ describe("shortcutRegistry editor actions", () => {
expect(shortcuts.undo).toBe("Mod+Z");
expect(shortcuts.redo).toBe("Shift+Mod+Z");
expect(shortcuts.selectAll).toBe("Mod+A");
expect(shortcuts.uppercaseSelection).toBe("Shift+Alt+U");
expect(shortcuts.lowercaseSelection).toBe("Shift+Alt+L");
});
it("detects conflicts between formatter editor shortcuts and other editor shortcuts", () => {
@@ -38,6 +40,12 @@ describe("shortcutRegistry editor actions", () => {
expect(findShortcutConflict("duplicateLine", shortcuts.duplicateLine, shortcuts)).toBe("find");
});
it("detects conflicts for SQL selection case shortcuts", () => {
const shortcuts = normalizeShortcutSettings({ uppercaseSelection: "Mod+A" });
expect(findShortcutConflict("uppercaseSelection", shortcuts.uppercaseSelection, shortcuts)).toBe("selectAll");
});
it("registers sidebar shortcuts in the sidebar scope", () => {
for (const actionId of sidebarShortcutActionIds) {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
@@ -0,0 +1,37 @@
export function isMacShortcutPlatform(platform = globalThis.navigator?.platform || ""): boolean {
return platform.toLowerCase().includes("mac");
}
export function shortcutKeyLabel(part: string, platform = globalThis.navigator?.platform || ""): string {
const isMac = isMacShortcutPlatform(platform);
if (part === "Mod") return isMac ? "⌘" : "Ctrl";
if (part === "Cmd") return isMac ? "⌘" : "Cmd";
if (part === "Meta") return isMac ? "⌘" : "Meta";
if (part === "Alt") return isMac ? "⌥" : "Alt";
if (part === "Shift") return isMac ? "⇧" : "Shift";
if (part === "Control" || part === "Ctrl") return isMac ? "⌃" : "Ctrl";
if (part === "Delete") return isMac ? "⌦" : "Del";
if (part === "Backspace") return "⌫";
if (part === "Enter") return "↵";
if (part === "Escape") return "Esc";
if (part === "ArrowUp") return "↑";
if (part === "ArrowDown") return "↓";
if (part === "ArrowLeft") return "←";
if (part === "ArrowRight") return "→";
if (part === " ") return "Space";
return part.length === 1 ? part.toUpperCase() : part;
}
export function shortcutDisplayKeys(shortcut?: string, platform = globalThis.navigator?.platform || ""): string[] {
return (
shortcut
?.split("+")
.filter(Boolean)
.map((part) => shortcutKeyLabel(part, platform)) || []
);
}
export function formatShortcutDisplay(shortcut: string, platform = globalThis.navigator?.platform || ""): string {
if (!shortcut) return "—";
return shortcutDisplayKeys(shortcut, platform).join(isMacShortcutPlatform(platform) ? " " : " + ");
}
@@ -14,6 +14,8 @@ export type ShortcutActionId =
| "undo"
| "redo"
| "selectAll"
| "uppercaseSelection"
| "lowercaseSelection"
| "copyCurrentRow"
| "deleteCurrentRow"
| "newQuery"
@@ -147,6 +149,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "editor",
defaultShortcut: "Mod+A",
},
{
id: "uppercaseSelection",
labelKey: "settings.shortcutUppercaseSelection",
scope: "editor",
defaultShortcut: "Shift+Alt+U",
},
{
id: "lowercaseSelection",
labelKey: "settings.shortcutLowercaseSelection",
scope: "editor",
defaultShortcut: "Shift+Alt+L",
},
{
id: "copyCurrentRow",
labelKey: "settings.shortcutCopyCurrentRow",
+12 -9
View File
@@ -9,15 +9,18 @@ DBX 为常用操作提供可配置的键盘快捷键。使用默认快捷键或
### 查询编辑器
| 快捷键 | 操作 |
| -------------------------------------- | -------------- |
| `Ctrl+Enter` / `Cmd+Enter` | 执行当前查询 |
| `Ctrl+Shift+Enter` / `Cmd+Shift+Enter` | 执行所有查询 |
| `Ctrl+/` / `Cmd+/` | 切换行注释 |
| `Ctrl+F` / `Cmd+F` | 在编辑器中搜索 |
| `Ctrl+S` / `Cmd+S` | 保存当前标签页 |
| `Tab` | 缩进选中行 |
| `Shift+Tab` | 取消缩进选中行 |
| 快捷键 | 操作 |
| -------------------------------------- | ---------------- |
| `Ctrl+Enter` / `Cmd+Enter` | 执行当前查询 |
| `Ctrl+Shift+Enter` / `Cmd+Shift+Enter` | 执行所有查询 |
| `Ctrl+/` / `Cmd+/` | 切换行注释 |
| `Ctrl+F` / `Cmd+F` | 在编辑器中搜索 |
| `Ctrl+S` / `Cmd+S` | 保存当前标签页 |
| `Ctrl+A` / `Cmd+A` | 全选 |
| `Shift+Alt+U` | 选中内容转为大写 |
| `Shift+Alt+L` | 选中内容转为小写 |
| `Tab` | 缩进选中行 |
| `Shift+Tab` | 取消缩进选中行 |
### 数据表格
+12 -9
View File
@@ -9,15 +9,18 @@ DBX provides configurable keyboard shortcuts for common actions. Use the default
### Query Editor
| Shortcut | Action |
| -------------------------------------- | ---------------------- |
| `Ctrl+Enter` / `Cmd+Enter` | Execute current query |
| `Ctrl+Shift+Enter` / `Cmd+Shift+Enter` | Execute all queries |
| `Ctrl+/` / `Cmd+/` | Toggle line comment |
| `Ctrl+F` / `Cmd+F` | Search in editor |
| `Ctrl+S` / `Cmd+S` | Save current tab |
| `Tab` | Indent selected lines |
| `Shift+Tab` | Outdent selected lines |
| Shortcut | Action |
| -------------------------------------- | ------------------------------ |
| `Ctrl+Enter` / `Cmd+Enter` | Execute current query |
| `Ctrl+Shift+Enter` / `Cmd+Shift+Enter` | Execute all queries |
| `Ctrl+/` / `Cmd+/` | Toggle line comment |
| `Ctrl+F` / `Cmd+F` | Search in editor |
| `Ctrl+S` / `Cmd+S` | Save current tab |
| `Ctrl+A` / `Cmd+A` | Select all text |
| `Shift+Alt+U` | Convert selection to uppercase |
| `Shift+Alt+L` | Convert selection to lowercase |
| `Tab` | Indent selected lines |
| `Shift+Tab` | Outdent selected lines |
### Data Grid