diff --git a/src/features/settings/__tests__/keymap.test.ts b/src/features/settings/__tests__/keymap.test.ts index 89d5ff6da..de4c8337f 100644 --- a/src/features/settings/__tests__/keymap.test.ts +++ b/src/features/settings/__tests__/keymap.test.ts @@ -36,7 +36,7 @@ import { withCustomOverrides, type KeymapDraft, } from "../keymap/model"; -import { buildKeymapTree, keymapSearchLabels } from "../keymap/KeymapPage"; +import { buildKeymapTree, keymapJson, keymapSearchLabels } from "../keymap/KeymapPage"; import { groupPath } from "../keymap/groups"; import { registerEditorActions } from "@/editor/core/actions"; import { registerLspActions } from "@/editor/lsp/actions"; @@ -59,6 +59,32 @@ it("finds the translated declaration action by 声明 and its English title", as } }); +describe("keymapJson", () => { + it("exports the keymap's name, base preset and every bound action", () => { + registerEditorActions(); + const out = JSON.parse( + keymapJson("My Keymap", "intellij-macos", allActions(), { preset: "intellij-macos", overrides: {} }), + ); + expect(out.name).toBe("My Keymap"); + expect(out.basedOn).toBe("intellij-macos"); + expect(out.actions["code.duplicateLine"]).toEqual(["Meta+D"]); + expect(out.actions["navigate.gotoLine"]).toEqual(["Meta+L"]); + // Unbound actions are omitted, not exported as empty lists. + expect(Object.values(out.actions).every((keys) => Array.isArray(keys) && keys.length > 0)).toBe(true); + }); + + it("exports overrides over the preset", () => { + registerEditorActions(); + const out = JSON.parse( + keymapJson("Custom", "intellij-macos", allActions(), { + preset: "intellij-macos", + overrides: { "code.duplicateLine": ["Ctrl+T"] }, + }), + ); + expect(out.actions["code.duplicateLine"]).toEqual(["Ctrl+T"]); + }); +}); + const key = ( code: string, mods: Partial> = {}, diff --git a/src/features/settings/keymap/KeymapPage.tsx b/src/features/settings/keymap/KeymapPage.tsx index 5f915c566..7c31ca179 100644 --- a/src/features/settings/keymap/KeymapPage.tsx +++ b/src/features/settings/keymap/KeymapPage.tsx @@ -11,6 +11,7 @@ import clsx from "clsx"; import { useActions, allActions, type Action } from "@/platform/actions"; import { defaultPreset, formatBinding, type Keymap, PRESET_TITLES, PRESETS, useKeymap } from "@/platform/keymap"; import { call, commands } from "@/platform/ipc"; +import { notifications } from "@/workbench/contributions"; import { ask, confirm, showError } from "@/workbench/messages"; import { HighlightedText, @@ -73,6 +74,21 @@ export function keymapSearchLabels(): string[] { const groupKey = (path: readonly string[]) => `grp:${path.join("/")}`; +/** + * The JSON written by "Export Keymap as JSON…": the keymap's name, the preset + * it is based on, and every configurable action's resolved bindings, so the + * mapping can be read or compared to IDEA offline. + */ +export function keymapJson(name: string, basedOn: string, actions: readonly Action[], draft: KeymapDraft): string { + const bindings: Keymap = {}; + for (const action of actions) { + if (!isKeymapAction(action, draft)) continue; + const keys = resolveBindings(draft, action.id); + if (keys.length) bindings[action.id] = keys; + } + return JSON.stringify({ name, basedOn, actions: bindings }, null, 2); +} + function sortGroup(g: GroupNode): void { const actions = g.children.filter((c) => c.kind === "action"); const groups = g.children.filter((c): c is GroupNode => c.kind === "group"); @@ -261,6 +277,41 @@ export function KeymapPage() { }); }; + const exportKeymap = async () => { + const { save } = await import("@/platform/dialog"); + const name = keymapName(selectedId, customs); + const path = await save({ + title: tx("Export Keymap"), + defaultPath: `${name.replace(/[/\\:*?"<>|]/g, "_")}.json`, + filters: [{ name: tx("OpenIDEA Keymap"), extensions: ["json"] }], + }); + if (!path) return; + const json = keymapJson( + name, + isCustom ? (customs[selectedId]?.parent ?? draft.preset) : selectedId, + allActions(), + draft, + ); + try { + // The save dialog already confirmed replacing an existing file. + await call(commands.fsDelete(path)).catch(() => {}); + await call(commands.fsCreateFileBytes(path, Array.from(new TextEncoder().encode(json)))); + notifications().notify({ + level: "info", + title: tx("Keymap exported"), + body: path, + key: "keymap.export", + }); + } catch (e) { + notifications().notify({ + level: "error", + title: tx("Cannot export keymap"), + body: e instanceof Error ? e.message : String(e), + key: "keymap.export", + }); + } + }; + const gearMenu = (): MenuItem[] => [ { label: "Duplicate…", @@ -268,6 +319,7 @@ export function KeymapPage() { run: () => setNameDialog({ mode: "duplicate", initial: copyName(keymapName(selectedId, customs), customs) }), }, { label: "Import IntelliJ IDEA Keymap…", run: () => void importIdeaKeymap() }, + { label: "Export Keymap as JSON…", run: () => void exportKeymap() }, { label: "Rename…", icon: "edit", diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index ebd1aa4f7..6ab903147 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -1331,5 +1331,10 @@ "Examples": "Examples", "Remove File": "Remove File", "Delegate IDE build/run actions to Maven": "Delegate IDE build/run actions to Maven", - "Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server.": "Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server." + "Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server.": "Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server.", + "Export Keymap as JSON…": "Export Keymap as JSON…", + "Export Keymap": "Export Keymap", + "OpenIDEA Keymap": "OpenIDEA Keymap", + "Keymap exported": "Keymap exported", + "Cannot export keymap": "Cannot export keymap" } diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 376d11a58..fc3607e0e 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -7435,6 +7435,11 @@ "Not available in OpenIDEA ({length}): {join}.": "OpenIDEA 中不可用({length}):{join}。", "Skipped {skippedShortcuts} unsupported shortcuts.": "已跳过 {skippedShortcuts} 个不支持的快捷键。", "Import IntelliJ IDEA Keymap…": "导入 IntelliJ IDEA 键盘映射…", + "Export Keymap as JSON…": "将键盘映射导出为 JSON…", + "Export Keymap": "导出键盘映射", + "OpenIDEA Keymap": "OpenIDEA 键盘映射", + "Keymap exported": "键盘映射已导出", + "Cannot export keymap": "无法导出键盘映射", "Color picker": "颜色选择器", "HEX": "十六进制", "Alpha percent": "透明度百分比",