feat(keymap): Eclipse and Eclipse (macOS) keymap presets (#219)

Settings › Keymap offers Eclipse and Eclipse (macOS), built on the default presets with the bundled Eclipse keymap bindings for every action we have. Numpad keys are matched by physical key only when the active keymap binds them explicitly (other presets keep the + - * / aliases) and display as NumPad keys; undo/redo stay with the focused view.

Not verified on Windows or Linux machines.

Fixes #192

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-29 12:10:33 +08:00
1 джерело 9fbd57ed15
коміт fff2c7110c
6 files changed
+544 -7

No files matched your search

+5
Переглянути файл
@@ -24,6 +24,11 @@ describe("keymap", () => {
expect(formatBinding("Shift Shift")).toBe("⇧⇧");
});
it("labels numeric-keypad operators", () => {
expect(formatBinding("Meta+Shift+NumpadDivide", true)).toBe("⇧⌘NumPad /");
expect(formatBinding("Ctrl+NumpadSubtract", false)).toBe("Ctrl+NumPad -");
});
it("uses physical key codes", () => {
expect(strokeOf({ code: "KeyF", key: "ƒ", metaKey: false, ctrlKey: false, altKey: true, shiftKey: false })).toBe(
"Alt+F",
+34 -7
Переглянути файл
@@ -15,6 +15,8 @@ import { getAction, isActionKeyEnabled, executeAction } from "./actions";
import { getContext } from "./context";
import { intellijMacos } from "./keymaps/intellij-macos";
import { intellijWindows } from "./keymaps/intellij-windows";
import { eclipseMacos } from "./keymaps/eclipse-macos";
import { eclipseWindows } from "./keymaps/eclipse-windows";
import { vscodeMacos } from "./keymaps/vscode-macos";
import { vscodeWindows } from "./keymaps/vscode-windows";
import { usePlatform } from "./platformInfo";
@@ -35,6 +37,8 @@ export const PRESETS: Record<string, Keymap> = {
"intellij-windows": intellijWindows,
"vscode-macos": vscodeMacos,
"vscode-windows": vscodeWindows,
"eclipse-macos": eclipseMacos,
"eclipse-windows": eclipseWindows,
};
/** Display names of the presets, in menu order. */
@@ -43,6 +47,8 @@ export const PRESET_TITLES: Record<string, string> = {
"intellij-windows": "Windows",
"vscode-macos": "VS Code (macOS)",
"vscode-windows": "VS Code (Windows)",
"eclipse-macos": "Eclipse (macOS)",
"eclipse-windows": "Eclipse",
};
/** ⌘ held on macOS, Ctrl elsewhere: the modifier of ⌘/Ctrl-click and copy/paste. */
@@ -241,10 +247,10 @@ const CODE_NAMES: Record<string, string> = {
Backquote: "`",
Minus: "-",
Equal: "=",
NumpadAdd: "+",
NumpadSubtract: "-",
NumpadMultiply: "*",
NumpadDivide: "/",
NumpadAdd: "NumpadAdd",
NumpadSubtract: "NumpadSubtract",
NumpadMultiply: "NumpadMultiply",
NumpadDivide: "NumpadDivide",
};
/** Converts a keydown event into a canonical stroke, or null for bare modifiers. */
@@ -284,6 +290,13 @@ const SYMBOLS: Record<string, string> = {
PageDown: "⇟",
};
const KEY_LABELS: Record<string, string> = {
NumpadAdd: "NumPad +",
NumpadSubtract: "NumPad -",
NumpadMultiply: "NumPad *",
NumpadDivide: "NumPad /",
};
/**
* Human display. macOS: `Meta+Shift+O` → `⇧⌘O`, `Shift Shift` → `⇧⇧`, `Alt Alt+Up` → `⌥⌥↑`.
* Windows/Linux: `Ctrl+Shift+N`, chords as `Ctrl+K, Ctrl+I`, `Shift, Shift`, `Ctrl, Ctrl+Up`.
@@ -294,7 +307,11 @@ export function formatBinding(binding: string, mac = isMacHost()): string {
const button = `${parts.pop()} Click`;
return mac ? parts.map((p) => SYMBOLS[p] ?? p).join("") + button : [...parts, button].join("+");
}
if (!mac) return normalizeBinding(binding).split(" ").join(", ");
if (!mac)
return normalizeBinding(binding)
.split(" ")
.map((stroke) => stroke.replace(/[^+]+$/, (key) => KEY_LABELS[key] ?? key))
.join(", ");
const double = parseDoubleModifier(binding);
if (double) return SYMBOLS[double.modifier]!.repeat(2) + (double.key ? (SYMBOLS[double.key] ?? double.key) : "");
return normalizeBinding(binding)
@@ -303,7 +320,7 @@ export function formatBinding(binding: string, mac = isMacHost()): string {
const parts = stroke.split("+");
const key = parts.pop()!;
// macOS menu order: ⌃⌥⇧⌘
return parts.map((p) => SYMBOLS[p] ?? p).join("") + (SYMBOLS[key] ?? key);
return parts.map((p) => SYMBOLS[p] ?? p).join("") + (SYMBOLS[key] ?? KEY_LABELS[key] ?? key);
})
.join(" ");
}
@@ -456,8 +473,18 @@ export function installKeyboardDispatcher(
if (isClaimed(e, stroke)) return;
}
const seq = [...pending, stroke].join(" ");
const bindings = effectiveBindings();
const physicalSeq = [...pending, stroke].join(" ");
// Preserve older presets' numeric-keypad aliases unless a preset distinguishes the physical key.
const keypadAliases: Record<string, string> = {
NumpadAdd: "+",
NumpadSubtract: "-",
NumpadMultiply: "*",
NumpadDivide: "/",
};
const seq = bindings.some(([b]) => b === physicalSeq || b.startsWith(physicalSeq + " "))
? physicalSeq
: physicalSeq.replace(/Numpad(Add|Subtract|Multiply|Divide)/g, (key) => keypadAliases[key]!);
const ctx = getContext();
const exact = bindings.filter(([b]) => b === seq).map(([, id]) => id);
// Later presets/overrides win; the first enabled match runs.
+154
Переглянути файл
@@ -0,0 +1,154 @@
/**
* Eclipse preset inheriting Mac OS X 10.5+, with explicit removals preserved.
* Source: JetBrains/intellij-community@74dbd42b7a8787b0707437cb857c68e78199f611
* plugins/keymaps/eclipse-keymap/resources/keymaps/Eclipse (Mac OS X).xml
*/
import { intellijMacos } from "./intellij-macos";
export const eclipseMacos: Record<string, string[]> = {
...intellijMacos,
// $Undo / $Redo stay unbound as in the parent keymap: the focused editor or view handles them.
"code.autoIndentLines": ["Meta+I"],
"navigate.back": ["Meta+[", "Button4"],
"refactor.changeSignature": ["Alt+Meta+C"],
"db.modifyTable": ["Alt+Meta+C"],
"vcs.commit": [],
"editor.closeTab": ["Meta+W"],
"toolwindow.closeContent": ["Meta+W"],
"editor.closeAllTabs": ["Meta+Shift+W"],
"analyze.inspectFile": [],
"project.collapseAll": ["Meta+Shift+NumpadDivide"],
"code.foldAll": ["Meta+Shift+NumpadDivide"],
"code.fold": ["Meta+NumpadSubtract"],
"code.commentBlock": ["Shift+Meta+/"],
"build.buildProject": ["Meta+B"],
"project.copyElement": [],
"run.debug": ["Meta+F11"],
"project.open": ["F3"],
"debug.jumpToSource": ["F3"],
"diff.jumpToSource": ["F3"],
"find.popup.openResult": ["F3"],
"editor.codeBlockEnd": ["Ctrl+]", "Shift+Ctrl+P"],
"code.deleteLine": ["Meta+D"],
"code.duplicateLine": [],
"code.extendSelection": ["Shift+Ctrl+Up"],
"code.toggleCase": ["Shift+Meta+X", "Shift+Meta+Y"],
"code.shrinkSelection": ["Shift+Ctrl+Down"],
"debug.evaluate": ["Meta+U"],
"project.expandAll": ["Meta+Shift+NumpadMultiply"],
"code.unfoldAll": ["Meta+NumpadMultiply"],
"code.unfold": ["Meta+NumpadAdd"],
"code.expandAllToLevel1": [],
"code.expandAllToLevel2": [],
"code.expandAllToLevel3": [],
"code.expandAllToLevel4": [],
"code.expandAllToLevel5": [],
"code.expandToLevel1": [],
"code.expandToLevel2": [],
"code.expandToLevel3": [],
"code.expandToLevel4": [],
"code.expandToLevel5": [],
"refactor.extractMethod": ["Meta+Alt+M"],
"navigate.fileStructure": ["Meta+O"],
"find.inFile": [],
"find.inPath": ["Ctrl+H"],
"find.next": ["Meta+K"],
// The parent's Push binding would shadow Find Previous whenever a repository is open.
"vcs.push": [],
"find.previous": ["Meta+Shift+K"],
"find.popup.nextResult": ["Meta+K"],
"find.popup.previousResult": ["Shift+Meta+K"],
"find.usages": ["Meta+Shift+G"],
"project.findUsages": ["Meta+Shift+G"],
"navigate.forward": ["Meta+]", "Button5"],
"navigate.gotoTest": [],
"navigate.gotoClass": ["Shift+Meta+T"],
"navigate.declaration": ["F3"],
"db.goToDdl": ["F3"],
"navigate.gotoFile": ["Shift+Meta+R"],
"navigate.implementation": [],
"navigate.gotoLine": ["Meta+L"],
"navigate.nextError": ["Meta+."],
"navigate.prevError": ["Meta+Shift+."],
"navigate.superMethod": [],
"navigate.typeDeclaration": [],
"find.highlightUsages": [],
"code.implementMethods": [],
"refactor.inline": ["Meta+Alt+I"],
"code.insertLiveTemplate": [],
"refactor.extractConstant": [],
"refactor.extractVariable": ["Meta+Alt+L"],
"navigate.lastEditLocation": ["Ctrl+Q"],
"toolwindow.jumpToLast": [],
"navigate.nextHighlightedUsage": [],
"navigate.nextMethod": ["Ctrl+Alt+Down"],
"navigate.prevHighlightedUsage": [],
"navigate.previousMethod": ["Ctrl+Alt+Up"],
"refactor.move": ["Meta+Alt+V"],
"project.move": ["Meta+Alt+V"],
"code.moveStatementDown": ["Alt+Down"],
"code.moveStatementUp": ["Alt+Up"],
"diff.nextDifference": ["Ctrl+F7"],
"diff.prevFile": [],
"diff.nextFile": [],
"navigate.nextOccurrence": [],
"editor.nextTab": ["Meta+F6", "Meta+Alt+Right"],
"code.optimizeImports": ["Shift+Meta+O"],
"project.optimizeImports": ["Shift+Meta+O"],
"code.overrideMethods": [],
"code.parameterInfo": ["Ctrl+Shift+Space"],
"code.expressionTypeInfo": [],
"navigate.prevOccurrence": [],
"editor.prevTab": ["Shift+Meta+F6", "Meta+Alt+Left"],
"code.quickDocumentation": ["F2"],
"db.quickDoc": ["F2"],
"localHistory.recentChanges": [],
"code.reformat": ["Shift+Meta+F"],
"project.reformatCode": ["Shift+Meta+F"],
"code.unwrap": [],
"find.replaceInFile": [],
"find.replaceInPath": [],
"run.rerun": [],
"debug.rerun": [],
"find.rerun": [],
"usages.rerun": [],
"debug.resume": ["F8"],
"run.run": ["Meta+Shift+F11"],
"debug.runToCursor": ["Meta+R"],
"code.showIntentions": ["Meta+1"],
"debug.smartStepInto": ["Alt+F5"],
"debug.stepInto": ["F5"],
"debug.forceStepInto": ["Meta+Ctrl+F5"],
"debug.stepOut": ["F7"],
"debug.stepOver": ["F6"],
"debug.forceStepOver": ["Meta+Ctrl+F6"],
"code.surroundWith": ["Meta+Alt+Z"],
"file.reloadFromDisk": [],
"project.reloadFromDisk": [],
"navigate.toggleBookmark": [],
"debug.toggleBreakpoint": ["Shift+Meta+B"],
"navigate.typeHierarchy": ["F4"],
"vcs.update": [],
"navigate.navigationBar": ["Alt+Meta+B"],
"code.moveLineDown": [],
"code.moveLineUp": [],
"code.cyclicExpandWord": ["Ctrl+."],
"code.cyclicExpandWordBackward": [],
"refactor.safeDelete": [],
"navigate.selectIn": ["Alt+Meta+W"],
"navigate.findAction": ["Meta+3"],
"vcs.rollback": ["Ctrl+Shift+Alt+Z"],
"toolwindow.project": [],
"toolwindow.bookmarks": [],
"toolwindow.find": ["Alt+Meta+Q S"],
"code.foldSelection": [],
"code.collapseBlock": [],
"navigate.showFilePath": [],
"toolwindow.services": ["Alt+Meta+H"],
"toolwindow.resizeLeft": [],
"toolwindow.resizeRight": [],
"toolwindow.resizeUp": [],
"toolwindow.resizeDown": [],
"editor.decreaseFontSize": ["Meta+-"],
"editor.increaseFontSize": ["Meta+="],
};
+156
Переглянути файл
@@ -0,0 +1,156 @@
/**
* Eclipse preset inheriting $default, with explicit removals preserved.
* Source: JetBrains/intellij-community@74dbd42b7a8787b0707437cb857c68e78199f611
* plugins/keymaps/eclipse-keymap/resources/keymaps/Eclipse.xml
*/
import { intellijWindows } from "./intellij-windows";
export const eclipseWindows: Record<string, string[]> = {
...intellijWindows,
// $Undo / $Redo stay unbound as in the parent keymap: the focused editor or view handles them.
"code.autoIndentLines": ["Ctrl+I"],
"navigate.back": ["Alt+Left", "Button4"],
"refactor.changeSignature": ["Shift+Alt+C"],
"db.modifyTable": ["Shift+Alt+C"],
"vcs.commit": [],
"editor.closeTab": ["Ctrl+W"],
"toolwindow.closeContent": ["Ctrl+W"],
"editor.closeAllTabs": ["Shift+Ctrl+F4", "Shift+Ctrl+W"],
"analyze.inspectFile": [],
"project.collapseAll": ["Ctrl+Shift+NumpadDivide"],
"code.foldAll": ["Ctrl+Shift+NumpadDivide"],
"code.fold": ["Ctrl+NumpadSubtract"],
"code.commentBlock": ["Shift+Ctrl+/"],
"build.buildProject": ["Ctrl+F9", "Ctrl+B"],
"project.copyElement": [],
"run.debug": ["Shift+Alt+D", "Shift+F9"],
"project.open": ["F12", "Shift+Ctrl+E"],
"debug.jumpToSource": ["F12", "Shift+Ctrl+E"],
"diff.jumpToSource": ["F12", "Shift+Ctrl+E"],
"find.popup.openResult": ["F12", "Shift+Ctrl+E"],
"editor.codeBlockEnd": ["Ctrl+]"],
"code.deleteLine": ["Ctrl+D"],
"code.duplicateLine": [],
"code.moveElementLeft": ["Alt+Shift+Left"],
"code.moveElementRight": ["Alt+Shift+Right"],
"code.extendSelection": ["Shift+Alt+Up"],
"code.toggleCase": ["Shift+Ctrl+U", "Shift+Ctrl+X", "Shift+Ctrl+Y"],
"code.shrinkSelection": ["Shift+Alt+Down"],
"debug.evaluate": ["Ctrl+U"],
"project.expandAll": ["Ctrl+Shift+NumpadMultiply"],
"code.unfoldAll": ["Ctrl+NumpadMultiply"],
"code.unfold": ["Ctrl+NumpadAdd"],
"code.expandAllToLevel1": [],
"code.expandAllToLevel2": [],
"code.expandAllToLevel3": [],
"code.expandAllToLevel4": [],
"code.expandAllToLevel5": [],
"code.expandToLevel1": [],
"code.expandToLevel2": [],
"code.expandToLevel3": [],
"code.expandToLevel4": [],
"code.expandToLevel5": [],
"refactor.extractMethod": ["Shift+Alt+M"],
"navigate.fileStructure": ["Ctrl+F3", "Ctrl+O"],
"find.inFile": [],
"find.inPath": ["Ctrl+H"],
"find.next": ["Ctrl+K"],
// The parent's Push binding would shadow Find Previous whenever a repository is open.
"vcs.push": [],
"find.previous": ["Ctrl+Shift+K"],
"find.popup.nextResult": ["Ctrl+K"],
"find.popup.previousResult": ["Shift+Ctrl+K"],
"code.selectAllOccurrences": ["Alt+Ctrl+Y"],
"code.addCaretNextOccurrence": ["Alt+Y"],
"code.unselectOccurrence": ["Alt+Shift+Y"],
"find.usages": ["Ctrl+G"],
"project.findUsages": ["Ctrl+G"],
"navigate.forward": ["Alt+Right", "Button5"],
"navigate.gotoTest": [],
"navigate.gotoClass": ["Shift+Ctrl+T"],
"navigate.declaration": ["F3"],
"db.goToDdl": ["F3"],
"navigate.gotoFile": ["Shift+Ctrl+R"],
"navigate.implementation": ["Ctrl+T", "Ctrl+Alt+B"],
"navigate.gotoLine": ["Ctrl+L"],
"navigate.nextError": [],
"navigate.prevError": ["Shift+F1"],
"navigate.superMethod": [],
"navigate.typeDeclaration": [],
"find.highlightUsages": ["Shift+Alt+O", "Shift+Ctrl+F7"],
"code.implementMethods": ["Shift+Alt+P"],
"refactor.inline": ["Shift+Alt+I"],
"code.insertLiveTemplate": ["Shift+Ctrl+Alt+J"],
"refactor.extractVariable": ["Shift+Alt+L", "Ctrl+Alt+V"],
"navigate.lastEditLocation": ["Ctrl+Q"],
"toolwindow.jumpToLast": [],
"navigate.nextMethod": ["Shift+Ctrl+Down"],
"navigate.previousMethod": ["Shift+Ctrl+Up"],
"refactor.move": ["Shift+Alt+V"],
"project.move": ["Shift+Alt+V"],
"code.moveStatementDown": ["Alt+Down"],
"code.moveStatementUp": ["Alt+Up"],
"diff.nextDifference": ["Ctrl+F7"],
"diff.prevFile": ["Ctrl+Alt+Shift+Right"],
"diff.nextFile": ["Ctrl+Alt+Shift+Left"],
"diff.applyLeftSide": [],
"diff.applyRightSide": [],
"navigate.nextOccurrence": [],
"editor.nextTab": ["Ctrl+F6", "Ctrl+Alt+Left", "Ctrl+PageDown"],
"code.optimizeImports": ["Ctrl+Alt+O", "Shift+Ctrl+O"],
"project.optimizeImports": ["Ctrl+Alt+O", "Shift+Ctrl+O"],
"code.overrideMethods": [],
"code.parameterInfo": [],
"code.expressionTypeInfo": [],
"navigate.prevOccurrence": [],
"editor.prevTab": ["Shift+Ctrl+F6", "Ctrl+Alt+Right", "Ctrl+PageUp"],
"code.quickDocumentation": ["Shift+Ctrl+Space", "F2"],
"db.quickDoc": ["Shift+Ctrl+Space", "F2"],
"localHistory.recentChanges": [],
"code.reformat": ["Ctrl+Alt+L", "Shift+Ctrl+F"],
"project.reformatCode": ["Ctrl+Alt+L", "Shift+Ctrl+F"],
"code.unwrap": ["Alt+R"],
"refactor.rename": ["Shift+Alt+R"],
"project.rename": ["Shift+Alt+R"],
"db.rename": ["Shift+Alt+R"],
"find.replaceInFile": ["Ctrl+F"],
"find.replaceInPath": [],
"run.rerun": ["Ctrl+F11"],
"debug.rerun": ["Ctrl+F11"],
"find.rerun": ["Ctrl+F11"],
"usages.rerun": ["Ctrl+F11"],
"code.addCaretsToLineEnds": [],
"debug.resume": ["F8", "F9"],
"run.run": ["Shift+Alt+X", "Shift+F10"],
"debug.runToCursor": ["Ctrl+R"],
"code.showIntentions": ["Ctrl+1", "Alt+Enter"],
"debug.smartStepInto": ["Ctrl+F5"],
"debug.stepInto": ["F5"],
"debug.stepOut": ["F7", "Shift+F8"],
"debug.stepOver": ["F6"],
"code.surroundWith": ["Shift+Alt+Z", "Ctrl+Alt+T"],
"file.reloadFromDisk": [],
"project.reloadFromDisk": [],
"navigate.toggleBookmark": ["Shift+Ctrl+F11"],
"navigate.toggleBookmarkWithMnemonic": ["Shift+Alt+Ctrl+F11"],
"debug.toggleBreakpoint": ["Shift+Ctrl+B", "Ctrl+F8"],
"navigate.typeHierarchy": ["F4"],
"vcs.update": [],
"navigate.navigationBar": ["Alt+Home"],
"code.moveLineDown": [],
"code.moveLineUp": [],
"editor.matchBrace": ["Shift+Ctrl+P"],
"run.rerunFailed": ["Alt+Shift+G"],
"vcs.shelf.rename": ["Alt+Shift+R"],
"vcs.log.editMessage": ["Alt+Shift+R"],
"vcs.changelist.edit": ["Shift+Alt+R"],
"toolwindow.services": ["Shift+Alt+H"],
"vcs.changelist.move": [],
"navigate.gotoBookmark1": [],
"toolwindow.resizeLeft": [],
"toolwindow.resizeRight": [],
"toolwindow.resizeUp": [],
"toolwindow.resizeDown": [],
"editor.decreaseFontSize": ["Ctrl+-"],
"editor.increaseFontSize": ["Ctrl+="],
};
+193
Переглянути файл
@@ -0,0 +1,193 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { registerAction } from "../actions";
import {
applyKeymapSettings,
bindingsFor,
installKeyboardDispatcher,
isValidBinding,
normalizeBinding,
PRESETS,
PRESET_TITLES,
useKeymap,
} from "../keymap";
import { eclipseMacos } from "./eclipse-macos";
import { eclipseWindows } from "./eclipse-windows";
import { intellijMacos } from "./intellij-macos";
import { intellijWindows } from "./intellij-windows";
const initial = useKeymap.getState();
afterEach(() => useKeymap.setState(initial));
const presets = [
["eclipse-windows", "Eclipse", eclipseWindows, intellijWindows, "Ctrl"],
["eclipse-macos", "Eclipse (macOS)", eclipseMacos, intellijMacos, "Meta"],
] as const;
const normalized = (keys: string[]) => keys.map(normalizeBinding);
const owners = (km: Record<string, string[]>, key: string) =>
Object.keys(km).filter((id) => normalized(km[id]!).includes(normalizeBinding(key)));
// Shared shortcuts are intentional only on these separate focused surfaces. Existing
// parent collisions are not introduced by this preset; new pairs must be reviewed here.
const shared = [
["navigate.gotoClass", "terminal.new"],
["navigate.recentLocations", "project.open", "debug.jumpToSource", "diff.jumpToSource", "find.popup.openResult"],
[
"navigate.declaration",
"db.goToDdl",
"project.open",
"debug.jumpToSource",
"diff.jumpToSource",
"find.popup.openResult",
],
["navigate.typeHierarchy", "db.editData"],
["find.replaceInFile", "agents.findInConversation", "vcs.log.goToRef"],
["find.next", "find.popup.nextResult"],
["find.previous", "find.popup.previousResult"],
["find.usages", "project.findUsages"],
["code.quickDocumentation", "db.quickDoc", "debug.setValue", "vcs.changelist.edit"],
["code.reformat", "project.reformatCode"],
["code.optimizeImports", "project.optimizeImports"],
["code.deleteLine", "db.duplicateSource", "project.compareWith", "vcs.showDiff"],
["code.moveStatementDown", "find.field.recentSearches", "find.popup.recentSearches"],
["code.foldAll", "project.collapseAll"],
["refactor.rename", "project.rename", "db.rename", "vcs.changelist.edit", "vcs.log.editMessage", "vcs.shelf.rename"],
["refactor.changeSignature", "db.modifyTable"],
["refactor.move", "project.move"],
["editor.closeTab", "toolwindow.closeContent"],
["run.rerun", "debug.rerun", "find.rerun", "usages.rerun"],
["debug.smartStepInto", "db.refresh"],
["debug.runToCursor", "db.refresh"],
["editor.increaseFontSize", "image.zoomIn"],
["editor.decreaseFontSize", "image.zoomOut"],
];
describe.each(presets)("%s", (preset, title, km, parent, mod) => {
it("is listed, loads, switches without stale overrides, and can parent a custom keymap", () => {
expect(PRESETS[preset]).toBe(km);
expect(PRESET_TITLES[preset]).toBe(title);
applyKeymapSettings("vscode-macos", { "navigate.gotoFile": ["Alt+X"] });
applyKeymapSettings(preset, {});
expect(useKeymap.getState().preset).toBe(preset);
expect(bindingsFor("navigate.gotoFile")).toEqual(km["navigate.gotoFile"]);
applyKeymapSettings(
"custom.eclipse",
{},
{
"custom.eclipse": { parent: preset, overrides: { "file.save": ["Alt+S"] } },
},
);
expect(useKeymap.getState().preset).toBe(preset);
expect(bindingsFor("file.save")).toEqual(["Alt+S"]);
expect(bindingsFor("navigate.gotoFile")).toEqual(km["navigate.gotoFile"]);
});
it("has valid bindings, no duplicate entries, and no unreviewed new conflicts", () => {
for (const [id, keys] of Object.entries(km)) {
expect(keys.every(isValidBinding), id).toBe(true);
expect(new Set(normalized(keys)).size, id).toBe(keys.length);
for (const key of keys) {
for (const other of owners(km, key).filter((other) => other !== id)) {
const inherited = owners(parent, key);
expect(
(inherited.includes(id) && inherited.includes(other)) ||
shared.some((group) => group.includes(id) && group.includes(other)),
`${key}: ${id} / ${other}`,
).toBe(true);
}
}
}
});
it.each([
["navigate.gotoFile", "Shift+R"],
["navigate.gotoClass", "Shift+T"],
["code.deleteLine", "D"],
["code.showIntentions", "1"],
["code.optimizeImports", "Shift+O"],
["code.reformat", "Shift+F"],
["code.commentLine", "/"],
])("binds %s", (id, suffix) => {
expect(normalized(km[id]!)).toContain(normalizeBinding(`${mod}+${suffix}`));
});
it("preserves official removals and platform-specific bindings", () => {
expect(km["navigate.declaration"]).toEqual(["F3"]);
expect(km["diff.nextDifference"]).toEqual(["Ctrl+F7"]);
expect(km["diff.prevDifference"]).toEqual(["Shift+F7"]);
expect(km["code.moveStatementUp"]).toEqual(["Alt+Up"]);
expect(km["code.moveStatementDown"]).toEqual(["Alt+Down"]);
for (const id of ["code.moveLineUp", "code.moveLineDown", "navigate.gotoTest", "find.replaceInPath"])
expect(km[id], id).toEqual([]);
expect(km["code.completion"]).toEqual(parent["code.completion"]);
expect(km["navigate.back"]).toContain("Button4");
expect(km["navigate.forward"]).toContain("Button5");
expect(km["vcs.push"]).toEqual([]);
});
it("leaves undo / redo to the focused component", () => {
for (const key of [`${mod}+Z`, `${mod}+Shift+Z`]) expect(owners(km, key), key).toEqual([]);
// Eclipse's Ctrl+Y Redo reaches the editor once Delete Line moves to Ctrl+D.
if (mod === "Ctrl") expect(owners(km, "Ctrl+Y")).toEqual([]);
});
it("distinguishes keypad folding from font size and block comments during dispatch", () => {
applyKeymapSettings(preset, {});
const actions = ["code.fold", "editor.decreaseFontSize", "code.foldAll", "code.commentBlock"];
const runs = actions.map(() => vi.fn());
const disposers = actions.map((id, i) => registerAction({ id, title: id, run: runs[i]! }));
const uninstall = installKeyboardDispatcher(window);
try {
for (const [code, shiftKey, index] of [
["NumpadSubtract", false, 0],
["Minus", false, 1],
["NumpadDivide", true, 2],
["Slash", true, 3],
] as const) {
window.dispatchEvent(
new KeyboardEvent("keydown", {
code,
key: code,
ctrlKey: mod === "Ctrl",
metaKey: mod === "Meta",
shiftKey,
}),
);
expect(runs[index]).toHaveBeenCalledOnce();
}
} finally {
uninstall();
disposers.forEach((dispose) => dispose());
}
});
});
it("uses the bundled Run / Debug choices rather than assuming Ctrl becomes Cmd", () => {
expect(normalized(eclipseWindows["run.debug"]!)).toEqual(normalized(["Alt+Shift+D", "Shift+F9"]));
expect(normalized(eclipseWindows["run.run"]!)).toEqual(normalized(["Alt+Shift+X", "Shift+F10"]));
expect(eclipseWindows["run.rerun"]).toEqual(["Ctrl+F11"]);
expect(owners(eclipseWindows, "F11")).toEqual([]);
expect(eclipseMacos["run.debug"]).toEqual(["Meta+F11"]);
expect(eclipseMacos["run.run"]).toEqual(["Meta+Shift+F11"]);
expect(eclipseMacos["run.rerun"]).toEqual([]);
expect(eclipseMacos["navigate.navigationBar"]).toEqual(["Alt+Meta+B"]);
expect(
Object.values(eclipseWindows)
.flat()
.some((key) => key.includes("Meta")),
).toBe(false);
});
it("keeps numeric-keypad aliases working for existing presets", () => {
applyKeymapSettings("intellij-windows", {});
const run = vi.fn();
const dispose = registerAction({ id: "code.fold", title: "code.fold", run });
const uninstall = installKeyboardDispatcher(window);
try {
window.dispatchEvent(new KeyboardEvent("keydown", { code: "NumpadSubtract", key: "-", ctrlKey: true }));
expect(run).toHaveBeenCalledOnce();
} finally {
uninstall();
dispose();
}
});
+2
Переглянути файл
@@ -585,6 +585,8 @@
"Duplicate groups": "重复组",
"Duplicates": "重复项",
"Duplicate…": "复制…",
"Eclipse": "Eclipse",
"Eclipse (macOS)": "Eclipse (macOS)",
"Edit": "编辑",
"Edit Cell": "编辑单元格",
"Edit Changelist": "编辑更改列表",