123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { implies, expressionsAreEqualWithConstantSubstitution } from '../../contextkey/common/contextkey.js';
- export class KeybindingResolver {
- constructor(defaultKeybindings, overrides, log) {
- this._log = log;
- this._defaultKeybindings = defaultKeybindings;
- this._defaultBoundCommands = new Map();
- for (let i = 0, len = defaultKeybindings.length; i < len; i++) {
- const command = defaultKeybindings[i].command;
- if (command) {
- this._defaultBoundCommands.set(command, true);
- }
- }
- this._map = new Map();
- this._lookupMap = new Map();
- this._keybindings = KeybindingResolver.combine(defaultKeybindings, overrides);
- for (let i = 0, len = this._keybindings.length; i < len; i++) {
- let k = this._keybindings[i];
- if (k.keypressParts.length === 0) {
- // unbound
- continue;
- }
- if (k.when && k.when.type === 0 /* False */) {
- // when condition is false
- continue;
- }
- // TODO@chords
- this._addKeyPress(k.keypressParts[0], k);
- }
- }
- static _isTargetedForRemoval(defaultKb, keypressFirstPart, keypressChordPart, command, when) {
- if (defaultKb.command !== command) {
- return false;
- }
- // TODO@chords
- if (keypressFirstPart && defaultKb.keypressParts[0] !== keypressFirstPart) {
- return false;
- }
- // TODO@chords
- if (keypressChordPart && defaultKb.keypressParts[1] !== keypressChordPart) {
- return false;
- }
- if (when) {
- if (!defaultKb.when) {
- return false;
- }
- if (!expressionsAreEqualWithConstantSubstitution(when, defaultKb.when)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Looks for rules containing -command in `overrides` and removes them directly from `defaults`.
- */
- static combine(defaults, rawOverrides) {
- defaults = defaults.slice(0);
- let overrides = [];
- for (const override of rawOverrides) {
- if (!override.command || override.command.length === 0 || override.command.charAt(0) !== '-') {
- overrides.push(override);
- continue;
- }
- const command = override.command.substr(1);
- // TODO@chords
- const keypressFirstPart = override.keypressParts[0];
- const keypressChordPart = override.keypressParts[1];
- const when = override.when;
- for (let j = defaults.length - 1; j >= 0; j--) {
- if (this._isTargetedForRemoval(defaults[j], keypressFirstPart, keypressChordPart, command, when)) {
- defaults.splice(j, 1);
- }
- }
- }
- return defaults.concat(overrides);
- }
- _addKeyPress(keypress, item) {
- const conflicts = this._map.get(keypress);
- if (typeof conflicts === 'undefined') {
- // There is no conflict so far
- this._map.set(keypress, [item]);
- this._addToLookupMap(item);
- return;
- }
- for (let i = conflicts.length - 1; i >= 0; i--) {
- let conflict = conflicts[i];
- if (conflict.command === item.command) {
- continue;
- }
- const conflictIsChord = (conflict.keypressParts.length > 1);
- const itemIsChord = (item.keypressParts.length > 1);
- // TODO@chords
- if (conflictIsChord && itemIsChord && conflict.keypressParts[1] !== item.keypressParts[1]) {
- // The conflict only shares the chord start with this command
- continue;
- }
- if (KeybindingResolver.whenIsEntirelyIncluded(conflict.when, item.when)) {
- // `item` completely overwrites `conflict`
- // Remove conflict from the lookupMap
- this._removeFromLookupMap(conflict);
- }
- }
- conflicts.push(item);
- this._addToLookupMap(item);
- }
- _addToLookupMap(item) {
- if (!item.command) {
- return;
- }
- let arr = this._lookupMap.get(item.command);
- if (typeof arr === 'undefined') {
- arr = [item];
- this._lookupMap.set(item.command, arr);
- }
- else {
- arr.push(item);
- }
- }
- _removeFromLookupMap(item) {
- if (!item.command) {
- return;
- }
- let arr = this._lookupMap.get(item.command);
- if (typeof arr === 'undefined') {
- return;
- }
- for (let i = 0, len = arr.length; i < len; i++) {
- if (arr[i] === item) {
- arr.splice(i, 1);
- return;
- }
- }
- }
- /**
- * Returns true if it is provable `a` implies `b`.
- */
- static whenIsEntirelyIncluded(a, b) {
- if (!b || b.type === 1 /* True */) {
- return true;
- }
- if (!a || a.type === 1 /* True */) {
- return false;
- }
- return implies(a, b);
- }
- getKeybindings() {
- return this._keybindings;
- }
- lookupPrimaryKeybinding(commandId, context) {
- const items = this._lookupMap.get(commandId);
- if (typeof items === 'undefined' || items.length === 0) {
- return null;
- }
- if (items.length === 1) {
- return items[0];
- }
- for (let i = items.length - 1; i >= 0; i--) {
- const item = items[i];
- if (context.contextMatchesRules(item.when)) {
- return item;
- }
- }
- return items[items.length - 1];
- }
- resolve(context, currentChord, keypress) {
- this._log(`| Resolving ${keypress}${currentChord ? ` chorded from ${currentChord}` : ``}`);
- let lookupMap = null;
- if (currentChord !== null) {
- // Fetch all chord bindings for `currentChord`
- const candidates = this._map.get(currentChord);
- if (typeof candidates === 'undefined') {
- // No chords starting with `currentChord`
- this._log(`\\ No keybinding entries.`);
- return null;
- }
- lookupMap = [];
- for (let i = 0, len = candidates.length; i < len; i++) {
- let candidate = candidates[i];
- // TODO@chords
- if (candidate.keypressParts[1] === keypress) {
- lookupMap.push(candidate);
- }
- }
- }
- else {
- const candidates = this._map.get(keypress);
- if (typeof candidates === 'undefined') {
- // No bindings with `keypress`
- this._log(`\\ No keybinding entries.`);
- return null;
- }
- lookupMap = candidates;
- }
- let result = this._findCommand(context, lookupMap);
- if (!result) {
- this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`);
- return null;
- }
- // TODO@chords
- if (currentChord === null && result.keypressParts.length > 1 && result.keypressParts[1] !== null) {
- this._log(`\\ From ${lookupMap.length} keybinding entries, matched chord, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
- return {
- enterChord: true,
- leaveChord: false,
- commandId: null,
- commandArgs: null,
- bubble: false
- };
- }
- this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
- return {
- enterChord: false,
- leaveChord: result.keypressParts.length > 1,
- commandId: result.command,
- commandArgs: result.commandArgs,
- bubble: result.bubble
- };
- }
- _findCommand(context, matches) {
- for (let i = matches.length - 1; i >= 0; i--) {
- let k = matches[i];
- if (!KeybindingResolver.contextMatchesRules(context, k.when)) {
- continue;
- }
- return k;
- }
- return null;
- }
- static contextMatchesRules(context, rules) {
- if (!rules) {
- return true;
- }
- return rules.evaluate(context);
- }
- }
- function printWhenExplanation(when) {
- if (!when) {
- return `no when condition`;
- }
- return `${when.serialize()}`;
- }
- function printSourceExplanation(kb) {
- return (kb.extensionId
- ? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)
- : (kb.isDefault ? `built-in` : `user`));
- }
|