Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { IntervalTimer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode, Keybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingEvent, IKeybindingService, IKeyboardEvent, KeybindingsSchemaContribution } from 'vs/platform/keybinding/common/keybinding';
import { IResolveResult, KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { ILogService } from 'vs/platform/log/common/log';
interface CurrentChord {
keypress: string;
label: string | null;
}
export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService {
public _serviceBrand: undefined;
protected readonly _onDidUpdateKeybindings: Emitter<IKeybindingEvent> = this._register(new Emitter<IKeybindingEvent>());
get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
}
private _currentChord: CurrentChord | null;
private _currentChordChecker: IntervalTimer;
private _currentChordStatusMessage: IDisposable | null;
protected _logging: boolean;
public get inChordMode(): boolean {
return !!this._currentChord;
}
constructor(
private _contextKeyService: IContextKeyService,
protected _commandService: ICommandService,
protected _telemetryService: ITelemetryService,
private _notificationService: INotificationService,
protected _logService: ILogService,
) {
super();
this._currentChord = null;
this._currentChordChecker = new IntervalTimer();
this._currentChordStatusMessage = null;
this._logging = false;
}
public dispose(): void {
super.dispose();
}
protected abstract _getResolver(): KeybindingResolver;
protected abstract _documentHasFocus(): boolean;
public abstract resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
public abstract resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
public abstract resolveUserBinding(userBinding: string): ResolvedKeybinding[];
public abstract registerSchemaContribution(contribution: KeybindingsSchemaContribution): void;
public abstract _dumpDebugInfo(): string;
public abstract _dumpDebugInfoJSON(): string;
public getDefaultKeybindingsContent(): string {
return '';
}
public toggleLogging(): boolean {
this._logging = !this._logging;
return this._logging;
}
protected _log(str: string): void {
if (this._logging) {
this._logService.info(`[KeybindingService]: ${str}`);
}
}
public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
return this._getResolver().getDefaultKeybindings();
}
public getKeybindings(): readonly ResolvedKeybindingItem[] {
return this._getResolver().getKeybindings();
}
public customKeybindingsCount(): number {
return 0;
}
public lookupKeybindings(commandId: string): ResolvedKeybinding[] {
return arrays.coalesce(
this._getResolver().lookupKeybindings(commandId).map(item => item.resolvedKeybinding)
);
}
public lookupKeybinding(commandId: string): ResolvedKeybinding | undefined {
const result = this._getResolver().lookupPrimaryKeybinding(commandId);
if (!result) {
return undefined;
}
return result.resolvedKeybinding;
}
public dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
return this._dispatch(e, target);
}
public softDispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): IResolveResult | null {
const keybinding = this.resolveKeyboardEvent(e);
if (keybinding.isChord()) {
console.warn('Unexpected keyboard event mapped to a chord');
return null;
}
const [firstPart,] = keybinding.getDispatchParts();
if (firstPart === null) {
// cannot be dispatched, probably only modifier keys
return null;
}
const contextValue = this._contextKeyService.getContext(target);
const currentChord = this._currentChord ? this._currentChord.keypress : null;
return this._getResolver().resolve(contextValue, currentChord, firstPart);
}
private _enterChordMode(firstPart: string, keypressLabel: string | null): void {
this._currentChord = {
keypress: firstPart,
label: keypressLabel
};
this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
const chordEnterTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
if (!this._documentHasFocus()) {
// Focus has been lost => leave chord mode
this._leaveChordMode();
return;
}
if (Date.now() - chordEnterTime > 5000) {
// 5 seconds elapsed => leave chord mode
this._leaveChordMode();
}
}, 500);
}
private _leaveChordMode(): void {
if (this._currentChordStatusMessage) {
this._currentChordStatusMessage.dispose();
this._currentChordStatusMessage = null;
}
this._currentChordChecker.cancel();
this._currentChord = null;
}
public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void {
const keybindings = this.resolveUserBinding(userSettingsLabel);
if (keybindings.length >= 1) {
this._doDispatch(keybindings[0], target);
}
}
protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
return this._doDispatch(this.resolveKeyboardEvent(e), target);
}
private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget): boolean {
let shouldPreventDefault = false;
if (keybinding.isChord()) {
console.warn('Unexpected keyboard event mapped to a chord');
return false;
}
const [firstPart,] = keybinding.getDispatchParts();
if (firstPart === null) {
this._log(`\\ Keyboard event cannot be dispatched.`);
// cannot be dispatched, probably only modifier keys
return shouldPreventDefault;
}
const contextValue = this._contextKeyService.getContext(target);
const currentChord = this._currentChord ? this._currentChord.keypress : null;
const keypressLabel = keybinding.getLabel();
const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart);
this._logService.trace('KeybindingService#dispatch', keypressLabel, resolveResult?.commandId);
if (resolveResult && resolveResult.enterChord) {
shouldPreventDefault = true;
this._enterChordMode(firstPart, keypressLabel);
return shouldPreventDefault;
}
if (this._currentChord) {
if (!resolveResult || !resolveResult.commandId) {
this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
shouldPreventDefault = true;
}
}
this._leaveChordMode();
if (resolveResult && resolveResult.commandId) {
if (!resolveResult.bubble) {
shouldPreventDefault = true;
}
if (typeof resolveResult.commandArgs === 'undefined') {
this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err));
} else {
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
}
this._telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' });
}
return shouldPreventDefault;
}
mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
if (event.ctrlKey || event.metaKey) {
// ignore ctrl/cmd-combination but not shift/alt-combinatios
return false;
}
// weak check for certain ranges. this is properly implemented in a subclass
// with access to the KeyboardMapperFactory.
if ((event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z)
|| (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OperatingSystem } from 'vs/base/common/platform';
import { illegalArgument } from 'vs/base/common/errors';
import { Modifiers, UILabelProvider, AriaLabelProvider, ElectronAcceleratorLabelProvider, UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
export abstract class BaseResolvedKeybinding<T extends Modifiers> extends ResolvedKeybinding {
protected readonly _os: OperatingSystem;
protected readonly _parts: T[];
constructor(os: OperatingSystem, parts: T[]) {
super();
if (parts.length === 0) {
throw illegalArgument(`parts`);
}
this._os = os;
this._parts = parts;
}
public getLabel(): string | null {
return UILabelProvider.toLabel(this._os, this._parts, (keybinding) => this._getLabel(keybinding));
}
public getAriaLabel(): string | null {
return AriaLabelProvider.toLabel(this._os, this._parts, (keybinding) => this._getAriaLabel(keybinding));
}
public getElectronAccelerator(): string | null {
if (this._parts.length > 1) {
// Electron cannot handle chords
return null;
}
return ElectronAcceleratorLabelProvider.toLabel(this._os, this._parts, (keybinding) => this._getElectronAccelerator(keybinding));
}
public getUserSettingsLabel(): string | null {
return UserSettingsLabelProvider.toLabel(this._os, this._parts, (keybinding) => this._getUserSettingsLabel(keybinding));
}
public isWYSIWYG(): boolean {
return this._parts.every((keybinding) => this._isWYSIWYG(keybinding));
}
public isChord(): boolean {
return (this._parts.length > 1);
}
public getParts(): ResolvedKeybindingPart[] {
return this._parts.map((keybinding) => this._getPart(keybinding));
}
private _getPart(keybinding: T): ResolvedKeybindingPart {
return new ResolvedKeybindingPart(
keybinding.ctrlKey,
keybinding.shiftKey,
keybinding.altKey,
keybinding.metaKey,
this._getLabel(keybinding),
this._getAriaLabel(keybinding)
);
}
public getDispatchParts(): (string | null)[] {
return this._parts.map((keybinding) => this._getDispatchPart(keybinding));
}
protected abstract _getLabel(keybinding: T): string | null;
protected abstract _getAriaLabel(keybinding: T): string | null;
protected abstract _getElectronAccelerator(keybinding: T): string | null;
protected abstract _getUserSettingsLabel(keybinding: T): string | null;
protected abstract _isWYSIWYG(keybinding: T): boolean;
protected abstract _getDispatchPart(keybinding: T): string | null;
}

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
export interface IUserFriendlyKeybinding {
key: string;
command: string;
args?: any;
when?: string;
}
export const enum KeybindingSource {
Default = 1,
User
}
export interface IKeybindingEvent {
source: KeybindingSource;
keybindings?: IUserFriendlyKeybinding[];
}
export interface IKeyboardEvent {
readonly _standardKeyboardEventBrand: true;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
}
export interface KeybindingsSchemaContribution {
readonly onDidChange?: Event<void>;
getSchemaAdditions(): IJSONSchema[];
}
export const IKeybindingService = createDecorator<IKeybindingService>('keybindingService');
export interface IKeybindingService {
readonly _serviceBrand: undefined;
readonly inChordMode: boolean;
onDidUpdateKeybindings: Event<IKeybindingEvent>;
/**
* Returns none, one or many (depending on keyboard layout)!
*/
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveUserBinding(userBinding: string): ResolvedKeybinding[];
/**
* Resolve and dispatch `keyboardEvent` and invoke the command.
*/
dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean;
/**
* Resolve and dispatch `keyboardEvent`, but do not invoke the command or change inner state.
*/
softDispatch(keyboardEvent: IKeyboardEvent, target: IContextKeyServiceTarget): IResolveResult | null;
dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void;
/**
* Look up keybindings for a command.
* Use `lookupKeybinding` if you are interested in the preferred keybinding.
*/
lookupKeybindings(commandId: string): ResolvedKeybinding[];
/**
* Look up the preferred (last defined) keybinding for a command.
* @returns The preferred keybinding or null if the command is not bound.
*/
lookupKeybinding(commandId: string): ResolvedKeybinding | undefined;
getDefaultKeybindingsContent(): string;
getDefaultKeybindings(): readonly ResolvedKeybindingItem[];
getKeybindings(): readonly ResolvedKeybindingItem[];
customKeybindingsCount(): number;
/**
* Will the given key event produce a character that's rendered on screen, e.g. in a
* text box. *Note* that the results of this function can be incorrect.
*/
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
registerSchemaContribution(contribution: KeybindingsSchemaContribution): void;
toggleLogging(): boolean;
_dumpDebugInfo(): string;
_dumpDebugInfoJSON(): string;
}

View File

@@ -0,0 +1,396 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
export interface IResolveResult {
/** Whether the resolved keybinding is entering a chord */
enterChord: boolean;
/** Whether the resolved keybinding is leaving (and executing) a chord */
leaveChord: boolean;
commandId: string | null;
commandArgs: any;
bubble: boolean;
}
export class KeybindingResolver {
private readonly _log: (str: string) => void;
private readonly _defaultKeybindings: ResolvedKeybindingItem[];
private readonly _keybindings: ResolvedKeybindingItem[];
private readonly _defaultBoundCommands: Map<string, boolean>;
private readonly _map: Map<string, ResolvedKeybindingItem[]>;
private readonly _lookupMap: Map<string, ResolvedKeybindingItem[]>;
constructor(
defaultKeybindings: ResolvedKeybindingItem[],
overrides: ResolvedKeybindingItem[],
log: (str: string) => void
) {
this._log = log;
this._defaultKeybindings = defaultKeybindings;
this._defaultBoundCommands = new Map<string, boolean>();
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<string, ResolvedKeybindingItem[]>();
this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();
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 === ContextKeyExprType.False) {
// when condition is false
continue;
}
// TODO@chords
this._addKeyPress(k.keypressParts[0], k);
}
}
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypressFirstPart: string | null, keypressChordPart: string | null, command: string, when: ContextKeyExpression | undefined): boolean {
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 (!when.equals(defaultKb.when)) {
return false;
}
}
return true;
}
/**
* Looks for rules containing -command in `overrides` and removes them directly from `defaults`.
*/
public static combine(defaults: ResolvedKeybindingItem[], rawOverrides: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
defaults = defaults.slice(0);
let overrides: ResolvedKeybindingItem[] = [];
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);
}
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--) {
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);
}
private _addToLookupMap(item: ResolvedKeybindingItem): void {
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);
}
}
private _removeFromLookupMap(item: ResolvedKeybindingItem): void {
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`.
*/
public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
if (!b) {
return true;
}
if (!a) {
return false;
}
return this._implies(a, b);
}
/**
* Returns true if it is provable `p` implies `q`.
*/
private static _implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean {
const notP = p.negate();
const terminals = (node: ContextKeyExpression) => {
if (node.type === ContextKeyExprType.Or) {
return node.expr;
}
return [node];
};
let expr = terminals(notP).concat(terminals(q));
for (let i = 0; i < expr.length; i++) {
const a = expr[i];
const notA = a.negate();
for (let j = i + 1; j < expr.length; j++) {
const b = expr[j];
if (notA.equals(b)) {
return true;
}
}
}
return false;
}
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[] {
let items = this._lookupMap.get(commandId);
if (typeof items === 'undefined' || items.length === 0) {
return [];
}
// Reverse to get the most specific item first
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
for (let i = items.length - 1; i >= 0; i--) {
result[resultLen++] = items[i];
}
return result;
}
public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null {
let items = this._lookupMap.get(commandId);
if (typeof items === 'undefined' || items.length === 0) {
return null;
}
return items[items.length - 1];
}
public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null {
this._log(`| Resolving ${keypress}${currentChord ? ` chorded from ${currentChord}` : ``}`);
let lookupMap: ResolvedKeybindingItem[] | null = 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
};
}
private _findCommand(context: IContext, matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {
for (let i = matches.length - 1; i >= 0; i--) {
let k = matches[i];
if (!KeybindingResolver.contextMatchesRules(context, k.when)) {
continue;
}
return k;
}
return null;
}
public static contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean {
if (!rules) {
return true;
}
return rules.evaluate(context);
}
public static getAllUnboundCommands(boundCommands: Map<string, boolean>): string[] {
const unboundCommands: string[] = [];
const seenMap: Map<string, boolean> = new Map<string, boolean>();
const addCommand = (id: string, includeCommandWithArgs: boolean) => {
if (seenMap.has(id)) {
return;
}
seenMap.set(id, true);
if (id[0] === '_' || id.indexOf('vscode.') === 0) { // private command
return;
}
if (boundCommands.get(id) === true) {
return;
}
if (!includeCommandWithArgs) {
const command = CommandsRegistry.getCommand(id);
if (command && typeof command.description === 'object'
&& isNonEmptyArray((<ICommandHandlerDescription>command.description).args)) { // command with args
return;
}
}
unboundCommands.push(id);
};
for (const id of MenuRegistry.getCommands().keys()) {
addCommand(id, true);
}
for (const id of CommandsRegistry.getCommands().keys()) {
addCommand(id, false);
}
return unboundCommands;
}
}
function printWhenExplanation(when: ContextKeyExpression | undefined): string {
if (!when) {
return `no when condition`;
}
return `${when.serialize()}`;
}
function printSourceExplanation(kb: ResolvedKeybindingItem): string {
if (kb.isDefault) {
if (kb.extensionId) {
return `built-in extension ${kb.extensionId}`;
}
return `built-in`;
}
if (kb.extensionId) {
return `user extension ${kb.extensionId}`;
}
return `user`;
}

View File

@@ -0,0 +1,258 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { CommandsRegistry, ICommandHandler, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { Registry } from 'vs/platform/registry/common/platform';
export interface IKeybindingItem {
keybinding: Keybinding;
command: string;
commandArgs?: any;
when: ContextKeyExpression | null | undefined;
weight1: number;
weight2: number;
extensionId: string | null;
}
export interface IKeybindings {
primary?: number;
secondary?: number[];
win?: {
primary: number;
secondary?: number[];
};
linux?: {
primary: number;
secondary?: number[];
};
mac?: {
primary: number;
secondary?: number[];
};
}
export interface IKeybindingRule extends IKeybindings {
id: string;
weight: number;
args?: any;
when?: ContextKeyExpression | null | undefined;
}
export interface IKeybindingRule2 {
primary: Keybinding | null;
win?: { primary: Keybinding | null; } | null;
linux?: { primary: Keybinding | null; } | null;
mac?: { primary: Keybinding | null; } | null;
id: string;
args?: any;
weight: number;
when: ContextKeyExpression | undefined;
extensionId?: string;
}
export const enum KeybindingWeight {
EditorCore = 0,
EditorContrib = 100,
WorkbenchContrib = 200,
BuiltinExtension = 300,
ExternalExtension = 400
}
export interface ICommandAndKeybindingRule extends IKeybindingRule {
handler: ICommandHandler;
description?: ICommandHandlerDescription | null;
}
export interface IKeybindingsRegistry {
registerKeybindingRule(rule: IKeybindingRule): void;
setExtensionKeybindings(rules: IKeybindingRule2[]): void;
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void;
getDefaultKeybindings(): IKeybindingItem[];
}
class KeybindingsRegistryImpl implements IKeybindingsRegistry {
private _coreKeybindings: IKeybindingItem[];
private _extensionKeybindings: IKeybindingItem[];
private _cachedMergedKeybindings: IKeybindingItem[] | null;
constructor() {
this._coreKeybindings = [];
this._extensionKeybindings = [];
this._cachedMergedKeybindings = null;
}
/**
* Take current platform into account and reduce to primary & secondary.
*/
private static bindToCurrentPlatform(kb: IKeybindings): { primary?: number; secondary?: number[]; } {
if (OS === OperatingSystem.Windows) {
if (kb && kb.win) {
return kb.win;
}
} else if (OS === OperatingSystem.Macintosh) {
if (kb && kb.mac) {
return kb.mac;
}
} else {
if (kb && kb.linux) {
return kb.linux;
}
}
return kb;
}
/**
* Take current platform into account and reduce to primary & secondary.
*/
private static bindToCurrentPlatform2(kb: IKeybindingRule2): { primary?: Keybinding | null; } {
if (OS === OperatingSystem.Windows) {
if (kb && kb.win) {
return kb.win;
}
} else if (OS === OperatingSystem.Macintosh) {
if (kb && kb.mac) {
return kb.mac;
}
} else {
if (kb && kb.linux) {
return kb.linux;
}
}
return kb;
}
public registerKeybindingRule(rule: IKeybindingRule): void {
const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule);
if (actualKb && actualKb.primary) {
const kk = createKeybinding(actualKb.primary, OS);
if (kk) {
this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, 0, rule.when);
}
}
if (actualKb && Array.isArray(actualKb.secondary)) {
for (let i = 0, len = actualKb.secondary.length; i < len; i++) {
const k = actualKb.secondary[i];
const kk = createKeybinding(k, OS);
if (kk) {
this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, -i - 1, rule.when);
}
}
}
}
public setExtensionKeybindings(rules: IKeybindingRule2[]): void {
let result: IKeybindingItem[] = [], keybindingsLen = 0;
for (let i = 0, len = rules.length; i < len; i++) {
const rule = rules[i];
let actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform2(rule);
if (actualKb && actualKb.primary) {
result[keybindingsLen++] = {
keybinding: actualKb.primary,
command: rule.id,
commandArgs: rule.args,
when: rule.when,
weight1: rule.weight,
weight2: 0,
extensionId: rule.extensionId || null
};
}
}
this._extensionKeybindings = result;
this._cachedMergedKeybindings = null;
}
public registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void {
this.registerKeybindingRule(desc);
CommandsRegistry.registerCommand(desc);
}
private static _mightProduceChar(keyCode: KeyCode): boolean {
if (keyCode >= KeyCode.KEY_0 && keyCode <= KeyCode.KEY_9) {
return true;
}
if (keyCode >= KeyCode.KEY_A && keyCode <= KeyCode.KEY_Z) {
return true;
}
return (
keyCode === KeyCode.US_SEMICOLON
|| keyCode === KeyCode.US_EQUAL
|| keyCode === KeyCode.US_COMMA
|| keyCode === KeyCode.US_MINUS
|| keyCode === KeyCode.US_DOT
|| keyCode === KeyCode.US_SLASH
|| keyCode === KeyCode.US_BACKTICK
|| keyCode === KeyCode.ABNT_C1
|| keyCode === KeyCode.ABNT_C2
|| keyCode === KeyCode.US_OPEN_SQUARE_BRACKET
|| keyCode === KeyCode.US_BACKSLASH
|| keyCode === KeyCode.US_CLOSE_SQUARE_BRACKET
|| keyCode === KeyCode.US_QUOTE
|| keyCode === KeyCode.OEM_8
|| keyCode === KeyCode.OEM_102
);
}
private _assertNoCtrlAlt(keybinding: SimpleKeybinding, commandId: string): void {
if (keybinding.ctrlKey && keybinding.altKey && !keybinding.metaKey) {
if (KeybindingsRegistryImpl._mightProduceChar(keybinding.keyCode)) {
console.warn('Ctrl+Alt+ keybindings should not be used by default under Windows. Offender: ', keybinding, ' for ', commandId);
}
}
}
private _registerDefaultKeybinding(keybinding: Keybinding, commandId: string, commandArgs: any, weight1: number, weight2: number, when: ContextKeyExpression | null | undefined): void {
if (OS === OperatingSystem.Windows) {
this._assertNoCtrlAlt(keybinding.parts[0], commandId);
}
this._coreKeybindings.push({
keybinding: keybinding,
command: commandId,
commandArgs: commandArgs,
when: when,
weight1: weight1,
weight2: weight2,
extensionId: null
});
this._cachedMergedKeybindings = null;
}
public getDefaultKeybindings(): IKeybindingItem[] {
if (!this._cachedMergedKeybindings) {
this._cachedMergedKeybindings = (<IKeybindingItem[]>[]).concat(this._coreKeybindings).concat(this._extensionKeybindings);
this._cachedMergedKeybindings.sort(sorter);
}
return this._cachedMergedKeybindings.slice(0);
}
}
export const KeybindingsRegistry: IKeybindingsRegistry = new KeybindingsRegistryImpl();
// Define extension point ids
export const Extensions = {
EditorModes: 'platform.keybindingsRegistry'
};
Registry.add(Extensions.EditorModes, KeybindingsRegistry);
function sorter(a: IKeybindingItem, b: IKeybindingItem): number {
if (a.weight1 !== b.weight1) {
return a.weight1 - b.weight1;
}
if (a.command < b.command) {
return -1;
}
if (a.command > b.command) {
return 1;
}
return a.weight2 - b.weight2;
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from 'vs/base/common/charCode';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
export class ResolvedKeybindingItem {
_resolvedKeybindingItemBrand: void;
public readonly resolvedKeybinding: ResolvedKeybinding | undefined;
public readonly keypressParts: string[];
public readonly bubble: boolean;
public readonly command: string | null;
public readonly commandArgs: any;
public readonly when: ContextKeyExpression | undefined;
public readonly isDefault: boolean;
public readonly extensionId: string | null;
constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null) {
this.resolvedKeybinding = resolvedKeybinding;
this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : [];
this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false);
this.command = this.bubble ? command!.substr(1) : command;
this.commandArgs = commandArgs;
this.when = when;
this.isDefault = isDefault;
this.extensionId = extensionId;
}
}
export function removeElementsAfterNulls<T>(arr: (T | null)[]): T[] {
let result: T[] = [];
for (let i = 0, len = arr.length; i < len; i++) {
const element = arr[i];
if (!element) {
// stop processing at first encountered null
return result;
}
result.push(element);
}
return result;
}

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyCodeUtils, Keybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { OperatingSystem } from 'vs/base/common/platform';
import { BaseResolvedKeybinding } from 'vs/platform/keybinding/common/baseResolvedKeybinding';
/**
* Do not instantiate. Use KeybindingService to get a ResolvedKeybinding seeded with information about the current kb layout.
*/
export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding<SimpleKeybinding> {
constructor(actual: Keybinding, os: OperatingSystem) {
super(os, actual.parts);
}
private _keyCodeToUILabel(keyCode: KeyCode): string {
if (this._os === OperatingSystem.Macintosh) {
switch (keyCode) {
case KeyCode.LeftArrow:
return '←';
case KeyCode.UpArrow:
return '↑';
case KeyCode.RightArrow:
return '→';
case KeyCode.DownArrow:
return '↓';
}
}
return KeyCodeUtils.toString(keyCode);
}
protected _getLabel(keybinding: SimpleKeybinding): string | null {
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return this._keyCodeToUILabel(keybinding.keyCode);
}
protected _getAriaLabel(keybinding: SimpleKeybinding): string | null {
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return KeyCodeUtils.toString(keybinding.keyCode);
}
private _keyCodeToElectronAccelerator(keyCode: KeyCode): string | null {
if (keyCode >= KeyCode.NUMPAD_0 && keyCode <= KeyCode.NUMPAD_DIVIDE) {
// Electron cannot handle numpad keys
return null;
}
switch (keyCode) {
case KeyCode.UpArrow:
return 'Up';
case KeyCode.DownArrow:
return 'Down';
case KeyCode.LeftArrow:
return 'Left';
case KeyCode.RightArrow:
return 'Right';
}
return KeyCodeUtils.toString(keyCode);
}
protected _getElectronAccelerator(keybinding: SimpleKeybinding): string | null {
if (keybinding.isDuplicateModifierCase()) {
return null;
}
return this._keyCodeToElectronAccelerator(keybinding.keyCode);
}
protected _getUserSettingsLabel(keybinding: SimpleKeybinding): string | null {
if (keybinding.isDuplicateModifierCase()) {
return '';
}
const result = KeyCodeUtils.toUserSettingsUS(keybinding.keyCode);
return (result ? result.toLowerCase() : result);
}
protected _isWYSIWYG(): boolean {
return true;
}
protected _getDispatchPart(keybinding: SimpleKeybinding): string | null {
return USLayoutResolvedKeybinding.getDispatchStr(keybinding);
}
public static getDispatchStr(keybinding: SimpleKeybinding): string | null {
if (keybinding.isModifierKey()) {
return null;
}
let result = '';
if (keybinding.ctrlKey) {
result += 'ctrl+';
}
if (keybinding.shiftKey) {
result += 'shift+';
}
if (keybinding.altKey) {
result += 'alt+';
}
if (keybinding.metaKey) {
result += 'meta+';
}
result += KeyCodeUtils.toString(keybinding.keyCode);
return result;
}
}