From bdd39e8a3780b06a8e7ccd9227822f1c2acbbe31 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 14 Jan 2020 04:37:20 +0000 Subject: [PATCH 1/2] [keybinding] fix #6878: allow a user to change default keybindings Signed-off-by: Anton Kosyakov --- packages/core/src/browser/keybinding.ts | 26 +- packages/core/src/common/keybinding.ts | 26 ++ .../src/browser/keybindings-widget.tsx | 233 ++++++++---------- .../src/browser/keymaps-frontend-module.ts | 2 - .../src/browser/keymaps-parser.spec.ts | 120 --------- .../keymaps/src/browser/keymaps-parser.ts | 112 --------- .../keymaps/src/browser/keymaps-service.ts | 121 ++++----- .../src/browser/monaco-editor-provider.ts | 20 ++ packages/monaco/src/typings/monaco/index.d.ts | 5 + 9 files changed, 219 insertions(+), 446 deletions(-) delete mode 100644 packages/keymaps/src/browser/keymaps-parser.spec.ts delete mode 100644 packages/keymaps/src/browser/keymaps-parser.ts diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index c9f820f78d5e8..abba68e69e811 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -38,35 +38,11 @@ export namespace KeybindingScope { export const length = KeybindingScope.END - KeybindingScope.DEFAULT; } -export namespace Keybinding { - - /** - * Returns with the string representation of the binding. - * Any additional properties which are not described on - * the `Keybinding` API will be ignored. - * - * @param binding the binding to stringify. - */ - export function stringify(binding: Keybinding): string { - const copy: Keybinding = { - command: binding.command, - keybinding: binding.keybinding, - context: binding.context - }; - return JSON.stringify(copy); - } - - /* Determine whether object is a KeyBinding */ - // tslint:disable-next-line:no-any - export function is(arg: Keybinding | any): arg is Keybinding { - return !!arg && arg === Object(arg) && 'command' in arg && 'keybinding' in arg; - } -} - /** * @deprecated import from `@theia/core/lib/common/keybinding` instead */ export type Keybinding = common.Keybinding; +export const Keybinding = common.Keybinding; export interface ResolvedKeybinding extends Keybinding { /** diff --git a/packages/core/src/common/keybinding.ts b/packages/core/src/common/keybinding.ts index cdffc95b36ae7..422d9cf633160 100644 --- a/packages/core/src/common/keybinding.ts +++ b/packages/core/src/common/keybinding.ts @@ -36,3 +36,29 @@ export interface Keybinding { // tslint:disable-next-line no-any args?: any; } +export namespace Keybinding { + + /** + * Returns with the string representation of the binding. + * Any additional properties which are not described on + * the `Keybinding` API will be ignored. + * + * @param binding the binding to stringify. + */ + export function stringify(binding: Keybinding): string { + const copy: Keybinding = { + command: binding.command, + keybinding: binding.keybinding, + context: binding.context, + when: binding.when, + args: binding.args + }; + return JSON.stringify(copy); + } + + /* Determine whether object is a KeyBinding */ + // tslint:disable-next-line:no-any + export function is(arg: Keybinding | any): arg is Keybinding { + return !!arg && arg === Object(arg) && 'command' in arg && 'keybinding' in arg; + } +} diff --git a/packages/keymaps/src/browser/keybindings-widget.tsx b/packages/keymaps/src/browser/keybindings-widget.tsx index 986d4f947b2c9..1aa86d8a17c66 100644 --- a/packages/keymaps/src/browser/keybindings-widget.tsx +++ b/packages/keymaps/src/browser/keybindings-widget.tsx @@ -18,37 +18,27 @@ import React = require('react'); import debounce = require('lodash.debounce'); import * as fuzzy from 'fuzzy'; import { injectable, inject, postConstruct } from 'inversify'; -import { CommandRegistry, Emitter, Event } from '@theia/core/lib/common'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { CommandRegistry, Command } from '@theia/core/lib/common/command'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; -import { KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope, SingleTextInputDialogProps, Key } from '@theia/core/lib/browser'; -import { KeymapsParser } from './keymaps-parser'; -import { KeymapsService, KeybindingJson } from './keymaps-service'; +import { KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope, SingleTextInputDialogProps, Key, ScopedKeybinding } from '@theia/core/lib/browser'; +import { KeymapsService } from './keymaps-service'; import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message'; /** * Representation of a keybinding item for the view. */ export interface KeybindingItem { - /** - * The id of the command. - */ - id: string, - /** - * The human-readable label of the command. - */ - command: string, - /** - * The keybinding of the command. - */ - keybinding?: string, - /** - * The context / when closure of the command. - */ - context?: string, - /** - * The source of the command. - */ - source?: string, + command: Command + keybinding?: ScopedKeybinding + /** human-readable labels can contain highlighting */ + labels: { + id: string + command: string + keybinding: string + context: string + source: string + } } /** @@ -74,9 +64,6 @@ export class KeybindingWidget extends ReactWidget { @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; - @inject(KeymapsParser) - protected readonly keymapsParser: KeymapsParser; - @inject(KeymapsService) protected readonly keymapsService: KeymapsService; @@ -178,14 +165,14 @@ export class KeybindingWidget extends ReactWidget { this.query = searchField ? searchField.value.trim().toLocaleLowerCase() : ''; const items = this.getItems(); items.forEach(item => { - const keys: (keyof KeybindingItem)[] = ['command', 'keybinding', 'context', 'source']; + const keys: (keyof KeybindingItem['labels'])[] = ['command', 'keybinding', 'context', 'source']; let matched = false; for (const key of keys) { - const string = item[key]; + const string = item.labels[key]; if (string) { const fuzzyMatch = fuzzy.match(this.query, string, this.fuzzyOptions); if (fuzzyMatch) { - item[key] = fuzzyMatch.rendered; + item.labels[key] = fuzzyMatch.rendered; matched = true; } else { // Match identical keybindings that have different orders. @@ -250,10 +237,10 @@ export class KeybindingWidget extends ReactWidget { chordRenderedResult.concat('+' + resultKey); } }); - item[key] = chordRenderedResult; + item.labels[key] = chordRenderedResult; } - item[key] = renderedResult.join('+'); + item.labels[key] = renderedResult.join('+'); matched = true; } } @@ -354,30 +341,32 @@ export class KeybindingWidget extends ReactWidget { */ protected renderRows(): React.ReactNode { return - { - this.items.map((item, index) => - this.editKeybinding(item)}> - - {this.renderActions(item)} - - - {this.renderMatchedData(item.command)} - - - {item.keybinding ? this.renderKeybinding(item.keybinding) : ''} - - - {(item.context) ? this.renderMatchedData(item.context) : ''} - - - {item.source ? this.renderMatchedData(item.source) : ''} - - - ) - } + {this.items.map((item, index) => this.renderRow(item, index))} ; } + protected renderRow(item: KeybindingItem, index: number): React.ReactNode { + const { command, keybinding } = item; + // TODO get rid of array functions in event handlers + return this.editKeybinding(item)}> + + {this.renderActions(item)} + + + {this.renderMatchedData(item.labels.command)} + + + {this.renderKeybinding(item.labels.keybinding)} + + + {this.renderMatchedData(item.labels.context)} + + + {this.renderMatchedData(item.labels.source)} + + ; + } + /** * Render the actions container with action icons. * @param item the keybinding item for the row. @@ -400,7 +389,7 @@ export class KeybindingWidget extends ReactWidget { * @param item the keybinding item for the row. */ protected renderReset(item: KeybindingItem): React.ReactNode { - return (item.source && this.getRawValue(item.source) === KeybindingScope[1].toLocaleLowerCase()) + return (item.keybinding && item.keybinding.scope === KeybindingScope.USER) ? this.resetKeybinding(item)}> : ''; } @@ -409,6 +398,9 @@ export class KeybindingWidget extends ReactWidget { * @param keybinding the keybinding value. */ protected renderKeybinding(keybinding: string): React.ReactNode { + if (!keybinding.length) { + return undefined; + } const regex = new RegExp(this.keybindingSeparator); keybinding = keybinding.replace(regex, '+'); const keys = keybinding.split('+'); @@ -455,37 +447,58 @@ export class KeybindingWidget extends ReactWidget { const items: KeybindingItem[] = []; // Build the keybinding items. for (let i = 0; i < commands.length; i++) { + const command = commands[i]; // Skip internal commands prefixed by `_`. - if (commands[i].id.startsWith('_')) { + if (command.id.startsWith('_')) { continue; } - // Obtain the keybinding for the given command. - const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commands[i].id); - const item: KeybindingItem = { - id: commands[i].id, - // Get the command label if available, else use the keybinding id. - command: commands[i].label || commands[i].id, - keybinding: (keybindings && keybindings[0]) ? keybindings[0].keybinding : '', - context: (keybindings && keybindings[0]) - ? keybindings[0].context - ? keybindings[0].context : keybindings[0].when - : '', - source: (keybindings && keybindings[0] && typeof keybindings[0].scope !== 'undefined') - ? KeybindingScope[keybindings[0].scope!].toLocaleLowerCase() : '', - }; - items.push(item); + const keybinding = this.keybindingRegistry.getKeybindingsForCommand(command.id)[0]; + items.push({ + command, + keybinding, + labels: { + id: command.id, + command: this.getCommandLabel(command), + keybinding: this.getKeybindingLabel(keybinding) || '', + context: this.getContextLabel(keybinding) || '', + source: this.getScopeLabel(keybinding) || '' + } + }); } // Sort the keybinding item by label. - const sorted: KeybindingItem[] = items.sort((a: KeybindingItem, b: KeybindingItem) => this.compareItem(a.command, b.command)); + const sorted: KeybindingItem[] = items.sort((a, b) => this.compareItem(a.labels.id, b.labels.id)); // Get the list of keybinding item with keybindings (visually put them at the top of the table). - const keyItems: KeybindingItem[] = sorted.filter((a: KeybindingItem) => !!a.keybinding); + const keyItems: KeybindingItem[] = sorted.filter(a => !!a.labels.keybinding); // Get the remaining keybinding items (without keybindings). - const otherItems: KeybindingItem[] = sorted.filter((a: KeybindingItem) => !a.keybinding); + const otherItems: KeybindingItem[] = sorted.filter(a => !a.labels.keybinding); // Return the list of keybinding items prioritizing those with a defined keybinding. return [...keyItems, ...otherItems]; } + protected getCommandLabel(command: Command): string { + return command.label || command.id; + } + + protected getKeybindingLabel(keybinding: ScopedKeybinding | undefined): string | undefined { + return keybinding && keybinding.keybinding; + } + + protected getContextLabel(keybinding: ScopedKeybinding | undefined): string | undefined { + return keybinding ? keybinding.context || keybinding.when : undefined; + } + + protected getScopeLabel(keybinding: ScopedKeybinding | undefined): string | undefined { + let scope = keybinding && keybinding.scope; + if (scope !== undefined) { + if (scope < KeybindingScope.USER) { + scope = KeybindingScope.DEFAULT; + } + return KeybindingScope[scope].toLocaleLowerCase(); + } + return undefined; + } + /** * Compare two strings. * - Strings are first normalized before comparison (`toLowerCase`). @@ -504,37 +517,25 @@ export class KeybindingWidget extends ReactWidget { return 0; } - /** - * Determine if the keybinding currently exists in a user's `keymaps.json`. - * - * @returns `true` if the keybinding exists. - */ - protected keybindingExistsInJson(keybindings: KeybindingJson[], command: string): boolean { - for (let i = 0; i < keybindings.length; i++) { - if (keybindings[i].command === command) { - return true; - } - } - return false; - } - /** * Prompt users to update the keybinding for the given command. * @param item the keybinding item. */ protected editKeybinding(item: KeybindingItem): void { - const command = this.getRawValue(item.command); - const id = this.getRawValue(item.id); - const keybinding = (item.keybinding) ? this.getRawValue(item.keybinding) : ''; - const context = (item.context) ? this.getRawValue(item.context) : ''; + const command = item.command.id; + const oldKeybinding = item.keybinding && item.keybinding.keybinding; const dialog = new EditKeybindingDialog({ title: `Edit Keybinding For ${command}`, - initialValue: keybinding, - validate: newKeybinding => this.validateKeybinding(command, keybinding, newKeybinding), + initialValue: oldKeybinding, + validate: newKeybinding => this.validateKeybinding(command, oldKeybinding, newKeybinding), }, this.keymapsService, item); - dialog.open().then(async newKeybinding => { - if (newKeybinding) { - await this.keymapsService.setKeybinding({ 'command': id, 'keybinding': newKeybinding, 'context': context }); + dialog.open().then(async keybinding => { + if (keybinding) { + await this.keymapsService.setKeybinding({ + ...item.keybinding, + command, + keybinding + }, oldKeybinding); } }); } @@ -545,9 +546,9 @@ export class KeybindingWidget extends ReactWidget { * * @returns a Promise which resolves to `true` if a user accepts resetting. */ - protected async confirmResetKeybinding(command: string): Promise { + protected async confirmResetKeybinding(item: KeybindingItem): Promise { const dialog = new ConfirmDialog({ - title: `Reset keybinding for '${command}'`, + title: `Reset keybinding for '${this.getCommandLabel(item.command)}'`, msg: 'Do you really want to reset this keybinding to its default value?' }); return !!await dialog.open(); @@ -558,11 +559,9 @@ export class KeybindingWidget extends ReactWidget { * @param item the keybinding item. */ protected async resetKeybinding(item: KeybindingItem): Promise { - const rawCommandId = this.getRawValue(item.id); - const rawCommand = this.getRawValue(item.command); - const confirmed = await this.confirmResetKeybinding(rawCommand); + const confirmed = await this.confirmResetKeybinding(item); if (confirmed) { - this.keymapsService.removeKeybinding(rawCommandId); + this.keymapsService.removeKeybinding(item.command.id); } } @@ -574,12 +573,12 @@ export class KeybindingWidget extends ReactWidget { * * @returns the end user message to display. */ - protected validateKeybinding(command: string, oldKeybinding: string, keybinding: string): string { + protected validateKeybinding(command: string, oldKeybinding: string | undefined, keybinding: string): string { if (!keybinding) { return 'keybinding value is required'; } try { - const binding = { 'command': command, 'keybinding': keybinding }; + const binding = { command, keybinding }; KeySequence.parse(keybinding); if (oldKeybinding === keybinding) { return ' '; // if old and new keybindings match, quietly reject update @@ -649,14 +648,6 @@ export class KeybindingWidget extends ReactWidget { } } - /** - * Render the raw value of a item without fuzzy highlighting. - * @param property one of the `KeybindingItem` properties. - */ - protected getRawValue(property: string): string { - return property.replace(new RegExp(this.regexp), '$1'); - } - } /** * Dialog used to edit keybindings, and reset custom keybindings. @@ -682,8 +673,7 @@ class EditKeybindingDialog extends SingleTextInputDialog { super(props); this.item = item; // Add the `Reset` button if the command currently has a custom keybinding. - if (this.item.source && - this.getRaw(this.item.source) === KeybindingScope[1].toLocaleLowerCase()) { + if (this.item.keybinding && this.item.keybinding.scope === KeybindingScope.USER) { this.appendResetButton(); } } @@ -726,20 +716,7 @@ class EditKeybindingDialog extends SingleTextInputDialog { * Perform keybinding reset. */ protected reset(): void { - // Extract the raw id from the keybinding item (without fuzzy matching). - const id = this.getRaw(this.item.id); - // Remove the custom keybinding, resetting it to its default value. - this.keymapsService.removeKeybinding(id); - } - - /** - * Extract the raw value from a string (without fuzzy matching). - * @param a given string value for extraction. - * - * @returns the raw value of a string without any fuzzy matching. - */ - protected getRaw(a: string): string { - return a.replace(new RegExp(/(.*?)<\/match>/g), '$1'); + this.keymapsService.removeKeybinding(this.item.command.id); } } diff --git a/packages/keymaps/src/browser/keymaps-frontend-module.ts b/packages/keymaps/src/browser/keymaps-frontend-module.ts index f81e86a282013..7b1728738528a 100644 --- a/packages/keymaps/src/browser/keymaps-frontend-module.ts +++ b/packages/keymaps/src/browser/keymaps-frontend-module.ts @@ -19,7 +19,6 @@ import { KeymapsService } from './keymaps-service'; import { KeymapsFrontendContribution } from './keymaps-frontend-contribution'; import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; import { KeybindingContribution } from '@theia/core/lib/browser/keybinding'; -import { KeymapsParser } from './keymaps-parser'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import './keymaps-monaco-contribution'; @@ -29,7 +28,6 @@ import { KeybindingWidget } from './keybindings-widget'; import '../../src/browser/style/index.css'; export default new ContainerModule(bind => { - bind(KeymapsParser).toSelf().inSingletonScope(); bind(KeymapsService).toSelf().inSingletonScope(); bind(KeymapsFrontendContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(KeymapsFrontendContribution); diff --git a/packages/keymaps/src/browser/keymaps-parser.spec.ts b/packages/keymaps/src/browser/keymaps-parser.spec.ts deleted file mode 100644 index 8be9ed5ccfc05..0000000000000 --- a/packages/keymaps/src/browser/keymaps-parser.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 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 * as assert from 'assert'; -import { KeymapsParser } from './keymaps-parser'; - -describe('keymaps-parser', () => { - - const parser = new KeymapsParser(); - - it('well formatted raw text', () => { - assertParsing(`{ - "keybindings": [ - { - "keybinding": "ctrl+p", - "command": "command1" - }, - { - "keybinding": "ctrl+shift+p", - "command": "command2" - } - ], - "errors": [] -}`, `[ - { - "keybinding": "ctrl+p", - "command": "command1" - }, - { - "keybinding": "ctrl+shift+p", - "command": "command2" - } -]`); - }); - - it('no array', () => { - assertParsing(`{ - "keybindings": [], - "errors": [ - "should be array at " - ] -}`, `{ - "keybinding": "ctrl+p", - "command": "command" -}`); - }); - - it('additional property', () => { - assertParsing(`{ - "keybindings": [], - "errors": [ - "should NOT have additional properties at /0" - ] -}`, `[ - { - "keybinding": "ctrl+p", - "command": "command", - "extra": 0 - } -]`); - }); - - it('wrong type', () => { - assertParsing(`{ - "keybindings": [], - "errors": [ - "should be string at /0/keybinding" - ] -}`, `[ - { - "keybinding": 0, - "command": "command1" - }, - { - "keybinding": "ctrl+shift+p", - "command": 0 - } -]`); - }); - - it('missing property', () => { - assertParsing(`{ - "keybindings": [], - "errors": [ - "PropertyNameExpected at 44 offset of 1 length", - "ValueExpected at 44 offset of 1 length", - "should have required property 'command' at /0" - ] -}`, `[ - { - "keybinding": "ctrl+p", - } -]`); - }); - - /** - * Assert that the content equals the expected content. - * @param expectation the expected string. - * @param content the content to verify. - */ - function assertParsing(expectation: string, content: string): void { - const errors: string[] = []; - const keybindings = parser.parse(content, errors); - assert.deepEqual(expectation, JSON.stringify({ keybindings, errors }, undefined, 2)); - } - -}); diff --git a/packages/keymaps/src/browser/keymaps-parser.ts b/packages/keymaps/src/browser/keymaps-parser.ts deleted file mode 100644 index b0548bb5c74a2..0000000000000 --- a/packages/keymaps/src/browser/keymaps-parser.ts +++ /dev/null @@ -1,112 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 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 * as Ajv from 'ajv'; -import * as parser from 'jsonc-parser'; -import { injectable } from 'inversify'; -import { Keybinding } from '@theia/core/lib/browser'; - -export const keymapsSchema = { - type: 'array', - items: { - type: 'object', - properties: { - keybinding: { - type: 'string' - }, - command: { - type: 'string' - }, - context: { - type: 'string' - }, - when: { - type: 'string' - }, - args: {} - }, - required: ['command', 'keybinding'], - optional: ['context', 'when', 'args'], - additionalProperties: false - } -}; - -@injectable() -export class KeymapsParser { - - protected readonly validate: Ajv.ValidateFunction; - - constructor() { - // https://github.com/epoberezkin/ajv#options - this.validate = new Ajv({ - jsonPointers: true - }).compile(keymapsSchema); - } - - /** - * Parse the keybindings for potential errors. - * @param content the content. - * @param errors the optional list of parsing errors. - */ - parse(content: string, errors?: string[]): Keybinding[] { - const strippedContent = parser.stripComments(content); - const parsingErrors: parser.ParseError[] | undefined = errors ? [] : undefined; - const bindings = parser.parse(strippedContent, parsingErrors); - if (parsingErrors && errors) { - for (const error of parsingErrors) { - errors.push(`${this.printParseErrorCode(error.error)} at ${error.offset} offset of ${error.length} length`); - } - } - if (this.validate(bindings)) { - return bindings; - } - if (errors && this.validate.errors) { - for (const error of this.validate.errors) { - errors.push(`${error.message} at ${error.dataPath}`); - } - } - return []; - } - - /** - * Print the parsed error code. - * @param code the error code if available. - */ - // https://github.com/Microsoft/node-jsonc-parser/issues/13 - // tslint:disable-next-line:typedef - protected printParseErrorCode(code: number | undefined) { - switch (code) { - case parser.ParseErrorCode.InvalidSymbol: return 'InvalidSymbol'; - case parser.ParseErrorCode.InvalidNumberFormat: return 'InvalidNumberFormat'; - case parser.ParseErrorCode.PropertyNameExpected: return 'PropertyNameExpected'; - case parser.ParseErrorCode.ValueExpected: return 'ValueExpected'; - case parser.ParseErrorCode.ColonExpected: return 'ColonExpected'; - case parser.ParseErrorCode.CommaExpected: return 'CommaExpected'; - case parser.ParseErrorCode.CloseBraceExpected: return 'CloseBraceExpected'; - case parser.ParseErrorCode.CloseBracketExpected: return 'CloseBracketExpected'; - case parser.ParseErrorCode.EndOfFileExpected: return 'EndOfFileExpected'; - case parser.ParseErrorCode.InvalidCommentToken: return 'InvalidCommentToken'; - case parser.ParseErrorCode.UnexpectedEndOfComment: return 'UnexpectedEndOfComment'; - case parser.ParseErrorCode.UnexpectedEndOfString: return 'UnexpectedEndOfString'; - case parser.ParseErrorCode.UnexpectedEndOfNumber: return 'UnexpectedEndOfNumber'; - case parser.ParseErrorCode.InvalidUnicode: return 'InvalidUnicode'; - case parser.ParseErrorCode.InvalidEscapeCharacter: return 'InvalidEscapeCharacter'; - case parser.ParseErrorCode.InvalidCharacter: return 'InvalidCharacter'; - } - return ''; - } - -} diff --git a/packages/keymaps/src/browser/keymaps-service.ts b/packages/keymaps/src/browser/keymaps-service.ts index 27b4ad362cb29..a99b974778e55 100644 --- a/packages/keymaps/src/browser/keymaps-service.ts +++ b/packages/keymaps/src/browser/keymaps-service.ts @@ -16,30 +16,13 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { ResourceProvider, Resource } from '@theia/core/lib/common'; -import { Keybinding, KeybindingRegistry, KeybindingScope, OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser'; +import { ResourceProvider, Resource } from '@theia/core/lib/common/resource'; +import { OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser'; +import { KeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding'; +import { Keybinding } from '@theia/core/lib/common/keybinding'; import { UserStorageUri } from '@theia/userstorage/lib/browser'; -import { KeymapsParser } from './keymaps-parser'; import * as jsoncparser from 'jsonc-parser'; -import { Emitter } from '@theia/core/lib/common/'; - -/** - * Representation of a JSON keybinding. - */ -export interface KeybindingJson { - /** - * The keybinding command. - */ - command: string, - /** - * The actual keybinding. - */ - keybinding: string, - /** - * The keybinding context. - */ - context: string, -} +import { Emitter } from '@theia/core/lib/common/event'; @injectable() export class KeymapsService { @@ -53,11 +36,8 @@ export class KeymapsService { @inject(OpenerService) protected readonly opener: OpenerService; - @inject(KeymapsParser) - protected readonly parser: KeymapsParser; - protected readonly changeKeymapEmitter = new Emitter(); - onDidChangeKeymaps = this.changeKeymapEmitter.event; + readonly onDidChangeKeymaps = this.changeKeymapEmitter.event; protected resource: Resource; @@ -87,12 +67,17 @@ export class KeymapsService { * Parsed the read keybindings. */ protected async parseKeybindings(): Promise { - try { - const content = await this.resource.readContents(); - return this.parser.parse(content); - } catch (error) { - return error; + const content = await this.resource.readContents(); + const keybindings: Keybinding[] = []; + const json = jsoncparser.parse(content, undefined, { disallowComments: false }); + if (Array.isArray(json)) { + for (const value of json) { + if (Keybinding.is(value)) { + keybindings.push(value); + } + } } + return keybindings; } /** @@ -109,25 +94,52 @@ export class KeymapsService { /** * Set the keybinding in the JSON. - * @param keybindingJson the JSON keybindings. + * @param newKeybinding the JSON keybindings. */ - async setKeybinding(keybindingJson: KeybindingJson): Promise { + async setKeybinding(newKeybinding: Keybinding, oldKeybinding: string | undefined): Promise { if (!this.resource.saveContents) { return; } - const content = await this.resource.readContents(); - const keybindings: KeybindingJson[] = content ? jsoncparser.parse(content) : []; - let updated = false; - for (let i = 0; i < keybindings.length; i++) { - if (keybindings[i].command === keybindingJson.command) { - updated = true; - keybindings[i].keybinding = keybindingJson.keybinding; + const keybindings = await this.parseKeybindings(); + let newAdded = false; + let oldRemoved = false; + for (const keybinding of keybindings) { + if (keybinding.command === newKeybinding.command && + (keybinding.context || '') === (newKeybinding.context || '') && + (keybinding.when || '') === (newKeybinding.when || '')) { + newAdded = true; + keybinding.keybinding = newKeybinding.keybinding; + } + if (oldKeybinding && keybinding.keybinding === oldKeybinding && + keybinding.command === '-' + newKeybinding.command && + (keybinding.context || '') === (newKeybinding.context || '') && + (keybinding.when || '') === (newKeybinding.when || '')) { + oldRemoved = true; } } - if (!updated) { - const item: KeybindingJson = { ...keybindingJson }; - keybindings.push(item); + if (!newAdded) { + keybindings.push({ + command: newKeybinding.command, + keybinding: newKeybinding.keybinding, + context: newKeybinding.context, + when: newKeybinding.when, + args: newKeybinding.args + }); + } + if (!oldRemoved && oldKeybinding) { + keybindings.push({ + command: '-' + newKeybinding.command, + // TODO key: oldKeybinding, see https://github.com/eclipse-theia/theia/issues/6879 + keybinding: oldKeybinding, + context: newKeybinding.context, + when: newKeybinding.when, + args: newKeybinding.args + }); } + // TODO use preference values to get proper json settings + // TODO handle dirty models properly + // TODO handle race conditions properly + // TODO only apply mimimal edits await this.resource.saveContents(JSON.stringify(keybindings, undefined, 4)); } @@ -139,23 +151,14 @@ export class KeymapsService { if (!this.resource.saveContents) { return; } - const content = await this.resource.readContents(); - const keybindings: KeybindingJson[] = content ? jsoncparser.parse(content) : []; - const filtered = keybindings.filter(a => a.command !== commandId); + const keybindings = await this.parseKeybindings(); + const removedCommand = '-' + commandId; + const filtered = keybindings.filter(a => a.command !== commandId && a.command !== removedCommand); + // TODO use preference values to get proper json settings + // TODO handle dirty models properly + // TODO handle race conditions properly + // TODO only apply mimimal edits await this.resource.saveContents(JSON.stringify(filtered, undefined, 4)); } - /** - * Get the list of keybindings from the JSON. - * - * @returns the list of keybindings in JSON. - */ - async getKeybindings(): Promise { - if (!this.resource.saveContents) { - return []; - } - const content = await this.resource.readContents(); - return content ? jsoncparser.parse(content) : []; - } - } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 3efec40d79afc..d4a0970b7aa39 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -125,6 +125,8 @@ export class MonacoEditorProvider { }, toDispose); editor.onDispose(() => toDispose.dispose()); + this.suppressMonaconKeybindingListener(editor); + const standaloneCommandService = new monaco.services.StandaloneCommandService(editor.instantiationService); commandService.setDelegate(standaloneCommandService); this.installQuickOpenService(editor); @@ -144,6 +146,24 @@ export class MonacoEditorProvider { return editor; } + /** + * Suppresses Monaco keydown listener to avoid triggering default Monaco keybindings + * 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 { + let keydownListener: monaco.IDisposable | undefined; + for (const listener of editor.getControl()._standaloneKeybindingService._store._toDispose) { + if ('_type' in listener && listener['_type'] === 'keydown') { + keydownListener = listener; + break; + } + } + if (keydownListener) { + keydownListener.dispose(); + } + } + 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/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 39a847a28fad3..22bc2c62aad69 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -48,6 +48,11 @@ declare module monaco.editor { setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void; setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void; trigger(source: string, handlerId: string, payload: any): void + _standaloneKeybindingService: { + _store: { + _toDispose: monaco.IDisposable[] + } + } } // https://github.com/TypeFox/vscode/blob/monaco/0.18.0/src/vs/editor/browser/widget/codeEditorWidget.ts#L107 From 47ac87014a1c125bb7af30a264172fe604a5cf98 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Wed, 15 Jan 2020 04:24:45 +0000 Subject: [PATCH 2/2] [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