@21epub/epub-thirdparty
Version:
epub-thirdparty
250 lines (249 loc) • 9.33 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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`));
}