Path: blob/main/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.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 * as json from '../../../../../base/common/json.js';7import { KeyCode } from '../../../../../base/common/keyCodes.js';8import { KeyCodeChord } from '../../../../../base/common/keybindings.js';9import { OS } from '../../../../../base/common/platform.js';10import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';11import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';12import { IFileService } from '../../../../../platform/files/common/files.js';13import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';14import { IUserFriendlyKeybinding } from '../../../../../platform/keybinding/common/keybinding.js';15import { ResolvedKeybindingItem } from '../../../../../platform/keybinding/common/resolvedKeybindingItem.js';16import { USLayoutResolvedKeybinding } from '../../../../../platform/keybinding/common/usLayoutResolvedKeybinding.js';17import { NullLogService } from '../../../../../platform/log/common/log.js';18import { KeybindingsEditingService } from '../../common/keybindingEditing.js';19import { ITextFileService } from '../../../textfile/common/textfiles.js';20import { TestEnvironmentService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';21import { FileService } from '../../../../../platform/files/common/fileService.js';22import { Schemas } from '../../../../../base/common/network.js';23import { URI } from '../../../../../base/common/uri.js';24import { FileUserDataProvider } from '../../../../../platform/userData/common/fileUserDataProvider.js';25import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';26import { joinPath } from '../../../../../base/common/resources.js';27import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js';28import { VSBuffer } from '../../../../../base/common/buffer.js';29import { UserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js';30import { UserDataProfileService } from '../../../userDataProfile/common/userDataProfileService.js';31import { IUserDataProfileService } from '../../../userDataProfile/common/userDataProfile.js';32import { UriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentityService.js';33import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';3435interface Modifiers {36metaKey?: boolean;37ctrlKey?: boolean;38altKey?: boolean;39shiftKey?: boolean;40}4142const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });4344suite('KeybindingsEditing', () => {4546const disposables = ensureNoDisposablesAreLeakedInTestSuite();47let instantiationService: TestInstantiationService;48let fileService: IFileService;49let environmentService: IEnvironmentService;50let userDataProfileService: IUserDataProfileService;51let testObject: KeybindingsEditingService;5253setup(async () => {5455environmentService = TestEnvironmentService;5657const logService = new NullLogService();58fileService = disposables.add(new FileService(logService));59const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());60disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider));6162const userFolder = joinPath(ROOT, 'User');63await fileService.createFolder(userFolder);6465const configService = new TestConfigurationService();66configService.setUserConfiguration('files', { 'eol': '\n' });6768const uriIdentityService = disposables.add(new UriIdentityService(fileService));69const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService));70userDataProfileService = disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile));71disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, userDataProfilesService, uriIdentityService, new NullLogService()))));7273instantiationService = workbenchInstantiationService({74fileService: () => fileService,75configurationService: () => configService,76environmentService: () => environmentService77}, disposables);7879testObject = disposables.add(instantiationService.createInstance(KeybindingsEditingService));80});8182test('errors cases - parse errors', async () => {83await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,'));84try {85await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape } }), 'alt+c', undefined);86assert.fail('Should fail with parse errors');87} catch (error) {88assert.strictEqual(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.');89}90});9192test('errors cases - parse errors 2', async () => {93await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString('[{"key": }]'));94try {95await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape } }), 'alt+c', undefined);96assert.fail('Should fail with parse errors');97} catch (error) {98assert.strictEqual(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.');99}100});101102test('errors cases - dirty', () => {103instantiationService.stub(ITextFileService, 'isDirty', true);104return testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape } }), 'alt+c', undefined)105.then(() => assert.fail('Should fail with dirty error'),106error => assert.strictEqual(error.message, 'Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again.'));107});108109test('errors cases - did not find an array', async () => {110await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString('{"key": "alt+c", "command": "hello"}'));111try {112await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape } }), 'alt+c', undefined);113assert.fail('Should fail');114} catch (error) {115assert.strictEqual(error.message, 'Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again.');116}117});118119test('edit a default keybinding to an empty file', async () => {120await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(''));121const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }, { key: 'escape', command: '-a' }];122await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined);123assert.deepStrictEqual(await getUserKeybindings(), expected);124});125126test('edit a default keybinding to an empty array', async () => {127await writeToKeybindingsFile();128const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }, { key: 'escape', command: '-a' }];129await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined);130return assert.deepStrictEqual(await getUserKeybindings(), expected);131});132133test('edit a default keybinding in an existing array', async () => {134await writeToKeybindingsFile({ command: 'b', key: 'shift+c' });135const expected: IUserFriendlyKeybinding[] = [{ key: 'shift+c', command: 'b' }, { key: 'alt+c', command: 'a' }, { key: 'escape', command: '-a' }];136await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined);137return assert.deepStrictEqual(await getUserKeybindings(), expected);138});139140test('add another keybinding', async () => {141const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];142await testObject.addKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined);143return assert.deepStrictEqual(await getUserKeybindings(), expected);144});145146test('add a new default keybinding', async () => {147const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];148await testObject.addKeybinding(aResolvedKeybindingItem({ command: 'a' }), 'alt+c', undefined);149return assert.deepStrictEqual(await getUserKeybindings(), expected);150});151152test('add a new default keybinding using edit', async () => {153const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];154await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a' }), 'alt+c', undefined);155assert.deepStrictEqual(await getUserKeybindings(), expected);156});157158test('edit an user keybinding', async () => {159await writeToKeybindingsFile({ key: 'escape', command: 'b' });160const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'b' }];161await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'b', isDefault: false }), 'alt+c', undefined);162assert.deepStrictEqual(await getUserKeybindings(), expected);163});164165test('edit an user keybinding with more than one element', async () => {166await writeToKeybindingsFile({ key: 'escape', command: 'b' }, { key: 'alt+shift+g', command: 'c' });167const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'b' }, { key: 'alt+shift+g', command: 'c' }];168await testObject.editKeybinding(aResolvedKeybindingItem({ firstChord: { keyCode: KeyCode.Escape }, command: 'b', isDefault: false }), 'alt+c', undefined);169assert.deepStrictEqual(await getUserKeybindings(), expected);170});171172test('remove a default keybinding', async () => {173const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }];174await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));175assert.deepStrictEqual(await getUserKeybindings(), expected);176});177178test('remove a default keybinding should not ad duplicate entries', async () => {179const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }];180await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));181await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));182await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));183await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));184await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'a', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } } }));185assert.deepStrictEqual(await getUserKeybindings(), expected);186});187188test('remove a user keybinding', async () => {189await writeToKeybindingsFile({ key: 'alt+c', command: 'b' });190await testObject.removeKeybinding(aResolvedKeybindingItem({ command: 'b', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } }, isDefault: false }));191assert.deepStrictEqual(await getUserKeybindings(), []);192});193194test('reset an edited keybinding', async () => {195await writeToKeybindingsFile({ key: 'alt+c', command: 'b' });196await testObject.resetKeybinding(aResolvedKeybindingItem({ command: 'b', firstChord: { keyCode: KeyCode.KeyC, modifiers: { altKey: true } }, isDefault: false }));197assert.deepStrictEqual(await getUserKeybindings(), []);198});199200test('reset a removed keybinding', async () => {201await writeToKeybindingsFile({ key: 'alt+c', command: '-b' });202await testObject.resetKeybinding(aResolvedKeybindingItem({ command: 'b', isDefault: false }));203assert.deepStrictEqual(await getUserKeybindings(), []);204});205206test('reset multiple removed keybindings', async () => {207await writeToKeybindingsFile({ key: 'alt+c', command: '-b' });208await writeToKeybindingsFile({ key: 'alt+shift+c', command: '-b' });209await writeToKeybindingsFile({ key: 'escape', command: '-b' });210await testObject.resetKeybinding(aResolvedKeybindingItem({ command: 'b', isDefault: false }));211assert.deepStrictEqual(await getUserKeybindings(), []);212});213214test('add a new keybinding to unassigned keybinding', async () => {215await writeToKeybindingsFile({ key: 'alt+c', command: '-a' });216const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }, { key: 'shift+alt+c', command: 'a' }];217await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a', isDefault: false }), 'shift+alt+c', undefined);218assert.deepStrictEqual(await getUserKeybindings(), expected);219});220221test('add when expression', async () => {222await writeToKeybindingsFile({ key: 'alt+c', command: '-a' });223const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }, { key: 'shift+alt+c', command: 'a', when: 'editorTextFocus' }];224await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a', isDefault: false }), 'shift+alt+c', 'editorTextFocus');225assert.deepStrictEqual(await getUserKeybindings(), expected);226});227228test('update command and when expression', async () => {229await writeToKeybindingsFile({ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' });230const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' }, { key: 'shift+alt+c', command: 'a', when: 'editorTextFocus' }];231await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a', isDefault: false }), 'shift+alt+c', 'editorTextFocus');232assert.deepStrictEqual(await getUserKeybindings(), expected);233});234235test('update when expression', async () => {236await writeToKeybindingsFile({ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' }, { key: 'shift+alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' });237const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' }, { key: 'shift+alt+c', command: 'a', when: 'editorTextFocus' }];238await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a', isDefault: false, when: 'editorTextFocus && !editorReadonly' }), 'shift+alt+c', 'editorTextFocus');239assert.deepStrictEqual(await getUserKeybindings(), expected);240});241242test('remove when expression', async () => {243await writeToKeybindingsFile({ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' });244const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a', when: 'editorTextFocus && !editorReadonly' }, { key: 'shift+alt+c', command: 'a' }];245await testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a', isDefault: false }), 'shift+alt+c', undefined);246assert.deepStrictEqual(await getUserKeybindings(), expected);247});248249async function writeToKeybindingsFile(...keybindings: IUserFriendlyKeybinding[]): Promise<void> {250await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(JSON.stringify(keybindings || [])));251}252253async function getUserKeybindings(): Promise<IUserFriendlyKeybinding[]> {254return json.parse((await fileService.readFile(userDataProfileService.currentProfile.keybindingsResource)).value.toString());255}256257function aResolvedKeybindingItem({ command, when, isDefault, firstChord, secondChord }: { command?: string; when?: string; isDefault?: boolean; firstChord?: { keyCode: KeyCode; modifiers?: Modifiers }; secondChord?: { keyCode: KeyCode; modifiers?: Modifiers } }): ResolvedKeybindingItem {258const aSimpleKeybinding = function (chord: { keyCode: KeyCode; modifiers?: Modifiers }): KeyCodeChord {259const { ctrlKey, shiftKey, altKey, metaKey } = chord.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false };260return new KeyCodeChord(ctrlKey!, shiftKey!, altKey!, metaKey!, chord.keyCode);261};262const chords: KeyCodeChord[] = [];263if (firstChord) {264chords.push(aSimpleKeybinding(firstChord));265if (secondChord) {266chords.push(aSimpleKeybinding(secondChord));267}268}269const keybinding = chords.length > 0 ? new USLayoutResolvedKeybinding(chords, OS) : undefined;270return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null, false);271}272});273274275