UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

554 lines (446 loc) • 20.7 kB
// ***************************************************************************** // Copyright (C) 2017 Ericsson and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { enableJSDOM } from './test/jsdom'; let disableJSDOM = enableJSDOM(); import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; FrontendApplicationConfigProvider.set({}); import { Container, injectable, ContainerModule } from 'inversify'; import { bindContributionProvider } from '../common/contribution-provider'; import { KeyboardLayoutProvider, NativeKeyboardLayout, KeyboardLayoutChangeNotifier } from '../common/keyboard/keyboard-layout-provider'; import { ILogger } from '../common/logger'; import { KeybindingRegistry, KeybindingContext, KeybindingContribution, KeybindingScope } from './keybinding'; import { Keybinding } from '../common/keybinding'; import { KeyCode, Key, KeyModifier, KeySequence } from './keyboard/keys'; import { KeyboardLayoutService } from './keyboard/keyboard-layout-service'; import { CommandRegistry, CommandService, CommandContribution, Command } from '../common/command'; import { LabelParser } from './label-parser'; import { MockLogger } from '../common/test/mock-logger'; import { FrontendApplicationStateService } from './frontend-application-state'; import { ContextKeyService, ContextKeyServiceDummyImpl } from './context-key-service'; import { CorePreferences } from './core-preferences'; import * as os from '../common/os'; import * as chai from 'chai'; import * as sinon from 'sinon'; import { Emitter, Event } from '../common/event'; import { bindPreferenceService } from './frontend-application-bindings'; import { MarkdownRenderer, MarkdownRendererFactory, MarkdownRendererImpl } from './markdown-rendering/markdown-renderer'; import { StatusBar } from './status-bar'; disableJSDOM(); const expect = chai.expect; let keybindingRegistry: KeybindingRegistry; let commandRegistry: CommandRegistry; let testContainer: Container; let stub: sinon.SinonStub; before(async () => { disableJSDOM = enableJSDOM(); testContainer = new Container(); const module = new ContainerModule((bind, unbind, isBound, rebind) => { /* Mock logger binding*/ bind(ILogger).to(MockLogger); bind(KeyboardLayoutService).toSelf().inSingletonScope(); bind(MockKeyboardLayoutProvider).toSelf().inSingletonScope(); bind(KeyboardLayoutProvider).toService(MockKeyboardLayoutProvider); bind(MockKeyboardLayoutChangeNotifier).toSelf().inSingletonScope(); bind(KeyboardLayoutChangeNotifier).toService(MockKeyboardLayoutChangeNotifier); bindContributionProvider(bind, KeybindingContext); bind(CommandRegistry).toSelf().inSingletonScope(); bindContributionProvider(bind, CommandContribution); bind(KeybindingRegistry).toSelf(); bindContributionProvider(bind, KeybindingContribution); bind(TestContribution).toSelf().inSingletonScope(); [CommandContribution, KeybindingContribution].forEach(serviceIdentifier => bind(serviceIdentifier).toService(TestContribution) ); bind(KeybindingContext).toConstantValue({ id: 'testContext', isEnabled(arg?: Keybinding): boolean { return true; } }); bind(StatusBar).toConstantValue({} as StatusBar); bind(MarkdownRendererImpl).toSelf().inSingletonScope(); bind(MarkdownRenderer).toService(MarkdownRendererImpl); bind(MarkdownRendererFactory).toFactory(({ container }) => container.get(MarkdownRenderer)); bind(CommandService).toService(CommandRegistry); bind(LabelParser).toSelf().inSingletonScope(); bind(ContextKeyService).to(ContextKeyServiceDummyImpl).inSingletonScope(); bind(FrontendApplicationStateService).toSelf().inSingletonScope(); bind(CorePreferences).toConstantValue(<CorePreferences>{}); bindPreferenceService(bind); }); testContainer.load(module); commandRegistry = testContainer.get(CommandRegistry); commandRegistry.onStart(); }); after(() => { disableJSDOM(); }); beforeEach(async () => { stub = sinon.stub(os, 'isOSX').value(false); keybindingRegistry = testContainer.get<KeybindingRegistry>(KeybindingRegistry); await keybindingRegistry.onStart(); }); afterEach(() => { stub.restore(); }); describe('keybindings', () => { it('should register the default keybindings', () => { const keybinding = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); expect(keybinding).is.not.undefined; const keybinding2 = keybindingRegistry.getKeybindingsForCommand('undefined.command'); expect(keybinding2.length).is.equal(0); }); it('should set a keymap', () => { const keybindings: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrl+c' }]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); const bindings = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); if (bindings) { const keyCode = KeyCode.parse(bindings[0].keybinding); expect(keyCode.key).to.be.equal(Key.KEY_C); expect(keyCode.ctrl).to.be.true; } }); it('should reset to default in case of invalid keybinding', () => { const keybindings: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrl+invalid' }]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); const bindings = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); if (bindings) { const keyCode = KeyCode.parse(bindings[0].keybinding); expect(keyCode.key).to.be.equal(Key.KEY_A); expect(keyCode.ctrl).to.be.true; } }); it('should remove all disabled keybindings from a command that has multiple keybindings', () => { const keybindings: Keybinding[] = [{ command: TEST_COMMAND2.id, keybinding: 'F3' }, { command: '-' + TEST_COMMAND2.id, context: 'testContext', keybinding: 'ctrl+f1' }, ]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); const bindings = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND2.id); if (bindings) { // a USER one and a DEFAULT one expect(bindings.length).to.be.equal(2); const keyCode = KeyCode.parse(bindings[0].keybinding); expect(keyCode.key).to.be.equal(Key.F3); expect(keyCode.ctrl).to.be.false; const keyCode2 = KeyCode.parse(bindings[1].keybinding); expect(keyCode2.key).to.be.equal(Key.F2); expect(keyCode2.ctrl).to.be.true; } }); it('should register a keybinding', () => { const keybinding: Keybinding = { command: TEST_COMMAND2.id, keybinding: 'F5' }; expect(isKeyBindingRegistered(keybinding)).to.be.false; keybindingRegistry.registerKeybinding(keybinding); expect(isKeyBindingRegistered(keybinding)).to.be.true; } ); it('should unregister all keybindings from a specific command', () => { const otherKeybinding: Keybinding = { command: TEST_COMMAND.id, keybinding: 'F4' }; keybindingRegistry.registerKeybinding(otherKeybinding); expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; const keybinding: Keybinding = { command: TEST_COMMAND2.id, keybinding: 'F5' }; const keybinding2: Keybinding = { command: TEST_COMMAND2.id, keybinding: 'F6' }; keybindingRegistry.registerKeybinding(keybinding); keybindingRegistry.registerKeybinding(keybinding2); expect(isKeyBindingRegistered(keybinding)).to.be.true; expect(isKeyBindingRegistered(keybinding2)).to.be.true; keybindingRegistry.unregisterKeybinding(TEST_COMMAND2); expect(isKeyBindingRegistered(keybinding)).to.be.false; expect(isKeyBindingRegistered(keybinding2)).to.be.false; const bindingsAfterUnregister = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND2.id); expect(bindingsAfterUnregister).not.to.be.undefined; expect(bindingsAfterUnregister.length).to.be.equal(0); expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; }); it('should unregister a specific keybinding', () => { const otherKeybinding: Keybinding = { command: TEST_COMMAND2.id, keybinding: 'F4' }; keybindingRegistry.registerKeybinding(otherKeybinding); const keybinding: Keybinding = { command: TEST_COMMAND2.id, keybinding: 'F5' }; keybindingRegistry.registerKeybinding(keybinding); expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; expect(isKeyBindingRegistered(keybinding)).to.be.true; keybindingRegistry.unregisterKeybinding(keybinding); expect(isKeyBindingRegistered(keybinding)).to.be.false; expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; } ); it('should unregister a specific key', () => { const otherKeybinding: Keybinding = { command: TEST_COMMAND.id, keybinding: 'F4' }; keybindingRegistry.registerKeybinding(otherKeybinding); const testKey = 'F5'; const keybinding: Keybinding = { command: TEST_COMMAND2.id, keybinding: testKey }; const keybinding2: Keybinding = { command: TEST_COMMAND.id, keybinding: testKey }; keybindingRegistry.registerKeybinding(keybinding); keybindingRegistry.registerKeybinding(keybinding2); expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; expect(isKeyBindingRegistered(keybinding)).to.be.true; expect(isKeyBindingRegistered(keybinding2)).to.be.true; keybindingRegistry.unregisterKeybinding(testKey); expect(isKeyBindingRegistered(otherKeybinding)).to.be.true; expect(isKeyBindingRegistered(keybinding)).to.be.false; expect(isKeyBindingRegistered(keybinding2)).to.be.false; } ); it('should register a correct keybinding, then default back to the original for a wrong one after', () => { let keybindings: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrl+c' }]; // Get default binding const keystroke = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); // Set correct new binding keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); const bindings = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); if (bindings) { const keyCode = KeyCode.parse(bindings[0].keybinding); expect(keyCode.key).to.be.equal(Key.KEY_C); expect(keyCode.ctrl).to.be.true; } // Set invalid binding keybindings = [{ command: TEST_COMMAND.id, keybinding: 'ControlLeft+Invalid' }]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); const defaultBindings = keybindingRegistry.getKeybindingsForCommand(TEST_COMMAND.id); if (defaultBindings) { if (keystroke) { const keyCode = KeyCode.parse(defaultBindings[0].keybinding); const keyStrokeCode = KeyCode.parse(keystroke[0].keybinding); expect(keyCode.key).to.be.equal(keyStrokeCode.key); } } }); it('should only return the more specific keybindings when a keystroke is entered', () => { const keybindingsUser: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrl+b' }]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindingsUser); const keybindingsSpecific: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrl+c' }]; const validKeyCode = KeyCode.createKeyCode({ first: Key.KEY_C, modifiers: [KeyModifier.CtrlCmd] }); keybindingRegistry.setKeymap(KeybindingScope.WORKSPACE, keybindingsSpecific); let match = keybindingRegistry.matchKeybinding([KeyCode.createKeyCode({ first: Key.KEY_A, modifiers: [KeyModifier.CtrlCmd] })]); expect(match && match.kind).to.be.equal('full'); match = keybindingRegistry.matchKeybinding([KeyCode.createKeyCode({ first: Key.KEY_B, modifiers: [KeyModifier.CtrlCmd] })]); expect(match && match.kind).to.be.equal('full'); match = keybindingRegistry.matchKeybinding([KeyCode.createKeyCode({ first: Key.KEY_C, modifiers: [KeyModifier.CtrlCmd] })]); const keyCode = match && KeyCode.parse(match.binding.keybinding); expect(keyCode?.key).to.be.equal(validKeyCode.key); }); it('should return partial keybinding matches', () => { const keybindingsUser: Keybinding[] = [{ command: TEST_COMMAND.id, keybinding: 'ctrlcmd+x t' }]; keybindingRegistry.setKeymap(KeybindingScope.USER, keybindingsUser); const validKeyCodes = []; validKeyCodes.push(KeyCode.createKeyCode({ first: Key.KEY_C, modifiers: [KeyModifier.CtrlCmd] })); validKeyCodes.push(KeyCode.createKeyCode({ first: Key.KEY_T })); const match = keybindingRegistry.matchKeybinding(KeySequence.parse('ctrlcmd+x')); expect(match && match.kind).to.be.equal('partial'); }); it('should possible to override keybinding', () => { const overriddenKeybinding = 'ctrlcmd+b a'; const command = TEST_COMMAND_SHADOW.id; const keybindingShadowing: Keybinding[] = [ { command, keybinding: overriddenKeybinding }, { command, keybinding: 'ctrlcmd+b' } ]; keybindingRegistry.registerKeybindings(...keybindingShadowing); const bindings = keybindingRegistry.getKeybindingsForCommand(command); expect(bindings.length).to.be.equal(2); expect(bindings[0].keybinding).to.be.equal('ctrlcmd+b'); expect(bindings[1].keybinding).to.be.equal(overriddenKeybinding); }); it('overridden bindings should be returned last', () => { const keyCode = KeyCode.createKeyCode({ first: Key.KEY_A, modifiers: [KeyModifier.Shift] }); const overriddenDefaultBinding: Keybinding = { keybinding: keyCode.toString(), command: 'test.overridden-default-command' }; const defaultBinding: Keybinding = { keybinding: keyCode.toString(), command: 'test.default-command' }; const userBinding: Keybinding = { keybinding: keyCode.toString(), command: 'test.user-command' }; const workspaceBinding: Keybinding = { keybinding: keyCode.toString(), command: 'test.workspace-command' }; keybindingRegistry.setKeymap(KeybindingScope.DEFAULT, [overriddenDefaultBinding, defaultBinding]); keybindingRegistry.setKeymap(KeybindingScope.USER, [userBinding]); keybindingRegistry.setKeymap(KeybindingScope.WORKSPACE, [workspaceBinding]); // now WORKSPACE bindings are overriding the other scopes let match = keybindingRegistry.matchKeybinding([keyCode]); expect(match?.kind).to.be.equal('full'); expect(match?.binding?.command).to.be.equal(workspaceBinding.command); keybindingRegistry.resetKeybindingsForScope(KeybindingScope.WORKSPACE); // now it should find USER bindings match = keybindingRegistry.matchKeybinding([keyCode]); expect(match?.kind).to.be.equal('full'); expect(match?.binding?.command).to.be.equal(userBinding.command); keybindingRegistry.resetKeybindingsForScope(KeybindingScope.USER); // and finally it should fallback to DEFAULT bindings. match = keybindingRegistry.matchKeybinding([keyCode]); expect(match?.kind).to.be.equal('full'); expect(match?.binding?.command).to.be.equal(defaultBinding.command); keybindingRegistry.resetKeybindingsForScope(KeybindingScope.DEFAULT); // now the registry should be empty match = keybindingRegistry.matchKeybinding([keyCode]); expect(match).to.be.undefined; }); it('should not match disabled keybindings', () => { const keyCode = KeyCode.createKeyCode({ first: Key.KEY_A, modifiers: [KeyModifier.Shift] }); const defaultBinding: Keybinding = { keybinding: keyCode.toString(), command: 'test.workspace-command' }; const disableDefaultBinding: Keybinding = { keybinding: keyCode.toString(), command: '-test.workspace-command' }; keybindingRegistry.setKeymap(KeybindingScope.DEFAULT, [defaultBinding]); let match = keybindingRegistry.matchKeybinding([keyCode]); expect(match?.kind).to.be.equal('full'); expect(match?.binding?.command).to.be.equal(defaultBinding.command); keybindingRegistry.setKeymap(KeybindingScope.USER, [disableDefaultBinding]); match = keybindingRegistry.matchKeybinding([keyCode]); expect(match).to.be.undefined; keybindingRegistry.resetKeybindingsForScope(KeybindingScope.USER); match = keybindingRegistry.matchKeybinding([keyCode]); expect(match?.kind).to.be.equal('full'); expect(match?.binding?.command).to.be.equal(defaultBinding.command); }); }); const TEST_COMMAND: Command = { id: 'test.command' }; const TEST_COMMAND2: Command = { id: 'test.command2' }; const TEST_COMMAND_SHADOW: Command = { id: 'test.command-shadow' }; @injectable() class MockKeyboardLayoutProvider implements KeyboardLayoutProvider { getNativeLayout(): Promise<NativeKeyboardLayout> { return Promise.resolve({ info: { id: 'mock', lang: 'en' }, mapping: {} }); } } @injectable() class MockKeyboardLayoutChangeNotifier implements KeyboardLayoutChangeNotifier { private emitter = new Emitter<NativeKeyboardLayout>(); get onDidChangeNativeLayout(): Event<NativeKeyboardLayout> { return this.emitter.event; } } @injectable() class TestContribution implements CommandContribution, KeybindingContribution { registerCommands(commands: CommandRegistry): void { commands.registerCommand(TEST_COMMAND); commands.registerCommand(TEST_COMMAND2); commands.registerCommand(TEST_COMMAND_SHADOW); } registerKeybindings(keybindings: KeybindingRegistry): void { [{ command: TEST_COMMAND.id, context: 'testContext', keybinding: 'ctrl+a' }, { command: TEST_COMMAND2.id, context: 'testContext', keybinding: 'ctrl+f1' }, { command: TEST_COMMAND2.id, context: 'testContext', keybinding: 'ctrl+f2' }, ].forEach(binding => { keybindings.registerKeybinding(binding); }); } } function isKeyBindingRegistered(keybinding: Keybinding): boolean { const bindings = keybindingRegistry.getKeybindingsForCommand(keybinding.command); expect(bindings).not.to.be.undefined; let keyBindingFound = false; bindings.forEach( (value: Keybinding) => { if (value.command === keybinding.command && value.keybinding === keybinding.keybinding) { keyBindingFound = true; } } ); return keyBindingFound; }