diff --git a/src/cm/commandRegistry.js b/src/cm/commandRegistry.js index 5dd59adf3..01838010f 100644 --- a/src/cm/commandRegistry.js +++ b/src/cm/commandRegistry.js @@ -108,6 +108,7 @@ const commandKeymapCompartment = new Compartment(); * run: (view?: EditorView | null) => boolean | void; * requiresView?: boolean; * defaultDescription?: string; + * defaultKey?: string | null; * key?: string | null; * }} CommandEntry */ @@ -118,6 +119,11 @@ const commandMap = new Map(); /** @type {Record} */ let resolvedKeyBindings = keyBindings; +/** @type {Record} */ +let cachedResolvedKeyBindings = {}; + +let resolvedKeyBindingsVersion = 0; + /** @type {import("@codemirror/view").KeyBinding[]} */ let cachedKeymap = []; @@ -1177,6 +1183,7 @@ function addCommand(entry) { const command = { ...entry, defaultDescription: entry.description || entry.name, + defaultKey: entry.key ?? null, key: entry.key ?? null, }; commandMap.set(entry.name, command); @@ -1314,6 +1321,39 @@ function parseKeyString(keyString) { .filter(Boolean); } +function hasOwnBindingOverride(name) { + return Object.prototype.hasOwnProperty.call(resolvedKeyBindings ?? {}, name); +} + +function resolveBindingInfo(name) { + const baseBinding = keyBindings[name] ?? null; + if (!hasOwnBindingOverride(name)) return baseBinding; + + const override = resolvedKeyBindings?.[name]; + if (override === null) { + return baseBinding ? { ...baseBinding, key: null } : { key: null }; + } + + if (!override || typeof override !== "object") { + return baseBinding; + } + + return baseBinding ? { ...baseBinding, ...override } : override; +} + +function buildResolvedKeyBindingsSnapshot() { + const bindingNames = new Set([ + ...Object.keys(keyBindings), + ...Object.keys(resolvedKeyBindings ?? {}), + ]); + + return Object.fromEntries( + Array.from(bindingNames, (name) => [name, resolveBindingInfo(name)]).filter( + ([, binding]) => binding, + ), + ); +} + function toCodeMirrorKey(combo) { if (!combo) return null; const parts = combo @@ -1355,11 +1395,15 @@ function toCodeMirrorKey(combo) { function rebuildKeymap() { const bindings = []; + cachedResolvedKeyBindings = buildResolvedKeyBindingsSnapshot(); commandMap.forEach((command, name) => { - const bindingInfo = resolvedKeyBindings?.[name]; + const bindingInfo = resolveBindingInfo(name); command.description = bindingInfo?.description || command.defaultDescription; - const keySource = bindingInfo?.key ?? command.key ?? null; + const keySource = + bindingInfo && Object.prototype.hasOwnProperty.call(bindingInfo, "key") + ? bindingInfo.key + : (command.defaultKey ?? null); command.key = keySource; const combos = parseKeyString(keySource); combos.forEach((combo) => { @@ -1373,6 +1417,7 @@ function rebuildKeymap() { }); }); cachedKeymap = bindings; + resolvedKeyBindingsVersion += 1; return bindings; } @@ -1410,6 +1455,14 @@ export function getRegisteredCommands() { })); } +export function getResolvedKeyBindings() { + return cachedResolvedKeyBindings; +} + +export function getResolvedKeyBindingsVersion() { + return resolvedKeyBindingsVersion; +} + export function getCommandKeymapExtension() { return commandKeymapCompartment.of(keymap.of(cachedKeymap)); } diff --git a/src/components/terminal/terminal.js b/src/components/terminal/terminal.js index b8b1b4d84..2ebf7d50b 100644 --- a/src/components/terminal/terminal.js +++ b/src/components/terminal/terminal.js @@ -11,10 +11,13 @@ import { Unicode11Addon } from "@xterm/addon-unicode11"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; import { Terminal as Xterm } from "@xterm/xterm"; +import { + getResolvedKeyBindings, + getResolvedKeyBindingsVersion, +} from "cm/commandRegistry"; import toast from "components/toast"; import confirm from "dialogs/confirm"; import fonts from "lib/fonts"; -import keyBindings from "lib/keyBindings"; import appSettings from "lib/settings"; import LigaturesAddon from "./ligatures"; import { getTerminalSettings } from "./terminalDefaults"; @@ -61,6 +64,8 @@ export default class TerminalComponent { this.isConnected = false; this.serverMode = options.serverMode !== false; // Default true this.touchSelection = null; + this.parsedAppKeybindings = []; + this.parsedAppKeybindingsVersion = -1; this.init(); } @@ -335,9 +340,14 @@ export default class TerminalComponent { * Parse app keybindings into a format usable by the keyboard handler */ parseAppKeybindings() { + const version = getResolvedKeyBindingsVersion(); + if (this.parsedAppKeybindingsVersion === version) { + return this.parsedAppKeybindings; + } + const parsedBindings = []; - Object.values(keyBindings).forEach((binding) => { + Object.values(getResolvedKeyBindings()).forEach((binding) => { if (!binding.key) return; // Skip editor-only keybindings in terminal @@ -368,7 +378,7 @@ export default class TerminalComponent { parsed.meta = true; } else { // This is the actual key - parsed.key = part; + parsed.key = part.toLowerCase(); } }); @@ -378,7 +388,10 @@ export default class TerminalComponent { }); }); - return parsedBindings; + this.parsedAppKeybindings = parsedBindings; + this.parsedAppKeybindingsVersion = version; + + return this.parsedAppKeybindings; } /** @@ -432,7 +445,7 @@ export default class TerminalComponent { binding.shift === event.shiftKey && binding.alt === event.altKey && binding.meta === event.metaKey && - binding.key === event.key, + binding.key === event.key.toLowerCase(), ); if (isAppKeybinding) {