fix(keymap): match remaining letter shortcuts by typed character (#292)

#291 named letter keys by the typed character (e.key) when Alt is not held, so AZERTY Ctrl+Z is no longer Ctrl+W. Hard-coded handlers still compared the physical KeyboardEvent.code. A shared letterOf(e) in platform/keymap.ts now backs strokeOf and those handlers:

- terminal Ctrl+Shift+C / V / T (copy, paste, new tab) and Ctrl+letter control codes
- Run/Debug Configurations Ctrl/Cmd+N
- browser shortcut guards (blocked reload/print etc.)

Alt combos keep the physical key (Option / AltGr type symbols).
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-29 23:05:26 +08:00
1 джерело d1a597898d
коміт aeec0e6759
8 files changed
+64 -18

No files matched your search

+3 -1
Переглянути файл
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEven
import clsx from "clsx";
import { executeAction } from "@/platform/actions";
import { call, commands, isTauri, type RunConfig } from "@/platform/ipc";
import { letterOf } from "@/platform/keymap";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { Button, Dialog, Icon, IconButton, Input, T } from "@/ui";
import {
@@ -312,7 +313,8 @@ function EditConfigsDialog() {
// IDEA: ⌘N / Alt+Insert adds a configuration, ⌥M opens "Modify options".
const onKeyDown = (e: KeyboardEvent) => {
const add =
(e.code === "KeyN" && (isMac() ? e.metaKey || e.ctrlKey : e.ctrlKey)) || (e.altKey && e.key === "Insert");
(letterOf(e) === "N" && !e.altKey && (isMac() ? e.metaKey || e.ctrlKey : e.ctrlKey)) ||
(e.altKey && e.key === "Insert");
if (add && mode === "configs") {
e.preventDefault();
openAdd();
+11
Переглянути файл
@@ -318,6 +318,17 @@ describe("Run/Debug Configurations tree", () => {
fireEvent.click(await screen.findByText("Redirect input"));
expect(screen.getByLabelText("Redirect input from")).toBeTruthy();
});
it("opens Add New Configuration on Ctrl+N by typed letter", () => {
useRun.setState({ configs: [], loaded: true, editorOpen: true });
render(<EditConfigsOverlay />);
const tree = screen.getByRole("tree", { name: "Run configurations" });
// Dvorak: physical KeyN types "b", physical KeyL types "n".
fireEvent.keyDown(tree, { key: "b", code: "KeyN", ctrlKey: true });
expect(screen.queryByLabelText("Search configuration types")).toBeNull();
fireEvent.keyDown(tree, { key: "n", code: "KeyL", ctrlKey: true });
expect(screen.getByLabelText("Search configuration types")).toBeTruthy();
});
});
describe("debug-only types (Remote JVM Debug)", () => {
+6 -6
Переглянути файл
@@ -15,7 +15,7 @@
* there is no terminal, so there is nothing to handle.
*/
import { executeAction } from "@/platform/actions";
import { effectiveBindings, strokeOf } from "@/platform/keymap";
import { effectiveBindings, letterOf, strokeOf } from "@/platform/keymap";
import { workflowSettings } from "@/platform/workflowSettings";
import { terminalRuntime } from "./loader";
import { isImeKey } from "@/platform/ime";
@@ -53,16 +53,16 @@ export function terminalKeyAction(e: KeyLike, hasSelection: boolean): TerminalKe
if (e.key === "Backspace") return { kind: "send", data: "\x17" };
return { kind: "pass" };
}
const letter = letterOf(e);
if (ctrl && shift && !meta && !alt) {
if (e.code === "KeyC") return hasSelection ? { kind: "copy" } : { kind: "pass" };
if (e.code === "KeyV") return { kind: "paste" };
if (letter === "C") return hasSelection ? { kind: "copy" } : { kind: "pass" };
if (letter === "V") return { kind: "paste" };
// New Tab on Windows / Linux (IDEA keymap); plain Ctrl+T still reaches the shell.
if (e.code === "KeyT") return { kind: "pass" };
if (letter === "T") return { kind: "pass" };
}
if (ctrl && !meta && !alt) {
// The letter the key types, so Shift doesn't change the control code and AZERTY's Ctrl+Z is ^Z, not ^W.
const letter = /^[a-zA-Z]$/.test(e.key) ? e.key : /^Key([A-Z])$/.exec(e.code)?.[1];
if (letter) return { kind: "send", data: String.fromCharCode(letter.toUpperCase().charCodeAt(0) - 64) };
if (letter) return { kind: "send", data: String.fromCharCode(letter.charCodeAt(0) - 64) };
if (e.code === "BracketLeft") return { kind: "send", data: "\x1b" };
if (e.code === "Backslash") return { kind: "send", data: "\x1c" };
if (e.code === "BracketRight") return { kind: "send", data: "\x1d" };
+14
Переглянути файл
@@ -133,6 +133,20 @@ describe("terminal keys", () => {
kind: "send",
data: "\x1a",
});
// Non-QWERTY Ctrl+Shift+C / V / T match the typed letter, not the physical key.
expect(terminalKeyAction(key({ key: "C", code: "KeyI", ctrlKey: true, shiftKey: true }), true)).toEqual({
kind: "copy",
});
expect(terminalKeyAction(key({ key: "V", code: "KeyPeriod", ctrlKey: true, shiftKey: true }), false)).toEqual({
kind: "paste",
});
expect(terminalKeyAction(key({ key: "T", code: "KeyK", ctrlKey: true, shiftKey: true }), false)).toEqual({
kind: "pass",
});
expect(terminalKeyAction(key({ key: "J", code: "KeyC", ctrlKey: true, shiftKey: true }), true)).toEqual({
kind: "send",
data: "\x0a",
});
});
it("implements macOS conveniences", () => {