feat(keymap): export the current keymap as JSON (#2852)

Settings › Keymap's gear menu gets Export Keymap as JSON…: the selected keymap's name, the preset it is based on and every configurable action's resolved bindings, so the full mapping can be read or diffed outside the IDE. Written with the fs commands like the color scheme export; strings in the en/zh catalog. Part of #287.

Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
This commit is contained in:
arookieofc
2026-11-12 10:24:35 +08:00
committed by GitHub
co-authored by a-rookie-of-C-language
parent c54253fba2
commit feef7db39d
4 changed files with 90 additions and 2 deletions
+27 -1
View File
@@ -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<Record<"metaKey" | "ctrlKey" | "altKey" | "shiftKey", boolean>> = {},
@@ -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",
+6 -1
View File
@@ -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"
}
+5
View File
@@ -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": "透明度百分比",