From 5a9ab5c0366b6b35f3030a76e7ff49e7ff4ea475 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 7 Jul 2026 18:32:36 +0800 Subject: [PATCH] feat(editor): add configurable line comment shortcut --- .../editor/EditorSettingsDialog.vue | 22 +++++++++++++-- .../src/components/editor/QueryEditor.vue | 6 +++- apps/desktop/src/i18n/locales/en.ts | 3 ++ apps/desktop/src/i18n/locales/es.ts | 3 ++ apps/desktop/src/i18n/locales/it.ts | 3 ++ apps/desktop/src/i18n/locales/ja.ts | 3 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 3 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 3 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 3 ++ .../__tests__/editor/shortcutDisplay.spec.ts | 4 +++ .../__tests__/editor/shortcutRegistry.spec.ts | 28 ++++++++++++++++++- .../desktop/src/lib/editor/shortcutDisplay.ts | 18 ++++++++++-- .../src/lib/editor/shortcutRegistry.ts | 21 ++++++++++---- 13 files changed, 109 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 4f65080dd..8a0e71a8c 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -3,7 +3,7 @@ import { ref, watch, shallowRef, computed, onMounted, onUnmounted, nextTick } fr import type { Ref } from "vue"; import type { EditorView as EditorViewType } from "@codemirror/view"; import { useI18n } from "vue-i18n"; -import { AlertTriangle, CheckCircle2, CircleHelp, Cloud, Copy, Download, ExternalLink, GripVertical, Loader2, Moon, PackageSearch, Pencil, Plus, RefreshCw, RotateCcw, Settings, Sun, SunMoon, Terminal, Trash2, Upload, X } from "@lucide/vue"; +import { AlertTriangle, CheckCircle2, CircleHelp, Cloud, Copy, Download, ExternalLink, GripVertical, Loader2, Moon, PackageSearch, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings, Sun, SunMoon, Terminal, Trash2, Upload, X } from "@lucide/vue"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -617,8 +617,10 @@ const shortcutConflicts = computed(() => return conflict ? [definition.id] : []; }), ); +const shortcutSearchQuery = ref(""); const formatterEditorShortcutIds: ShortcutActionId[] = [ "formatSql", + "toggleLineComment", "find", "replace", "saveSql", @@ -638,6 +640,15 @@ const formatterEditorShortcutIds: ShortcutActionId[] = [ "lowercaseSelection", ]; const formatterEditorShortcutDefinitions = computed(() => formatterEditorShortcutIds.map((id) => SHORTCUT_DEFINITIONS.find((definition) => definition.id === id)).filter((definition): definition is (typeof SHORTCUT_DEFINITIONS)[number] => !!definition)); +const filteredShortcutDefinitions = computed(() => { + const query = shortcutSearchQuery.value.trim().toLowerCase(); + if (!query) return SHORTCUT_DEFINITIONS; + return SHORTCUT_DEFINITIONS.filter((definition) => { + const scope = t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`); + const shortcut = formatShortcutPill(editShortcuts.value[definition.id]); + return [definition.id, t(definition.labelKey), scope, shortcut].some((value) => value.toLowerCase().includes(query)); + }); +}); const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0); const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts)); const duckDbWorkerSettingsRequireRestart = computed(() => editDuckDbWorkerProcessIsolation.value !== startupDuckDbWorkerProcessIsolation.value || normalizeDuckDbWorkerMaxProcesses(editDuckDbWorkerMaxProcesses.value) !== startupDuckDbWorkerMaxProcesses.value); @@ -3407,8 +3418,15 @@ onUnmounted(cleanupPreviewEditor);
+
+ + +
-
+
+ {{ t("settings.shortcutSearchNoResults") }} +
+
diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 97feaa4c1..9444d45d8 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -231,6 +231,7 @@ let codeMirrorUndo: typeof import("@codemirror/commands").undo | null = null; let codeMirrorRedo: typeof import("@codemirror/commands").redo | null = null; let codeMirrorSelectAll: typeof import("@codemirror/commands").selectAll | null = null; let codeMirrorInsertNewlineKeepIndent: typeof import("@codemirror/commands").insertNewlineKeepIndent | null = null; +let codeMirrorToggleLineComment: typeof import("@codemirror/commands").toggleLineComment | null = null; let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType | null = null; let setPreviewRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null; let previewRangeComp: import("@codemirror/state").Compartment | null = null; @@ -932,6 +933,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") ...binding(shortcuts.selectAll, (view) => codeMirrorSelectAll?.(view) ?? false), ...binding(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")), ...binding(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")), + ...binding(shortcuts.toggleLineComment, (view) => codeMirrorToggleLineComment?.(view) ?? false), ...binding(shortcuts.exPasteSqlInCondition, () => { if (!supportsSqlInListPaste(props.databaseType)) return false; void pasteClipboardAsSqlInCondition(); @@ -2464,7 +2466,7 @@ onMounted(async () => { { EditorState, EditorSelection, Compartment, Prec, StateEffect, StateField }, langSql, { autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField }, - { copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, history, defaultKeymap, historyKeymap }, + { copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, history, defaultKeymap, historyKeymap }, { bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap }, { searchKeymap }, ] = await Promise.all([import("@codemirror/view"), import("@codemirror/state"), import("@codemirror/lang-sql"), import("@codemirror/autocomplete"), import("@codemirror/commands"), import("@codemirror/language"), import("@codemirror/search")]); @@ -2500,6 +2502,7 @@ onMounted(async () => { codeMirrorRedo = redo; codeMirrorSelectAll = selectAll; codeMirrorInsertNewlineKeepIndent = insertNewlineKeepIndent; + codeMirrorToggleLineComment = toggleLineComment; codeMirrorIndentUnit = indentUnit; window.addEventListener("keyup", clearTableNavigationHoverOnModifierRelease); window.addEventListener("blur", clearTableNavigationHover); @@ -2725,6 +2728,7 @@ onMounted(async () => { dropCursor(), EditorView.dragMovesSelection.of((event) => !event.ctrlKey && !event.metaKey), EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((event) => event.altKey && event.button === 0), indentOnInput(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), crosshairCursor(), diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 48e0a81cf..4e57768cd 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -3041,6 +3041,7 @@ export default { shortcutSaveSql: "Save SQL", shortcutAcceptCompletion: "Accept completion", shortcutFormatSql: "Format SQL", + shortcutToggleLineComment: "Toggle line comment", shortcutIndentMore: "Indent more", shortcutIndentLess: "Indent less", shortcutDuplicateLine: "Duplicate current line", @@ -3086,6 +3087,8 @@ export default { shortcutScopeSearch: "Search fields", shortcutScopeSidebar: "Sidebar", shortcutPressShortcut: "Press shortcut", + shortcutSearchPlaceholder: "Search shortcuts", + shortcutSearchNoResults: "No shortcuts match your search.", shortcutConflict: "This shortcut conflicts with another action in the same scope.", shortcutClear: "Clear shortcut", preview: "Live Preview", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 2ca735be4..07ff81aab 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2952,6 +2952,7 @@ export default withEnglishFallback({ shortcutSaveSql: "Guardar SQL", shortcutAcceptCompletion: "Aceptar completado", shortcutFormatSql: "Formatear SQL", + shortcutToggleLineComment: "Alternar comentario de línea", shortcutIndentMore: "Aumentar sangría", shortcutIndentLess: "Reducir sangría", shortcutDuplicateLine: "Duplicar línea actual", @@ -2997,6 +2998,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "Campos de búsqueda", shortcutScopeSidebar: "Barra lateral", shortcutPressShortcut: "Presiona un atajo", + shortcutSearchPlaceholder: "Buscar atajos", + shortcutSearchNoResults: "Ningún atajo coincide con la búsqueda.", shortcutConflict: "Este atajo entra en conflicto con otra acción del mismo ámbito.", shortcutClear: "Borrar atajo", preview: "Vista previa en tiempo real", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 147d2fa42..ad474c94c 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2950,6 +2950,7 @@ export default withEnglishFallback({ shortcutSaveSql: "Salva SQL", shortcutAcceptCompletion: "Accetta completamento", shortcutFormatSql: "Formatta SQL", + shortcutToggleLineComment: "Attiva/disattiva commento di riga", shortcutIndentMore: "Aumenta rientro", shortcutIndentLess: "Riduci rientro", shortcutDuplicateLine: "Duplica riga corrente", @@ -2995,6 +2996,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "Cerca campi", shortcutScopeSidebar: "Barra laterale", shortcutPressShortcut: "Premi scorciatoia", + shortcutSearchPlaceholder: "Cerca scorciatoie", + shortcutSearchNoResults: "Nessuna scorciatoia corrisponde alla ricerca.", shortcutConflict: "Questa scorciatoia è in conflitto con un'altra azione nello stesso ambito.", shortcutClear: "Cancella scorciatoia", preview: "Anteprima in tempo reale", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index da06c3b8d..6952d7477 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2934,6 +2934,7 @@ export default withEnglishFallback({ shortcutSaveSql: "SQLを保存", shortcutAcceptCompletion: "補完を受け入れる", shortcutFormatSql: "SQLをフォーマット", + shortcutToggleLineComment: "行コメントを切り替え", shortcutIndentMore: "インデントを増やす", shortcutIndentLess: "インデントを減らす", shortcutDuplicateLine: "現在行を複製", @@ -2979,6 +2980,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "フィールド検索", shortcutScopeSidebar: "サイドバー", shortcutPressShortcut: "ショートカットを押してください", + shortcutSearchPlaceholder: "ショートカットを検索", + shortcutSearchNoResults: "一致するショートカットはありません。", shortcutConflict: "このショートカットは同じスコープ内の別のアクションと競合しています。", shortcutClear: "ショートカットをクリア", preview: "ライブプレビュー", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 8fa0a2567..380ccfa94 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2951,6 +2951,7 @@ export default withEnglishFallback({ shortcutSaveSql: "Salvar SQL", shortcutAcceptCompletion: "Aceitar autocompletar", shortcutFormatSql: "Formatar SQL", + shortcutToggleLineComment: "Alternar comentário de linha", shortcutIndentMore: "Aumentar recuo", shortcutIndentLess: "Reduzir recuo", shortcutDuplicateLine: "Duplicar linha atual", @@ -2996,6 +2997,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "Campos de pesquisa", shortcutScopeSidebar: "Barra lateral", shortcutPressShortcut: "Pressione o atalho", + shortcutSearchPlaceholder: "Pesquisar atalhos", + shortcutSearchNoResults: "Nenhum atalho corresponde à pesquisa.", shortcutConflict: "Este atalho conflita com outra ação no mesmo escopo.", shortcutClear: "Limpar atalho", preview: "Pré-visualização ao vivo", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index ba10f7355..77be2259f 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -3041,6 +3041,7 @@ export default withEnglishFallback({ shortcutSaveSql: "保存 SQL", shortcutAcceptCompletion: "接受补全", shortcutFormatSql: "格式化 SQL", + shortcutToggleLineComment: "切换行注释", shortcutIndentMore: "增加缩进", shortcutIndentLess: "减少缩进", shortcutDuplicateLine: "复制当前行", @@ -3089,6 +3090,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "搜索框", shortcutScopeSidebar: "侧边栏", shortcutPressShortcut: "按下快捷键", + shortcutSearchPlaceholder: "搜索快捷键", + shortcutSearchNoResults: "没有匹配的快捷键。", shortcutConflict: "这个快捷键与同一作用域内的其他操作冲突。", shortcutClear: "清除快捷键", preview: "实时预览", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 0b525344b..71797b78d 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2838,6 +2838,7 @@ export default withEnglishFallback({ shortcutSaveSql: "儲存 SQL", shortcutAcceptCompletion: "接受補全", shortcutFormatSql: "格式化 SQL", + shortcutToggleLineComment: "切換行註解", shortcutIndentMore: "增加縮排", shortcutIndentLess: "減少縮排", shortcutDuplicateLine: "複製目前行", @@ -2883,6 +2884,8 @@ export default withEnglishFallback({ shortcutScopeSearch: "搜尋框", shortcutScopeSidebar: "側邊欄", shortcutPressShortcut: "按下快速鍵", + shortcutSearchPlaceholder: "搜尋快速鍵", + shortcutSearchNoResults: "沒有符合的快速鍵。", shortcutConflict: "這個快速鍵與同一作用域內的其他操作衝突。", shortcutClear: "清除快速鍵", preview: "即時預覽", diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutDisplay.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutDisplay.spec.ts index da4bf9913..e6e6de30d 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutDisplay.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutDisplay.spec.ts @@ -34,4 +34,8 @@ describe("shortcut display", () => { expect(formatShortcutDisplay("Mod+Plus", "Win32")).toBe("Ctrl + +"); expect(formatShortcutDisplay("Shift+Mod++", "Win32")).toBe("Ctrl + Shift + +"); }); + + it("displays multi-stroke shortcuts", () => { + expect(formatShortcutDisplay("Ctrl+K Ctrl+C", "Win32")).toBe("Ctrl + K, Ctrl + C"); + }); }); diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index b8964363f..edf2db04c 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -2,7 +2,24 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict, formatShortcut, normalizeShortcutSettings, shortcutToCodeMirrorKey, 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", "uppercaseSelection", "lowercaseSelection", "exPasteSqlInCondition"]; + const formatterEditorActionIds: ShortcutActionId[] = [ + "formatSql", + "toggleLineComment", + "indentMore", + "indentLess", + "duplicateLine", + "deleteLine", + "moveLineUp", + "moveLineDown", + "copyLineUp", + "copyLineDown", + "undo", + "redo", + "selectAll", + "uppercaseSelection", + "lowercaseSelection", + "exPasteSqlInCondition", + ]; const sidebarShortcutActionIds: ShortcutActionId[] = ["copySidebarSelection", "pasteSidebarSelection", "editSidebarConnection"]; it("registers formatter editor shortcuts in the generic editor scope", () => { @@ -19,6 +36,7 @@ describe("shortcutRegistry editor actions", () => { expect(shortcuts.executeSql).toBe("Mod+Shift+Enter"); expect(shortcuts.formatSql).toBe("Shift+Mod+F"); + expect(shortcuts.toggleLineComment).toBe("Mod+/"); expect(shortcuts.indentMore).toBe(""); expect(shortcuts.indentLess).toBe("Shift+Tab"); expect(shortcuts.duplicateLine).toBe("Mod+D"); @@ -71,4 +89,12 @@ describe("shortcutRegistry editor actions", () => { expect(shortcutToCodeMirrorKey("Mod+Plus")).toBe("Mod-+"); expect(shortcutToCodeMirrorKey("Shift+Mod++")).toBe("Shift-Mod-+"); }); + + it("converts slash shortcuts for CodeMirror keymaps", () => { + expect(shortcutToCodeMirrorKey("Mod+/")).toBe("Mod-/"); + }); + + it("converts multi-stroke shortcuts for CodeMirror keymaps", () => { + expect(shortcutToCodeMirrorKey("Ctrl+K Ctrl+C")).toBe("Ctrl-k Ctrl-c"); + }); }); diff --git a/apps/desktop/src/lib/editor/shortcutDisplay.ts b/apps/desktop/src/lib/editor/shortcutDisplay.ts index 9bbc4e0e6..b82c63ba5 100644 --- a/apps/desktop/src/lib/editor/shortcutDisplay.ts +++ b/apps/desktop/src/lib/editor/shortcutDisplay.ts @@ -13,6 +13,11 @@ export function parseShortcutParts(shortcut?: string): string[] { return shortcut.split("+").filter(Boolean); } +export function parseShortcutStrokes(shortcut?: string): string[][] { + if (!shortcut) return []; + return shortcut.trim().split(/\s+/).filter(Boolean).map(parseShortcutParts); +} + function shortcutDisplayOrder(parts: string[], platform = globalThis.navigator?.platform || ""): string[] { if (parts.length <= 2 || isMacShortcutPlatform(platform)) return parts; @@ -43,6 +48,10 @@ export function shortcutDisplayParts(shortcut?: string, platform = globalThis.na return shortcutDisplayOrder(parseShortcutParts(shortcut), platform); } +export function shortcutDisplayStrokes(shortcut?: string, platform = globalThis.navigator?.platform || ""): string[][] { + return parseShortcutStrokes(shortcut).map((parts) => shortcutDisplayOrder(parts, platform)); +} + export function shortcutKeyLabel(part: string, platform = globalThis.navigator?.platform || ""): string { const isMac = isMacShortcutPlatform(platform); if (part === "Mod") return isMac ? "⌘" : "Ctrl"; @@ -65,10 +74,15 @@ export function shortcutKeyLabel(part: string, platform = globalThis.navigator?. } export function shortcutDisplayKeys(shortcut?: string, platform = globalThis.navigator?.platform || ""): string[] { - return shortcutDisplayParts(shortcut, platform).map((part) => shortcutKeyLabel(part, platform)); + return shortcutDisplayStrokes(shortcut, platform) + .flat() + .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) ? " " : " + "); + const keySeparator = isMacShortcutPlatform(platform) ? " " : " + "; + return shortcutDisplayStrokes(shortcut, platform) + .map((parts) => parts.map((part) => shortcutKeyLabel(part, platform)).join(keySeparator)) + .join(", "); } diff --git a/apps/desktop/src/lib/editor/shortcutRegistry.ts b/apps/desktop/src/lib/editor/shortcutRegistry.ts index 2735aaaa5..9d033bcaf 100644 --- a/apps/desktop/src/lib/editor/shortcutRegistry.ts +++ b/apps/desktop/src/lib/editor/shortcutRegistry.ts @@ -1,8 +1,9 @@ -import { parseShortcutParts, shortcutDisplayParts } from "@/lib/editor/shortcutDisplay"; +import { parseShortcutStrokes, shortcutDisplayParts } from "@/lib/editor/shortcutDisplay"; export type ShortcutActionId = | "executeSql" | "formatSql" + | "toggleLineComment" | "saveSql" | "acceptCompletion" | "indentMore" @@ -74,6 +75,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "editor", defaultShortcut: "Shift+Mod+F", }, + { + id: "toggleLineComment", + labelKey: "settings.shortcutToggleLineComment", + scope: "editor", + defaultShortcut: "Mod+/", + }, { id: "saveSql", labelKey: "settings.shortcutSaveSql", @@ -359,10 +366,14 @@ export function normalizeShortcutSettings(settings?: Partial): } export function shortcutToCodeMirrorKey(shortcut: string): string { - return parseShortcutParts(shortcut) - .map((part) => (part.length === 1 ? part.toLowerCase() : part)) - .map((part) => (part === "Plus" ? "+" : part)) - .join("-"); + return parseShortcutStrokes(shortcut) + .map((parts) => + parts + .map((part) => (part.length === 1 ? part.toLowerCase() : part)) + .map((part) => (part === "Plus" ? "+" : part)) + .join("-"), + ) + .join(" "); } export function formatShortcut(shortcut: string, platform = globalThis.navigator?.platform || ""): string {