Path: blob/main/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';6import { createSimpleKeybinding, ResolvedKeybinding, KeyCodeChord, Keybinding } from '../../../../base/common/keybindings.js';7import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';8import { OS } from '../../../../base/common/platform.js';9import Severity from '../../../../base/common/severity.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';11import { ICommandService } from '../../../commands/common/commands.js';12import { ContextKeyExpr, ContextKeyExpression, IContext, IContextKeyService, IContextKeyServiceTarget } from '../../../contextkey/common/contextkey.js';13import { AbstractKeybindingService } from '../../common/abstractKeybindingService.js';14import { IKeyboardEvent } from '../../common/keybinding.js';15import { KeybindingResolver } from '../../common/keybindingResolver.js';16import { ResolvedKeybindingItem } from '../../common/resolvedKeybindingItem.js';17import { USLayoutResolvedKeybinding } from '../../common/usLayoutResolvedKeybinding.js';18import { createUSLayoutResolvedKeybinding } from './keybindingsTestUtils.js';19import { NullLogService } from '../../../log/common/log.js';20import { INotification, INotificationService, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification } from '../../../notification/common/notification.js';21import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js';2223function createContext(ctx: any) {24return {25getValue: (key: string) => {26return ctx[key];27}28};29}3031suite('AbstractKeybindingService', () => {3233class TestKeybindingService extends AbstractKeybindingService {34private _resolver: KeybindingResolver;3536constructor(37resolver: KeybindingResolver,38contextKeyService: IContextKeyService,39commandService: ICommandService,40notificationService: INotificationService41) {42super(contextKeyService, commandService, NullTelemetryService, notificationService, new NullLogService());43this._resolver = resolver;44}4546protected _getResolver(): KeybindingResolver {47return this._resolver;48}4950protected _documentHasFocus(): boolean {51return true;52}5354public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {55return USLayoutResolvedKeybinding.resolveKeybinding(kb, OS);56}5758public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {59const chord = new KeyCodeChord(60keyboardEvent.ctrlKey,61keyboardEvent.shiftKey,62keyboardEvent.altKey,63keyboardEvent.metaKey,64keyboardEvent.keyCode65).toKeybinding();66return this.resolveKeybinding(chord)[0];67}6869public resolveUserBinding(userBinding: string): ResolvedKeybinding[] {70return [];71}7273public testDispatch(kb: number): boolean {74const keybinding = createSimpleKeybinding(kb, OS);75return this._dispatch({76_standardKeyboardEventBrand: true,77ctrlKey: keybinding.ctrlKey,78shiftKey: keybinding.shiftKey,79altKey: keybinding.altKey,80metaKey: keybinding.metaKey,81altGraphKey: false,82keyCode: keybinding.keyCode,83code: null!84}, null!);85}8687public _dumpDebugInfo(): string {88return '';89}9091public _dumpDebugInfoJSON(): string {92return '';93}9495public registerSchemaContribution(): IDisposable {96return Disposable.None;97}9899public enableKeybindingHoldMode() {100return undefined;101}102}103104let createTestKeybindingService: (items: ResolvedKeybindingItem[], contextValue?: any) => TestKeybindingService = null!;105let currentContextValue: IContext | null = null;106let executeCommandCalls: { commandId: string; args: any[] }[] = null!;107let showMessageCalls: { sev: Severity; message: any }[] = null!;108let statusMessageCalls: string[] | null = null;109let statusMessageCallsDisposed: string[] | null = null;110111112teardown(() => {113currentContextValue = null;114executeCommandCalls = null!;115showMessageCalls = null!;116createTestKeybindingService = null!;117statusMessageCalls = null;118statusMessageCallsDisposed = null;119});120121ensureNoDisposablesAreLeakedInTestSuite();122123setup(() => {124executeCommandCalls = [];125showMessageCalls = [];126statusMessageCalls = [];127statusMessageCallsDisposed = [];128129createTestKeybindingService = (items: ResolvedKeybindingItem[]): TestKeybindingService => {130131const contextKeyService: IContextKeyService = {132_serviceBrand: undefined,133onDidChangeContext: undefined!,134bufferChangeEvents() { },135createKey: undefined!,136contextMatchesRules: undefined!,137getContextKeyValue: undefined!,138createScoped: undefined!,139createOverlay: undefined!,140getContext: (target: IContextKeyServiceTarget): any => {141return currentContextValue;142},143updateParent: () => { }144};145146const commandService: ICommandService = {147_serviceBrand: undefined,148onWillExecuteCommand: () => Disposable.None,149onDidExecuteCommand: () => Disposable.None,150executeCommand: (commandId: string, ...args: any[]): Promise<any> => {151executeCommandCalls.push({152commandId: commandId,153args: args154});155return Promise.resolve(undefined);156}157};158159const notificationService: INotificationService = {160_serviceBrand: undefined,161onDidChangeFilter: undefined!,162notify: (notification: INotification) => {163showMessageCalls.push({ sev: notification.severity, message: notification.message });164return new NoOpNotification();165},166info: (message: any) => {167showMessageCalls.push({ sev: Severity.Info, message });168return new NoOpNotification();169},170warn: (message: any) => {171showMessageCalls.push({ sev: Severity.Warning, message });172return new NoOpNotification();173},174error: (message: any) => {175showMessageCalls.push({ sev: Severity.Error, message });176return new NoOpNotification();177},178prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions) {179throw new Error('not implemented');180},181status(message: string, options?: IStatusMessageOptions) {182statusMessageCalls!.push(message);183return {184close: () => {185statusMessageCallsDisposed!.push(message);186}187};188},189setFilter() {190throw new Error('not implemented');191},192getFilter() {193throw new Error('not implemented');194},195getFilters() {196throw new Error('not implemented');197},198removeFilter() {199throw new Error('not implemented');200}201};202203const resolver = new KeybindingResolver(items, [], () => { });204205return new TestKeybindingService(resolver, contextKeyService, commandService, notificationService);206};207});208209function kbItem(keybinding: number | number[], command: string | null, when?: ContextKeyExpression): ResolvedKeybindingItem {210return new ResolvedKeybindingItem(211createUSLayoutResolvedKeybinding(keybinding, OS),212command,213null,214when,215true,216null,217false218);219}220221function toUsLabel(keybinding: number): string {222return createUSLayoutResolvedKeybinding(keybinding, OS)!.getLabel()!;223}224225suite('simple tests: single- and multi-chord keybindings are dispatched', () => {226227test('a single-chord keybinding is dispatched correctly; this test makes sure the dispatch in general works before we test empty-string/null command ID', () => {228229const key = KeyMod.CtrlCmd | KeyCode.KeyK;230const kbService = createTestKeybindingService([231kbItem(key, 'myCommand'),232]);233234currentContextValue = createContext({});235const shouldPreventDefault = kbService.testDispatch(key);236assert.deepStrictEqual(shouldPreventDefault, true);237assert.deepStrictEqual(executeCommandCalls, ([{ commandId: "myCommand", args: [null] }]));238assert.deepStrictEqual(showMessageCalls, []);239assert.deepStrictEqual(statusMessageCalls, []);240assert.deepStrictEqual(statusMessageCallsDisposed, []);241242kbService.dispose();243});244245test('a multi-chord keybinding is dispatched correctly', () => {246247const chord0 = KeyMod.CtrlCmd | KeyCode.KeyK;248const chord1 = KeyMod.CtrlCmd | KeyCode.KeyI;249const key = [chord0, chord1];250const kbService = createTestKeybindingService([251kbItem(key, 'myCommand'),252]);253254currentContextValue = createContext({});255256let shouldPreventDefault = kbService.testDispatch(chord0);257assert.deepStrictEqual(shouldPreventDefault, true);258assert.deepStrictEqual(executeCommandCalls, []);259assert.deepStrictEqual(showMessageCalls, []);260assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));261assert.deepStrictEqual(statusMessageCallsDisposed, []);262263shouldPreventDefault = kbService.testDispatch(chord1);264assert.deepStrictEqual(shouldPreventDefault, true);265assert.deepStrictEqual(executeCommandCalls, ([{ commandId: "myCommand", args: [null] }]));266assert.deepStrictEqual(showMessageCalls, []);267assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));268assert.deepStrictEqual(statusMessageCallsDisposed, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));269270kbService.dispose();271});272});273274suite('keybindings with empty-string/null command ID', () => {275276test('a single-chord keybinding with an empty string command ID unbinds the keybinding (shouldPreventDefault = false)', () => {277278const kbService = createTestKeybindingService([279kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'myCommand'),280kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, ''),281]);282283// send Ctrl/Cmd + K284currentContextValue = createContext({});285const shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);286assert.deepStrictEqual(shouldPreventDefault, false);287assert.deepStrictEqual(executeCommandCalls, []);288assert.deepStrictEqual(showMessageCalls, []);289assert.deepStrictEqual(statusMessageCalls, []);290assert.deepStrictEqual(statusMessageCallsDisposed, []);291292kbService.dispose();293});294295test('a single-chord keybinding with a null command ID unbinds the keybinding (shouldPreventDefault = false)', () => {296297const kbService = createTestKeybindingService([298kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'myCommand'),299kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, null),300]);301302// send Ctrl/Cmd + K303currentContextValue = createContext({});304const shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);305assert.deepStrictEqual(shouldPreventDefault, false);306assert.deepStrictEqual(executeCommandCalls, []);307assert.deepStrictEqual(showMessageCalls, []);308assert.deepStrictEqual(statusMessageCalls, []);309assert.deepStrictEqual(statusMessageCallsDisposed, []);310311kbService.dispose();312});313314test('a multi-chord keybinding with an empty-string command ID keeps the keybinding (shouldPreventDefault = true)', () => {315316const chord0 = KeyMod.CtrlCmd | KeyCode.KeyK;317const chord1 = KeyMod.CtrlCmd | KeyCode.KeyI;318const key = [chord0, chord1];319const kbService = createTestKeybindingService([320kbItem(key, 'myCommand'),321kbItem(key, ''),322]);323324currentContextValue = createContext({});325326let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);327assert.deepStrictEqual(shouldPreventDefault, true);328assert.deepStrictEqual(executeCommandCalls, []);329assert.deepStrictEqual(showMessageCalls, []);330assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));331assert.deepStrictEqual(statusMessageCallsDisposed, []);332333shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyI);334assert.deepStrictEqual(shouldPreventDefault, true);335assert.deepStrictEqual(executeCommandCalls, []);336assert.deepStrictEqual(showMessageCalls, []);337assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`, `The key combination (${toUsLabel(chord0)}, ${toUsLabel(chord1)}) is not a command.`]));338assert.deepStrictEqual(statusMessageCallsDisposed, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));339340kbService.dispose();341});342343test('a multi-chord keybinding with a null command ID keeps the keybinding (shouldPreventDefault = true)', () => {344345const chord0 = KeyMod.CtrlCmd | KeyCode.KeyK;346const chord1 = KeyMod.CtrlCmd | KeyCode.KeyI;347const key = [chord0, chord1];348const kbService = createTestKeybindingService([349kbItem(key, 'myCommand'),350kbItem(key, null),351]);352353currentContextValue = createContext({});354355let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);356assert.deepStrictEqual(shouldPreventDefault, true);357assert.deepStrictEqual(executeCommandCalls, []);358assert.deepStrictEqual(showMessageCalls, []);359assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));360assert.deepStrictEqual(statusMessageCallsDisposed, []);361362shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyI);363assert.deepStrictEqual(shouldPreventDefault, true);364assert.deepStrictEqual(executeCommandCalls, []);365assert.deepStrictEqual(showMessageCalls, []);366assert.deepStrictEqual(statusMessageCalls, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`, `The key combination (${toUsLabel(chord0)}, ${toUsLabel(chord1)}) is not a command.`]));367assert.deepStrictEqual(statusMessageCallsDisposed, ([`(${toUsLabel(chord0)}) was pressed. Waiting for second key of chord...`]));368369kbService.dispose();370});371372});373374test('issue #16498: chord mode is quit for invalid chords', () => {375376const kbService = createTestKeybindingService([377kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand'),378kbItem(KeyCode.Backspace, 'simpleCommand'),379]);380381// send Ctrl/Cmd + K382let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);383assert.strictEqual(shouldPreventDefault, true);384assert.deepStrictEqual(executeCommandCalls, []);385assert.deepStrictEqual(showMessageCalls, []);386assert.deepStrictEqual(statusMessageCalls, [387`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...`388]);389assert.deepStrictEqual(statusMessageCallsDisposed, []);390executeCommandCalls = [];391showMessageCalls = [];392statusMessageCalls = [];393statusMessageCallsDisposed = [];394395// send backspace396shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);397assert.strictEqual(shouldPreventDefault, true);398assert.deepStrictEqual(executeCommandCalls, []);399assert.deepStrictEqual(showMessageCalls, []);400assert.deepStrictEqual(statusMessageCalls, [401`The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.`402]);403assert.deepStrictEqual(statusMessageCallsDisposed, [404`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...`405]);406executeCommandCalls = [];407showMessageCalls = [];408statusMessageCalls = [];409statusMessageCallsDisposed = [];410411// send backspace412shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);413assert.strictEqual(shouldPreventDefault, true);414assert.deepStrictEqual(executeCommandCalls, [{415commandId: 'simpleCommand',416args: [null]417}]);418assert.deepStrictEqual(showMessageCalls, []);419assert.deepStrictEqual(statusMessageCalls, []);420assert.deepStrictEqual(statusMessageCallsDisposed, []);421executeCommandCalls = [];422showMessageCalls = [];423statusMessageCalls = [];424statusMessageCallsDisposed = [];425426kbService.dispose();427});428429test('issue #16833: Keybinding service should not testDispatch on modifier keys', () => {430431const kbService = createTestKeybindingService([432kbItem(KeyCode.Ctrl, 'nope'),433kbItem(KeyCode.Meta, 'nope'),434kbItem(KeyCode.Alt, 'nope'),435kbItem(KeyCode.Shift, 'nope'),436437kbItem(KeyMod.CtrlCmd, 'nope'),438kbItem(KeyMod.WinCtrl, 'nope'),439kbItem(KeyMod.Alt, 'nope'),440kbItem(KeyMod.Shift, 'nope'),441]);442443function assertIsIgnored(keybinding: number): void {444const shouldPreventDefault = kbService.testDispatch(keybinding);445assert.strictEqual(shouldPreventDefault, false);446assert.deepStrictEqual(executeCommandCalls, []);447assert.deepStrictEqual(showMessageCalls, []);448assert.deepStrictEqual(statusMessageCalls, []);449assert.deepStrictEqual(statusMessageCallsDisposed, []);450executeCommandCalls = [];451showMessageCalls = [];452statusMessageCalls = [];453statusMessageCallsDisposed = [];454}455456assertIsIgnored(KeyCode.Ctrl);457assertIsIgnored(KeyCode.Meta);458assertIsIgnored(KeyCode.Alt);459assertIsIgnored(KeyCode.Shift);460461assertIsIgnored(KeyMod.CtrlCmd);462assertIsIgnored(KeyMod.WinCtrl);463assertIsIgnored(KeyMod.Alt);464assertIsIgnored(KeyMod.Shift);465466kbService.dispose();467});468469test('can trigger command that is sharing keybinding with chord', () => {470471const kbService = createTestKeybindingService([472kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand'),473kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'simpleCommand', ContextKeyExpr.has('key1')),474]);475476477// send Ctrl/Cmd + K478currentContextValue = createContext({479key1: true480});481let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);482assert.strictEqual(shouldPreventDefault, true);483assert.deepStrictEqual(executeCommandCalls, [{484commandId: 'simpleCommand',485args: [null]486}]);487assert.deepStrictEqual(showMessageCalls, []);488assert.deepStrictEqual(statusMessageCalls, []);489assert.deepStrictEqual(statusMessageCallsDisposed, []);490executeCommandCalls = [];491showMessageCalls = [];492statusMessageCalls = [];493statusMessageCallsDisposed = [];494495// send Ctrl/Cmd + K496currentContextValue = createContext({});497shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);498assert.strictEqual(shouldPreventDefault, true);499assert.deepStrictEqual(executeCommandCalls, []);500assert.deepStrictEqual(showMessageCalls, []);501assert.deepStrictEqual(statusMessageCalls, [502`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...`503]);504assert.deepStrictEqual(statusMessageCallsDisposed, []);505executeCommandCalls = [];506showMessageCalls = [];507statusMessageCalls = [];508statusMessageCallsDisposed = [];509510// send Ctrl/Cmd + X511currentContextValue = createContext({});512shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyX);513assert.strictEqual(shouldPreventDefault, true);514assert.deepStrictEqual(executeCommandCalls, [{515commandId: 'chordCommand',516args: [null]517}]);518assert.deepStrictEqual(showMessageCalls, []);519assert.deepStrictEqual(statusMessageCalls, []);520assert.deepStrictEqual(statusMessageCallsDisposed, [521`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...`522]);523executeCommandCalls = [];524showMessageCalls = [];525statusMessageCalls = [];526statusMessageCallsDisposed = [];527528kbService.dispose();529});530531test('cannot trigger chord if command is overwriting', () => {532533const kbService = createTestKeybindingService([534kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand', ContextKeyExpr.has('key1')),535kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'simpleCommand'),536]);537538539// send Ctrl/Cmd + K540currentContextValue = createContext({});541let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);542assert.strictEqual(shouldPreventDefault, true);543assert.deepStrictEqual(executeCommandCalls, [{544commandId: 'simpleCommand',545args: [null]546}]);547assert.deepStrictEqual(showMessageCalls, []);548assert.deepStrictEqual(statusMessageCalls, []);549assert.deepStrictEqual(statusMessageCallsDisposed, []);550executeCommandCalls = [];551showMessageCalls = [];552statusMessageCalls = [];553statusMessageCallsDisposed = [];554555// send Ctrl/Cmd + K556currentContextValue = createContext({557key1: true558});559shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);560assert.strictEqual(shouldPreventDefault, true);561assert.deepStrictEqual(executeCommandCalls, [{562commandId: 'simpleCommand',563args: [null]564}]);565assert.deepStrictEqual(showMessageCalls, []);566assert.deepStrictEqual(statusMessageCalls, []);567assert.deepStrictEqual(statusMessageCallsDisposed, []);568executeCommandCalls = [];569showMessageCalls = [];570statusMessageCalls = [];571statusMessageCallsDisposed = [];572573// send Ctrl/Cmd + X574currentContextValue = createContext({575key1: true576});577shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyX);578assert.strictEqual(shouldPreventDefault, false);579assert.deepStrictEqual(executeCommandCalls, []);580assert.deepStrictEqual(showMessageCalls, []);581assert.deepStrictEqual(statusMessageCalls, []);582assert.deepStrictEqual(statusMessageCallsDisposed, []);583executeCommandCalls = [];584showMessageCalls = [];585statusMessageCalls = [];586statusMessageCallsDisposed = [];587588kbService.dispose();589});590591test('can have spying command', () => {592593const kbService = createTestKeybindingService([594kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, '^simpleCommand'),595]);596597// send Ctrl/Cmd + K598currentContextValue = createContext({});599const shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK);600assert.strictEqual(shouldPreventDefault, false);601assert.deepStrictEqual(executeCommandCalls, [{602commandId: 'simpleCommand',603args: [null]604}]);605assert.deepStrictEqual(showMessageCalls, []);606assert.deepStrictEqual(statusMessageCalls, []);607assert.deepStrictEqual(statusMessageCallsDisposed, []);608executeCommandCalls = [];609showMessageCalls = [];610statusMessageCalls = [];611statusMessageCallsDisposed = [];612613kbService.dispose();614});615});616617618