diff --git a/src/platform/keymap.test.ts b/src/platform/keymap.test.ts index 2b159e493..81ed0aab3 100644 --- a/src/platform/keymap.test.ts +++ b/src/platform/keymap.test.ts @@ -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", diff --git a/src/platform/keymap.ts b/src/platform/keymap.ts index 501b8f885..5ce56c952 100644 --- a/src/platform/keymap.ts +++ b/src/platform/keymap.ts @@ -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 = { "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 = { "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 = { 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 = { PageDown: "⇟", }; +const KEY_LABELS: Record = { + 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 = { + 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. diff --git a/src/platform/keymaps/eclipse-macos.ts b/src/platform/keymaps/eclipse-macos.ts new file mode 100644 index 000000000..bf54ab70f --- /dev/null +++ b/src/platform/keymaps/eclipse-macos.ts @@ -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 = { + ...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+="], +}; diff --git a/src/platform/keymaps/eclipse-windows.ts b/src/platform/keymaps/eclipse-windows.ts new file mode 100644 index 000000000..b346e0dd8 --- /dev/null +++ b/src/platform/keymaps/eclipse-windows.ts @@ -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 = { + ...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+="], +}; diff --git a/src/platform/keymaps/eclipse.test.ts b/src/platform/keymaps/eclipse.test.ts new file mode 100644 index 000000000..cd6a5fd91 --- /dev/null +++ b/src/platform/keymaps/eclipse.test.ts @@ -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, 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(); + } +}); diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index ec3e85689..dade38b35 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -585,6 +585,8 @@ "Duplicate groups": "重复组", "Duplicates": "重复项", "Duplicate…": "复制…", + "Eclipse": "Eclipse", + "Eclipse (macOS)": "Eclipse (macOS)", "Edit": "编辑", "Edit Cell": "编辑单元格", "Edit Changelist": "编辑更改列表",