From 4c5df98bb8f2dbdd79bcd6d6a98aaea24fb0593c Mon Sep 17 00:00:00 2001 From: Vadim Lazuk Date: Wed, 11 Nov 2026 22:34:06 +0300 Subject: [PATCH] fix(keymap): let the keymap own the project view's shortcuts (#2811) The project view handled Shift+F6, F6, F4, Delete, copy/cut/paste, Copy Path and New in its own onKeyDown, so unbinding or rebinding those actions in the keymap had no effect there (a cleared Rename File still renamed on Shift+F6, a rebound one was ignored, and Eclipse's Shift+Alt+R opened the file rename). Those keys now go only through the global dispatcher and the preset bindings; the view keeps just the strokes no preset binds (Cmd+Z undoing the last file refactoring, Ctrl+Enter's New popup on macOS). The Eclipse presets bind Delete Project Item explicitly, since the hardcoded Delete used to cover for them. Fixes #2801 Co-authored-by: Inf1mum <8135225+Inf1mum@users.noreply.github.com> --- .../project-tree/ProjectView.test.tsx | 52 ++++++++++++++++++- src/features/project-tree/ProjectView.tsx | 40 ++------------ src/platform/keymaps/eclipse-macos.ts | 1 + src/platform/keymaps/eclipse-windows.ts | 1 + 4 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/features/project-tree/ProjectView.test.tsx b/src/features/project-tree/ProjectView.test.tsx index eef702444..bd58ed172 100644 --- a/src/features/project-tree/ProjectView.test.tsx +++ b/src/features/project-tree/ProjectView.test.tsx @@ -2,7 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { emitLocalIdeEvent } from "@/platform/ipc"; import { installRepeatedDoubleClick } from "@/platform/doubleClick"; -import { getContext } from "@/platform/context"; +import { getContext, setContext } from "@/platform/context"; +import { applyKeymapSettings, installKeyboardDispatcher, useKeymap } from "@/platform/keymap"; +import { usePlatform } from "@/platform/platformInfo"; import { provideEditorService, registerProjectViewRoot, @@ -54,6 +56,19 @@ let fs: MemoryFs; let openFile: ReturnType; let dispose: () => void; +const initialPlatform = usePlatform.getState(); +const initialKeymap = useKeymap.getState(); +let uninstallDispatcher: (() => void) | null = null; + +/** Presses the presets' shortcuts: the global dispatcher runs the keymap's actions. */ +function dispatchViaKeymap() { + usePlatform.setState({ os: "macos" }); + useKeymap.setState({ preset: "intellij-macos", overrides: {} }); + // The workbench activation that publishes this in the app does not run here. + setContext("projectOpen", true); + uninstallDispatcher = installKeyboardDispatcher(window); +} + beforeEach(() => { localStorage.clear(); fs = new MemoryFs( @@ -78,6 +93,10 @@ beforeEach(() => { afterEach(() => { dispose(); setProjectFsForTests(null); + uninstallDispatcher?.(); + uninstallDispatcher = null; + usePlatform.setState(initialPlatform); + useKeymap.setState(initialKeymap); }); function setup() { @@ -153,6 +172,7 @@ describe("ProjectView", () => { }); it("pastes the files Explorer copied into the selected folder, as copies", async () => { + dispatchViaKeymap(); // What Explorer leaves on the clipboard: a path outside the project. fs.nodes.set("/elsewhere/Photo.png", { kind: "file", size: 3 }); setOsClipboardForTests({ @@ -176,6 +196,7 @@ describe("ProjectView", () => { }); it("clears cut dimming and disables Paste on focus after the OS clipboard becomes text", async () => { + dispatchViaKeymap(); let files: string[] = []; setOsClipboardForTests({ files: async () => files, @@ -412,6 +433,7 @@ describe("ProjectView", () => { }); it("renames in IDEA's Rename dialog with Shift+F6, validating conflicts, and undoes with ⌘Z", async () => { + dispatchViaKeymap(); const { tree, row, rows } = setup(); await waitFor(() => expect(rows()).toContain("README.md")); fireEvent.mouseDown(row("README.md")); @@ -439,7 +461,34 @@ describe("ProjectView", () => { expect(fs.nodes.has("/w/acme/GUIDE.md")).toBe(false); }); + it("stops renaming with Shift+F6 once Rename File is unbound, following the keymap", async () => { + dispatchViaKeymap(); + // Unbinding used to leave the component's hardcoded Shift+F6 in place (issue #2801). + applyKeymapSettings("intellij-macos", { "project.rename": [] }); + const { tree, row, rows } = setup(); + await waitFor(() => expect(rows()).toContain("README.md")); + fireEvent.mouseDown(row("README.md")); + tree.focus(); + fireEvent.keyDown(tree, { key: "F6", code: "F6", shiftKey: true }); + expect(screen.queryByRole("dialog", { name: "Rename" })).toBeNull(); + }); + + it("renames with the shortcut the keymap rebinds Rename File to", async () => { + dispatchViaKeymap(); + applyKeymapSettings("intellij-macos", { "project.rename": ["F2"] }); + const { tree, row, rows } = setup(); + await waitFor(() => expect(rows()).toContain("README.md")); + fireEvent.mouseDown(row("README.md")); + tree.focus(); + fireEvent.keyDown(tree, { key: "F6", code: "F6", shiftKey: true }); + expect(screen.queryByRole("dialog", { name: "Rename" })).toBeNull(); + fireEvent.keyDown(tree, { key: "F2", code: "F2" }); + const dialog = await screen.findByRole("dialog", { name: "Rename" }); + fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); + }); + it("moves with F6 through the Move dialog", async () => { + dispatchViaKeymap(); const { tree, row, rows } = setup(); await waitFor(() => expect(rows()).toContain("README.md")); fireEvent.mouseDown(row("README.md")); @@ -458,6 +507,7 @@ describe("ProjectView", () => { }); it("deletes the selection after confirmation", async () => { + dispatchViaKeymap(); const { tree, row, rows } = setup(); await waitFor(() => expect(rows()).toContain("README.md")); fireEvent.mouseDown(row("README.md")); diff --git a/src/features/project-tree/ProjectView.tsx b/src/features/project-tree/ProjectView.tsx index 4677a9fc3..91d87eb65 100644 --- a/src/features/project-tree/ProjectView.tsx +++ b/src/features/project-tree/ProjectView.tsx @@ -71,16 +71,9 @@ import { cancelEditing, checkEditingName, commitEditing, - copyPaths, - copySelection, - deleteSelection, dropPaths, importExternal, openFile, - openMoveDialog, - openRenameDialog, - openSelection, - paste, refreshOsClipboard, targetFolder, undoLastFileOperation, @@ -757,43 +750,20 @@ export function ProjectView() { return () => window.removeEventListener("focus", refresh); }, []); + // The keymap owns the action keys (Delete, Shift+F6, F6, F4, copy/paste, New …): + // the global dispatcher runs them from the presets in the capture phase, so a + // cleared binding stays cleared and a rebound one follows the user (issue #2801). + // Only the strokes no preset binds stay local: ⌘Z undoing the last file + // refactoring (`project.undoMove` is unbound) and ⌃Enter's New popup on macOS. const onKeyDown = (e: ReactKeyboardEvent) => { if ((e.target as HTMLElement).tagName === "INPUT") return; const stroke = strokeOf(e.nativeEvent); let handled = true; switch (stroke && macStroke(stroke)) { - case "Delete": - case "Meta+Backspace": - void deleteSelection(); - break; - case "Shift+F6": - void openRenameDialog(); - break; - case "F6": - openMoveDialog(); - break; case "Meta+Z": handled = undoLastFileOperation(); break; - case "Meta+C": - copySelection(false); - break; - case "Meta+X": - copySelection(true); - break; - case "Meta+V": - void paste(); - break; - case "Shift+Meta+C": - void copyPaths("absolute"); - break; - case "F4": - case "Meta+Down": - openSelection(); - break; - case "Meta+N": case "Ctrl+Enter": - case "Alt+Insert": showNewPopup(); break; default: diff --git a/src/platform/keymaps/eclipse-macos.ts b/src/platform/keymaps/eclipse-macos.ts index 51ba46f94..f01007070 100644 --- a/src/platform/keymaps/eclipse-macos.ts +++ b/src/platform/keymaps/eclipse-macos.ts @@ -23,6 +23,7 @@ export const eclipseMacos: Record = { "code.commentBlock": ["Shift+Meta+/"], "build.buildProject": ["Meta+B"], "project.copyElement": [], + "project.delete": ["Meta+Backspace"], "run.debug": ["Meta+F11"], "project.open": ["F3"], "debug.jumpToSource": ["F3"], diff --git a/src/platform/keymaps/eclipse-windows.ts b/src/platform/keymaps/eclipse-windows.ts index 4545e1d1c..d08463558 100644 --- a/src/platform/keymaps/eclipse-windows.ts +++ b/src/platform/keymaps/eclipse-windows.ts @@ -23,6 +23,7 @@ export const eclipseWindows: Record = { "code.commentBlock": ["Shift+Ctrl+/"], "build.buildProject": ["Ctrl+F9", "Ctrl+B"], "project.copyElement": [], + "project.delete": ["Delete"], "run.debug": ["Shift+Alt+D", "Shift+F9"], "project.open": ["F12", "Shift+Ctrl+E"], "debug.jumpToSource": ["F12", "Shift+Ctrl+E"],