Files
azuredatastudio/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts

379 lines
14 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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', []);
});
});