Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/doc-ts-class-full/keybindingResolver.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ContextKeyExprType, ContextKeyExpression, IContext, IContextKeyService, expressionsAreEqualWithConstantSubstitution, implies } from 'vs/platform/contextkey/common/contextkey';
7
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
8
9
//#region resolution-result
10
11
export const enum ResultKind {
12
/** No keybinding found this sequence of chords */
13
NoMatchingKb,
14
15
/** There're several keybindings that have the given sequence of chords as a prefix */
16
MoreChordsNeeded,
17
18
/** A single keybinding found to be dispatched/invoked */
19
KbFound
20
}
21
22
export type ResolutionResult =
23
| { kind: ResultKind.NoMatchingKb }
24
| { kind: ResultKind.MoreChordsNeeded }
25
| { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean };
26
27
28
// util definitions to make working with the above types easier within this module:
29
30
export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb };
31
const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded };
32
function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult {
33
return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };
34
}
35
36
//#endregion
37
38
export class KeybindingResolver {
39
private readonly _log: (str: string) => void;
40
private readonly _defaultKeybindings: ResolvedKeybindingItem[];
41
private readonly _keybindings: ResolvedKeybindingItem[];
42
private readonly _defaultBoundCommands: Map</* commandId */ string, boolean>;
43
private readonly _map: Map</* 1st chord's keypress */ string, ResolvedKeybindingItem[]>;
44
private readonly _lookupMap: Map</* commandId */ string, ResolvedKeybindingItem[]>;
45
46
constructor(
47
/** built-in and extension-provided keybindings */
48
defaultKeybindings: ResolvedKeybindingItem[],
49
/** user's keybindings */
50
overrides: ResolvedKeybindingItem[],
51
log: (str: string) => void
52
) {
53
this._log = log;
54
this._defaultKeybindings = defaultKeybindings;
55
56
this._defaultBoundCommands = new Map<string, boolean>();
57
for (const defaultKeybinding of defaultKeybindings) {
58
const command = defaultKeybinding.command;
59
if (command && command.charAt(0) !== '-') {
60
this._defaultBoundCommands.set(command, true);
61
}
62
}
63
64
this._map = new Map<string, ResolvedKeybindingItem[]>();
65
this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();
66
67
this._keybindings = KeybindingResolver.handleRemovals(([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides));
68
for (let i = 0, len = this._keybindings.length; i < len; i++) {
69
const k = this._keybindings[i];
70
if (k.chords.length === 0) {
71
// unbound
72
continue;
73
}
74
75
// substitute with constants that are registered after startup - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127
76
const when = k.when?.substituteConstants();
77
78
if (when && when.type === ContextKeyExprType.False) {
79
// when condition is false
80
continue;
81
}
82
83
this._addKeyPress(k.chords[0], k);
84
}
85
}
86
87
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null, when: ContextKeyExpression | undefined): boolean {
88
if (keypress) {
89
for (let i = 0; i < keypress.length; i++) {
90
if (keypress[i] !== defaultKb.chords[i]) {
91
return false;
92
}
93
}
94
}
95
96
// `true` means always, as does `undefined`
97
// so we will treat `true` === `undefined`
98
if (when && when.type !== ContextKeyExprType.True) {
99
if (!defaultKb.when) {
100
return false;
101
}
102
if (!expressionsAreEqualWithConstantSubstitution(when, defaultKb.when)) {
103
return false;
104
}
105
}
106
return true;
107
108
}
109
110
/**
111
* Looks for rules containing "-commandId" and removes them.
112
*/
113
public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
114
// Do a first pass and construct a hash-map for removals
115
const removals = new Map</* commandId */ string, ResolvedKeybindingItem[]>();
116
for (let i = 0, len = rules.length; i < len; i++) {
117
const rule = rules[i];
118
if (rule.command && rule.command.charAt(0) === '-') {
119
const command = rule.command.substring(1);
120
if (!removals.has(command)) {
121
removals.set(command, [rule]);
122
} else {
123
removals.get(command)!.push(rule);
124
}
125
}
126
}
127
128
if (removals.size === 0) {
129
// There are no removals
130
return rules;
131
}
132
133
// Do a second pass and keep only non-removed keybindings
134
const result: ResolvedKeybindingItem[] = [];
135
for (let i = 0, len = rules.length; i < len; i++) {
136
const rule = rules[i];
137
138
if (!rule.command || rule.command.length === 0) {
139
result.push(rule);
140
continue;
141
}
142
if (rule.command.charAt(0) === '-') {
143
continue;
144
}
145
const commandRemovals = removals.get(rule.command);
146
if (!commandRemovals || !rule.isDefault) {
147
result.push(rule);
148
continue;
149
}
150
let isRemoved = false;
151
for (const commandRemoval of commandRemovals) {
152
const when = commandRemoval.when;
153
if (this._isTargetedForRemoval(rule, commandRemoval.chords, when)) {
154
isRemoved = true;
155
break;
156
}
157
}
158
if (!isRemoved) {
159
result.push(rule);
160
continue;
161
}
162
}
163
return result;
164
}
165
166
private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void {
167
168
const conflicts = this._map.get(keypress);
169
170
if (typeof conflicts === 'undefined') {
171
// There is no conflict so far
172
this._map.set(keypress, [item]);
173
this._addToLookupMap(item);
174
return;
175
}
176
177
for (let i = conflicts.length - 1; i >= 0; i--) {
178
const conflict = conflicts[i];
179
180
if (conflict.command === item.command) {
181
continue;
182
}
183
184
// Test if the shorter keybinding is a prefix of the longer one.
185
// If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.
186
let isShorterKbPrefix = true;
187
for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {
188
if (conflict.chords[i] !== item.chords[i]) {
189
// The ith step does not conflict
190
isShorterKbPrefix = false;
191
break;
192
}
193
}
194
if (!isShorterKbPrefix) {
195
continue;
196
}
197
198
if (KeybindingResolver.whenIsEntirelyIncluded(conflict.when, item.when)) {
199
// `item` completely overwrites `conflict`
200
// Remove conflict from the lookupMap
201
this._removeFromLookupMap(conflict);
202
}
203
}
204
205
conflicts.push(item);
206
this._addToLookupMap(item);
207
}
208
209
private _addToLookupMap(item: ResolvedKeybindingItem): void {
210
if (!item.command) {
211
return;
212
}
213
214
let arr = this._lookupMap.get(item.command);
215
if (typeof arr === 'undefined') {
216
arr = [item];
217
this._lookupMap.set(item.command, arr);
218
} else {
219
arr.push(item);
220
}
221
}
222
223
private _removeFromLookupMap(item: ResolvedKeybindingItem): void {
224
if (!item.command) {
225
return;
226
}
227
const arr = this._lookupMap.get(item.command);
228
if (typeof arr === 'undefined') {
229
return;
230
}
231
for (let i = 0, len = arr.length; i < len; i++) {
232
if (arr[i] === item) {
233
arr.splice(i, 1);
234
return;
235
}
236
}
237
}
238
239
/**
240
* Returns true if it is provable `a` implies `b`.
241
*/
242
public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
243
if (!b || b.type === ContextKeyExprType.True) {
244
return true;
245
}
246
if (!a || a.type === ContextKeyExprType.True) {
247
return false;
248
}
249
250
return implies(a, b);
251
}
252
253
public getDefaultBoundCommands(): Map<string, boolean> {
254
return this._defaultBoundCommands;
255
}
256
257
public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
258
return this._defaultKeybindings;
259
}
260
261
public getKeybindings(): readonly ResolvedKeybindingItem[] {
262
return this._keybindings;
263
}
264
265
public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] {
266
const items = this._lookupMap.get(commandId);
267
if (typeof items === 'undefined' || items.length === 0) {
268
return [];
269
}
270
271
// Reverse to get the most specific item first
272
const result: ResolvedKeybindingItem[] = [];
273
let resultLen = 0;
274
for (let i = items.length - 1; i >= 0; i--) {
275
result[resultLen++] = items[i];
276
}
277
return result;
278
}
279
280
public lookupPrimaryKeybinding(commandId: string, context: IContextKeyService): ResolvedKeybindingItem | null {
281
const items = this._lookupMap.get(commandId);
282
if (typeof items === 'undefined' || items.length === 0) {
283
return null;
284
}
285
if (items.length === 1) {
286
return items[0];
287
}
288
289
for (let i = items.length - 1; i >= 0; i--) {
290
const item = items[i];
291
if (context.contextMatchesRules(item.when)) {
292
return item;
293
}
294
}
295
296
return items[items.length - 1];
297
}
298
299
/**
300
* Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]`
301
*
302
* Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`:
303
* `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord
304
*/
305
public resolve(context: IContext, currentChords: string[], keypress: string): ResolutionResult {
306
307
const pressedChords = [...currentChords, keypress];
308
309
this._log(`| Resolving ${pressedChords}`);
310
311
const kbCandidates = this._map.get(pressedChords[0]);
312
if (kbCandidates === undefined) {
313
// No bindings with such 0-th chord
314
this._log(`\\ No keybinding entries.`);
315
return NoMatchingKb;
316
}
317
318
let lookupMap: ResolvedKeybindingItem[] | null = null;
319
320
if (pressedChords.length < 2) {
321
lookupMap = kbCandidates;
322
} else {
323
// Fetch all chord bindings for `currentChords`
324
lookupMap = [];
325
for (let i = 0, len = kbCandidates.length; i < len; i++) {
326
327
const candidate = kbCandidates[i];
328
329
if (pressedChords.length > candidate.chords.length) { // # of pressed chords can't be less than # of chords in a keybinding to invoke
330
continue;
331
}
332
333
let prefixMatches = true;
334
for (let i = 1; i < pressedChords.length; i++) {
335
if (candidate.chords[i] !== pressedChords[i]) {
336
prefixMatches = false;
337
break;
338
}
339
}
340
if (prefixMatches) {
341
lookupMap.push(candidate);
342
}
343
}
344
}
345
346
// check there's a keybinding with a matching when clause
347
const result = this._findCommand(context, lookupMap);
348
if (!result) {
349
this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`);
350
return NoMatchingKb;
351
}
352
353
// check we got all chords necessary to be sure a particular keybinding needs to be invoked
354
if (pressedChords.length < result.chords.length) {
355
// The chord sequence is not complete
356
this._log(`\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
357
return MoreChordsNeeded;
358
}
359
360
this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
361
362
return KbFound(result.command, result.commandArgs, result.bubble);
363
}
364
365
private _findCommand(context: IContext, matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {
366
for (let i = matches.length - 1; i >= 0; i--) {
367
const k = matches[i];
368
369
if (!KeybindingResolver._contextMatchesRules(context, k.when)) {
370
continue;
371
}
372
373
return k;
374
}
375
376
return null;
377
}
378
379
private static _contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean {
380
if (!rules) {
381
return true;
382
}
383
return rules.evaluate(context);
384
}
385
}
386
387
function printWhenExplanation(when: ContextKeyExpression | undefined): string {
388
if (!when) {
389
return `no when condition`;
390
}
391
return `${when.serialize()}`;
392
}
393
394
function printSourceExplanation(kb: ResolvedKeybindingItem): string {
395
return (
396
kb.extensionId
397
? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)
398
: (kb.isDefault ? `built-in` : `user`)
399
);
400
}
401
402