diff --git a/src/ide/keys.ts b/src/ide/keys.ts index e9fb277b..a3e48292 100644 --- a/src/ide/keys.ts +++ b/src/ide/keys.ts @@ -97,3 +97,76 @@ export class KeyBinder { window.removeEventListener('keydown', this.handler); } } + +// --- CodeMirror keymap interop --- +// +// CodeMirror calls preventDefault() on any key one of its keymaps handles, and +// KeyBinder (above) deliberately defers to that, so a key CodeMirror binds can +// never reach the IDE while an editor is focused. Handing a key over to the IDE +// therefore means removing CodeMirror's binding for it. +// +// The specs can't be compared as strings: CodeMirror writes modifiers in +// whatever order each binding's author chose ("Shift-Mod-k" for deleteLine, +// "Mod-Shift-l" for selectSelectionMatches), and a binding may carry +// per-platform specs that override its default one. + +export interface CMKeyBinding { + key?: string; + mac?: string; + win?: string; + linux?: string; +} + +const CM_MODIFIERS: { [k: string]: string } = { + mod: 'Mod', cmd: 'Mod', meta: 'Mod', ctrl: 'Ctrl', control: 'Ctrl', + shift: 'Shift', alt: 'Alt', option: 'Alt', +}; + +var _isWin: boolean | null = null; + +function isWindows(): boolean { + if (_isWin === null) { + var nav = typeof navigator !== 'undefined' ? navigator : null; + _isWin = !!nav && /win/i.test(nav.platform || nav.userAgent || ''); + } + return _isWin; +} + +// "Shift-Mod-k" and "Mod-Shift-K" both become "Mod-Shift-k" +export function normalizeCMKeySpec(spec: string): string { + var parts = spec.split('-'); + var key = parts.pop() as string; + if (key === '') key = '-'; // trailing dash means the key itself is '-' + var mods: string[] = []; + for (var p of parts) { + if (p === '') continue; // "Mod--" splits to ['Mod', '', ''] + var m = CM_MODIFIERS[p.toLowerCase()] || p; + if (m == 'Ctrl' && !isMacOS()) m = 'Mod'; // off macOS, Ctrl *is* Mod + if (mods.indexOf(m) < 0) mods.push(m); + } + mods.sort(); + return mods.concat([key.toLowerCase()]).join('-'); +} + +// the spec CodeMirror will actually use on this platform +export function effectiveCMKeySpec(b: CMKeyBinding): string | undefined { + var platform = isMacOS() ? b.mac : (isWindows() ? b.win : b.linux); + return platform || b.key; +} + +// every mod+shift+ the IDE binds -- see the shortcuts in ui.ts, which +// testkeys.ts checks this list against +export const IDE_RESERVED_KEYS = [ + 'Mod-Shift-a', 'Mod-Shift-d', 'Mod-Shift-e', 'Mod-Shift-f', 'Mod-Shift-g', + 'Mod-Shift-h', 'Mod-Shift-i', 'Mod-Shift-j', 'Mod-Shift-k', 'Mod-Shift-l', + 'Mod-Shift-r', 'Mod-Shift-x', 'Mod-Shift-y', +]; + +// drop the bindings that would swallow one of the IDE's own shortcuts +export function stripCMKeymap(keymap: readonly T[], specs: string[]): T[] { + var freed = specs.map(normalizeCMKeySpec); + return keymap.filter(function (b) { + var spec = effectiveCMKeySpec(b); + return !spec || freed.indexOf(normalizeCMKeySpec(spec)) < 0; + }); +} diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index b350c49a..67e7d808 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -19,16 +19,14 @@ import { mbo } from "../../themes/mbo"; import { loadSettings, registerEditor, settingsExtensions } from "../settings"; import { asmSpacesKeymap } from "./tabs"; import { clearBreakpoint, current_project, lastDebugState, openHeaderFile, platform, qs, runToPC } from "../ui"; +import { IDE_RESERVED_KEYS, stripCMKeymap } from "../keys"; -// Free Mod-Shift-K (deleteLine) and Mod-Shift-L (selectSelectionMatches) so the -// IDE's debug shortcuts (see src/ide/keys.ts) work while the editor is focused; -// deleteLine moves to Mod-Shift-Backspace. -const freedDebugKeys = ["Mod-Shift-k", "Mod-Shift-l"]; -function stripKeys(kb: readonly KeyBinding[], keys: string[]): KeyBinding[] { - return kb.filter((b) => keys.indexOf(b.key) < 0); -} -const ideDefaultKeymap: KeyBinding[] = [...stripKeys(defaultKeymap, freedDebugKeys), { key: "Mod-Shift-Backspace", run: deleteLine }]; -const ideSearchKeymap: KeyBinding[] = stripKeys(searchKeymap, freedDebugKeys); +// Free the mod+shift+ keys the IDE binds (see IDE_RESERVED_KEYS) so its +// debug shortcuts work while an editor is focused. CodeMirror claims two of +// them today: deleteLine ("Shift-Mod-k") and selectSelectionMatches +// ("Mod-Shift-l"); deleteLine keeps a home on Mod-Shift-Backspace. +const ideDefaultKeymap: KeyBinding[] = [...stripCMKeymap(defaultKeymap, IDE_RESERVED_KEYS), { key: "Mod-Shift-Backspace", run: deleteLine }]; +const ideSearchKeymap: KeyBinding[] = stripCMKeymap(searchKeymap, IDE_RESERVED_KEYS); import { createAssetHeaderPlugin } from "./assetdecorations"; import { createIncludeLinkPlugin } from "./includedecorations"; import { isMobileDevice, ProjectView } from "./baseviews";