Path: blob/main/extensions/copilot/test/simulation/fixtures/doc-ts-class-full/keybindingResolver.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ContextKeyExprType, ContextKeyExpression, IContext, IContextKeyService, expressionsAreEqualWithConstantSubstitution, implies } from 'vs/platform/contextkey/common/contextkey';6import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';78//#region resolution-result910export const enum ResultKind {11/** No keybinding found this sequence of chords */12NoMatchingKb,1314/** There're several keybindings that have the given sequence of chords as a prefix */15MoreChordsNeeded,1617/** A single keybinding found to be dispatched/invoked */18KbFound19}2021export type ResolutionResult =22| { kind: ResultKind.NoMatchingKb }23| { kind: ResultKind.MoreChordsNeeded }24| { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean };252627// util definitions to make working with the above types easier within this module:2829export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb };30const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded };31function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult {32return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };33}3435//#endregion3637export class KeybindingResolver {38private readonly _log: (str: string) => void;39private readonly _defaultKeybindings: ResolvedKeybindingItem[];40private readonly _keybindings: ResolvedKeybindingItem[];41private readonly _defaultBoundCommands: Map</* commandId */ string, boolean>;42private readonly _map: Map</* 1st chord's keypress */ string, ResolvedKeybindingItem[]>;43private readonly _lookupMap: Map</* commandId */ string, ResolvedKeybindingItem[]>;4445constructor(46/** built-in and extension-provided keybindings */47defaultKeybindings: ResolvedKeybindingItem[],48/** user's keybindings */49overrides: ResolvedKeybindingItem[],50log: (str: string) => void51) {52this._log = log;53this._defaultKeybindings = defaultKeybindings;5455this._defaultBoundCommands = new Map<string, boolean>();56for (const defaultKeybinding of defaultKeybindings) {57const command = defaultKeybinding.command;58if (command && command.charAt(0) !== '-') {59this._defaultBoundCommands.set(command, true);60}61}6263this._map = new Map<string, ResolvedKeybindingItem[]>();64this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();6566this._keybindings = KeybindingResolver.handleRemovals(([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides));67for (let i = 0, len = this._keybindings.length; i < len; i++) {68const k = this._keybindings[i];69if (k.chords.length === 0) {70// unbound71continue;72}7374// substitute with constants that are registered after startup - https://github.com/microsoft/vscode/issues/174218#issuecomment-143797212775const when = k.when?.substituteConstants();7677if (when && when.type === ContextKeyExprType.False) {78// when condition is false79continue;80}8182this._addKeyPress(k.chords[0], k);83}84}8586private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null, when: ContextKeyExpression | undefined): boolean {87if (keypress) {88for (let i = 0; i < keypress.length; i++) {89if (keypress[i] !== defaultKb.chords[i]) {90return false;91}92}93}9495// `true` means always, as does `undefined`96// so we will treat `true` === `undefined`97if (when && when.type !== ContextKeyExprType.True) {98if (!defaultKb.when) {99return false;100}101if (!expressionsAreEqualWithConstantSubstitution(when, defaultKb.when)) {102return false;103}104}105return true;106107}108109/**110* Looks for rules containing "-commandId" and removes them.111*/112public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {113// Do a first pass and construct a hash-map for removals114const removals = new Map</* commandId */ string, ResolvedKeybindingItem[]>();115for (let i = 0, len = rules.length; i < len; i++) {116const rule = rules[i];117if (rule.command && rule.command.charAt(0) === '-') {118const command = rule.command.substring(1);119if (!removals.has(command)) {120removals.set(command, [rule]);121} else {122removals.get(command)!.push(rule);123}124}125}126127if (removals.size === 0) {128// There are no removals129return rules;130}131132// Do a second pass and keep only non-removed keybindings133const result: ResolvedKeybindingItem[] = [];134for (let i = 0, len = rules.length; i < len; i++) {135const rule = rules[i];136137if (!rule.command || rule.command.length === 0) {138result.push(rule);139continue;140}141if (rule.command.charAt(0) === '-') {142continue;143}144const commandRemovals = removals.get(rule.command);145if (!commandRemovals || !rule.isDefault) {146result.push(rule);147continue;148}149let isRemoved = false;150for (const commandRemoval of commandRemovals) {151const when = commandRemoval.when;152if (this._isTargetedForRemoval(rule, commandRemoval.chords, when)) {153isRemoved = true;154break;155}156}157if (!isRemoved) {158result.push(rule);159continue;160}161}162return result;163}164165private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void {166167const conflicts = this._map.get(keypress);168169if (typeof conflicts === 'undefined') {170// There is no conflict so far171this._map.set(keypress, [item]);172this._addToLookupMap(item);173return;174}175176for (let i = conflicts.length - 1; i >= 0; i--) {177const conflict = conflicts[i];178179if (conflict.command === item.command) {180continue;181}182183// Test if the shorter keybinding is a prefix of the longer one.184// If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.185let isShorterKbPrefix = true;186for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {187if (conflict.chords[i] !== item.chords[i]) {188// The ith step does not conflict189isShorterKbPrefix = false;190break;191}192}193if (!isShorterKbPrefix) {194continue;195}196197if (KeybindingResolver.whenIsEntirelyIncluded(conflict.when, item.when)) {198// `item` completely overwrites `conflict`199// Remove conflict from the lookupMap200this._removeFromLookupMap(conflict);201}202}203204conflicts.push(item);205this._addToLookupMap(item);206}207208private _addToLookupMap(item: ResolvedKeybindingItem): void {209if (!item.command) {210return;211}212213let arr = this._lookupMap.get(item.command);214if (typeof arr === 'undefined') {215arr = [item];216this._lookupMap.set(item.command, arr);217} else {218arr.push(item);219}220}221222private _removeFromLookupMap(item: ResolvedKeybindingItem): void {223if (!item.command) {224return;225}226const arr = this._lookupMap.get(item.command);227if (typeof arr === 'undefined') {228return;229}230for (let i = 0, len = arr.length; i < len; i++) {231if (arr[i] === item) {232arr.splice(i, 1);233return;234}235}236}237238/**239* Returns true if it is provable `a` implies `b`.240*/241public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {242if (!b || b.type === ContextKeyExprType.True) {243return true;244}245if (!a || a.type === ContextKeyExprType.True) {246return false;247}248249return implies(a, b);250}251252public getDefaultBoundCommands(): Map<string, boolean> {253return this._defaultBoundCommands;254}255256public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {257return this._defaultKeybindings;258}259260public getKeybindings(): readonly ResolvedKeybindingItem[] {261return this._keybindings;262}263264public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] {265const items = this._lookupMap.get(commandId);266if (typeof items === 'undefined' || items.length === 0) {267return [];268}269270// Reverse to get the most specific item first271const result: ResolvedKeybindingItem[] = [];272let resultLen = 0;273for (let i = items.length - 1; i >= 0; i--) {274result[resultLen++] = items[i];275}276return result;277}278279public lookupPrimaryKeybinding(commandId: string, context: IContextKeyService): ResolvedKeybindingItem | null {280const items = this._lookupMap.get(commandId);281if (typeof items === 'undefined' || items.length === 0) {282return null;283}284if (items.length === 1) {285return items[0];286}287288for (let i = items.length - 1; i >= 0; i--) {289const item = items[i];290if (context.contextMatchesRules(item.when)) {291return item;292}293}294295return items[items.length - 1];296}297298/**299* Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]`300*301* Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`:302* `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord303*/304public resolve(context: IContext, currentChords: string[], keypress: string): ResolutionResult {305306const pressedChords = [...currentChords, keypress];307308this._log(`| Resolving ${pressedChords}`);309310const kbCandidates = this._map.get(pressedChords[0]);311if (kbCandidates === undefined) {312// No bindings with such 0-th chord313this._log(`\\ No keybinding entries.`);314return NoMatchingKb;315}316317let lookupMap: ResolvedKeybindingItem[] | null = null;318319if (pressedChords.length < 2) {320lookupMap = kbCandidates;321} else {322// Fetch all chord bindings for `currentChords`323lookupMap = [];324for (let i = 0, len = kbCandidates.length; i < len; i++) {325326const candidate = kbCandidates[i];327328if (pressedChords.length > candidate.chords.length) { // # of pressed chords can't be less than # of chords in a keybinding to invoke329continue;330}331332let prefixMatches = true;333for (let i = 1; i < pressedChords.length; i++) {334if (candidate.chords[i] !== pressedChords[i]) {335prefixMatches = false;336break;337}338}339if (prefixMatches) {340lookupMap.push(candidate);341}342}343}344345// check there's a keybinding with a matching when clause346const result = this._findCommand(context, lookupMap);347if (!result) {348this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`);349return NoMatchingKb;350}351352// check we got all chords necessary to be sure a particular keybinding needs to be invoked353if (pressedChords.length < result.chords.length) {354// The chord sequence is not complete355this._log(`\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);356return MoreChordsNeeded;357}358359this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);360361return KbFound(result.command, result.commandArgs, result.bubble);362}363364private _findCommand(context: IContext, matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {365for (let i = matches.length - 1; i >= 0; i--) {366const k = matches[i];367368if (!KeybindingResolver._contextMatchesRules(context, k.when)) {369continue;370}371372return k;373}374375return null;376}377378private static _contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean {379if (!rules) {380return true;381}382return rules.evaluate(context);383}384}385386function printWhenExplanation(when: ContextKeyExpression | undefined): string {387if (!when) {388return `no when condition`;389}390return `${when.serialize()}`;391}392393function printSourceExplanation(kb: ResolvedKeybindingItem): string {394return (395kb.extensionId396? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)397: (kb.isDefault ? `built-in` : `user`)398);399}400401402