src/vs/platform/keybinding/common/keybindingResolver.ts

415 LOC · 390 covered · 25 uncovered · 121 ranges · 1187 concepts · 36 introducers · 591 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- keybindingResolver.ts ×18
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 { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService, implies } from '../../contextkey/common/contextkey.js';
7 > import { ResolvedKeybindingItem } from './resolvedKeybindingItem.js';
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 { keybindingResolver.ts ×14
33 > return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };
34 > }
36 > //#endregion
37 >
38 > /**
39 > * Stores mappings from keybindings to commands and from commands to keybindings.
40 > * Given a sequence of chords, `resolve`s which keybinding it matches
41 > */
42 > export class KeybindingResolver {
43 > private readonly _log: (str: string) => void;
44 > private readonly _defaultKeybindings: ResolvedKeybindingItem[];
45 > private readonly _keybindings: ResolvedKeybindingItem[];
46 > private readonly _defaultBoundCommands: Map</* commandId */ string, boolean>;
47 > private readonly _map: Map</* 1st chord's keypress */ string, ResolvedKeybindingItem[]>;
48 > private readonly _lookupMap: Map</* commandId */ string, ResolvedKeybindingItem[]>;
49 >
50 > constructor(
51 > /** built-in and extension-provided keybindings */ keybindingResolver.ts ×6
52 > defaultKeybindings: ResolvedKeybindingItem[],
53 > /** user's keybindings */
54 > overrides: ResolvedKeybindingItem[],
55 > log: (str: string) => void
56 > ) {
57 > this._log = log;
58 > this._defaultKeybindings = defaultKeybindings;
59 >
60 > this._defaultBoundCommands = new Map<string, boolean>();
61 > for (const defaultKeybinding of defaultKeybindings) {
62 > const command = defaultKeybinding.command; keybindingResolver.ts ×11
63 > if (command && command.charAt(0) !== '-') {
64 > this._defaultBoundCommands.set(command, true);
65 > }
66 > }
68 > this._map = new Map<string, ResolvedKeybindingItem[]>();
69 > this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();
70 >
71 > this._keybindings = KeybindingResolver.handleRemovals(([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides));
72 > for (let i = 0, len = this._keybindings.length; i < len; i++) {
73 > const k = this._keybindings[i]; keybindingResolver.ts ×11
74 > if (k.chords.length === 0) {
75 > // unbound keybindingResolver.ts ×2
76 > continue;
77 > }
79 > // substitute with constants that are registered after startup - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127
80 > const when = k.when?.substituteConstants();
81 >
82 > if (when && when.type === ContextKeyExprType.False) {
83 // when condition is false
84 continue;
85 }
87 > this._addKeyPress(k.chords[0], k);
88 > }
91 > private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null, when: ContextKeyExpression | undefined): boolean {
92 > if (keypress) { keybindingResolver.ts ×10
93 > for (let i = 0; i < keypress.length; i++) {
94 > if (keypress[i] !== defaultKb.chords[i]) { keybindingResolver.ts ×2
95 > return false; keybindingResolver.ts ×1
96 > }
99 >
100 > // `true` means always, as does `undefined`
101 > // so we will treat `true` === `undefined`
102 > if (when && when.type !== ContextKeyExprType.True) { keybindingResolver.ts ×10
103 > if (!defaultKb.when) { keybindingResolver.ts ×3
104 return false;
105 }
107 > // Use implication instead of strict equality so that a removal still matches
108 > // when the default keybinding's when clause becomes more specific across
109 > // updates (e.g. "inChatInput" → "inChatInput && !withinEditSessionDiff").
110 > // See https://github.com/microsoft/vscode/issues/293802
111 > const defaultWhen = defaultKb.when.substituteConstants();
112 > const removalWhen = when.substituteConstants();
113 > if (!KeybindingResolver.whenIsEntirelyIncluded(defaultWhen, removalWhen)) {
114 > return false; keybindingResolver.ts ×1
115 > }
117 > return true; keybindingResolver.ts ×2
119 > }
121 > /**
122 > * Looks for rules containing "-commandId" and removes them.
123 > */
124 > public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
125 > // Do a first pass and construct a hash-map for removals keybindingResolver.ts ×6
126 > const removals = new Map</* commandId */ string, ResolvedKeybindingItem[]>();
127 > for (let i = 0, len = rules.length; i < len; i++) {
128 > const rule = rules[i]; keybindingResolver.ts ×11
129 > if (rule.command && rule.command.charAt(0) === '-') {
130 > const command = rule.command.substring(1); keybindingResolver.ts ×10
131 > if (!removals.has(command)) {
132 > removals.set(command, [rule]);
133 > } else {
134 removals.get(command)!.push(rule);
135 }
139 > if (removals.size === 0) {
140 > // There are no removals
141 > return rules;
142 > }
144 > // Do a second pass and keep only non-removed keybindings
145 > const result: ResolvedKeybindingItem[] = [];
146 > for (let i = 0, len = rules.length; i < len; i++) {
147 > const rule = rules[i];
148 >
149 > if (!rule.command || rule.command.length === 0) {
150 result.push(rule);
151 continue;
152 }
153 > if (rule.command.charAt(0) === '-') { keybindingResolver.ts ×10
154 > continue;
155 > }
156 > const commandRemovals = removals.get(rule.command);
157 > if (!commandRemovals || !rule.isDefault) {
158 > result.push(rule); keybindingResolver.ts ×1
159 > continue;
160 > }
161 > let isRemoved = false; keybindingResolver.ts ×10
162 > for (const commandRemoval of commandRemovals) {
163 > const when = commandRemoval.when;
164 > if (this._isTargetedForRemoval(rule, commandRemoval.chords, when)) {
165 > isRemoved = true; keybindingResolver.ts ×2
166 > break;
167 > }
169 > if (!isRemoved) {
170 > result.push(rule); keybindingResolver.ts ×1
171 > continue;
172 > }
174 > return result;
177 > private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void {
179 > const conflicts = this._map.get(keypress);
180 >
181 > if (typeof conflicts === 'undefined') {
182 > // There is no conflict so far
183 > this._map.set(keypress, [item]);
184 > this._addToLookupMap(item);
185 > return;
186 > }
188 > for (let i = conflicts.length - 1; i >= 0; i--) {
189 > const conflict = conflicts[i];
190 >
191 > if (conflict.command === item.command) {
192 > continue; contextkey.ts ×5
193 > }
195 > // Test if the shorter keybinding is a prefix of the longer one.
196 > // If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.
197 > let isShorterKbPrefix = true;
198 > for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {
199 > if (conflict.chords[i] !== item.chords[i]) { keybindingResolver.ts ×2
200 > // The ith step does not conflict contextkey.ts ×5
201 > isShorterKbPrefix = false;
202 > break;
203 > }
205 > if (!isShorterKbPrefix) { keybindingResolver.ts ×8
206 > continue; contextkey.ts ×5
207 > }
209 > if (KeybindingResolver.whenIsEntirelyIncluded(conflict.when, item.when)) {
210 > // `item` completely overwrites `conflict` keybindingResolver.ts ×5
211 > // Remove conflict from the lookupMap
212 > this._removeFromLookupMap(conflict);
213 > }
215 >
216 > conflicts.push(item);
217 > this._addToLookupMap(item);
220 > private _addToLookupMap(item: ResolvedKeybindingItem): void {
221 > if (!item.command) { keybindingResolver.ts ×11
223 > }
225 > let arr = this._lookupMap.get(item.command);
226 > if (typeof arr === 'undefined') {
227 > arr = [item];
228 > this._lookupMap.set(item.command, arr);
229 > } else {
230 > arr.push(item); keybindingResolver.ts ×2
231 > }
234 > private _removeFromLookupMap(item: ResolvedKeybindingItem): void {
235 > if (!item.command) { keybindingResolver.ts ×5
236 return;
237 }
238 > const arr = this._lookupMap.get(item.command); keybindingResolver.ts ×5
239 > if (typeof arr === 'undefined') {
240 return;
241 }
242 > for (let i = 0, len = arr.length; i < len; i++) { keybindingResolver.ts ×5
243 > if (arr[i] === item) {
244 > arr.splice(i, 1);
245 > return;
246 > }
247 > }
248 > }
250 > /**
251 > * Returns true if it is provable `a` implies `b`.
252 > */
253 > public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
254 > if (!b || b.type === ContextKeyExprType.True) { keybindingResolver.ts ×8
255 > return true; keybindingResolver.ts ×5
256 > }
257 > if (!a || a.type === ContextKeyExprType.True) { keybindingResolver.ts ×8
258 > return false; keybindingResolver.ts ×1
259 > }
261 > return implies(a, b);
264 > public getDefaultBoundCommands(): Map<string, boolean> {
265 return this._defaultBoundCommands;
266 }
268 > public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
269 return this._defaultKeybindings;
270 }
272 > public getKeybindings(): readonly ResolvedKeybindingItem[] {
273 return this._keybindings;
274 }
276 > public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] {
277 > const items = this._lookupMap.get(commandId); keybindingResolver.ts ×2
278 > if (typeof items === 'undefined' || items.length === 0) {
279 > return []; keybindingResolver.ts ×1
280 > }
282 > // Reverse to get the most specific item first
283 > const result: ResolvedKeybindingItem[] = [];
284 > let resultLen = 0;
285 > for (let i = items.length - 1; i >= 0; i--) {
286 > result[resultLen++] = items[i];
287 > }
288 > return result;
291 > public lookupPrimaryKeybinding(commandId: string, context: IContextKeyService, enforceContextCheck = false): ResolvedKeybindingItem | null {
292 > const items = this._lookupMap.get(commandId); abstractKeybindingService.ts ×4
293 > if (typeof items === 'undefined' || items.length === 0) {
294 > return null; keybindingResolver.ts ×1
295 > }
296 > if (items.length === 1 && !enforceContextCheck) { abstractKeybindingService.ts ×4
297 > return items[0]; abstractKeybindingService.ts ×2
298 > }
300 > for (let i = items.length - 1; i >= 0; i--) {
301 > const item = items[i];
302 > if (context.contextMatchesRules(item.when)) {
303 return item;
304 }
306 >
307 > if (enforceContextCheck) {
308 > return null;
309 > }
310
311 return items[items.length - 1];
314 > /**
315 > * Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]`
316 > *
317 > * Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`:
318 > * `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord
319 > */
320 > public resolve(context: IContext, currentChords: string[], keypress: string): ResolutionResult {
322 > const pressedChords = [...currentChords, keypress];
323 >
324 > this._log(`| Resolving ${pressedChords}`);
325 >
326 > const kbCandidates = this._map.get(pressedChords[0]);
327 > if (kbCandidates === undefined) {
328 > // No bindings with such 0-th chord keybindingResolver.ts ×1
329 > this._log(`\\ No keybinding entries.`);
330 > return NoMatchingKb;
331 > }
333 > let lookupMap: ResolvedKeybindingItem[] | null = null;
334 >
335 > if (pressedChords.length < 2) {
336 > lookupMap = kbCandidates;
337 > } else {
338 > // Fetch all chord bindings for `currentChords` keybindingResolver.ts ×5
339 > lookupMap = [];
340 > for (let i = 0, len = kbCandidates.length; i < len; i++) {
341 >
342 > const candidate = kbCandidates[i];
343 >
344 > if (pressedChords.length > candidate.chords.length) { // # of pressed chords can't be less than # of chords in a keybinding to invoke
345 > continue; keybindingResolver.ts ×1
346 > }
348 > let prefixMatches = true;
349 > for (let i = 1; i < pressedChords.length; i++) {
350 > if (candidate.chords[i] !== pressedChords[i]) {
351 > prefixMatches = false; keybindingResolver.ts ×1
352 > break;
353 > }
355 > if (prefixMatches) {
356 > lookupMap.push(candidate); keybindingResolver.ts ×1
357 > }
359 > }
361 > // check there's a keybinding with a matching when clause
362 > const result = this._findCommand(context, lookupMap);
363 > if (!result) {
364 > this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`); keybindingResolver.ts ×2
365 > return NoMatchingKb;
366 > }
368 > // check we got all chords necessary to be sure a particular keybinding needs to be invoked
369 > if (pressedChords.length < result.chords.length) {
370 > // The chord sequence is not complete keybindingResolver.ts ×5
371 > this._log(`\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
372 > return MoreChordsNeeded;
373 > }
375 > this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
376 >
377 > return KbFound(result.command, result.commandArgs, result.bubble);
378 > }
380 > private _findCommand(context: IContext, matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {
381 > for (let i = matches.length - 1; i >= 0; i--) { keybindingResolver.ts ×14
382 > const k = matches[i];
383 >
384 > if (!KeybindingResolver._contextMatchesRules(context, k.when)) {
385 > continue; keybindingResolver.ts ×1
386 > }
388 > return k;
389 > }
391 > return null;
394 > private static _contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean {
395 > if (!rules) { keybindingResolver.ts ×14
396 > return true; keybindingResolver.ts ×2
397 > }
398 > return rules.evaluate(context); keybindingResolver.ts ×2
401 >
402 > function printWhenExplanation(when: ContextKeyExpression | undefined): string { keybindingResolver.ts ×14
403 > if (!when) {
404 > return `no when condition`; keybindingResolver.ts ×2
405 > }
406 > return `${when.serialize()}`; keybindingResolver.ts ×2
407 > }
409 > function printSourceExplanation(kb: ResolvedKeybindingItem): string { keybindingResolver.ts ×14
410 > return (
411 > kb.extensionId
412 ? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)
413 > : (kb.isDefault ? `built-in` : `user`) keybindingResolver.ts ×14
414 > );
415 > }