From 84c855e70363a334eb6ba06fda4281738c47d2e4 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:16:56 +0530 Subject: [PATCH 1/2] fix(keybindings): honor null overrides and terminal shortcut forwarding --- src/cm/commandRegistry.js | 42 +++++++++++++++++++++++++++-- src/components/terminal/terminal.js | 8 +++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/cm/commandRegistry.js b/src/cm/commandRegistry.js index 5dd59adf3..94c2e35c2 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 */ @@ -1177,6 +1178,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 +1316,26 @@ 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 toCodeMirrorKey(combo) { if (!combo) return null; const parts = combo @@ -1356,10 +1378,13 @@ function toCodeMirrorKey(combo) { function rebuildKeymap() { const bindings = []; 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) => { @@ -1410,6 +1435,19 @@ export function getRegisteredCommands() { })); } +export function getResolvedKeyBindings() { + const bindingNames = new Set([ + ...Object.keys(keyBindings), + ...Object.keys(resolvedKeyBindings ?? {}), + ]); + + return Object.fromEntries( + Array.from(bindingNames, (name) => [name, resolveBindingInfo(name)]).filter( + ([, binding]) => binding, + ), + ); +} + 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..c597a00c6 100644 --- a/src/components/terminal/terminal.js +++ b/src/components/terminal/terminal.js @@ -11,10 +11,10 @@ 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 } 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"; @@ -337,7 +337,7 @@ export default class TerminalComponent { parseAppKeybindings() { const parsedBindings = []; - Object.values(keyBindings).forEach((binding) => { + Object.values(getResolvedKeyBindings()).forEach((binding) => { if (!binding.key) return; // Skip editor-only keybindings in terminal @@ -368,7 +368,7 @@ export default class TerminalComponent { parsed.meta = true; } else { // This is the actual key - parsed.key = part; + parsed.key = part.toLowerCase(); } }); @@ -432,7 +432,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) { From 143451a2209e1cf4c06c6dce67d380049798716e Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:24:52 +0530 Subject: [PATCH 2/2] fix --- src/cm/commandRegistry.js | 33 +++++++++++++++++++++-------- src/components/terminal/terminal.js | 17 +++++++++++++-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/cm/commandRegistry.js b/src/cm/commandRegistry.js index 94c2e35c2..01838010f 100644 --- a/src/cm/commandRegistry.js +++ b/src/cm/commandRegistry.js @@ -119,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 = []; @@ -1336,6 +1341,19 @@ function resolveBindingInfo(name) { 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 @@ -1377,6 +1395,7 @@ function toCodeMirrorKey(combo) { function rebuildKeymap() { const bindings = []; + cachedResolvedKeyBindings = buildResolvedKeyBindingsSnapshot(); commandMap.forEach((command, name) => { const bindingInfo = resolveBindingInfo(name); command.description = @@ -1398,6 +1417,7 @@ function rebuildKeymap() { }); }); cachedKeymap = bindings; + resolvedKeyBindingsVersion += 1; return bindings; } @@ -1436,16 +1456,11 @@ export function getRegisteredCommands() { } export function getResolvedKeyBindings() { - const bindingNames = new Set([ - ...Object.keys(keyBindings), - ...Object.keys(resolvedKeyBindings ?? {}), - ]); + return cachedResolvedKeyBindings; +} - return Object.fromEntries( - Array.from(bindingNames, (name) => [name, resolveBindingInfo(name)]).filter( - ([, binding]) => binding, - ), - ); +export function getResolvedKeyBindingsVersion() { + return resolvedKeyBindingsVersion; } export function getCommandKeymapExtension() { diff --git a/src/components/terminal/terminal.js b/src/components/terminal/terminal.js index c597a00c6..2ebf7d50b 100644 --- a/src/components/terminal/terminal.js +++ b/src/components/terminal/terminal.js @@ -11,7 +11,10 @@ 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 } from "cm/commandRegistry"; +import { + getResolvedKeyBindings, + getResolvedKeyBindingsVersion, +} from "cm/commandRegistry"; import toast from "components/toast"; import confirm from "dialogs/confirm"; import fonts from "lib/fonts"; @@ -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,6 +340,11 @@ 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(getResolvedKeyBindings()).forEach((binding) => { @@ -378,7 +388,10 @@ export default class TerminalComponent { }); }); - return parsedBindings; + this.parsedAppKeybindings = parsedBindings; + this.parsedAppKeybindingsVersion = version; + + return this.parsedAppKeybindings; } /**