Path: blob/main/src/vs/platform/keybinding/test/common/keybindingResolver.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*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { decodeKeybinding, createSimpleKeybinding, KeyCodeChord } from '../../../../base/common/keybindings.js';7import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';8import { OS } from '../../../../base/common/platform.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';10import { ContextKeyExpr, ContextKeyExpression, IContext } from '../../../contextkey/common/contextkey.js';11import { KeybindingResolver, ResultKind } from '../../common/keybindingResolver.js';12import { ResolvedKeybindingItem } from '../../common/resolvedKeybindingItem.js';13import { USLayoutResolvedKeybinding } from '../../common/usLayoutResolvedKeybinding.js';14import { createUSLayoutResolvedKeybinding } from './keybindingsTestUtils.js';1516function createContext(ctx: any) {17return {18getValue: (key: string) => {19return ctx[key];20}21};22}2324suite('KeybindingResolver', () => {2526ensureNoDisposablesAreLeakedInTestSuite();2728function kbItem(keybinding: number | number[], command: string, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean): ResolvedKeybindingItem {29const resolvedKeybinding = createUSLayoutResolvedKeybinding(keybinding, OS);30return new ResolvedKeybindingItem(31resolvedKeybinding,32command,33commandArgs,34when,35isDefault,36null,37false38);39}4041function getDispatchStr(chord: KeyCodeChord): string {42return USLayoutResolvedKeybinding.getDispatchStr(chord)!;43}4445test('resolve key', () => {46const keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ;47const runtimeKeybinding = createSimpleKeybinding(keybinding, OS);48const contextRules = ContextKeyExpr.equals('bar', 'baz');49const keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true);5051assert.strictEqual(contextRules.evaluate(createContext({ bar: 'baz' })), true);52assert.strictEqual(contextRules.evaluate(createContext({ bar: 'bz' })), false);5354const resolver = new KeybindingResolver([keybindingItem], [], () => { });5556const r1 = resolver.resolve(createContext({ bar: 'baz' }), [], getDispatchStr(runtimeKeybinding));57assert.ok(r1.kind === ResultKind.KbFound);58assert.strictEqual(r1.commandId, 'yes');5960const r2 = resolver.resolve(createContext({ bar: 'bz' }), [], getDispatchStr(runtimeKeybinding));61assert.strictEqual(r2.kind, ResultKind.NoMatchingKb);62});6364test('resolve key with arguments', () => {65const commandArgs = { text: 'no' };66const keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ;67const runtimeKeybinding = createSimpleKeybinding(keybinding, OS);68const contextRules = ContextKeyExpr.equals('bar', 'baz');69const keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true);7071const resolver = new KeybindingResolver([keybindingItem], [], () => { });7273const r = resolver.resolve(createContext({ bar: 'baz' }), [], getDispatchStr(runtimeKeybinding));74assert.ok(r.kind === ResultKind.KbFound);75assert.strictEqual(r.commandArgs, commandArgs);76});7778suite('handle keybinding removals', () => {7980test('simple 1', () => {81const defaults = [82kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true)83];84const overrides = [85kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false)86];87const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);88assert.deepStrictEqual(actual, [89kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),90kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false),91]);92});9394test('simple 2', () => {95const defaults = [96kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),97kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)98];99const overrides = [100kbItem(KeyCode.KeyC, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false)101];102const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);103assert.deepStrictEqual(actual, [104kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),105kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true),106kbItem(KeyCode.KeyC, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false),107]);108});109110test('removal with not matching when', () => {111const defaults = [112kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),113kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)114];115const overrides = [116kbItem(KeyCode.KeyA, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false)117];118const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);119assert.deepStrictEqual(actual, [120kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),121kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)122]);123});124125test('removal with not matching keybinding', () => {126const defaults = [127kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),128kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)129];130const overrides = [131kbItem(KeyCode.KeyB, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)132];133const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);134assert.deepStrictEqual(actual, [135kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),136kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)137]);138});139140test('removal with matching keybinding and when', () => {141const defaults = [142kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),143kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)144];145const overrides = [146kbItem(KeyCode.KeyA, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)147];148const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);149assert.deepStrictEqual(actual, [150kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)151]);152});153154test('removal with unspecified keybinding', () => {155const defaults = [156kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),157kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)158];159const overrides = [160kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)161];162const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);163assert.deepStrictEqual(actual, [164kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)165]);166});167168test('removal with unspecified when', () => {169const defaults = [170kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),171kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)172];173const overrides = [174kbItem(KeyCode.KeyA, '-yes1', null, undefined, false)175];176const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);177assert.deepStrictEqual(actual, [178kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)179]);180});181182test('removal with unspecified when and unspecified keybinding', () => {183const defaults = [184kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),185kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)186];187const overrides = [188kbItem(0, '-yes1', null, undefined, false)189];190const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);191assert.deepStrictEqual(actual, [192kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)193]);194});195196test('issue #138997 - removal in default list', () => {197const defaults = [198kbItem(KeyCode.KeyA, 'yes1', null, undefined, true),199kbItem(KeyCode.KeyB, 'yes2', null, undefined, true),200kbItem(0, '-yes1', null, undefined, false)201];202const overrides: ResolvedKeybindingItem[] = [];203const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);204assert.deepStrictEqual(actual, [205kbItem(KeyCode.KeyB, 'yes2', null, undefined, true)206]);207});208209test('issue #612#issuecomment-222109084 cannot remove keybindings for commands with ^', () => {210const defaults = [211kbItem(KeyCode.KeyA, '^yes1', null, ContextKeyExpr.equals('1', 'a'), true),212kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)213];214const overrides = [215kbItem(KeyCode.KeyA, '-yes1', null, undefined, false)216];217const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);218assert.deepStrictEqual(actual, [219kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)220]);221});222223test('issue #140884 Unable to reassign F1 as keybinding for Show All Commands', () => {224const defaults = [225kbItem(KeyCode.KeyA, 'command1', null, undefined, true),226];227const overrides = [228kbItem(KeyCode.KeyA, '-command1', null, undefined, false),229kbItem(KeyCode.KeyA, 'command1', null, undefined, false),230];231const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);232assert.deepStrictEqual(actual, [233kbItem(KeyCode.KeyA, 'command1', null, undefined, false)234]);235});236237test('issue #141638: Keyboard Shortcuts: Change When Expression might actually remove keybinding in Insiders', () => {238const defaults = [239kbItem(KeyCode.KeyA, 'command1', null, undefined, true),240];241const overrides = [242kbItem(KeyCode.KeyA, 'command1', null, ContextKeyExpr.equals('a', '1'), false),243kbItem(KeyCode.KeyA, '-command1', null, undefined, false),244];245const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);246assert.deepStrictEqual(actual, [247kbItem(KeyCode.KeyA, 'command1', null, ContextKeyExpr.equals('a', '1'), false)248]);249});250251test('issue #157751: Auto-quoting of context keys prevents removal of keybindings via UI', () => {252const defaults = [253kbItem(KeyCode.KeyA, 'command1', null, ContextKeyExpr.deserialize(`editorTextFocus && activeEditor != workbench.editor.notebook && editorLangId in julia.supportedLanguageIds`), true),254];255const overrides = [256kbItem(KeyCode.KeyA, '-command1', null, ContextKeyExpr.deserialize(`editorTextFocus && activeEditor != 'workbench.editor.notebook' && editorLangId in 'julia.supportedLanguageIds'`), false),257];258const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);259assert.deepStrictEqual(actual, []);260});261262test('issue #160604: Remove keybindings with when clause does not work', () => {263const defaults = [264kbItem(KeyCode.KeyA, 'command1', null, undefined, true),265];266const overrides = [267kbItem(KeyCode.KeyA, '-command1', null, ContextKeyExpr.true(), false),268];269const actual = KeybindingResolver.handleRemovals([...defaults, ...overrides]);270assert.deepStrictEqual(actual, []);271});272273test('contextIsEntirelyIncluded', () => {274const toContextKeyExpression = (expr: ContextKeyExpression | string | null) => {275if (typeof expr === 'string' || !expr) {276return ContextKeyExpr.deserialize(expr);277}278return expr;279};280const assertIsIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => {281assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), true);282};283const assertIsNotIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => {284assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), false);285};286287assertIsIncluded(null, null);288assertIsIncluded(null, ContextKeyExpr.true());289assertIsIncluded(ContextKeyExpr.true(), null);290assertIsIncluded(ContextKeyExpr.true(), ContextKeyExpr.true());291assertIsIncluded('key1', null);292assertIsIncluded('key1', '');293assertIsIncluded('key1', 'key1');294assertIsIncluded('key1', ContextKeyExpr.true());295assertIsIncluded('!key1', '');296assertIsIncluded('!key1', '!key1');297assertIsIncluded('key2', '');298assertIsIncluded('key2', 'key2');299assertIsIncluded('key1 && key1 && key2 && key2', 'key2');300assertIsIncluded('key1 && key2', 'key2');301assertIsIncluded('key1 && key2', 'key1');302assertIsIncluded('key1 && key2', '');303assertIsIncluded('key1', 'key1 || key2');304assertIsIncluded('key1 || !key1', 'key2 || !key2');305assertIsIncluded('key1', 'key1 || key2 && key3');306307assertIsNotIncluded('key1', '!key1');308assertIsNotIncluded('!key1', 'key1');309assertIsNotIncluded('key1 && key2', 'key3');310assertIsNotIncluded('key1 && key2', 'key4');311assertIsNotIncluded('key1', 'key2');312assertIsNotIncluded('key1 || key2', 'key2');313assertIsNotIncluded('', 'key2');314assertIsNotIncluded(null, 'key2');315});316});317318suite('resolve command', () => {319320function _kbItem(keybinding: number | number[], command: string, when: ContextKeyExpression | undefined): ResolvedKeybindingItem {321return kbItem(keybinding, command, null, when, true);322}323324const items = [325// This one will never match because its "when" is always overwritten by another one326_kbItem(327KeyCode.KeyX,328'first',329ContextKeyExpr.and(330ContextKeyExpr.equals('key1', true),331ContextKeyExpr.notEquals('key2', false)332)333),334// This one always overwrites first335_kbItem(336KeyCode.KeyX,337'second',338ContextKeyExpr.equals('key2', true)339),340// This one is a secondary mapping for `second`341_kbItem(342KeyCode.KeyZ,343'second',344undefined345),346// This one sometimes overwrites first347_kbItem(348KeyCode.KeyX,349'third',350ContextKeyExpr.equals('key3', true)351),352// This one is always overwritten by another one353_kbItem(354KeyMod.CtrlCmd | KeyCode.KeyY,355'fourth',356ContextKeyExpr.equals('key4', true)357),358// This one overwrites with a chord the previous one359_kbItem(360KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ),361'fifth',362undefined363),364// This one has no keybinding365_kbItem(3660,367'sixth',368undefined369),370_kbItem(371KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU),372'seventh',373undefined374),375_kbItem(376KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK),377'seventh',378undefined379),380_kbItem(381KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU),382'uncomment lines',383undefined384),385_kbItem(386KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), // cmd+k cmd+c387'comment lines',388undefined389),390_kbItem(391KeyChord(KeyMod.CtrlCmd | KeyCode.KeyG, KeyMod.CtrlCmd | KeyCode.KeyC), // cmd+g cmd+c392'unreachablechord',393undefined394),395_kbItem(396KeyMod.CtrlCmd | KeyCode.KeyG, // cmd+g397'eleven',398undefined399),400_kbItem(401[KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB], // cmd+k a b402'long multi chord',403undefined404),405_kbItem(406[KeyMod.CtrlCmd | KeyCode.KeyB, KeyMod.CtrlCmd | KeyCode.KeyC], // cmd+b cmd+c407'shadowed by long-multi-chord-2',408undefined409),410_kbItem(411[KeyMod.CtrlCmd | KeyCode.KeyB, KeyMod.CtrlCmd | KeyCode.KeyC, KeyCode.KeyI], // cmd+b cmd+c i412'long-multi-chord-2',413undefined414)415];416417const resolver = new KeybindingResolver(items, [], () => { });418419const testKbLookupByCommand = (commandId: string, expectedKeys: number[] | number[][]) => {420// Test lookup421const lookupResult = resolver.lookupKeybindings(commandId);422assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId);423for (let i = 0, len = lookupResult.length; i < len; i++) {424const expected = createUSLayoutResolvedKeybinding(expectedKeys[i], OS)!;425426assert.strictEqual(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId);427}428};429430const testResolve = (ctx: IContext, _expectedKey: number | number[], commandId: string) => {431const expectedKeybinding = decodeKeybinding(_expectedKey, OS)!;432433const previousChord: string[] = [];434435for (let i = 0, len = expectedKeybinding.chords.length; i < len; i++) {436437const chord = getDispatchStr(<KeyCodeChord>expectedKeybinding.chords[i]);438439const result = resolver.resolve(ctx, previousChord, chord);440441if (i === len - 1) {442// if it's the final chord, then we should find a valid command,443// and there should not be a chord.444assert.ok(result.kind === ResultKind.KbFound, `Enters multi chord for ${commandId} at chord ${i}`);445assert.strictEqual(result.commandId, commandId, `Enters multi chord for ${commandId} at chord ${i}`);446} else if (i > 0) {447// if this is an intermediate chord, we should not find a valid command,448// and there should be an open chord we continue.449assert.ok(result.kind === ResultKind.MoreChordsNeeded, `Continues multi chord for ${commandId} at chord ${i}`);450} else {451// if it's not the final chord and not an intermediate, then we should not452// find a valid command, and we should enter a chord.453assert.ok(result.kind === ResultKind.MoreChordsNeeded, `Enters multi chord for ${commandId} at chord ${i}`);454}455previousChord.push(chord);456}457};458459test('resolve command - 1', () => {460testKbLookupByCommand('first', []);461});462463test('resolve command - 2', () => {464testKbLookupByCommand('second', [KeyCode.KeyZ, KeyCode.KeyX]);465testResolve(createContext({ key2: true }), KeyCode.KeyX, 'second');466testResolve(createContext({}), KeyCode.KeyZ, 'second');467});468469test('resolve command - 3', () => {470testKbLookupByCommand('third', [KeyCode.KeyX]);471testResolve(createContext({ key3: true }), KeyCode.KeyX, 'third');472});473474test('resolve command - 4', () => {475testKbLookupByCommand('fourth', []);476});477478test('resolve command - 5', () => {479testKbLookupByCommand('fifth', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ)]);480testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ), 'fifth');481});482483test('resolve command - 6', () => {484testKbLookupByCommand('seventh', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK)]);485testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK), 'seventh');486});487488test('resolve command - 7', () => {489testKbLookupByCommand('uncomment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU)]);490testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU), 'uncomment lines');491});492493test('resolve command - 8', () => {494testKbLookupByCommand('comment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC)]);495testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), 'comment lines');496});497498test('resolve command - 9', () => {499testKbLookupByCommand('unreachablechord', []);500});501502test('resolve command - 10', () => {503testKbLookupByCommand('eleven', [KeyMod.CtrlCmd | KeyCode.KeyG]);504testResolve(createContext({}), KeyMod.CtrlCmd | KeyCode.KeyG, 'eleven');505});506507test('resolve command - 11', () => {508testKbLookupByCommand('sixth', []);509});510511test('resolve command - 12', () => {512testKbLookupByCommand('long multi chord', [[KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB]]);513testResolve(createContext({}), [KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyA, KeyCode.KeyB], 'long multi chord');514});515516const emptyContext = createContext({});517518test('KBs having common prefix - the one defined later is returned', () => {519testResolve(emptyContext, [KeyMod.CtrlCmd | KeyCode.KeyB, KeyMod.CtrlCmd | KeyCode.KeyC, KeyCode.KeyI], 'long-multi-chord-2');520});521});522});523524525