UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

683 lines 29.4 kB
"use strict"; // ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var KeybindingRegistry_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.KeybindingRegistry = exports.KeybindingContexts = exports.KeybindingContext = exports.KeybindingContribution = exports.Keybinding = exports.KeybindingScope = void 0; const inversify_1 = require("inversify"); const os_1 = require("../common/os"); const event_1 = require("../common/event"); const command_1 = require("../common/command"); const disposable_1 = require("../common/disposable"); const keys_1 = require("./keyboard/keys"); const keyboard_layout_service_1 = require("./keyboard/keyboard-layout-service"); const contribution_provider_1 = require("../common/contribution-provider"); const logger_1 = require("../common/logger"); const status_bar_1 = require("./status-bar/status-bar"); const context_key_service_1 = require("./context-key-service"); const core_preferences_1 = require("./core-preferences"); const common = require("../common/keybinding"); const nls_1 = require("../common/nls"); var KeybindingScope; (function (KeybindingScope) { KeybindingScope[KeybindingScope["DEFAULT"] = 0] = "DEFAULT"; KeybindingScope[KeybindingScope["USER"] = 1] = "USER"; KeybindingScope[KeybindingScope["WORKSPACE"] = 2] = "WORKSPACE"; KeybindingScope[KeybindingScope["END"] = 3] = "END"; })(KeybindingScope = exports.KeybindingScope || (exports.KeybindingScope = {})); (function (KeybindingScope) { KeybindingScope.length = KeybindingScope.END - KeybindingScope.DEFAULT; })(KeybindingScope = exports.KeybindingScope || (exports.KeybindingScope = {})); exports.Keybinding = common.Keybinding; exports.KeybindingContribution = Symbol('KeybindingContribution'); exports.KeybindingContext = Symbol('KeybindingContext'); var KeybindingContexts; (function (KeybindingContexts) { KeybindingContexts.NOOP_CONTEXT = { id: 'noop.keybinding.context', isEnabled: () => true }; KeybindingContexts.DEFAULT_CONTEXT = { id: 'default.keybinding.context', isEnabled: () => false }; })(KeybindingContexts = exports.KeybindingContexts || (exports.KeybindingContexts = {})); let KeybindingRegistry = KeybindingRegistry_1 = class KeybindingRegistry { constructor() { this.keySequence = []; this.contexts = {}; this.keymaps = [...Array(KeybindingScope.length)].map(() => []); this.keybindingsChanged = new event_1.Emitter(); this.toResetKeymap = new Map(); } async onStart() { await this.keyboardLayoutService.initialize(); this.keyboardLayoutService.onKeyboardLayoutChanged(newLayout => { this.clearResolvedKeybindings(); this.keybindingsChanged.fire(undefined); }); this.registerContext(KeybindingContexts.NOOP_CONTEXT); this.registerContext(KeybindingContexts.DEFAULT_CONTEXT); this.registerContext(...this.contextProvider.getContributions()); for (const contribution of this.contributions.getContributions()) { contribution.registerKeybindings(this); } } /** * Event that is fired when the resolved keybindings change due to a different keyboard layout * or when a new keymap is being set */ get onKeybindingsChanged() { return this.keybindingsChanged.event; } /** * Registers the keybinding context arguments into the application. Fails when an already registered * context is being registered. * * @param contexts the keybinding contexts to register into the application. */ registerContext(...contexts) { for (const context of contexts) { const { id } = context; if (this.contexts[id]) { this.logger.error(`A keybinding context with ID ${id} is already registered.`); } else { this.contexts[id] = context; } } } /** * Register a default keybinding to the registry. * * Keybindings registered later have higher priority during evaluation. * * @param binding the keybinding to be registered */ registerKeybinding(binding) { return this.doRegisterKeybinding(binding); } /** * Register multiple default keybindings to the registry * * @param bindings An array of keybinding to be registered */ registerKeybindings(...bindings) { return this.doRegisterKeybindings(bindings, KeybindingScope.DEFAULT); } unregisterKeybinding(arg) { const keymap = this.keymaps[KeybindingScope.DEFAULT]; const filter = command_1.Command.is(arg) ? ({ command }) => command === arg.id : ({ keybinding }) => exports.Keybinding.is(arg) ? keybinding === arg.keybinding : keybinding === arg; for (const binding of keymap.filter(filter)) { const idx = keymap.indexOf(binding); if (idx !== -1) { keymap.splice(idx, 1); } } } doRegisterKeybindings(bindings, scope = KeybindingScope.DEFAULT) { const toDispose = new disposable_1.DisposableCollection(); for (const binding of bindings) { toDispose.push(this.doRegisterKeybinding(binding, scope)); } return toDispose; } doRegisterKeybinding(binding, scope = KeybindingScope.DEFAULT) { try { this.resolveKeybinding(binding); const scoped = Object.assign(binding, { scope }); this.insertBindingIntoScope(scoped, scope); return disposable_1.Disposable.create(() => { const index = this.keymaps[scope].indexOf(scoped); if (index !== -1) { this.keymaps[scope].splice(index, 1); } }); } catch (error) { this.logger.warn(`Could not register keybinding:\n ${common.Keybinding.stringify(binding)}\n${error}`); return disposable_1.Disposable.NULL; } } /** * Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a * user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter) */ insertBindingIntoScope(item, scope) { const scopedKeymap = this.keymaps[scope]; const getNumberOfKeystrokes = (binding) => { var _a, _b; return ((_b = (_a = binding.keybinding.trim().match(/\s/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) + 1; }; const numberOfKeystrokesInBinding = getNumberOfKeystrokes(item); const indexOfFirstItemWithEqualStrokes = scopedKeymap.findIndex(existingBinding => getNumberOfKeystrokes(existingBinding) === numberOfKeystrokesInBinding); if (indexOfFirstItemWithEqualStrokes > -1) { scopedKeymap.splice(indexOfFirstItemWithEqualStrokes, 0, item); } else { scopedKeymap.push(item); } } /** * Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService. */ resolveKeybinding(binding) { if (!binding.resolved) { const sequence = keys_1.KeySequence.parse(binding.keybinding); binding.resolved = sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code)); } return binding.resolved; } /** * Clear all `resolved` properties of registered keybindings so the KeyboardLayoutService is called * again to resolve them. This is necessary when the user's keyboard layout has changed. */ clearResolvedKeybindings() { for (let i = KeybindingScope.DEFAULT; i < KeybindingScope.END; i++) { const bindings = this.keymaps[i]; for (let j = 0; j < bindings.length; j++) { const binding = bindings[j]; binding.resolved = undefined; } } } /** * Checks whether a colliding {@link common.Keybinding} exists in a specific scope. * @param binding the keybinding to check * @param scope the keybinding scope to check * @returns true if there is a colliding keybinding */ containsKeybindingInScope(binding, scope = KeybindingScope.USER) { const bindingKeySequence = this.resolveKeybinding(binding); const collisions = this.getKeySequenceCollisions(this.getUsableBindings(this.keymaps[scope]), bindingKeySequence) .filter(b => b.context === binding.context && !b.when && !binding.when); if (collisions.full.length > 0) { return true; } if (collisions.partial.length > 0) { return true; } if (collisions.shadow.length > 0) { return true; } return false; } /** * Get a user visible representation of a {@link common.Keybinding}. * @returns an array of strings representing all elements of the {@link KeySequence} defined by the {@link common.Keybinding} * @param keybinding the keybinding * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence} */ acceleratorFor(keybinding, separator = ' ', asciiOnly = false) { const bindingKeySequence = this.resolveKeybinding(keybinding); return this.acceleratorForSequence(bindingKeySequence, separator, asciiOnly); } /** * Get a user visible representation of a {@link KeySequence}. * @returns an array of strings representing all elements of the {@link KeySequence} * @param keySequence the keysequence * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence} */ acceleratorForSequence(keySequence, separator = ' ', asciiOnly = false) { return keySequence.map(keyCode => this.acceleratorForKeyCode(keyCode, separator, asciiOnly)); } /** * Get a user visible representation of a key code (a key with modifiers). * @returns a string representing the {@link KeyCode} * @param keyCode the keycode * @param separator the separator used to separate keys (key and modifiers) in the returning string * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus. */ acceleratorForKeyCode(keyCode, separator = ' ', asciiOnly = false) { return this.componentsForKeyCode(keyCode, asciiOnly).join(separator); } componentsForKeyCode(keyCode, asciiOnly = false) { const keyCodeResult = []; const useSymbols = os_1.isOSX && !asciiOnly; if (keyCode.meta && os_1.isOSX) { keyCodeResult.push(useSymbols ? '⌘' : 'Cmd'); } if (keyCode.ctrl) { keyCodeResult.push(useSymbols ? '⌃' : 'Ctrl'); } if (keyCode.alt) { keyCodeResult.push(useSymbols ? '⌥' : 'Alt'); } if (keyCode.shift) { keyCodeResult.push(useSymbols ? '⇧' : 'Shift'); } if (keyCode.key) { keyCodeResult.push(this.acceleratorForKey(keyCode.key, asciiOnly)); } return keyCodeResult; } /** * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus. * * Return a user visible representation of a single key. */ acceleratorForKey(key, asciiOnly = false) { if (os_1.isOSX && !asciiOnly) { if (key === keys_1.Key.ARROW_LEFT) { return '←'; } if (key === keys_1.Key.ARROW_RIGHT) { return '→'; } if (key === keys_1.Key.ARROW_UP) { return '↑'; } if (key === keys_1.Key.ARROW_DOWN) { return '↓'; } } const keyString = this.keyboardLayoutService.getKeyboardCharacter(key); if (key.keyCode >= keys_1.Key.KEY_A.keyCode && key.keyCode <= keys_1.Key.KEY_Z.keyCode || key.keyCode >= keys_1.Key.F1.keyCode && key.keyCode <= keys_1.Key.F24.keyCode) { return keyString.toUpperCase(); } else if (keyString.length > 1) { return keyString.charAt(0).toUpperCase() + keyString.slice(1); } else { return keyString; } } /** * Finds collisions for a key sequence inside a list of bindings (error-free) * * @param bindings the reference bindings * @param candidate the sequence to match */ getKeySequenceCollisions(bindings, candidate) { const result = new KeybindingRegistry_1.KeybindingsResult(); for (const binding of bindings) { try { const bindingKeySequence = this.resolveKeybinding(binding); const compareResult = keys_1.KeySequence.compare(candidate, bindingKeySequence); switch (compareResult) { case keys_1.KeySequence.CompareResult.FULL: { result.full.push(binding); break; } case keys_1.KeySequence.CompareResult.PARTIAL: { result.partial.push(binding); break; } case keys_1.KeySequence.CompareResult.SHADOW: { result.shadow.push(binding); break; } } } catch (error) { this.logger.warn(error); } } return result; } /** * Get all keybindings associated to a commandId. * * @param commandId The ID of the command for which we are looking for keybindings. * @returns an array of {@link ScopedKeybinding} */ getKeybindingsForCommand(commandId) { const result = []; const disabledBindings = new Set(); for (let scope = KeybindingScope.END - 1; scope >= KeybindingScope.DEFAULT; scope--) { this.keymaps[scope].forEach(binding => { var _a; if ((_a = binding.command) === null || _a === void 0 ? void 0 : _a.startsWith('-')) { disabledBindings.add(JSON.stringify({ command: binding.command.substring(1), binding: binding.keybinding, context: binding.context, when: binding.when })); } else { const command = this.commandRegistry.getCommand(binding.command); if (command && command.id === commandId && !disabledBindings.has(JSON.stringify({ command: binding.command, binding: binding.keybinding, context: binding.context, when: binding.when }))) { result.push({ ...binding, scope }); } } }); } return result; } isActive(binding) { /* Pseudo commands like "passthrough" are always active (and not found in the command registry). */ if (this.isPseudoCommand(binding.command)) { return true; } const command = this.commandRegistry.getCommand(binding.command); return !!command && !!this.commandRegistry.getActiveHandler(command.id); } /** * Tries to execute a keybinding. * * @param binding to execute * @param event keyboard event. */ executeKeyBinding(binding, event) { if (this.isPseudoCommand(binding.command)) { /* Don't do anything, let the event propagate. */ } else { const command = this.commandRegistry.getCommand(binding.command); if (command) { if (this.commandRegistry.isEnabled(binding.command, binding.args)) { this.commandRegistry.executeCommand(binding.command, binding.args) .catch(e => console.error('Failed to execute command:', e)); } /* Note that if a keybinding is in context but the command is not active we still stop the processing here. */ event.preventDefault(); event.stopPropagation(); } } } /** * Only execute if it has no context (global context) or if we're in that context. */ isEnabled(binding, event) { return this.isEnabledInScope(binding, event.target); } isEnabledInScope(binding, target) { const context = binding.context && this.contexts[binding.context]; if (context && !context.isEnabled(binding)) { return false; } if (binding.when && !this.whenContextService.match(binding.when, target)) { return false; } return true; } dispatchCommand(id, target) { const keybindings = this.getKeybindingsForCommand(id); if (keybindings.length) { for (const keyCode of this.resolveKeybinding(keybindings[0])) { this.dispatchKeyDown(keyCode, target); } } } dispatchKeyDown(input, target = document.activeElement || window) { const eventInit = this.asKeyboardEventInit(input); const emulatedKeyboardEvent = new KeyboardEvent('keydown', eventInit); target.dispatchEvent(emulatedKeyboardEvent); } asKeyboardEventInit(input) { if (typeof input === 'string') { return this.asKeyboardEventInit(keys_1.KeyCode.createKeyCode(input)); } if (input instanceof keys_1.KeyCode) { return { metaKey: input.meta, shiftKey: input.shift, altKey: input.alt, ctrlKey: input.ctrl, code: input.key && input.key.code, key: (input && input.character) || (input.key && input.key.code), keyCode: input.key && input.key.keyCode }; } return input; } registerEventListeners(win) { /* vvv HOTFIX begin vvv * * This is a hotfix against issues eclipse/theia#6459 and gitpod-io/gitpod#875 . * It should be reverted after Theia was updated to the newer Monaco. */ let inComposition = false; const compositionStart = () => { inComposition = true; }; win.document.addEventListener('compositionstart', compositionStart); const compositionEnd = () => { inComposition = false; }; win.document.addEventListener('compositionend', compositionEnd); const keydown = (event) => { if (inComposition !== true) { this.run(event); } }; win.document.addEventListener('keydown', keydown, true); return disposable_1.Disposable.create(() => { win.document.removeEventListener('compositionstart', compositionStart); win.document.removeEventListener('compositionend', compositionEnd); win.document.removeEventListener('keydown', keydown); }); } /** * Run the command matching to the given keyboard event. */ run(event) { if (event.defaultPrevented) { return; } const eventDispatch = this.corePreferences['keyboard.dispatch']; const keyCode = keys_1.KeyCode.createKeyCode(event, eventDispatch); /* Keycode is only a modifier, next keycode will be modifier + key. Ignore this one. */ if (keyCode.isModifierOnly()) { return; } this.keyboardLayoutService.validateKeyCode(keyCode); this.keySequence.push(keyCode); const match = this.matchKeybinding(this.keySequence, event); if (match && match.kind === 'partial') { /* Accumulate the keysequence */ event.preventDefault(); event.stopPropagation(); this.statusBar.setElement('keybinding-status', { text: nls_1.nls.localize('theia/core/keybindingStatus', '{0} was pressed, waiting for more keys', `(${this.acceleratorForSequence(this.keySequence, '+')})`), alignment: status_bar_1.StatusBarAlignment.LEFT, priority: 2 }); } else { if (match && match.kind === 'full') { this.executeKeyBinding(match.binding, event); } this.keySequence = []; this.statusBar.removeElement('keybinding-status'); } } /** * Match first binding in the current context. * Keybindings ordered by a scope and by a registration order within the scope. * * FIXME: * This method should run very fast since it happens on each keystroke. We should reconsider how keybindings are stored. * It should be possible to look up full and partial keybinding for given key sequence for constant time using some kind of tree. * Such tree should not contain disabled keybindings and be invalidated whenever the registry is changed. */ matchKeybinding(keySequence, event) { let disabled; const isEnabled = (binding) => { if (event && !this.isEnabled(binding, event)) { return false; } const { command, context, when, keybinding } = binding; if (!this.isUsable(binding)) { disabled = disabled || new Set(); disabled.add(JSON.stringify({ command: command.substring(1), context, when, keybinding })); return false; } return !(disabled === null || disabled === void 0 ? void 0 : disabled.has(JSON.stringify({ command, context, when, keybinding }))); }; for (let scope = KeybindingScope.END; --scope >= KeybindingScope.DEFAULT;) { for (const binding of this.keymaps[scope]) { const resolved = this.resolveKeybinding(binding); const compareResult = keys_1.KeySequence.compare(keySequence, resolved); if (compareResult === keys_1.KeySequence.CompareResult.FULL && isEnabled(binding)) { return { kind: 'full', binding }; } if (compareResult === keys_1.KeySequence.CompareResult.PARTIAL && isEnabled(binding)) { return { kind: 'partial', binding }; } } } return undefined; } /** * Returns true if the binding is usable * @param binding Binding to be checked */ isUsable(binding) { return binding.command.charAt(0) !== '-'; } /** * Return a new filtered array containing only the usable bindings among the input bindings * @param bindings Bindings to filter */ getUsableBindings(bindings) { return bindings.filter(binding => this.isUsable(binding)); } /** * Return true of string a pseudo-command id, in other words a command id * that has a special meaning and that we won't find in the command * registry. * * @param commandId commandId to test */ isPseudoCommand(commandId) { return commandId === KeybindingRegistry_1.PASSTHROUGH_PSEUDO_COMMAND; } /** * Sets a new keymap replacing all existing {@link common.Keybinding}s in the given scope. * @param scope the keybinding scope * @param bindings an array containing the new {@link common.Keybinding}s */ setKeymap(scope, bindings) { this.resetKeybindingsForScope(scope); this.toResetKeymap.set(scope, this.doRegisterKeybindings(bindings, scope)); this.keybindingsChanged.fire(undefined); } /** * Reset keybindings for a specific scope * @param scope scope to reset the keybindings for */ resetKeybindingsForScope(scope) { const toReset = this.toResetKeymap.get(scope); if (toReset) { toReset.dispose(); } } /** * Reset keybindings for all scopes(only leaves the default keybindings mapped) */ resetKeybindings() { for (let i = KeybindingScope.DEFAULT + 1; i < KeybindingScope.END; i++) { this.keymaps[i] = []; } } /** * Get all {@link common.Keybinding}s for a {@link KeybindingScope}. * @returns an array of {@link common.ScopedKeybinding} * @param scope the keybinding scope to retrieve the {@link common.Keybinding}s for. */ getKeybindingsByScope(scope) { return this.keymaps[scope]; } }; KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND = 'passthrough'; __decorate([ (0, inversify_1.inject)(core_preferences_1.CorePreferences), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "corePreferences", void 0); __decorate([ (0, inversify_1.inject)(keyboard_layout_service_1.KeyboardLayoutService), __metadata("design:type", keyboard_layout_service_1.KeyboardLayoutService) ], KeybindingRegistry.prototype, "keyboardLayoutService", void 0); __decorate([ (0, inversify_1.inject)(contribution_provider_1.ContributionProvider), (0, inversify_1.named)(exports.KeybindingContext), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "contextProvider", void 0); __decorate([ (0, inversify_1.inject)(command_1.CommandRegistry), __metadata("design:type", command_1.CommandRegistry) ], KeybindingRegistry.prototype, "commandRegistry", void 0); __decorate([ (0, inversify_1.inject)(contribution_provider_1.ContributionProvider), (0, inversify_1.named)(exports.KeybindingContribution), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "contributions", void 0); __decorate([ (0, inversify_1.inject)(status_bar_1.StatusBar), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "statusBar", void 0); __decorate([ (0, inversify_1.inject)(logger_1.ILogger), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "logger", void 0); __decorate([ (0, inversify_1.inject)(context_key_service_1.ContextKeyService), __metadata("design:type", Object) ], KeybindingRegistry.prototype, "whenContextService", void 0); KeybindingRegistry = KeybindingRegistry_1 = __decorate([ (0, inversify_1.injectable)() ], KeybindingRegistry); exports.KeybindingRegistry = KeybindingRegistry; (function (KeybindingRegistry) { class KeybindingsResult { constructor() { this.full = []; this.partial = []; this.shadow = []; } /** * Merge two results together inside `this` * * @param other the other KeybindingsResult to merge with * @return this */ merge(other) { this.full.push(...other.full); this.partial.push(...other.partial); this.shadow.push(...other.shadow); return this; } /** * Returns a new filtered KeybindingsResult * * @param fn callback filter on the results * @return filtered new result */ filter(fn) { const result = new KeybindingsResult(); result.full = this.full.filter(fn); result.partial = this.partial.filter(fn); result.shadow = this.shadow.filter(fn); return result; } } KeybindingRegistry.KeybindingsResult = KeybindingsResult; })(KeybindingRegistry = exports.KeybindingRegistry || (exports.KeybindingRegistry = {})); exports.KeybindingRegistry = KeybindingRegistry; //# sourceMappingURL=keybinding.js.map