mirror of
https://github.com/coder/code-server.git
synced 2026-05-25 21:57:27 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
111
lib/vscode/src/vs/platform/keybinding/common/keybinding.ts
Normal file
111
lib/vscode/src/vs/platform/keybinding/common/keybinding.ts
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { KeyChord, KeyCode, KeyMod, Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding, createSimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, IContext, IContextKeyService, IContextKeyServiceTarget, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService';
|
||||
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
import { INotification, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
function createContext(ctx: any) {
|
||||
return {
|
||||
getValue: (key: string) => {
|
||||
return ctx[key];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
suite('AbstractKeybindingService', () => {
|
||||
|
||||
class TestKeybindingService extends AbstractKeybindingService {
|
||||
private _resolver: KeybindingResolver;
|
||||
|
||||
constructor(
|
||||
resolver: KeybindingResolver,
|
||||
contextKeyService: IContextKeyService,
|
||||
commandService: ICommandService,
|
||||
notificationService: INotificationService
|
||||
) {
|
||||
super(contextKeyService, commandService, NullTelemetryService, notificationService, new NullLogService());
|
||||
this._resolver = resolver;
|
||||
}
|
||||
|
||||
protected _getResolver(): KeybindingResolver {
|
||||
return this._resolver;
|
||||
}
|
||||
|
||||
protected _documentHasFocus(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
|
||||
return [new USLayoutResolvedKeybinding(kb, OS)];
|
||||
}
|
||||
|
||||
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
|
||||
let keybinding = new SimpleKeybinding(
|
||||
keyboardEvent.ctrlKey,
|
||||
keyboardEvent.shiftKey,
|
||||
keyboardEvent.altKey,
|
||||
keyboardEvent.metaKey,
|
||||
keyboardEvent.keyCode
|
||||
).toChord();
|
||||
return this.resolveKeybinding(keybinding)[0];
|
||||
}
|
||||
|
||||
public resolveUserBinding(userBinding: string): ResolvedKeybinding[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public testDispatch(kb: number): boolean {
|
||||
const keybinding = createSimpleKeybinding(kb, OS);
|
||||
return this._dispatch({
|
||||
_standardKeyboardEventBrand: true,
|
||||
ctrlKey: keybinding.ctrlKey,
|
||||
shiftKey: keybinding.shiftKey,
|
||||
altKey: keybinding.altKey,
|
||||
metaKey: keybinding.metaKey,
|
||||
keyCode: keybinding.keyCode,
|
||||
code: null!
|
||||
}, null!);
|
||||
}
|
||||
|
||||
public _dumpDebugInfo(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public _dumpDebugInfoJSON(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public registerSchemaContribution() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
let createTestKeybindingService: (items: ResolvedKeybindingItem[], contextValue?: any) => TestKeybindingService = null!;
|
||||
let currentContextValue: IContext | null = null;
|
||||
let executeCommandCalls: { commandId: string; args: any[]; }[] = null!;
|
||||
let showMessageCalls: { sev: Severity, message: any; }[] = null!;
|
||||
let statusMessageCalls: string[] | null = null;
|
||||
let statusMessageCallsDisposed: string[] | null = null;
|
||||
|
||||
setup(() => {
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
createTestKeybindingService = (items: ResolvedKeybindingItem[]): TestKeybindingService => {
|
||||
|
||||
let contextKeyService: IContextKeyService = {
|
||||
_serviceBrand: undefined,
|
||||
dispose: undefined!,
|
||||
onDidChangeContext: undefined!,
|
||||
bufferChangeEvents() { },
|
||||
createKey: undefined!,
|
||||
contextMatchesRules: undefined!,
|
||||
getContextKeyValue: undefined!,
|
||||
createScoped: undefined!,
|
||||
getContext: (target: IContextKeyServiceTarget): any => {
|
||||
return currentContextValue;
|
||||
},
|
||||
updateParent: () => { }
|
||||
};
|
||||
|
||||
let commandService: ICommandService = {
|
||||
_serviceBrand: undefined,
|
||||
onWillExecuteCommand: () => Disposable.None,
|
||||
onDidExecuteCommand: () => Disposable.None,
|
||||
executeCommand: (commandId: string, ...args: any[]): Promise<any> => {
|
||||
executeCommandCalls.push({
|
||||
commandId: commandId,
|
||||
args: args
|
||||
});
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
let notificationService: INotificationService = {
|
||||
_serviceBrand: undefined,
|
||||
notify: (notification: INotification) => {
|
||||
showMessageCalls.push({ sev: notification.severity, message: notification.message });
|
||||
return new NoOpNotification();
|
||||
},
|
||||
info: (message: any) => {
|
||||
showMessageCalls.push({ sev: Severity.Info, message });
|
||||
return new NoOpNotification();
|
||||
},
|
||||
warn: (message: any) => {
|
||||
showMessageCalls.push({ sev: Severity.Warning, message });
|
||||
return new NoOpNotification();
|
||||
},
|
||||
error: (message: any) => {
|
||||
showMessageCalls.push({ sev: Severity.Error, message });
|
||||
return new NoOpNotification();
|
||||
},
|
||||
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions) {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
status(message: string, options?: IStatusMessageOptions) {
|
||||
statusMessageCalls!.push(message);
|
||||
return {
|
||||
dispose: () => {
|
||||
statusMessageCallsDisposed!.push(message);
|
||||
}
|
||||
};
|
||||
},
|
||||
setFilter() { }
|
||||
};
|
||||
|
||||
let resolver = new KeybindingResolver(items, [], () => { });
|
||||
|
||||
return new TestKeybindingService(resolver, contextKeyService, commandService, notificationService);
|
||||
};
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
currentContextValue = null;
|
||||
executeCommandCalls = null!;
|
||||
showMessageCalls = null!;
|
||||
createTestKeybindingService = null!;
|
||||
statusMessageCalls = null;
|
||||
statusMessageCallsDisposed = null;
|
||||
});
|
||||
|
||||
function kbItem(keybinding: number, command: string, when?: ContextKeyExpression): ResolvedKeybindingItem {
|
||||
const resolvedKeybinding = (keybinding !== 0 ? new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS) : undefined);
|
||||
return new ResolvedKeybindingItem(
|
||||
resolvedKeybinding,
|
||||
command,
|
||||
null,
|
||||
when,
|
||||
true,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function toUsLabel(keybinding: number): string {
|
||||
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
return usResolvedKeybinding.getLabel()!;
|
||||
}
|
||||
|
||||
test('issue #16498: chord mode is quit for invalid chords', () => {
|
||||
|
||||
let kbService = createTestKeybindingService([
|
||||
kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand'),
|
||||
kbItem(KeyCode.Backspace, 'simpleCommand'),
|
||||
]);
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, []);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, [
|
||||
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
|
||||
]);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send backspace
|
||||
shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, []);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, [
|
||||
`The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.`
|
||||
]);
|
||||
assert.deepEqual(statusMessageCallsDisposed, [
|
||||
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
|
||||
]);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send backspace
|
||||
shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'simpleCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
kbService.dispose();
|
||||
});
|
||||
|
||||
test('issue #16833: Keybinding service should not testDispatch on modifier keys', () => {
|
||||
|
||||
let kbService = createTestKeybindingService([
|
||||
kbItem(KeyCode.Ctrl, 'nope'),
|
||||
kbItem(KeyCode.Meta, 'nope'),
|
||||
kbItem(KeyCode.Alt, 'nope'),
|
||||
kbItem(KeyCode.Shift, 'nope'),
|
||||
|
||||
kbItem(KeyMod.CtrlCmd, 'nope'),
|
||||
kbItem(KeyMod.WinCtrl, 'nope'),
|
||||
kbItem(KeyMod.Alt, 'nope'),
|
||||
kbItem(KeyMod.Shift, 'nope'),
|
||||
]);
|
||||
|
||||
function assertIsIgnored(keybinding: number): void {
|
||||
let shouldPreventDefault = kbService.testDispatch(keybinding);
|
||||
assert.equal(shouldPreventDefault, false);
|
||||
assert.deepEqual(executeCommandCalls, []);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
}
|
||||
|
||||
assertIsIgnored(KeyCode.Ctrl);
|
||||
assertIsIgnored(KeyCode.Meta);
|
||||
assertIsIgnored(KeyCode.Alt);
|
||||
assertIsIgnored(KeyCode.Shift);
|
||||
|
||||
assertIsIgnored(KeyMod.CtrlCmd);
|
||||
assertIsIgnored(KeyMod.WinCtrl);
|
||||
assertIsIgnored(KeyMod.Alt);
|
||||
assertIsIgnored(KeyMod.Shift);
|
||||
|
||||
kbService.dispose();
|
||||
});
|
||||
|
||||
test('can trigger command that is sharing keybinding with chord', () => {
|
||||
|
||||
let kbService = createTestKeybindingService([
|
||||
kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand'),
|
||||
kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, 'simpleCommand', ContextKeyExpr.has('key1')),
|
||||
]);
|
||||
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
currentContextValue = createContext({
|
||||
key1: true
|
||||
});
|
||||
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'simpleCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
currentContextValue = createContext({});
|
||||
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, []);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, [
|
||||
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
|
||||
]);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send Ctrl/Cmd + X
|
||||
currentContextValue = createContext({});
|
||||
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'chordCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, [
|
||||
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
|
||||
]);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
kbService.dispose();
|
||||
});
|
||||
|
||||
test('cannot trigger chord if command is overwriting', () => {
|
||||
|
||||
let kbService = createTestKeybindingService([
|
||||
kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand', ContextKeyExpr.has('key1')),
|
||||
kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, 'simpleCommand'),
|
||||
]);
|
||||
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
currentContextValue = createContext({});
|
||||
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'simpleCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
currentContextValue = createContext({
|
||||
key1: true
|
||||
});
|
||||
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, true);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'simpleCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
// send Ctrl/Cmd + X
|
||||
currentContextValue = createContext({
|
||||
key1: true
|
||||
});
|
||||
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X);
|
||||
assert.equal(shouldPreventDefault, false);
|
||||
assert.deepEqual(executeCommandCalls, []);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
kbService.dispose();
|
||||
});
|
||||
|
||||
test('can have spying command', () => {
|
||||
|
||||
let kbService = createTestKeybindingService([
|
||||
kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, '^simpleCommand'),
|
||||
]);
|
||||
|
||||
// send Ctrl/Cmd + K
|
||||
currentContextValue = createContext({});
|
||||
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
|
||||
assert.equal(shouldPreventDefault, false);
|
||||
assert.deepEqual(executeCommandCalls, [{
|
||||
commandId: 'simpleCommand',
|
||||
args: [null]
|
||||
}]);
|
||||
assert.deepEqual(showMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCalls, []);
|
||||
assert.deepEqual(statusMessageCallsDisposed, []);
|
||||
executeCommandCalls = [];
|
||||
showMessageCalls = [];
|
||||
statusMessageCalls = [];
|
||||
statusMessageCallsDisposed = [];
|
||||
|
||||
kbService.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { KeyChord, KeyCode, KeyMod, createKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
|
||||
suite('KeybindingLabels', () => {
|
||||
|
||||
function assertUSLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
|
||||
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
assert.equal(usResolvedKeybinding.getLabel(), expected);
|
||||
}
|
||||
|
||||
test('Windows US label', () => {
|
||||
// no modifier
|
||||
assertUSLabel(OperatingSystem.Windows, KeyCode.KEY_A, 'A');
|
||||
|
||||
// one modifier
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.WinCtrl | KeyCode.KEY_A, 'Windows+A');
|
||||
|
||||
// two modifiers
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Windows+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Windows+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Windows+A');
|
||||
|
||||
// three modifiers
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Windows+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Windows+A');
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Windows+A');
|
||||
|
||||
// four modifiers
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Windows+A');
|
||||
|
||||
// chord
|
||||
assertUSLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B');
|
||||
});
|
||||
|
||||
test('Linux US label', () => {
|
||||
// no modifier
|
||||
assertUSLabel(OperatingSystem.Linux, KeyCode.KEY_A, 'A');
|
||||
|
||||
// one modifier
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Super+A');
|
||||
|
||||
// two modifiers
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Super+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Super+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Super+A');
|
||||
|
||||
// three modifiers
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Super+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Super+A');
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Super+A');
|
||||
|
||||
// four modifiers
|
||||
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A');
|
||||
|
||||
// chord
|
||||
assertUSLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B');
|
||||
});
|
||||
|
||||
test('Mac US label', () => {
|
||||
// no modifier
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyCode.KEY_A, 'A');
|
||||
|
||||
// one modifier
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyCode.KEY_A, '⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyCode.KEY_A, '⇧A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyCode.KEY_A, '⌥A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.WinCtrl | KeyCode.KEY_A, '⌃A');
|
||||
|
||||
// two modifiers
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, '⇧⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, '⌥⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, '⇧⌥A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌥A');
|
||||
|
||||
// three modifiers
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, '⇧⌥⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌥⌘A');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌥A');
|
||||
|
||||
// four modifiers
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌥⌘A');
|
||||
|
||||
// chord
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), '⌘A ⌘B');
|
||||
|
||||
// special keys
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyCode.LeftArrow, '←');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyCode.UpArrow, '↑');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyCode.RightArrow, '→');
|
||||
assertUSLabel(OperatingSystem.Macintosh, KeyCode.DownArrow, '↓');
|
||||
});
|
||||
|
||||
test('Aria label', () => {
|
||||
function assertAriaLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
|
||||
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
assert.equal(usResolvedKeybinding.getAriaLabel(), expected);
|
||||
}
|
||||
|
||||
assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A');
|
||||
assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Super+A');
|
||||
assertAriaLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Command+A');
|
||||
});
|
||||
|
||||
test('Electron Accelerator label', () => {
|
||||
function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string | null): void {
|
||||
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
assert.equal(usResolvedKeybinding.getElectronAccelerator(), expected);
|
||||
}
|
||||
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Cmd+A');
|
||||
|
||||
// electron cannot handle chords
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null);
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null);
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null);
|
||||
|
||||
// electron cannot handle numpad keys
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyCode.NUMPAD_1, null);
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyCode.NUMPAD_1, null);
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.NUMPAD_1, null);
|
||||
|
||||
// special
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.LeftArrow, 'Left');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.UpArrow, 'Up');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.RightArrow, 'Right');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.DownArrow, 'Down');
|
||||
});
|
||||
|
||||
test('User Settings label', () => {
|
||||
function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
|
||||
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
assert.equal(usResolvedKeybinding.getUserSettingsLabel(), expected);
|
||||
}
|
||||
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+meta+a');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+cmd+a');
|
||||
|
||||
// electron cannot handle chords
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'ctrl+a ctrl+b');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'ctrl+a ctrl+b');
|
||||
assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'cmd+a cmd+b');
|
||||
});
|
||||
|
||||
test('issue #91235: Do not end with a +', () => {
|
||||
assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Alt, 'Ctrl+Alt');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { KeyChord, KeyCode, KeyMod, SimpleKeybinding, createKeybinding, createSimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
import { ContextKeyExpr, IContext, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
|
||||
function createContext(ctx: any) {
|
||||
return {
|
||||
getValue: (key: string) => {
|
||||
return ctx[key];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
suite('KeybindingResolver', () => {
|
||||
|
||||
function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean): ResolvedKeybindingItem {
|
||||
const resolvedKeybinding = (keybinding !== 0 ? new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS) : undefined);
|
||||
return new ResolvedKeybindingItem(
|
||||
resolvedKeybinding,
|
||||
command,
|
||||
commandArgs,
|
||||
when,
|
||||
isDefault,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function getDispatchStr(runtimeKb: SimpleKeybinding): string {
|
||||
return USLayoutResolvedKeybinding.getDispatchStr(runtimeKb)!;
|
||||
}
|
||||
|
||||
test('resolve key', function () {
|
||||
let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z;
|
||||
let runtimeKeybinding = createSimpleKeybinding(keybinding, OS);
|
||||
let contextRules = ContextKeyExpr.equals('bar', 'baz');
|
||||
let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true);
|
||||
|
||||
assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true);
|
||||
assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false);
|
||||
|
||||
let resolver = new KeybindingResolver([keybindingItem], [], () => { });
|
||||
assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes');
|
||||
assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null);
|
||||
});
|
||||
|
||||
test('resolve key with arguments', function () {
|
||||
let commandArgs = { text: 'no' };
|
||||
let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z;
|
||||
let runtimeKeybinding = createSimpleKeybinding(keybinding, OS);
|
||||
let contextRules = ContextKeyExpr.equals('bar', 'baz');
|
||||
let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true);
|
||||
|
||||
let resolver = new KeybindingResolver([keybindingItem], [], () => { });
|
||||
assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine simple 1', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false),
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine simple 2', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true),
|
||||
kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false),
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with not matching when', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with not matching keybinding', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with matching keybinding and when', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with unspecified keybinding', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with unspecified when', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_A, '-yes1', null, null!, false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('KeybindingResolver.combine removal with unspecified when and unspecified keybinding', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(0, '-yes1', null, null!, false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('issue #612#issuecomment-222109084 cannot remove keybindings for commands with ^', function () {
|
||||
let defaults = [
|
||||
kbItem(KeyCode.KEY_A, '^yes1', null, ContextKeyExpr.equals('1', 'a'), true),
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
];
|
||||
let overrides = [
|
||||
kbItem(KeyCode.KEY_A, '-yes1', null, null!, false)
|
||||
];
|
||||
let actual = KeybindingResolver.combine(defaults, overrides);
|
||||
assert.deepEqual(actual, [
|
||||
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
|
||||
]);
|
||||
});
|
||||
|
||||
test('contextIsEntirelyIncluded', () => {
|
||||
const assertIsIncluded = (a: string | null, b: string | null) => {
|
||||
assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true);
|
||||
};
|
||||
const assertIsNotIncluded = (a: string | null, b: string | null) => {
|
||||
assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false);
|
||||
};
|
||||
|
||||
assertIsIncluded('key1', null);
|
||||
assertIsIncluded('key1', '');
|
||||
assertIsIncluded('key1', 'key1');
|
||||
assertIsIncluded('!key1', '');
|
||||
assertIsIncluded('!key1', '!key1');
|
||||
assertIsIncluded('key2', '');
|
||||
assertIsIncluded('key2', 'key2');
|
||||
assertIsIncluded('key1 && key1 && key2 && key2', 'key2');
|
||||
assertIsIncluded('key1 && key2', 'key2');
|
||||
assertIsIncluded('key1 && key2', 'key1');
|
||||
assertIsIncluded('key1 && key2', '');
|
||||
assertIsIncluded('key1', 'key1 || key2');
|
||||
assertIsIncluded('key1 || !key1', 'key2 || !key2');
|
||||
assertIsIncluded('key1', 'key1 || key2 && key3');
|
||||
|
||||
assertIsNotIncluded('key1', '!key1');
|
||||
assertIsNotIncluded('!key1', 'key1');
|
||||
assertIsNotIncluded('key1 && key2', 'key3');
|
||||
assertIsNotIncluded('key1 && key2', 'key4');
|
||||
assertIsNotIncluded('key1', 'key2');
|
||||
assertIsNotIncluded('key1 || key2', 'key2');
|
||||
assertIsNotIncluded('', 'key2');
|
||||
assertIsNotIncluded(null, 'key2');
|
||||
});
|
||||
|
||||
test('resolve command', function () {
|
||||
|
||||
function _kbItem(keybinding: number, command: string, when: ContextKeyExpression | undefined): ResolvedKeybindingItem {
|
||||
return kbItem(keybinding, command, null, when, true);
|
||||
}
|
||||
|
||||
let items = [
|
||||
// This one will never match because its "when" is always overwritten by another one
|
||||
_kbItem(
|
||||
KeyCode.KEY_X,
|
||||
'first',
|
||||
ContextKeyExpr.and(
|
||||
ContextKeyExpr.equals('key1', true),
|
||||
ContextKeyExpr.notEquals('key2', false)
|
||||
)
|
||||
),
|
||||
// This one always overwrites first
|
||||
_kbItem(
|
||||
KeyCode.KEY_X,
|
||||
'second',
|
||||
ContextKeyExpr.equals('key2', true)
|
||||
),
|
||||
// This one is a secondary mapping for `second`
|
||||
_kbItem(
|
||||
KeyCode.KEY_Z,
|
||||
'second',
|
||||
null!
|
||||
),
|
||||
// This one sometimes overwrites first
|
||||
_kbItem(
|
||||
KeyCode.KEY_X,
|
||||
'third',
|
||||
ContextKeyExpr.equals('key3', true)
|
||||
),
|
||||
// This one is always overwritten by another one
|
||||
_kbItem(
|
||||
KeyMod.CtrlCmd | KeyCode.KEY_Y,
|
||||
'fourth',
|
||||
ContextKeyExpr.equals('key4', true)
|
||||
),
|
||||
// This one overwrites with a chord the previous one
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z),
|
||||
'fifth',
|
||||
null!
|
||||
),
|
||||
// This one has no keybinding
|
||||
_kbItem(
|
||||
0,
|
||||
'sixth',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U),
|
||||
'seventh',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K),
|
||||
'seventh',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U),
|
||||
'uncomment lines',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C),
|
||||
'comment lines',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_G, KeyMod.CtrlCmd | KeyCode.KEY_C),
|
||||
'unreachablechord',
|
||||
null!
|
||||
),
|
||||
_kbItem(
|
||||
KeyMod.CtrlCmd | KeyCode.KEY_G,
|
||||
'eleven',
|
||||
null!
|
||||
)
|
||||
];
|
||||
|
||||
let resolver = new KeybindingResolver(items, [], () => { });
|
||||
|
||||
let testKey = (commandId: string, expectedKeys: number[]) => {
|
||||
// Test lookup
|
||||
let lookupResult = resolver.lookupKeybindings(commandId);
|
||||
assert.equal(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t'));
|
||||
for (let i = 0, len = lookupResult.length; i < len; i++) {
|
||||
const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS);
|
||||
|
||||
assert.equal(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId);
|
||||
}
|
||||
};
|
||||
|
||||
let testResolve = (ctx: IContext, _expectedKey: number, commandId: string) => {
|
||||
const expectedKey = createKeybinding(_expectedKey, OS)!;
|
||||
|
||||
let previousPart: (string | null) = null;
|
||||
for (let i = 0, len = expectedKey.parts.length; i < len; i++) {
|
||||
let part = getDispatchStr(expectedKey.parts[i]);
|
||||
let result = resolver.resolve(ctx, previousPart, part);
|
||||
if (i === len - 1) {
|
||||
// if it's the final part, then we should find a valid command,
|
||||
// and there should not be a chord.
|
||||
assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`);
|
||||
assert.equal(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`);
|
||||
assert.equal(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`);
|
||||
} else {
|
||||
// if it's not the final part, then we should not find a valid command,
|
||||
// and there should be a chord.
|
||||
assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`);
|
||||
assert.equal(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`);
|
||||
assert.equal(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`);
|
||||
}
|
||||
previousPart = part;
|
||||
}
|
||||
};
|
||||
|
||||
testKey('first', []);
|
||||
|
||||
testKey('second', [KeyCode.KEY_Z, KeyCode.KEY_X]);
|
||||
testResolve(createContext({ key2: true }), KeyCode.KEY_X, 'second');
|
||||
testResolve(createContext({}), KeyCode.KEY_Z, 'second');
|
||||
|
||||
testKey('third', [KeyCode.KEY_X]);
|
||||
testResolve(createContext({ key3: true }), KeyCode.KEY_X, 'third');
|
||||
|
||||
testKey('fourth', []);
|
||||
|
||||
testKey('fifth', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z)]);
|
||||
testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z), 'fifth');
|
||||
|
||||
testKey('seventh', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)]);
|
||||
testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), 'seventh');
|
||||
|
||||
testKey('uncomment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U)]);
|
||||
testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U), 'uncomment lines');
|
||||
|
||||
testKey('comment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C)]);
|
||||
testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), 'comment lines');
|
||||
|
||||
testKey('unreachablechord', []);
|
||||
|
||||
testKey('eleven', [KeyMod.CtrlCmd | KeyCode.KEY_G]);
|
||||
testResolve(createContext({}), KeyMod.CtrlCmd | KeyCode.KEY_G, 'eleven');
|
||||
|
||||
testKey('sixth', []);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
import { IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
|
||||
class MockKeybindingContextKey<T> implements IContextKey<T> {
|
||||
private _defaultValue: T | undefined;
|
||||
private _value: T | undefined;
|
||||
|
||||
constructor(defaultValue: T | undefined) {
|
||||
this._defaultValue = defaultValue;
|
||||
this._value = this._defaultValue;
|
||||
}
|
||||
|
||||
public set(value: T | undefined): void {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._value = this._defaultValue;
|
||||
}
|
||||
|
||||
public get(): T | undefined {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockContextKeyService implements IContextKeyService {
|
||||
|
||||
public _serviceBrand: undefined;
|
||||
private _keys = new Map<string, IContextKey<any>>();
|
||||
|
||||
public dispose(): void {
|
||||
//
|
||||
}
|
||||
public createKey<T>(key: string, defaultValue: T | undefined): IContextKey<T> {
|
||||
let ret = new MockKeybindingContextKey(defaultValue);
|
||||
this._keys.set(key, ret);
|
||||
return ret;
|
||||
}
|
||||
public contextMatchesRules(rules: ContextKeyExpression): boolean {
|
||||
return false;
|
||||
}
|
||||
public get onDidChangeContext(): Event<IContextKeyChangeEvent> {
|
||||
return Event.None;
|
||||
}
|
||||
public bufferChangeEvents(callback: () => void) { callback(); }
|
||||
public getContextKeyValue(key: string) {
|
||||
const value = this._keys.get(key);
|
||||
if (value) {
|
||||
return value.get();
|
||||
}
|
||||
}
|
||||
public getContext(domNode: HTMLElement): any {
|
||||
return null;
|
||||
}
|
||||
public createScoped(domNode: HTMLElement): IContextKeyService {
|
||||
return this;
|
||||
}
|
||||
updateParent(_parentContextKeyService: IContextKeyService): void {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
export class MockScopableContextKeyService extends MockContextKeyService {
|
||||
/**
|
||||
* Don't implement this for all tests since we rarely depend on this behavior and it isn't implemented fully
|
||||
*/
|
||||
public createScoped(domNote: HTMLElement): IContextKeyService {
|
||||
return new MockContextKeyService();
|
||||
}
|
||||
}
|
||||
|
||||
export class MockKeybindingService implements IKeybindingService {
|
||||
public _serviceBrand: undefined;
|
||||
|
||||
public readonly inChordMode: boolean = false;
|
||||
|
||||
public get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
|
||||
return Event.None;
|
||||
}
|
||||
|
||||
public getDefaultKeybindingsContent(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public getDefaultKeybindings(): ResolvedKeybindingItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getKeybindings(): ResolvedKeybindingItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
|
||||
return [new USLayoutResolvedKeybinding(keybinding, OS)];
|
||||
}
|
||||
|
||||
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
|
||||
let keybinding = new SimpleKeybinding(
|
||||
keyboardEvent.ctrlKey,
|
||||
keyboardEvent.shiftKey,
|
||||
keyboardEvent.altKey,
|
||||
keyboardEvent.metaKey,
|
||||
keyboardEvent.keyCode
|
||||
);
|
||||
return this.resolveKeybinding(keybinding.toChord())[0];
|
||||
}
|
||||
|
||||
public resolveUserBinding(userBinding: string): ResolvedKeybinding[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public lookupKeybindings(commandId: string): ResolvedKeybinding[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public lookupKeybinding(commandId: string): ResolvedKeybinding | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public customKeybindingsCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public softDispatch(keybinding: IKeyboardEvent, target: IContextKeyServiceTarget): IResolveResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void {
|
||||
|
||||
}
|
||||
|
||||
public dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public mightProducePrintableCharacter(e: IKeyboardEvent): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public toggleLogging(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public _dumpDebugInfo(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public _dumpDebugInfoJSON(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public registerSchemaContribution() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user