UNPKG

29.6 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { injectable, inject, named } from 'inversify';
18import { isOSX } from '../common/os';
19import { Emitter, Event } from '../common/event';
20import { CommandRegistry, Command } from '../common/command';
21import { Disposable, DisposableCollection } from '../common/disposable';
22import { KeyCode, KeySequence, Key } from './keyboard/keys';
23import { KeyboardLayoutService } from './keyboard/keyboard-layout-service';
24import { ContributionProvider } from '../common/contribution-provider';
25import { ILogger } from '../common/logger';
26import { StatusBarAlignment, StatusBar } from './status-bar/status-bar';
27import { ContextKeyService } from './context-key-service';
28import { CorePreferences } from './core-preferences';
29import * as common from '../common/keybinding';
30import { nls } from '../common/nls';
31
32export enum KeybindingScope {
33 DEFAULT,
34 USER,
35 WORKSPACE,
36 END
37}
38export namespace KeybindingScope {
39 export const length = KeybindingScope.END - KeybindingScope.DEFAULT;
40}
41
42/**
43 * @deprecated import from `@theia/core/lib/common/keybinding` instead
44 */
45export type Keybinding = common.Keybinding;
46export const Keybinding = common.Keybinding;
47
48export interface ResolvedKeybinding extends common.Keybinding {
49 /**
50 * The KeyboardLayoutService may transform the `keybinding` depending on the
51 * user's keyboard layout. This property holds the transformed keybinding that
52 * should be used in the UI. The value is undefined if the KeyboardLayoutService
53 * has not been called yet to resolve the keybinding.
54 */
55 resolved?: KeyCode[];
56}
57
58export interface ScopedKeybinding extends common.Keybinding {
59 /** Current keybinding scope */
60 scope: KeybindingScope;
61}
62
63export const KeybindingContribution = Symbol('KeybindingContribution');
64/**
65 * Allows extensions to contribute {@link common.Keybinding}s
66 */
67export interface KeybindingContribution {
68 /**
69 * Registers keybindings.
70 * @param keybindings the keybinding registry.
71 */
72 registerKeybindings(keybindings: KeybindingRegistry): void;
73}
74
75export const KeybindingContext = Symbol('KeybindingContext');
76export interface KeybindingContext {
77 /**
78 * The unique ID of the current context.
79 */
80 readonly id: string;
81
82 isEnabled(arg: common.Keybinding): boolean;
83}
84export namespace KeybindingContexts {
85
86 export const NOOP_CONTEXT: KeybindingContext = {
87 id: 'noop.keybinding.context',
88 isEnabled: () => true
89 };
90
91 export const DEFAULT_CONTEXT: KeybindingContext = {
92 id: 'default.keybinding.context',
93 isEnabled: () => false
94 };
95}
96
97@injectable()
98export class KeybindingRegistry {
99
100 static readonly PASSTHROUGH_PSEUDO_COMMAND = 'passthrough';
101 protected keySequence: KeySequence = [];
102
103 protected readonly contexts: { [id: string]: KeybindingContext } = {};
104 protected readonly keymaps: ScopedKeybinding[][] = [...Array(KeybindingScope.length)].map(() => []);
105
106 @inject(CorePreferences)
107 protected readonly corePreferences: CorePreferences;
108
109 @inject(KeyboardLayoutService)
110 protected readonly keyboardLayoutService: KeyboardLayoutService;
111
112 @inject(ContributionProvider) @named(KeybindingContext)
113 protected readonly contextProvider: ContributionProvider<KeybindingContext>;
114
115 @inject(CommandRegistry)
116 protected readonly commandRegistry: CommandRegistry;
117
118 @inject(ContributionProvider) @named(KeybindingContribution)
119 protected readonly contributions: ContributionProvider<KeybindingContribution>;
120
121 @inject(StatusBar)
122 protected readonly statusBar: StatusBar;
123
124 @inject(ILogger)
125 protected readonly logger: ILogger;
126
127 @inject(ContextKeyService)
128 protected readonly whenContextService: ContextKeyService;
129
130 async onStart(): Promise<void> {
131 await this.keyboardLayoutService.initialize();
132 this.keyboardLayoutService.onKeyboardLayoutChanged(newLayout => {
133 this.clearResolvedKeybindings();
134 this.keybindingsChanged.fire(undefined);
135 });
136 this.registerContext(KeybindingContexts.NOOP_CONTEXT);
137 this.registerContext(KeybindingContexts.DEFAULT_CONTEXT);
138 this.registerContext(...this.contextProvider.getContributions());
139 for (const contribution of this.contributions.getContributions()) {
140 contribution.registerKeybindings(this);
141 }
142 }
143
144 protected keybindingsChanged = new Emitter<void>();
145
146 /**
147 * Event that is fired when the resolved keybindings change due to a different keyboard layout
148 * or when a new keymap is being set
149 */
150 get onKeybindingsChanged(): Event<void> {
151 return this.keybindingsChanged.event;
152 }
153
154 /**
155 * Registers the keybinding context arguments into the application. Fails when an already registered
156 * context is being registered.
157 *
158 * @param contexts the keybinding contexts to register into the application.
159 */
160 protected registerContext(...contexts: KeybindingContext[]): void {
161 for (const context of contexts) {
162 const { id } = context;
163 if (this.contexts[id]) {
164 this.logger.error(`A keybinding context with ID ${id} is already registered.`);
165 } else {
166 this.contexts[id] = context;
167 }
168 }
169 }
170
171 /**
172 * Register a default keybinding to the registry.
173 *
174 * Keybindings registered later have higher priority during evaluation.
175 *
176 * @param binding the keybinding to be registered
177 */
178 registerKeybinding(binding: common.Keybinding): Disposable {
179 return this.doRegisterKeybinding(binding);
180 }
181
182 /**
183 * Register multiple default keybindings to the registry
184 *
185 * @param bindings An array of keybinding to be registered
186 */
187 registerKeybindings(...bindings: common.Keybinding[]): Disposable {
188 return this.doRegisterKeybindings(bindings, KeybindingScope.DEFAULT);
189 }
190
191 /**
192 * Unregister all keybindings from the registry that are bound to the key of the given keybinding
193 *
194 * @param binding a keybinding specifying the key to be unregistered
195 */
196 unregisterKeybinding(binding: common.Keybinding): void;
197 /**
198 * Unregister all keybindings with the given key from the registry
199 *
200 * @param key a key to be unregistered
201 */
202 unregisterKeybinding(key: string): void;
203 /**
204 * Unregister all existing keybindings for the given command
205 * @param command the command to unregister all keybindings for
206 */
207 unregisterKeybinding(command: Command): void;
208
209 unregisterKeybinding(arg: common.Keybinding | string | Command): void {
210 const keymap = this.keymaps[KeybindingScope.DEFAULT];
211 const filter = Command.is(arg)
212 ? ({ command }: common.Keybinding) => command === arg.id
213 : ({ keybinding }: common.Keybinding) => Keybinding.is(arg)
214 ? keybinding === arg.keybinding
215 : keybinding === arg;
216 for (const binding of keymap.filter(filter)) {
217 const idx = keymap.indexOf(binding);
218 if (idx !== -1) {
219 keymap.splice(idx, 1);
220 }
221 }
222 }
223
224 protected doRegisterKeybindings(bindings: common.Keybinding[], scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable {
225 const toDispose = new DisposableCollection();
226 for (const binding of bindings) {
227 toDispose.push(this.doRegisterKeybinding(binding, scope));
228 }
229 return toDispose;
230 }
231
232 protected doRegisterKeybinding(binding: common.Keybinding, scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable {
233 try {
234 this.resolveKeybinding(binding);
235 const scoped = Object.assign(binding, { scope });
236 this.insertBindingIntoScope(scoped, scope);
237 return Disposable.create(() => {
238 const index = this.keymaps[scope].indexOf(scoped);
239 if (index !== -1) {
240 this.keymaps[scope].splice(index, 1);
241 }
242 });
243 } catch (error) {
244 this.logger.warn(`Could not register keybinding:\n ${common.Keybinding.stringify(binding)}\n${error}`);
245 return Disposable.NULL;
246 }
247 }
248
249 /**
250 * Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a
251 * user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter)
252 */
253 protected insertBindingIntoScope(item: common.Keybinding & { scope: KeybindingScope; }, scope: KeybindingScope): void {
254 const scopedKeymap = this.keymaps[scope];
255 const getNumberOfKeystrokes = (binding: common.Keybinding): number => (binding.keybinding.trim().match(/\s/g)?.length ?? 0) + 1;
256 const numberOfKeystrokesInBinding = getNumberOfKeystrokes(item);
257 const indexOfFirstItemWithEqualStrokes = scopedKeymap.findIndex(existingBinding => getNumberOfKeystrokes(existingBinding) === numberOfKeystrokesInBinding);
258 if (indexOfFirstItemWithEqualStrokes > -1) {
259 scopedKeymap.splice(indexOfFirstItemWithEqualStrokes, 0, item);
260 } else {
261 scopedKeymap.push(item);
262 }
263 }
264
265 /**
266 * Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService.
267 */
268 resolveKeybinding(binding: ResolvedKeybinding): KeyCode[] {
269 if (!binding.resolved) {
270 const sequence = KeySequence.parse(binding.keybinding);
271 binding.resolved = sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code));
272 }
273 return binding.resolved;
274 }
275
276 /**
277 * Clear all `resolved` properties of registered keybindings so the KeyboardLayoutService is called
278 * again to resolve them. This is necessary when the user's keyboard layout has changed.
279 */
280 protected clearResolvedKeybindings(): void {
281 for (let i = KeybindingScope.DEFAULT; i < KeybindingScope.END; i++) {
282 const bindings = this.keymaps[i];
283 for (let j = 0; j < bindings.length; j++) {
284 const binding = bindings[j] as ResolvedKeybinding;
285 binding.resolved = undefined;
286 }
287 }
288 }
289
290 /**
291 * Checks whether a colliding {@link common.Keybinding} exists in a specific scope.
292 * @param binding the keybinding to check
293 * @param scope the keybinding scope to check
294 * @returns true if there is a colliding keybinding
295 */
296 containsKeybindingInScope(binding: common.Keybinding, scope = KeybindingScope.USER): boolean {
297 const bindingKeySequence = this.resolveKeybinding(binding);
298 const collisions = this.getKeySequenceCollisions(this.getUsableBindings(this.keymaps[scope]), bindingKeySequence)
299 .filter(b => b.context === binding.context && !b.when && !binding.when);
300 if (collisions.full.length > 0) {
301 return true;
302 }
303 if (collisions.partial.length > 0) {
304 return true;
305 }
306 if (collisions.shadow.length > 0) {
307 return true;
308 }
309 return false;
310 }
311
312 /**
313 * Get a user visible representation of a {@link common.Keybinding}.
314 * @returns an array of strings representing all elements of the {@link KeySequence} defined by the {@link common.Keybinding}
315 * @param keybinding the keybinding
316 * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence}
317 */
318 acceleratorFor(keybinding: common.Keybinding, separator: string = ' ', asciiOnly = false): string[] {
319 const bindingKeySequence = this.resolveKeybinding(keybinding);
320 return this.acceleratorForSequence(bindingKeySequence, separator, asciiOnly);
321 }
322
323 /**
324 * Get a user visible representation of a {@link KeySequence}.
325 * @returns an array of strings representing all elements of the {@link KeySequence}
326 * @param keySequence the keysequence
327 * @param separator the separator to be used to stringify {@link KeyCode}s that are part of the {@link KeySequence}
328 */
329 acceleratorForSequence(keySequence: KeySequence, separator: string = ' ', asciiOnly = false): string[] {
330 return keySequence.map(keyCode => this.acceleratorForKeyCode(keyCode, separator, asciiOnly));
331 }
332
333 /**
334 * Get a user visible representation of a key code (a key with modifiers).
335 * @returns a string representing the {@link KeyCode}
336 * @param keyCode the keycode
337 * @param separator the separator used to separate keys (key and modifiers) in the returning string
338 * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus.
339 */
340 acceleratorForKeyCode(keyCode: KeyCode, separator: string = ' ', asciiOnly = false): string {
341 return this.componentsForKeyCode(keyCode, asciiOnly).join(separator);
342 }
343
344 componentsForKeyCode(keyCode: KeyCode, asciiOnly = false): string[] {
345 const keyCodeResult = [];
346 const useSymbols = isOSX && !asciiOnly;
347 if (keyCode.meta && isOSX) {
348 keyCodeResult.push(useSymbols ? '⌘' : 'Cmd');
349 }
350 if (keyCode.ctrl) {
351 keyCodeResult.push(useSymbols ? '⌃' : 'Ctrl');
352 }
353 if (keyCode.alt) {
354 keyCodeResult.push(useSymbols ? '⌥' : 'Alt');
355 }
356 if (keyCode.shift) {
357 keyCodeResult.push(useSymbols ? '⇧' : 'Shift');
358 }
359 if (keyCode.key) {
360 keyCodeResult.push(this.acceleratorForKey(keyCode.key, asciiOnly));
361 }
362 return keyCodeResult;
363 }
364
365 /**
366 * @param asciiOnly if `true`, no special characters will be substituted into the string returned. Ensures correct keyboard shortcuts in Electron menus.
367 *
368 * Return a user visible representation of a single key.
369 */
370 acceleratorForKey(key: Key, asciiOnly = false): string {
371 if (isOSX && !asciiOnly) {
372 if (key === Key.ARROW_LEFT) {
373 return '←';
374 }
375 if (key === Key.ARROW_RIGHT) {
376 return '→';
377 }
378 if (key === Key.ARROW_UP) {
379 return '↑';
380 }
381 if (key === Key.ARROW_DOWN) {
382 return '↓';
383 }
384 }
385 const keyString = this.keyboardLayoutService.getKeyboardCharacter(key);
386 if (key.keyCode >= Key.KEY_A.keyCode && key.keyCode <= Key.KEY_Z.keyCode ||
387 key.keyCode >= Key.F1.keyCode && key.keyCode <= Key.F24.keyCode) {
388 return keyString.toUpperCase();
389 } else if (keyString.length > 1) {
390 return keyString.charAt(0).toUpperCase() + keyString.slice(1);
391 } else {
392 return keyString;
393 }
394 }
395
396 /**
397 * Finds collisions for a key sequence inside a list of bindings (error-free)
398 *
399 * @param bindings the reference bindings
400 * @param candidate the sequence to match
401 */
402 protected getKeySequenceCollisions(bindings: ScopedKeybinding[], candidate: KeySequence): KeybindingRegistry.KeybindingsResult {
403 const result = new KeybindingRegistry.KeybindingsResult();
404 for (const binding of bindings) {
405 try {
406 const bindingKeySequence = this.resolveKeybinding(binding);
407 const compareResult = KeySequence.compare(candidate, bindingKeySequence);
408 switch (compareResult) {
409 case KeySequence.CompareResult.FULL: {
410 result.full.push(binding);
411 break;
412 }
413 case KeySequence.CompareResult.PARTIAL: {
414 result.partial.push(binding);
415 break;
416 }
417 case KeySequence.CompareResult.SHADOW: {
418 result.shadow.push(binding);
419 break;
420 }
421 }
422 } catch (error) {
423 this.logger.warn(error);
424 }
425 }
426 return result;
427 }
428
429 /**
430 * Get all keybindings associated to a commandId.
431 *
432 * @param commandId The ID of the command for which we are looking for keybindings.
433 * @returns an array of {@link ScopedKeybinding}
434 */
435 getKeybindingsForCommand(commandId: string): ScopedKeybinding[] {
436 const result: ScopedKeybinding[] = [];
437 const disabledBindings: ScopedKeybinding[] = [];
438 for (let scope = KeybindingScope.END - 1; scope >= KeybindingScope.DEFAULT; scope--) {
439 this.keymaps[scope].forEach(binding => {
440 if (binding.command?.startsWith('-')) {
441 disabledBindings.push(binding);
442 }
443 const command = this.commandRegistry.getCommand(binding.command);
444 if (command
445 && command.id === commandId
446 && !disabledBindings.some(disabled => common.Keybinding.equals(disabled, { ...binding, command: '-' + binding.command }, false, true))) {
447 result.push({ ...binding, scope });
448 }
449 });
450 }
451 return result;
452 }
453
454 protected isActive(binding: common.Keybinding): boolean {
455 /* Pseudo commands like "passthrough" are always active (and not found
456 in the command registry). */
457 if (this.isPseudoCommand(binding.command)) {
458 return true;
459 }
460
461 const command = this.commandRegistry.getCommand(binding.command);
462 return !!command && !!this.commandRegistry.getActiveHandler(command.id);
463 }
464
465 /**
466 * Tries to execute a keybinding.
467 *
468 * @param binding to execute
469 * @param event keyboard event.
470 */
471 protected executeKeyBinding(binding: common.Keybinding, event: KeyboardEvent): void {
472 if (this.isPseudoCommand(binding.command)) {
473 /* Don't do anything, let the event propagate. */
474 } else {
475 const command = this.commandRegistry.getCommand(binding.command);
476 if (command) {
477 if (this.commandRegistry.isEnabled(binding.command, binding.args)) {
478 this.commandRegistry.executeCommand(binding.command, binding.args)
479 .catch(e => console.error('Failed to execute command:', e));
480 }
481
482 /* Note that if a keybinding is in context but the command is
483 not active we still stop the processing here. */
484 event.preventDefault();
485 event.stopPropagation();
486 }
487 }
488 }
489
490 /**
491 * Only execute if it has no context (global context) or if we're in that context.
492 */
493 protected isEnabled(binding: common.Keybinding, event: KeyboardEvent): boolean {
494 const context = binding.context && this.contexts[binding.context];
495 if (context && !context.isEnabled(binding)) {
496 return false;
497 }
498 if (binding.when && !this.whenContextService.match(binding.when, <HTMLElement>event.target)) {
499 return false;
500 }
501 return true;
502 }
503
504 dispatchCommand(id: string, target?: EventTarget): void {
505 const keybindings = this.getKeybindingsForCommand(id);
506 if (keybindings.length) {
507 for (const keyCode of this.resolveKeybinding(keybindings[0])) {
508 this.dispatchKeyDown(keyCode, target);
509 }
510 }
511 }
512
513 dispatchKeyDown(input: KeyboardEventInit | KeyCode | string, target: EventTarget = document.activeElement || window): void {
514 const eventInit = this.asKeyboardEventInit(input);
515 const emulatedKeyboardEvent = new KeyboardEvent('keydown', eventInit);
516 target.dispatchEvent(emulatedKeyboardEvent);
517 }
518 protected asKeyboardEventInit(input: KeyboardEventInit | KeyCode | string): KeyboardEventInit & Partial<{ keyCode: number }> {
519 if (typeof input === 'string') {
520 return this.asKeyboardEventInit(KeyCode.createKeyCode(input));
521 }
522 if (input instanceof KeyCode) {
523 return {
524 metaKey: input.meta,
525 shiftKey: input.shift,
526 altKey: input.alt,
527 ctrlKey: input.ctrl,
528 code: input.key && input.key.code,
529 key: (input && input.character) || (input.key && input.key.code),
530 keyCode: input.key && input.key.keyCode
531 };
532 }
533 return input;
534 }
535
536 registerEventListeners(win: Window): Disposable {
537 /* vvv HOTFIX begin vvv
538 *
539 * This is a hotfix against issues eclipse/theia#6459 and gitpod-io/gitpod#875 .
540 * It should be reverted after Theia was updated to the newer Monaco.
541 */
542 let inComposition = false;
543 const compositionStart = () => {
544 inComposition = true;
545 };
546 win.document.addEventListener('compositionstart', compositionStart);
547
548 const compositionEnd = () => {
549 inComposition = false;
550 };
551 win.document.addEventListener('compositionend', compositionEnd);
552
553 const keydown = (event: KeyboardEvent) => {
554 if (inComposition !== true) {
555 this.run(event);
556 }
557 };
558 win.document.addEventListener('keydown', keydown, true);
559
560 return Disposable.create(() => {
561 win.document.removeEventListener('compositionstart', compositionStart);
562 win.document.removeEventListener('compositionend', compositionEnd);
563 win.document.removeEventListener('keydown', keydown);
564 });
565 }
566 /**
567 * Run the command matching to the given keyboard event.
568 */
569 run(event: KeyboardEvent): void {
570 if (event.defaultPrevented) {
571 return;
572 }
573
574 const eventDispatch = this.corePreferences['keyboard.dispatch'];
575 const keyCode = KeyCode.createKeyCode(event, eventDispatch);
576 /* Keycode is only a modifier, next keycode will be modifier + key.
577 Ignore this one. */
578 if (keyCode.isModifierOnly()) {
579 return;
580 }
581
582 this.keyboardLayoutService.validateKeyCode(keyCode);
583 this.keySequence.push(keyCode);
584 const match = this.matchKeybinding(this.keySequence, event);
585
586 if (match && match.kind === 'partial') {
587 /* Accumulate the keysequence */
588 event.preventDefault();
589 event.stopPropagation();
590
591 this.statusBar.setElement('keybinding-status', {
592 text: nls.localize('theia/core/keybindingStatus', '{0} was pressed, waiting for more keys', `(${this.acceleratorForSequence(this.keySequence, '+')})`),
593 alignment: StatusBarAlignment.LEFT,
594 priority: 2
595 });
596 } else {
597 if (match && match.kind === 'full') {
598 this.executeKeyBinding(match.binding, event);
599 }
600 this.keySequence = [];
601 this.statusBar.removeElement('keybinding-status');
602 }
603 }
604
605 /**
606 * Match first binding in the current context.
607 * Keybindings ordered by a scope and by a registration order within the scope.
608 *
609 * FIXME:
610 * This method should run very fast since it happens on each keystroke. We should reconsider how keybindings are stored.
611 * It should be possible to look up full and partial keybinding for given key sequence for constant time using some kind of tree.
612 * Such tree should not contain disabled keybindings and be invalidated whenever the registry is changed.
613 */
614 matchKeybinding(keySequence: KeySequence, event?: KeyboardEvent): KeybindingRegistry.Match {
615 let disabled: Set<string> | undefined;
616 const isEnabled = (binding: ScopedKeybinding) => {
617 if (event && !this.isEnabled(binding, event)) {
618 return false;
619 }
620 const { command, context, when, keybinding } = binding;
621 if (!this.isUsable(binding)) {
622 disabled = disabled || new Set<string>();
623 disabled.add(JSON.stringify({ command: command.substr(1), context, when, keybinding }));
624 return false;
625 }
626 return !disabled?.has(JSON.stringify({ command, context, when, keybinding }));
627 };
628
629 for (let scope = KeybindingScope.END; --scope >= KeybindingScope.DEFAULT;) {
630 for (const binding of this.keymaps[scope]) {
631 const resolved = this.resolveKeybinding(binding);
632 const compareResult = KeySequence.compare(keySequence, resolved);
633 if (compareResult === KeySequence.CompareResult.FULL && isEnabled(binding)) {
634 return { kind: 'full', binding };
635 }
636 if (compareResult === KeySequence.CompareResult.PARTIAL && isEnabled(binding)) {
637 return { kind: 'partial', binding };
638 }
639 }
640 }
641 return undefined;
642 }
643
644 /**
645 * Returns true if the binding is usable
646 * @param binding Binding to be checked
647 */
648 protected isUsable(binding: common.Keybinding): boolean {
649 return binding.command.charAt(0) !== '-';
650 }
651
652 /**
653 * Return a new filtered array containing only the usable bindings among the input bindings
654 * @param bindings Bindings to filter
655 */
656 protected getUsableBindings<T extends common.Keybinding>(bindings: T[]): T[] {
657 return bindings.filter(binding => this.isUsable(binding));
658 }
659
660 /**
661 * Return true of string a pseudo-command id, in other words a command id
662 * that has a special meaning and that we won't find in the command
663 * registry.
664 *
665 * @param commandId commandId to test
666 */
667 isPseudoCommand(commandId: string): boolean {
668 return commandId === KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND;
669 }
670
671 /**
672 * Sets a new keymap replacing all existing {@link common.Keybinding}s in the given scope.
673 * @param scope the keybinding scope
674 * @param bindings an array containing the new {@link common.Keybinding}s
675 */
676 setKeymap(scope: KeybindingScope, bindings: common.Keybinding[]): void {
677 this.resetKeybindingsForScope(scope);
678 this.toResetKeymap.set(scope, this.doRegisterKeybindings(bindings, scope));
679 this.keybindingsChanged.fire(undefined);
680 }
681
682 protected readonly toResetKeymap = new Map<KeybindingScope, Disposable>();
683
684 /**
685 * Reset keybindings for a specific scope
686 * @param scope scope to reset the keybindings for
687 */
688 resetKeybindingsForScope(scope: KeybindingScope): void {
689 const toReset = this.toResetKeymap.get(scope);
690 if (toReset) {
691 toReset.dispose();
692 }
693 }
694
695 /**
696 * Reset keybindings for all scopes(only leaves the default keybindings mapped)
697 */
698 resetKeybindings(): void {
699 for (let i = KeybindingScope.DEFAULT + 1; i < KeybindingScope.END; i++) {
700 this.keymaps[i] = [];
701 }
702 }
703
704 /**
705 * Get all {@link common.Keybinding}s for a {@link KeybindingScope}.
706 * @returns an array of {@link common.ScopedKeybinding}
707 * @param scope the keybinding scope to retrieve the {@link common.Keybinding}s for.
708 */
709 getKeybindingsByScope(scope: KeybindingScope): ScopedKeybinding[] {
710 return this.keymaps[scope];
711 }
712}
713
714export namespace KeybindingRegistry {
715 export type Match = {
716 kind: 'full' | 'partial'
717 binding: ScopedKeybinding
718 } | undefined;
719 export class KeybindingsResult {
720 full: ScopedKeybinding[] = [];
721 partial: ScopedKeybinding[] = [];
722 shadow: ScopedKeybinding[] = [];
723
724 /**
725 * Merge two results together inside `this`
726 *
727 * @param other the other KeybindingsResult to merge with
728 * @return this
729 */
730 merge(other: KeybindingsResult): KeybindingsResult {
731 this.full.push(...other.full);
732 this.partial.push(...other.partial);
733 this.shadow.push(...other.shadow);
734 return this;
735 }
736
737 /**
738 * Returns a new filtered KeybindingsResult
739 *
740 * @param fn callback filter on the results
741 * @return filtered new result
742 */
743 filter(fn: (binding: common.Keybinding) => boolean): KeybindingsResult {
744 const result = new KeybindingsResult();
745 result.full = this.full.filter(fn);
746 result.partial = this.partial.filter(fn);
747 result.shadow = this.shadow.filter(fn);
748 return result;
749 }
750 }
751}