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.
/*---------------------------------------------------------------------------------------------
keybindingResolver.ts ×18
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService, implies } from '../../contextkey/common/contextkey.js';
import { ResolvedKeybindingItem } from './resolvedKeybindingItem.js';
//#region resolution-result
export const enum ResultKind {
/** No keybinding found this sequence of chords */
NoMatchingKb,
/** There're several keybindings that have the given sequence of chords as a prefix */
MoreChordsNeeded,
/** A single keybinding found to be dispatched/invoked */
KbFound
}
export type ResolutionResult =
| { kind: ResultKind.NoMatchingKb }
| { kind: ResultKind.MoreChordsNeeded }
| { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean };
// util definitions to make working with the above types easier within this module:
export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb };
const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded };
function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult {
keybindingResolver.ts ×14
return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };
}
//#endregion
/**
* Stores mappings from keybindings to commands and from commands to keybindings.
* Given a sequence of chords, `resolve`s which keybinding it matches
*/
export class KeybindingResolver {
private readonly _log: (str: string) => void;
private readonly _defaultKeybindings: ResolvedKeybindingItem[];
private readonly _keybindings: ResolvedKeybindingItem[];
private readonly _defaultBoundCommands: Map</* commandId */ string, boolean>;
private readonly _map: Map</* 1st chord's keypress */ string, ResolvedKeybindingItem[]>;
private readonly _lookupMap: Map</* commandId */ string, ResolvedKeybindingItem[]>;
constructor(
defaultKeybindings: ResolvedKeybindingItem[],
/** user's keybindings */
overrides: ResolvedKeybindingItem[],
log: (str: string) => void
) {
this._log = log;
this._defaultKeybindings = defaultKeybindings;
this._defaultBoundCommands = new Map<string, boolean>();
for (const defaultKeybinding of defaultKeybindings) {
if (command && command.charAt(0) !== '-') {
this._defaultBoundCommands.set(command, true);
}
}
this._map = new Map<string, ResolvedKeybindingItem[]>();
this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();
this._keybindings = KeybindingResolver.handleRemovals(([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides));
for (let i = 0, len = this._keybindings.length; i < len; i++) {
if (k.chords.length === 0) {
continue;
}
// substitute with constants that are registered after startup - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127
const when = k.when?.substituteConstants();
if (when && when.type === ContextKeyExprType.False) {
// when condition is false
continue;
}
this._addKeyPress(k.chords[0], k);
}
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null, when: ContextKeyExpression | undefined): boolean {
for (let i = 0; i < keypress.length; i++) {
}
// `true` means always, as does `undefined`
// so we will treat `true` === `undefined`
return false;
}
// Use implication instead of strict equality so that a removal still matches
// when the default keybinding's when clause becomes more specific across
// updates (e.g. "inChatInput" → "inChatInput && !withinEditSessionDiff").
// See https://github.com/microsoft/vscode/issues/293802
const defaultWhen = defaultKb.when.substituteConstants();
const removalWhen = when.substituteConstants();
if (!KeybindingResolver.whenIsEntirelyIncluded(defaultWhen, removalWhen)) {
}
}
/**
* Looks for rules containing "-commandId" and removes them.
*/
public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
const removals = new Map</* commandId */ string, ResolvedKeybindingItem[]>();
for (let i = 0, len = rules.length; i < len; i++) {
if (rule.command && rule.command.charAt(0) === '-') {
if (!removals.has(command)) {
removals.set(command, [rule]);
} else {
removals.get(command)!.push(rule);
}
if (removals.size === 0) {
// There are no removals
return rules;
}
// Do a second pass and keep only non-removed keybindings
const result: ResolvedKeybindingItem[] = [];
for (let i = 0, len = rules.length; i < len; i++) {
const rule = rules[i];
if (!rule.command || rule.command.length === 0) {
result.push(rule);
continue;
}
continue;
}
const commandRemovals = removals.get(rule.command);
if (!commandRemovals || !rule.isDefault) {
continue;
}
for (const commandRemoval of commandRemovals) {
const when = commandRemoval.when;
if (this._isTargetedForRemoval(rule, commandRemoval.chords, when)) {
break;
}
if (!isRemoved) {
continue;
}
return result;
private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void {
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--) {
const conflict = conflicts[i];
if (conflict.command === item.command) {
}
// Test if the shorter keybinding is a prefix of the longer one.
// If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.
let isShorterKbPrefix = true;
for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {
isShorterKbPrefix = false;
break;
}
}
if (KeybindingResolver.whenIsEntirelyIncluded(conflict.when, item.when)) {
// Remove conflict from the lookupMap
this._removeFromLookupMap(conflict);
}
conflicts.push(item);
this._addToLookupMap(item);
private _addToLookupMap(item: ResolvedKeybindingItem): void {
}
let arr = this._lookupMap.get(item.command);
if (typeof arr === 'undefined') {
arr = [item];
this._lookupMap.set(item.command, arr);
} else {
}
private _removeFromLookupMap(item: ResolvedKeybindingItem): void {
return;
}
if (typeof arr === 'undefined') {
return;
}
if (arr[i] === item) {
arr.splice(i, 1);
return;
}
}
}
/**
* Returns true if it is provable `a` implies `b`.
*/
public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
}
}
return implies(a, b);
public getDefaultBoundCommands(): Map<string, boolean> {
return this._defaultBoundCommands;
}
public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
return this._defaultKeybindings;
}
public getKeybindings(): readonly ResolvedKeybindingItem[] {
return this._keybindings;
}
public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] {
if (typeof items === 'undefined' || items.length === 0) {
}
// Reverse to get the most specific item first
const result: ResolvedKeybindingItem[] = [];
let resultLen = 0;
for (let i = items.length - 1; i >= 0; i--) {
result[resultLen++] = items[i];
}
return result;
public lookupPrimaryKeybinding(commandId: string, context: IContextKeyService, enforceContextCheck = false): ResolvedKeybindingItem | null {
if (typeof items === 'undefined' || items.length === 0) {
}
}
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
if (context.contextMatchesRules(item.when)) {
return item;
}
if (enforceContextCheck) {
return null;
}
return items[items.length - 1];
/**
* Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]`
*
* Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`:
* `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord
*/
public resolve(context: IContext, currentChords: string[], keypress: string): ResolutionResult {
const pressedChords = [...currentChords, keypress];
this._log(`| Resolving ${pressedChords}`);
const kbCandidates = this._map.get(pressedChords[0]);
if (kbCandidates === undefined) {
this._log(`\\ No keybinding entries.`);
return NoMatchingKb;
}
let lookupMap: ResolvedKeybindingItem[] | null = null;
if (pressedChords.length < 2) {
lookupMap = kbCandidates;
} else {
lookupMap = [];
for (let i = 0, len = kbCandidates.length; i < len; i++) {
const candidate = kbCandidates[i];
if (pressedChords.length > candidate.chords.length) { // # of pressed chords can't be less than # of chords in a keybinding to invoke
}
let prefixMatches = true;
for (let i = 1; i < pressedChords.length; i++) {
if (candidate.chords[i] !== pressedChords[i]) {
break;
}
if (prefixMatches) {
}
}
// check there's a keybinding with a matching when clause
const result = this._findCommand(context, lookupMap);
if (!result) {
this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`);
keybindingResolver.ts ×2
return NoMatchingKb;
}
// check we got all chords necessary to be sure a particular keybinding needs to be invoked
if (pressedChords.length < result.chords.length) {
this._log(`\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
return MoreChordsNeeded;
}
this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`);
return KbFound(result.command, result.commandArgs, result.bubble);
}
private _findCommand(context: IContext, matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {
const k = matches[i];
if (!KeybindingResolver._contextMatchesRules(context, k.when)) {
}
return k;
}
return null;
private static _contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean {
}
function printWhenExplanation(when: ContextKeyExpression | undefined): string {
keybindingResolver.ts ×14
if (!when) {
}
}
function printSourceExplanation(kb: ResolvedKeybindingItem): string {
keybindingResolver.ts ×14
return (
kb.extensionId
? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)
);
}