Skip to content

Commit

Permalink
[monaco] hook into keybinding resolution
Browse files Browse the repository at this point in the history
to display proper keybinding in the quick command palette

Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Jan 17, 2020
1 parent d5f4a86 commit 6f16516
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 131 deletions.
28 changes: 28 additions & 0 deletions packages/keymaps/src/package.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
25 changes: 23 additions & 2 deletions packages/monaco/src/browser/monaco-editor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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') {
Expand All @@ -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<MonacoEditor> {
if (DiffUris.isDiffUri(uri)) {
return this.createMonacoDiffEditor(uri, override, toDispose);
Expand Down
49 changes: 4 additions & 45 deletions packages/monaco/src/browser/monaco-keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 });
}
Expand All @@ -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));
}
}
90 changes: 6 additions & 84 deletions packages/monaco/src/browser/monaco-quick-open-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]>;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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));
}
Expand Down Expand Up @@ -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;
}

}
Loading

0 comments on commit 6f16516

Please sign in to comment.