From 6f1651655d879a2a00a30d6aa46eeb86f7834b13 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Wed, 15 Jan 2020 04:24:45 +0000 Subject: [PATCH] [monaco] hook into keybinding resolution to display proper keybinding in the quick command palette Signed-off-by: Anton Kosyakov --- packages/keymaps/src/package.spec.ts | 28 ++++ .../src/browser/monaco-editor-provider.ts | 25 +++- .../monaco/src/browser/monaco-keybinding.ts | 49 +------ .../src/browser/monaco-quick-open-service.ts | 90 +----------- .../src/browser/monaco-resolved-keybinding.ts | 134 ++++++++++++++++++ packages/monaco/src/typings/monaco/index.d.ts | 3 + 6 files changed, 198 insertions(+), 131 deletions(-) create mode 100644 packages/keymaps/src/package.spec.ts create mode 100644 packages/monaco/src/browser/monaco-resolved-keybinding.ts diff --git a/packages/keymaps/src/package.spec.ts b/packages/keymaps/src/package.spec.ts new file mode 100644 index 0000000000000..e8c9b16f8d7a4 --- /dev/null +++ b/packages/keymaps/src/package.spec.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2017 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('keymaps package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index d4a0970b7aa39..64d965f56f2c4 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -36,6 +36,8 @@ import { MonacoBulkEditService } from './monaco-bulk-edit-service'; import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { OS } from '@theia/core'; +import { KeybindingRegistry } from '@theia/core/lib/browser'; +import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; @injectable() export class MonacoEditorProvider { @@ -46,6 +48,9 @@ export class MonacoEditorProvider { @inject(MonacoEditorServices) protected readonly services: MonacoEditorServices; + @inject(KeybindingRegistry) + protected keybindingRegistry: KeybindingRegistry; + private isWindowsBackend: boolean = false; protected _current: MonacoEditor | undefined; @@ -125,7 +130,8 @@ export class MonacoEditorProvider { }, toDispose); editor.onDispose(() => toDispose.dispose()); - this.suppressMonaconKeybindingListener(editor); + this.suppressMonacoKeybindingListener(editor); + this.injectKeybindingResolver(editor); const standaloneCommandService = new monaco.services.StandaloneCommandService(editor.instantiationService); commandService.setDelegate(standaloneCommandService); @@ -151,7 +157,7 @@ export class MonacoEditorProvider { * if they are overriden by a user. Monaco keybindings should be registered as Theia keybindings * to allow a user to customize them. */ - protected suppressMonaconKeybindingListener(editor: MonacoEditor): void { + protected suppressMonacoKeybindingListener(editor: MonacoEditor): void { let keydownListener: monaco.IDisposable | undefined; for (const listener of editor.getControl()._standaloneKeybindingService._store._toDispose) { if ('_type' in listener && listener['_type'] === 'keydown') { @@ -164,6 +170,21 @@ export class MonacoEditorProvider { } } + protected injectKeybindingResolver(editor: MonacoEditor): void { + const keybindingService = editor.getControl()._standaloneKeybindingService; + keybindingService.resolveKeybinding = keybinding => [new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding), this.keybindingRegistry)]; + keybindingService.resolveKeyboardEvent = keyboardEvent => { + const keybinding = new monaco.keybindings.SimpleKeybinding( + keyboardEvent.ctrlKey, + keyboardEvent.shiftKey, + keyboardEvent.altKey, + keyboardEvent.metaKey, + keyboardEvent.keyCode + ).toChord(); + return new MonacoResolvedKeybinding(MonacoResolvedKeybinding.keySequence(keybinding), this.keybindingRegistry); + }; + } + protected createEditor(uri: URI, override: IEditorOverrideServices, toDispose: DisposableCollection): Promise { if (DiffUris.isDiffUri(uri)) { return this.createMonacoDiffEditor(uri, override, toDispose); diff --git a/packages/monaco/src/browser/monaco-keybinding.ts b/packages/monaco/src/browser/monaco-keybinding.ts index 2a96e194e6357..b66dd53803e75 100644 --- a/packages/monaco/src/browser/monaco-keybinding.ts +++ b/packages/monaco/src/browser/monaco-keybinding.ts @@ -15,21 +15,12 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { KeybindingContribution, KeybindingRegistry, Key, KeyCode, Keystroke, KeyModifier, KeySequence } from '@theia/core/lib/browser'; +import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { EditorKeybindingContexts } from '@theia/editor/lib/browser'; import { MonacoCommands } from './monaco-command'; import { MonacoCommandRegistry } from './monaco-command-registry'; -import { KEY_CODE_MAP } from './monaco-keycode-map'; -import { isOSX, environment } from '@theia/core'; - -function monaco2BrowserKeyCode(keyCode: monaco.KeyCode): number { - for (let i = 0; i < KEY_CODE_MAP.length; i++) { - if (KEY_CODE_MAP[i] === keyCode) { - return i; - } - } - return -1; -} +import { environment } from '@theia/core'; +import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; @injectable() export class MonacoKeybindingContribution implements KeybindingContribution { @@ -45,15 +36,12 @@ export class MonacoKeybindingContribution implements KeybindingContribution { const item = defaultKeybindings[i]; const command = this.commands.validate(item.command); if (command) { - const raw = item.keybinding; const when = item.when && item.when.serialize(); let keybinding; if (item.command === MonacoCommands.GO_TO_DEFINITION && !environment.electron.is()) { keybinding = 'ctrlcmd+f11'; } else { - keybinding = raw instanceof monaco.keybindings.SimpleKeybinding - ? this.keyCode(raw).toString() - : this.keySequence(raw as monaco.keybindings.ChordKeybinding).join(' '); + keybinding = MonacoResolvedKeybinding.toKeybinding(item.keybinding); } registry.registerKeybinding({ command, keybinding, when }); } @@ -69,33 +57,4 @@ export class MonacoKeybindingContribution implements KeybindingContribution { }); } } - - protected keyCode(keybinding: monaco.keybindings.SimpleKeybinding): KeyCode { - const keyCode = keybinding.keyCode; - const sequence: Keystroke = { - first: Key.getKey(monaco2BrowserKeyCode(keyCode & 0xff)), - modifiers: [] - }; - if (keybinding.ctrlKey) { - if (isOSX) { - sequence.modifiers!.push(KeyModifier.MacCtrl); - } else { - sequence.modifiers!.push(KeyModifier.CtrlCmd); - } - } - if (keybinding.shiftKey) { - sequence.modifiers!.push(KeyModifier.Shift); - } - if (keybinding.altKey) { - sequence.modifiers!.push(KeyModifier.Alt); - } - if (keybinding.metaKey && sequence.modifiers!.indexOf(KeyModifier.CtrlCmd) === -1) { - sequence.modifiers!.push(KeyModifier.CtrlCmd); - } - return KeyCode.createKeyCode(sequence); - } - - protected keySequence(keybinding: monaco.keybindings.ChordKeybinding): KeySequence { - return keybinding.parts.map(part => this.keyCode(part)); - } } diff --git a/packages/monaco/src/browser/monaco-quick-open-service.ts b/packages/monaco/src/browser/monaco-quick-open-service.ts index 6c2fa6df948da..7b5591ebe7d68 100644 --- a/packages/monaco/src/browser/monaco-quick-open-service.ts +++ b/packages/monaco/src/browser/monaco-quick-open-service.ts @@ -18,14 +18,13 @@ import { injectable, inject, postConstruct } from 'inversify'; import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { QuickOpenService, QuickOpenOptions, QuickOpenItem, QuickOpenGroupItem, - QuickOpenMode, KeySequence, ResolvedKeybinding, - KeyCode, Key, KeybindingRegistry + QuickOpenMode, KeySequence, KeybindingRegistry } from '@theia/core/lib/browser'; import { QuickOpenModel, QuickOpenActionProvider, QuickOpenAction } from '@theia/core/lib/common/quick-open-model'; -import { KEY_CODE_MAP } from './monaco-keycode-map'; import { ContextKey } from '@theia/core/lib/browser/context-key-service'; import { MonacoContextKeyService } from './monaco-context-key-service'; import { QuickOpenHideReason } from '@theia/core/lib/common/quick-open-service'; +import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; export interface MonacoQuickOpenControllerOpts extends monaco.quickOpen.IQuickOpenControllerOpts { valueSelection?: Readonly<[number, number]>; @@ -289,7 +288,7 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl constructor( protected readonly model: QuickOpenModel, - protected readonly keybindingService: TheiaKeybindingService, + protected readonly keybindingService: KeybindingRegistry, options?: QuickOpenOptions ) { this.model = model; @@ -397,7 +396,7 @@ export class QuickOpenEntry extends monaco.quickOpen.QuickOpenEntry { constructor( public readonly item: QuickOpenItem, - protected readonly keybindingService: TheiaKeybindingService + protected readonly keybindingService: KeybindingRegistry ) { super(); } @@ -443,7 +442,7 @@ export class QuickOpenEntry extends monaco.quickOpen.QuickOpenEntry { } catch (error) { return undefined; } - return new TheiaResolvedKeybinding(keySequence, this.keybindingService); + return new MonacoResolvedKeybinding(keySequence, this.keybindingService); } run(mode: monaco.quickOpen.Mode): boolean { @@ -465,7 +464,7 @@ export class QuickOpenEntryGroup extends monaco.quickOpen.QuickOpenEntryGroup { constructor( public readonly item: QuickOpenGroupItem, - protected readonly keybindingService: TheiaKeybindingService + protected readonly keybindingService: KeybindingRegistry ) { super(new QuickOpenEntry(item, keybindingService)); } @@ -540,80 +539,3 @@ export class MonacoQuickOpenActionProvider implements monaco.quickOpen.IActionPr return actions.map(action => new MonacoQuickOpenAction(action)); } } - -interface TheiaKeybindingService { - resolveKeybinding(binding: ResolvedKeybinding): KeyCode[]; - acceleratorForKey(key: Key): string; - acceleratorForKeyCode(keyCode: KeyCode, separator?: string): string - acceleratorForSequence(keySequence: KeySequence, separator?: string): string[]; -} - -class TheiaResolvedKeybinding extends monaco.keybindings.ResolvedKeybinding { - - protected readonly parts: monaco.keybindings.ResolvedKeybindingPart[]; - - constructor(protected readonly keySequence: KeySequence, keybindingService: TheiaKeybindingService) { - super(); - this.parts = keySequence.map(keyCode => { - // tslint:disable-next-line:no-null-keyword - const keyLabel = keyCode.key ? keybindingService.acceleratorForKey(keyCode.key) : null; - const keyAriaLabel = keyLabel; - return new monaco.keybindings.ResolvedKeybindingPart( - keyCode.ctrl, - keyCode.shift, - keyCode.alt, - keyCode.meta, - keyLabel, - keyAriaLabel - ); - }); - } - - public getLabel(): string | null { - return monaco.keybindings.UILabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); - } - - public getAriaLabel(): string | null { - return monaco.keybindings.UILabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyAriaLabel); - } - - public getElectronAccelerator(): string | null { - if (this.isChord) { - // Electron cannot handle chords - // tslint:disable-next-line:no-null-keyword - return null; - } - return monaco.keybindings.ElectronAcceleratorLabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); - } - - public getUserSettingsLabel(): string | null { - return monaco.keybindings.UserSettingsLabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); - } - - public isWYSIWYG(): boolean { - return true; - } - - public isChord(): boolean { - return this.parts.length > 1; - } - - public getDispatchParts(): (string | null)[] { - return this.keySequence.map(keyCode => monaco.keybindings.USLayoutResolvedKeybinding.getDispatchStr(this.toKeybinding(keyCode))); - } - - private toKeybinding(keyCode: KeyCode): monaco.keybindings.SimpleKeybinding { - return new monaco.keybindings.SimpleKeybinding( - keyCode.ctrl, - keyCode.shift, - keyCode.alt, - keyCode.meta, - KEY_CODE_MAP[keyCode.key!.keyCode] - ); - } - - public getParts(): monaco.keybindings.ResolvedKeybindingPart[] { - return this.parts; - } - -} diff --git a/packages/monaco/src/browser/monaco-resolved-keybinding.ts b/packages/monaco/src/browser/monaco-resolved-keybinding.ts new file mode 100644 index 0000000000000..4825f42e3c334 --- /dev/null +++ b/packages/monaco/src/browser/monaco-resolved-keybinding.ts @@ -0,0 +1,134 @@ +/******************************************************************************** + * Copyright (C) 2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { KeyCode, KeySequence, Keystroke, Key, KeyModifier } from '@theia/core/lib/browser/keys'; +import { isOSX } from '@theia/core/lib/common/os'; +import { KEY_CODE_MAP } from './monaco-keycode-map'; + +export class MonacoResolvedKeybinding extends monaco.keybindings.ResolvedKeybinding { + + protected readonly parts: monaco.keybindings.ResolvedKeybindingPart[]; + + constructor(protected readonly keySequence: KeySequence, keybindingService: KeybindingRegistry) { + super(); + this.parts = keySequence.map(keyCode => { + // tslint:disable-next-line:no-null-keyword + const keyLabel = keyCode.key ? keybindingService.acceleratorForKey(keyCode.key) : null; + const keyAriaLabel = keyLabel; + return new monaco.keybindings.ResolvedKeybindingPart( + keyCode.ctrl, + keyCode.shift, + keyCode.alt, + keyCode.meta, + keyLabel, + keyAriaLabel + ); + }); + } + + public getLabel(): string | null { + return monaco.keybindings.UILabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); + } + + public getAriaLabel(): string | null { + return monaco.keybindings.UILabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyAriaLabel); + } + + public getElectronAccelerator(): string | null { + if (this.isChord) { + // Electron cannot handle chords + // tslint:disable-next-line:no-null-keyword + return null; + } + return monaco.keybindings.ElectronAcceleratorLabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); + } + + public getUserSettingsLabel(): string | null { + return monaco.keybindings.UserSettingsLabelProvider.toLabel(monaco.platform.OS, this.parts, p => p.keyLabel); + } + + public isWYSIWYG(): boolean { + return true; + } + + public isChord(): boolean { + return this.parts.length > 1; + } + + public getDispatchParts(): (string | null)[] { + return this.keySequence.map(keyCode => monaco.keybindings.USLayoutResolvedKeybinding.getDispatchStr(this.toKeybinding(keyCode))); + } + + private toKeybinding(keyCode: KeyCode): monaco.keybindings.SimpleKeybinding { + return new monaco.keybindings.SimpleKeybinding( + keyCode.ctrl, + keyCode.shift, + keyCode.alt, + keyCode.meta, + KEY_CODE_MAP[keyCode.key!.keyCode] + ); + } + + public getParts(): monaco.keybindings.ResolvedKeybindingPart[] { + return this.parts; + } + + static toKeybinding(keybinding: monaco.keybindings.Keybinding): string { + return keybinding instanceof monaco.keybindings.SimpleKeybinding + ? this.keyCode(keybinding).toString() + : this.keySequence(keybinding as monaco.keybindings.ChordKeybinding).join(' '); + } + + static keyCode(keybinding: monaco.keybindings.SimpleKeybinding): KeyCode { + const keyCode = keybinding.keyCode; + const sequence: Keystroke = { + first: Key.getKey(this.monaco2BrowserKeyCode(keyCode & 0xff)), + modifiers: [] + }; + if (keybinding.ctrlKey) { + if (isOSX) { + sequence.modifiers!.push(KeyModifier.MacCtrl); + } else { + sequence.modifiers!.push(KeyModifier.CtrlCmd); + } + } + if (keybinding.shiftKey) { + sequence.modifiers!.push(KeyModifier.Shift); + } + if (keybinding.altKey) { + sequence.modifiers!.push(KeyModifier.Alt); + } + if (keybinding.metaKey && sequence.modifiers!.indexOf(KeyModifier.CtrlCmd) === -1) { + sequence.modifiers!.push(KeyModifier.CtrlCmd); + } + return KeyCode.createKeyCode(sequence); + } + + static keySequence(keybinding: monaco.keybindings.ChordKeybinding): KeySequence { + return keybinding.parts.map(part => this.keyCode(part)); + } + + private static monaco2BrowserKeyCode(keyCode: monaco.KeyCode): number { + for (let i = 0; i < KEY_CODE_MAP.length; i++) { + if (KEY_CODE_MAP[i] === keyCode) { + return i; + } + } + return -1; + } + +} diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 22bc2c62aad69..6d558fc055410 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -52,6 +52,8 @@ declare module monaco.editor { _store: { _toDispose: monaco.IDisposable[] } + resolveKeybinding(keybinding: monaco.keybindings.ChordKeybinding): monaco.keybindings.ResolvedKeybinding[]; + resolveKeyboardEvent(keyboardEvent: monaco.IKeyboardEvent): monaco.keybindings.ResolvedKeybinding; } } @@ -351,6 +353,7 @@ declare module monaco.keybindings { public readonly keyCode: KeyCode; constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, keyCode: KeyCode); + toChord(): ChordKeybinding; } // https://github.com/TypeFox/vscode/blob/monaco/0.18.0/src/vs/base/common/keyCodes.ts#L503