From 179dfdb0bb4ff753a370db59175f457bc4b9e06b Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 14 Jan 2019 13:52:53 +0000 Subject: [PATCH] fix #3986: support vscode when closure contexts Signed-off-by: Anton Kosyakov --- .../src/browser/context-key-service.ts} | 50 +- .../browser/frontend-application-module.ts | 3 + packages/core/src/browser/keybinding.spec.ts | 2 + packages/core/src/browser/keybinding.ts | 29 +- .../src/browser/menu/browser-menu-plugin.ts | 7 +- packages/core/src/common/menu.ts | 1 + .../menu/electron-main-menu-factory.ts | 7 +- .../keymaps/src/browser/keymaps-parser.ts | 5 +- .../src/browser/monaco-context-key-service.ts | 49 ++ .../src/browser/monaco-editor-provider.ts | 7 +- .../src/browser/monaco-frontend-module.ts | 10 + packages/monaco/src/browser/monaco-loader.ts | 14 +- packages/monaco/src/typings/monaco/index.d.ts | 36 ++ .../plugin-ext/src/common/plugin-protocol.ts | 2 +- .../src/hosted/node/scanners/scanner-theia.ts | 2 +- .../context-key/context-key-service.ts | 409 ------------ .../browser/context-key/context-key.spec.ts | 113 ---- .../main/browser/context-key/context-key.ts | 588 ------------------ .../keybindings-contribution-handler.ts | 2 +- .../menus/menus-contribution-handler.spec.ts | 5 +- .../menus/menus-contribution-handler.ts | 26 +- .../browser/plugin-ext-frontend-module.ts | 3 - .../src/main/browser/view/tree-views-main.tsx | 2 +- 23 files changed, 184 insertions(+), 1188 deletions(-) rename packages/{plugin-ext/src/main/browser/context-key/mock-context-key-service.ts => core/src/browser/context-key-service.ts} (51%) create mode 100644 packages/monaco/src/browser/monaco-context-key-service.ts delete mode 100644 packages/plugin-ext/src/main/browser/context-key/context-key-service.ts delete mode 100644 packages/plugin-ext/src/main/browser/context-key/context-key.spec.ts delete mode 100644 packages/plugin-ext/src/main/browser/context-key/context-key.ts diff --git a/packages/plugin-ext/src/main/browser/context-key/mock-context-key-service.ts b/packages/core/src/browser/context-key-service.ts similarity index 51% rename from packages/plugin-ext/src/main/browser/context-key/mock-context-key-service.ts rename to packages/core/src/browser/context-key-service.ts index da506e1a9b03c..6a555793cfbc1 100644 --- a/packages/plugin-ext/src/main/browser/context-key/mock-context-key-service.ts +++ b/packages/core/src/browser/context-key-service.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. + * Copyright (C) 2019 TypeFox 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 @@ -15,39 +15,31 @@ ********************************************************************************/ import { injectable } from 'inversify'; -import { Event } from '@theia/core/lib/common'; -import { ContextKeyService, ContextKey, ContextKeyChangeEvent, ContextKeyExpr, ContextKeyServiceTarget, Context } from './context-key'; -@injectable() -export class MockContextKeyService implements ContextKeyService { - - dispose(): void { } - - onDidChangeContext: Event; +export interface ContextKey { + set(value: T | undefined): void; + reset(): void; + get(): T | undefined; +} +export namespace ContextKey { + // tslint:disable-next-line:no-any + export const None: ContextKey = Object.freeze({ + set: () => { }, + reset: () => { }, + get: () => undefined + }); +} +@injectable() +export class ContextKeyService { createKey(key: string, defaultValue: T | undefined): ContextKey { - return { - get: () => undefined, - set(v: T) { }, - reset() { } - }; + return ContextKey.None; } - - contextMatchesRules(rules: ContextKeyExpr | undefined): boolean { + /** + * It should be implemented by an extension, e.g. by the monaco extension. + */ + match(expression: string, context?: HTMLElement): boolean { return true; } - getContextKeyValue(key: string): T | undefined { - return undefined; - } - - createScoped(target?: ContextKeyServiceTarget): ContextKeyService { - return this; - } - - getContext(target: ContextKeyServiceTarget | null): Context { - return { - getValue: () => undefined - }; - } } diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 02f9e92a1e256..6315f2d25c3e6 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -67,6 +67,7 @@ import { TabBarToolbarRegistry, TabBarToolbarContribution, TabBarToolbarFactory, import { bindCorePreferences } from './core-preferences'; import { QuickPickServiceImpl } from './quick-open/quick-pick-service-impl'; import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; +import { ContextKeyService } from './context-key-service'; export const frontendApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) => { const themeService = ThemeService.get(); @@ -135,6 +136,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bindContributionProvider(bind, CommandContribution); bind(QuickOpenContribution).to(CommandQuickOpenContribution); + bind(ContextKeyService).toSelf().inSingletonScope(); + bind(MenuModelRegistry).toSelf().inSingletonScope(); bindContributionProvider(bind, MenuContribution); diff --git a/packages/core/src/browser/keybinding.spec.ts b/packages/core/src/browser/keybinding.spec.ts index e467e7098772a..373ece00b0f58 100644 --- a/packages/core/src/browser/keybinding.spec.ts +++ b/packages/core/src/browser/keybinding.spec.ts @@ -30,6 +30,7 @@ import { FrontendApplicationStateService } from './frontend-application-state'; import * as os from '../common/os'; import * as chai from 'chai'; import * as sinon from 'sinon'; +import { ContextKeyService } from './context-key-service'; disableJSDOM(); @@ -72,6 +73,7 @@ before(async () => { bind(StatusBar).toService(StatusBarImpl); bind(CommandService).toService(CommandRegistry); bind(LabelParser).toSelf().inSingletonScope(); + bind(ContextKeyService).toSelf().inSingletonScope(); bind(FrontendApplicationStateService).toSelf().inSingletonScope(); }); diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index e18c354482271..d6a5a5c8d7beb 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -21,6 +21,7 @@ import { ContributionProvider } from '../common/contribution-provider'; import { ILogger } from '../common/logger'; import { StatusBarAlignment, StatusBar } from './status-bar/status-bar'; import { isOSX } from '../common/os'; +import { ContextKeyService } from './context-key-service'; export enum KeybindingScope { DEFAULT, @@ -74,6 +75,10 @@ export interface Keybinding { * keybinding context. */ context?: string; + /** + * https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts + */ + when?: string; } export interface ScopedKeybinding extends Keybinding { @@ -132,6 +137,9 @@ export class KeybindingRegistry { @inject(ILogger) protected readonly logger: ILogger; + @inject(ContextKeyService) + protected readonly whenContextService: ContextKeyService; + onStart(): void { this.registerContext(KeybindingContexts.NOOP_CONTEXT); this.registerContext(KeybindingContexts.DEFAULT_CONTEXT); @@ -458,12 +466,7 @@ export class KeybindingRegistry { } for (const binding of bindings) { - const context = binding.context !== undefined && this.contexts[binding.context]; - - /* Only execute if it has no context (global context) or if we're in - that context. */ - if (!context || context.isEnabled(binding)) { - + if (this.isEnabled(binding, event)) { if (this.isPseudoCommand(binding.command)) { /* Don't do anything, let the event propagate. */ return true; @@ -489,6 +492,20 @@ export class KeybindingRegistry { return false; } + /** + * Only execute if it has no context (global context) or if we're in that context. + */ + protected isEnabled(binding: Keybinding, event: KeyboardEvent): boolean { + const context = binding.context && this.contexts[binding.context]; + if (context && !context.isEnabled(binding)) { + return false; + } + if (binding.when && !this.whenContextService.match(binding.when, event.target)) { + return false; + } + return true; + } + /** * Run the command matching to the given keyboard event. */ diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index bdc9ea3ed58d4..43bb80ad48388 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -23,6 +23,7 @@ import { } from '../../common'; import { KeybindingRegistry, Keybinding } from '../keybinding'; import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application'; +import { ContextKeyService } from '../context-key-service'; @injectable() export class BrowserMainMenuFactory { @@ -30,6 +31,9 @@ export class BrowserMainMenuFactory { @inject(ILogger) protected readonly logger: ILogger; + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + constructor( @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry, @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry, @@ -86,12 +90,13 @@ export class BrowserMainMenuFactory { this.logger.warn(`Command with ID ${command.id} is already registered`); return; } + const { when } = menu.action; commands.addCommand(command.id, { execute: () => this.commandRegistry.executeCommand(command.id), label: menu.label, icon: menu.icon, isEnabled: () => this.commandRegistry.isEnabled(command.id), - isVisible: () => this.commandRegistry.isVisible(command.id), + isVisible: () => this.commandRegistry.isVisible(command.id) && (!when || this.contextKeyService.match(when)), isToggled: () => this.commandRegistry.isToggled(command.id) }); diff --git a/packages/core/src/common/menu.ts b/packages/core/src/common/menu.ts index 95d7b75f96edb..aede4b03d81d4 100644 --- a/packages/core/src/common/menu.ts +++ b/packages/core/src/common/menu.ts @@ -24,6 +24,7 @@ export interface MenuAction { label?: string icon?: string order?: string + when?: string } export namespace MenuAction { diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index eba03e3b903c9..357b0ed57dcdd 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -21,6 +21,7 @@ import { MAIN_MENU_BAR, MenuModelRegistry, MenuPath } from '../../common'; import { PreferenceService, KeybindingRegistry, Keybinding, KeyCode, Key } from '../../browser'; +import { ContextKeyService } from '../../browser/context-key-service'; @injectable() export class ElectronMainMenuFactory { @@ -28,6 +29,9 @@ export class ElectronMainMenuFactory { protected _menu: Electron.Menu; protected _toggledCommands: Set = new Set(); + @inject(ContextKeyService) + protected readonly contextKeyService: ContextKeyService; + constructor( @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry, @inject(PreferenceService) protected readonly preferencesService: PreferenceService, @@ -106,7 +110,8 @@ export class ElectronMainMenuFactory { throw new Error(`Unknown command with ID: ${commandId}.`); } - if (!this.commandRegistry.isVisible(commandId)) { + if (!this.commandRegistry.isVisible(commandId) + || (!!menu.action.when && !this.contextKeyService.match(menu.action.when))) { continue; } diff --git a/packages/keymaps/src/browser/keymaps-parser.ts b/packages/keymaps/src/browser/keymaps-parser.ts index 4cbd943950dc8..eb3d133a1e215 100644 --- a/packages/keymaps/src/browser/keymaps-parser.ts +++ b/packages/keymaps/src/browser/keymaps-parser.ts @@ -32,10 +32,13 @@ export const keymapsSchema = { }, context: { type: 'string' + }, + when: { + type: 'string' } }, required: ['command', 'keybinding'], - optional: ['context'], + optional: ['context', 'when'], additionalProperties: false } }; diff --git a/packages/monaco/src/browser/monaco-context-key-service.ts b/packages/monaco/src/browser/monaco-context-key-service.ts new file mode 100644 index 0000000000000..601d6f55857ae --- /dev/null +++ b/packages/monaco/src/browser/monaco-context-key-service.ts @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (C) 2019 TypeFox 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; + +@injectable() +export class MonacoContextKeyService extends ContextKeyService { + + @inject(monaco.contextKeyService.ContextKeyService) + protected readonly contextKeyService: monaco.contextKeyService.ContextKeyService; + + createKey(key: string, defaultValue: T | undefined): ContextKey { + return this.contextKeyService.createKey(key, defaultValue); + } + + match(expression: string, context?: HTMLElement): boolean { + const parsed = this.parse(expression); + if (!context) { + return this.contextKeyService.contextMatchesRules(parsed); + } + const keyContext = this.contextKeyService.getContext(context); + return monaco.keybindings.KeybindingResolver.contextMatchesRules(keyContext, parsed); + } + + protected readonly expressions = new Map(); + protected parse(when: string): monaco.contextkey.ContextKeyExpr { + let expression = this.expressions.get(when); + if (!expression) { + expression = monaco.contextkey.ContextKeyExpr.deserialize(when); + this.expressions.set(when, expression); + } + return expression; + } + +} diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index f6cbe6458c721..a8b0c0aa1f026 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -57,7 +57,8 @@ export class MonacoEditorProvider { @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences, @inject(MonacoQuickOpenService) protected readonly quickOpenService: MonacoQuickOpenService, @inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory, - @inject(ApplicationServer) protected readonly applicationServer: ApplicationServer + @inject(ApplicationServer) protected readonly applicationServer: ApplicationServer, + @inject(monaco.contextKeyService.ContextKeyService) protected readonly contextKeyService: monaco.contextKeyService.ContextKeyService ) { const init = monaco.services.StaticServices.init.bind(monaco.services.StaticServices); this.applicationServer.getBackendOS().then(os => { @@ -108,6 +109,7 @@ export class MonacoEditorProvider { protected async doCreateEditor(factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise): Promise { const commandService = this.commandServiceFactory(); + const contextKeyService = this.contextKeyService.createScoped(); const { codeEditorService, textModelService, contextMenuService } = this; const IWorkspaceEditService = this.bulkEditService; const toDispose = new DisposableCollection(); @@ -116,7 +118,8 @@ export class MonacoEditorProvider { textModelService, contextMenuService, commandService, - IWorkspaceEditService + IWorkspaceEditService, + contextKeyService }, toDispose); editor.onDispose(() => toDispose.dispose()); diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index ecabd08e36a64..03e2ce2f59d50 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -50,11 +50,17 @@ import { MonacoBulkEditService } from './monaco-bulk-edit-service'; import { MonacoOutlineDecorator } from './monaco-outline-decorator'; import { OutlineTreeDecorator } from '@theia/outline-view/lib/browser/outline-decorator-service'; import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { MonacoContextKeyService } from './monaco-context-key-service'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); +decorate(injectable(), monaco.contextKeyService.ContextKeyService); export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(MonacoContextKeyService).toSelf().inSingletonScope(); + rebind(ContextKeyService).toService(MonacoContextKeyService); + bind(MonacoSnippetSuggestProvider).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).to(MonacoFrontendApplicationContribution).inSingletonScope(); @@ -68,6 +74,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoWorkspace).toSelf().inSingletonScope(); bind(Workspace).toService(MonacoWorkspace); + // TODO: https://github.com/theia-ide/theia/issues/4073 + const configurationService = monaco.services.StaticServices.configurationService.get(); + const contextKeyService = new monaco.contextKeyService.ContextKeyService(configurationService); + bind(monaco.contextKeyService.ContextKeyService).toConstantValue(contextKeyService); bind(MonacoBulkEditService).toSelf().inSingletonScope(); bind(MonacoEditorService).toSelf().inSingletonScope(); bind(MonacoTextModelService).toSelf().inSingletonScope(); diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts index 0985470e3c0ec..42ee9bc220ac6 100644 --- a/packages/monaco/src/browser/monaco-loader.ts +++ b/packages/monaco/src/browser/monaco-loader.ts @@ -69,15 +69,19 @@ export function loadMonaco(vsRequire: any): Promise { 'vs/editor/contrib/snippet/snippetParser', 'vs/platform/configuration/common/configuration', 'vs/editor/browser/services/codeEditorService', - 'vs/editor/browser/services/codeEditorServiceImpl' - ], (css: any, html: any, commands: any, actions: any, registry: any, resolver: any, resolvedKeybinding: any, + 'vs/editor/browser/services/codeEditorServiceImpl', + 'vs/platform/contextkey/common/contextkey', + 'vs/platform/contextkey/browser/contextKeyService' + ], (css: any, html: any, commands: any, actions: any, + keybindingsRegistry: any, keybindingResolver: any, resolvedKeybinding: any, keyCodes: any, editorExtensions: any, simpleServices: any, standaloneServices: any, quickOpen: any, quickOpenWidget: any, quickOpenModel: any, filters: any, styler: any, platform: any, modes: any, suggest: any, suggestController: any, findController: any, rename: any, snippetParser: any, - configuration: any, codeEditorService: any, codeEditorServiceImpl: any) => { + configuration: any, codeEditorService: any, codeEditorServiceImpl: any, + contextKey: any, contextKeyService: any) => { const global: any = self; global.monaco.commands = commands; global.monaco.actions = actions; - global.monaco.keybindings = Object.assign({}, registry, resolver, resolvedKeybinding, keyCodes); + global.monaco.keybindings = Object.assign({}, keybindingsRegistry, keybindingResolver, resolvedKeybinding, keyCodes); global.monaco.services = Object.assign({}, simpleServices, standaloneServices, configuration, codeEditorService, codeEditorServiceImpl); global.monaco.quickOpen = Object.assign({}, quickOpen, quickOpenWidget, quickOpenModel); global.monaco.filters = filters; @@ -90,6 +94,8 @@ export function loadMonaco(vsRequire: any): Promise { global.monaco.findController = findController; global.monaco.rename = rename; global.monaco.snippetParser = snippetParser; + global.monaco.contextkey = contextKey; + global.monaco.contextKeyService = contextKeyService; resolve(); }); }); diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index ef34a92837576..8b4912915e3cb 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -68,6 +68,7 @@ declare module monaco.editor { contextMenuService?: IContextMenuService; commandService?: monaco.commands.ICommandService; IWorkspaceEditService?: IBulkEditService; + contextKeyService?: monaco.contextKeyService.IContextKeyService; } export interface IResourceInput { @@ -325,6 +326,10 @@ declare module monaco.platform { declare module monaco.keybindings { + export class KeybindingResolver { + static contextMatchesRules(context: monaco.contextKeyService.IContext, rules: monaco.contextkey.ContextKeyExpr): boolean; + } + export const enum KeybindingType { Simple = 1, Chord = 2 @@ -424,6 +429,8 @@ declare module monaco.services { export const ICodeEditorService: any; export const IConfigurationService: any; + export interface IConfigurationService { } + export abstract class CodeEditorServiceImpl implements monaco.editor.ICodeEditorService { constructor(themeService: IStandaloneThemeService); abstract getActiveCodeEditor(): monaco.editor.ICodeEditor | undefined; @@ -508,6 +515,7 @@ declare module monaco.services { export const standaloneThemeService: LazyStaticService; export const modeService: LazyStaticService; export const codeEditorService: LazyStaticService; + export const configurationService: LazyStaticService; } } @@ -980,3 +988,31 @@ declare module monaco.snippetParser { export class TextmateSnippet { } } + +declare module monaco.contextKeyService { + + export interface IContextKey { + set(value: T | undefined): void; + reset(): void; + get(): T | undefined; + } + + export interface IContextKeyService { } + + export interface IContext { } + + export class ContextKeyService implements IContextKeyService { + constructor(configurationService: monaco.services.IConfigurationService); + createScoped(target?: HTMLElement): IContextKeyService; + getContext(target?: HTMLElement): IContext; + createKey(key: string, defaultValue: T | undefined): IContextKey; + contextMatchesRules(rules: monaco.contextkey.ContextKeyExpr | undefined): boolean; + } + +} + +declare module monaco.contextkey { + export class ContextKeyExpr { + static deserialize(when: string): ContextKeyExpr; + } +} diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index a29816403eb7e..eac47f0190fc1 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -464,7 +464,7 @@ export interface Menu { export interface Keybinding { keybinding: string; command: string; - context?: string; + when?: string; } /** diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 453a8a1d15d7e..b5f31f16152ec 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -217,7 +217,7 @@ export class TheiaPluginScanner implements PluginScanner { return { keybinding: rawKeybinding.key, command: rawKeybinding.command, - context: rawKeybinding.when + when: rawKeybinding.when }; } diff --git a/packages/plugin-ext/src/main/browser/context-key/context-key-service.ts b/packages/plugin-ext/src/main/browser/context-key/context-key-service.ts deleted file mode 100644 index c73091f9e5143..0000000000000 --- a/packages/plugin-ext/src/main/browser/context-key/context-key-service.ts +++ /dev/null @@ -1,409 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -// adjusted to Theia APIs - -import { injectable, unmanaged, inject } from 'inversify'; -import { PreferenceService } from '@theia/core/lib/browser'; -import { Event, Emitter, DisposableCollection, Disposable } from '@theia/core/lib/common'; -import { ContextKeyExpr, Context, ContextKey, ContextKeyChangeEvent, ContextKeyService, ContextKeyServiceTarget, ReadableSet } from './context-key'; - -const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context'; - -// tslint:disable:no-any -export class DefaultContext implements Context { - - protected value: { [key: string]: any; }; - - constructor(protected readonly id: number, protected readonly parent: DefaultContext | undefined) { - // tslint:disable-next-line:no-null-keyword - this.value = Object.create(null); - this.value['_contextId'] = id; - } - - public setValue(key: string, value: any): boolean { - if (this.value[key] !== value) { - this.value[key] = value; - return true; - } - return false; - } - - public removeValue(key: string): boolean { - if (key in this.value) { - delete this.value[key]; - return true; - } - return false; - } - - public getValue(key: string): T | undefined { - const ret = this.value[key]; - if (typeof ret === 'undefined' && this.parent) { - return this.parent.getValue(key); - } - return ret; - } - - collectAllValues(): { [key: string]: any; } { - // tslint:disable-next-line:no-null-keyword - let result = this.parent ? this.parent.collectAllValues() : Object.create(null); - result = { ...result, ...this.value }; - delete result['_contextId']; - return result; - } -} - -class NullContext extends DefaultContext { - - static readonly INSTANCE = new NullContext(); - - constructor() { - super(-1, undefined); - } - - public setValue(key: string, value: any): boolean { - return false; - } - - public removeValue(key: string): boolean { - return false; - } - - public getValue(key: string): T | undefined { - return undefined; - } - - collectAllValues(): { [key: string]: any; } { - // tslint:disable-next-line:no-null-keyword - return Object.create(null); - } -} - -class ConfigAwareContextValuesContainer extends DefaultContext { - - private static keyPrefix = 'config.'; - - private readonly values = new Map(); - private readonly listener: Disposable; - - constructor(readonly id: number, private readonly configurationService: PreferenceService, readonly emitter: Emitter) { - super(id, undefined); - - this.listener = this.configurationService.onPreferenceChanged(event => { - const changedKeys: string[] = []; - const contextKey = `config.${event.preferenceName}`; - if (this.values.has(contextKey)) { - this.values.delete(contextKey); - changedKeys.push(contextKey); - } - emitter.fire(changedKeys); - }); - } - - dispose(): void { - this.listener.dispose(); - } - - getValue(key: string): any { - if (key.indexOf(ConfigAwareContextValuesContainer.keyPrefix) !== 0) { - return super.getValue(key); - } - - if (this.values.has(key)) { - return this.values.get(key); - } - - const configKey = key.substr(ConfigAwareContextValuesContainer.keyPrefix.length); - const configValue = this.configurationService.get(configKey); - let value: any = undefined; - switch (typeof configValue) { - case 'number': - case 'boolean': - case 'string': - value = configValue; - break; - } - - this.values.set(key, value); - return value; - } - - setValue(key: string, value: any): boolean { - return super.setValue(key, value); - } - - removeValue(key: string): boolean { - return super.removeValue(key); - } - - collectAllValues(): { [key: string]: any; } { - const result: { - [key: string]: any - // tslint:disable-next-line:no-null-keyword - } = Object.create(null); - this.values.forEach((value, index) => result[index] = value); - return { ...result, ...super.collectAllValues() }; - } -} - -class ContextKeyImpl implements ContextKey { - - constructor(private readonly parent: AbstractContextKeyService, private readonly key: string, private readonly defaultValue: T | undefined) { - this.reset(); - } - - public set(value: T): void { - this.parent.setContext(this.key, value); - } - - public reset(): void { - if (typeof this.defaultValue === 'undefined') { - this.parent.removeContext(this.key); - } else { - this.parent.setContext(this.key, this.defaultValue); - } - } - - public get(): T | undefined { - return this.parent.getContextKeyValue(this.key); - } -} - -class SimpleContextKeyChangeEvent implements ContextKeyChangeEvent { - constructor(private readonly key: string) { } - affectsSome(keys: ReadableSet): boolean { - return keys.has(this.key); - } -} - -class ArrayContextKeyChangeEvent implements ContextKeyChangeEvent { - constructor(private readonly keys: string[]) { } - affectsSome(keys: ReadableSet): boolean { - for (const key of this.keys) { - if (keys.has(key)) { - return true; - } - } - return false; - } -} - -@injectable() -export abstract class AbstractContextKeyService implements ContextKeyService { - - protected isDisposed: boolean; - protected onDidChangeContextEvent: Event; - protected onDidChangeContextKey: Emitter; - - constructor(@unmanaged() protected readonly myContextId: number) { - this.isDisposed = false; - this.onDidChangeContextKey = new Emitter(); - } - - abstract dispose(): void; - - public createKey(key: string, defaultValue: T | undefined): ContextKey { - if (this.isDisposed) { - throw new Error('AbstractContextKeyService has been disposed'); - } - return new ContextKeyImpl(this, key, defaultValue); - } - - public get onDidChangeContext(): Event { - if (!this.onDidChangeContextEvent) { - this.onDidChangeContextEvent = Event.map(this.onDidChangeContextKey.event, ((changedKeyOrKeys: string | string[]): ContextKeyChangeEvent => - typeof changedKeyOrKeys === 'string' - ? new SimpleContextKeyChangeEvent(changedKeyOrKeys) - : new ArrayContextKeyChangeEvent(changedKeyOrKeys) - )); - } - return this.onDidChangeContextEvent; - } - - public createScoped(domNode: ContextKeyServiceTarget): ContextKeyService { - if (this.isDisposed) { - throw new Error('AbstractContextKeyService has been disposed'); - } - return new ScopedContextKeyService(this, this.onDidChangeContextKey, domNode); - } - - public contextMatchesRules(rules: ContextKeyExpr | undefined): boolean { - if (this.isDisposed) { - throw new Error('AbstractContextKeyService has been disposed'); - } - const context = this.getContextValuesContainer(this.myContextId); - if (!rules) { - return true; - } - return rules.evaluate(context); - } - - public getContextKeyValue(key: string): T | undefined { - if (this.isDisposed) { - return undefined; - } - return this.getContextValuesContainer(this.myContextId).getValue(key); - } - - public setContext(key: string, value: any): void { - if (this.isDisposed) { - return; - } - const myContext = this.getContextValuesContainer(this.myContextId); - if (!myContext) { - return; - } - if (myContext.setValue(key, value)) { - this.onDidChangeContextKey.fire(key); - } - } - - public removeContext(key: string): void { - if (this.isDisposed) { - return; - } - if (this.getContextValuesContainer(this.myContextId).removeValue(key)) { - this.onDidChangeContextKey.fire(key); - } - } - - public getContext(target: ContextKeyServiceTarget | null): Context { - if (this.isDisposed) { - return NullContext.INSTANCE; - } - return this.getContextValuesContainer(findContextAttr(target)); - } - - public abstract getContextValuesContainer(contextId: number): DefaultContext; - public abstract createChildContext(parentContextId?: number): number; - public abstract disposeContext(contextId: number): void; -} - -@injectable() -export class ContextKeyServiceImpl extends AbstractContextKeyService implements ContextKeyService { - - private lastContextId: number; - private contexts: { - [contextId: string]: DefaultContext; - }; - - private toDispose: DisposableCollection; - - constructor(@inject(PreferenceService) configurationService: PreferenceService) { - super(0); - this.toDispose = new DisposableCollection(); - this.lastContextId = 0; - // tslint:disable-next-line:no-null-keyword - this.contexts = Object.create(null); - - const myContext = new ConfigAwareContextValuesContainer(this.myContextId, configurationService, this.onDidChangeContextKey); - this.contexts[String(this.myContextId)] = myContext; - this.toDispose.push(myContext); - } - - public dispose(): void { - this.isDisposed = true; - this.toDispose.dispose(); - } - - public getContextValuesContainer(contextId: number): DefaultContext { - if (this.isDisposed) { - return NullContext.INSTANCE; - } - return this.contexts[String(contextId)]; - } - - public createChildContext(parentContextId: number = this.myContextId): number { - if (this.isDisposed) { - throw new Error('ContextKeyService has been disposed'); - } - const id = (++this.lastContextId); - this.contexts[String(id)] = new DefaultContext(id, this.getContextValuesContainer(parentContextId)); - return id; - } - - public disposeContext(contextId: number): void { - if (this.isDisposed) { - return; - } - delete this.contexts[String(contextId)]; - } -} - -class ScopedContextKeyService extends AbstractContextKeyService { - - constructor(private readonly parent: AbstractContextKeyService, emitter: Emitter, private domNode?: ContextKeyServiceTarget) { - super(parent.createChildContext()); - this.onDidChangeContextKey = emitter; - - if (domNode) { - this.domNode = domNode; - this.domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this.myContextId)); - } - } - - public dispose(): void { - this.isDisposed = true; - this.parent.disposeContext(this.myContextId); - if (this.domNode) { - this.domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR); - this.domNode = undefined; - } - } - - public get onDidChangeContext(): Event { - return this.parent.onDidChangeContext; - } - - public getContextValuesContainer(contextId: number): DefaultContext { - if (this.isDisposed) { - return NullContext.INSTANCE; - } - return this.parent.getContextValuesContainer(contextId); - } - - public createChildContext(parentContextId: number = this.myContextId): number { - if (this.isDisposed) { - throw new Error('ScopedContextKeyService has been disposed'); - } - return this.parent.createChildContext(parentContextId); - } - - public disposeContext(contextId: number): void { - if (this.isDisposed) { - return; - } - this.parent.disposeContext(contextId); - } -} - -function findContextAttr(domNode: ContextKeyServiceTarget | null): number { - while (domNode) { - if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) { - const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR); - if (attr) { - return parseInt(attr, 10); - } - return NaN; - } - domNode = domNode.parentElement; - } - return 0; -} diff --git a/packages/plugin-ext/src/main/browser/context-key/context-key.spec.ts b/packages/plugin-ext/src/main/browser/context-key/context-key.spec.ts deleted file mode 100644 index f358b61ae8de8..0000000000000 --- a/packages/plugin-ext/src/main/browser/context-key/context-key.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -// adjusted to Theia APIs - -import 'mocha'; -import * as assert from 'assert'; -import { ContextKeyExpr } from './context-key'; - -// tslint:disable:no-any -function createContext(ctx: any) { - return { - getValue: (key: string) => ctx[key] - }; -} - -describe('ContextKeyExpr', () => { - it('ContextKeyExpr.equals', () => { - const a = ContextKeyExpr.and( - ContextKeyExpr.has('a1'), - ContextKeyExpr.and(ContextKeyExpr.has('and.a')), - ContextKeyExpr.has('a2'), - ContextKeyExpr.regex('d3', /d.*/), - ContextKeyExpr.regex('d4', /\*\*3*/), - ContextKeyExpr.equals('b1', 'bb1'), - ContextKeyExpr.equals('b2', 'bb2'), - ContextKeyExpr.notEquals('c1', 'cc1'), - ContextKeyExpr.notEquals('c2', 'cc2'), - ContextKeyExpr.not('d1'), - ContextKeyExpr.not('d2') - ); - const b = ContextKeyExpr.and( - ContextKeyExpr.equals('b2', 'bb2'), - ContextKeyExpr.notEquals('c1', 'cc1'), - ContextKeyExpr.not('d1'), - ContextKeyExpr.regex('d4', /\*\*3*/), - ContextKeyExpr.notEquals('c2', 'cc2'), - ContextKeyExpr.has('a2'), - ContextKeyExpr.equals('b1', 'bb1'), - ContextKeyExpr.regex('d3', /d.*/), - ContextKeyExpr.has('a1'), - ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)), - ContextKeyExpr.not('d2') - ); - assert(a.equals(b), 'expressions should be equal'); - }); - - it('normalize', () => { - const key1IsTrue = ContextKeyExpr.equals('key1', true); - const key1IsNotFalse = ContextKeyExpr.notEquals('key1', false); - const key1IsFalse = ContextKeyExpr.equals('key1', false); - const key1IsNotTrue = ContextKeyExpr.notEquals('key1', true); - - assert.ok(key1IsTrue.normalize()!.equals(ContextKeyExpr.has('key1'))); - assert.ok(key1IsNotFalse.normalize()!.equals(ContextKeyExpr.has('key1'))); - assert.ok(key1IsFalse.normalize()!.equals(ContextKeyExpr.not('key1'))); - assert.ok(key1IsNotTrue.normalize()!.equals(ContextKeyExpr.not('key1'))); - }); - - it('evaluate', () => { - /* tslint:disable:triple-equals */ - const context = createContext({ - 'a': true, - 'b': false, - 'c': '5', - 'd': 'd' - }); - function testExpression(expr: string, expected: boolean): void { - const rules = ContextKeyExpr.deserialize(expr); - assert.equal(rules!.evaluate(context), expected, expr); - } - function testBatch(expr: string, value: any): void { - testExpression(expr, !!value); - testExpression(expr + ' == true', !!value); - testExpression(expr + ' != true', !value); - testExpression(expr + ' == false', !value); - testExpression(expr + ' != false', !!value); - testExpression(expr + ' == 5', value == '5'); - testExpression(expr + ' != 5', value != '5'); - testExpression('!' + expr, !value); - testExpression(expr + ' =~ /d.*/', /d.*/.test(value)); - testExpression(expr + ' =~ /D/i', /D/i.test(value)); - } - - testBatch('a', true); - testBatch('b', false); - testBatch('c', '5'); - testBatch('d', 'd'); - testBatch('z', undefined); - - testExpression('a && !b', true && !false); - testExpression('a && b', true && false); - testExpression('a && !b && c == 5', true && !false && '5' == '5'); - testExpression('d =~ /e.*/', false); - /* tslint:enable:triple-equals */ - }); -}); diff --git a/packages/plugin-ext/src/main/browser/context-key/context-key.ts b/packages/plugin-ext/src/main/browser/context-key/context-key.ts deleted file mode 100644 index 80f689b8de1c2..0000000000000 --- a/packages/plugin-ext/src/main/browser/context-key/context-key.ts +++ /dev/null @@ -1,588 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0 - ********************************************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -// adjusted to Theia APIs - -import { Event } from '@theia/core/lib/common'; - -export const enum ContextKeyExprType { - Defined = 1, - Not = 2, - Equals = 3, - NotEquals = 4, - And = 5, - Regex = 6 -} - -// tslint:disable:no-any -export abstract class ContextKeyExpr { - - public static has(key: string): ContextKeyExpr { - return new ContextKeyDefinedExpr(key); - } - - public static equals(key: string, value: any): ContextKeyExpr { - return new ContextKeyEqualsExpr(key, value); - } - - public static notEquals(key: string, value: any): ContextKeyExpr { - return new ContextKeyNotEqualsExpr(key, value); - } - - public static regex(key: string, value: RegExp): ContextKeyExpr { - return new ContextKeyRegexExpr(key, value); - } - - public static not(key: string): ContextKeyExpr { - return new ContextKeyNotExpr(key); - } - - public static and(...expr: (ContextKeyExpr | undefined)[]): ContextKeyExpr { - return new ContextKeyAndExpr(expr); - } - - public static deserialize(serialized: string | undefined): ContextKeyExpr | undefined { - if (!serialized) { - return undefined; - } - - const pieces = serialized.split('&&'); - const result = new ContextKeyAndExpr(pieces.map(p => this.deserializeOne(p))); - return result.normalize(); - } - - private static deserializeOne(serializedOne: string): ContextKeyExpr { - serializedOne = serializedOne.trim(); - - if (serializedOne.indexOf('!=') >= 0) { - const pieces = serializedOne.split('!='); - return new ContextKeyNotEqualsExpr(pieces[0].trim(), this.deserializeValue(pieces[1])); - } - - if (serializedOne.indexOf('==') >= 0) { - const pieces = serializedOne.split('=='); - return new ContextKeyEqualsExpr(pieces[0].trim(), this.deserializeValue(pieces[1])); - } - - if (serializedOne.indexOf('=~') >= 0) { - const pieces = serializedOne.split('=~'); - return new ContextKeyRegexExpr(pieces[0].trim(), this.deserializeRegexValue(pieces[1])); - } - - if (/^\!\s*/.test(serializedOne)) { - return new ContextKeyNotExpr(serializedOne.substr(1).trim()); - } - - return new ContextKeyDefinedExpr(serializedOne); - } - - private static deserializeValue(serializedValue: string): any { - serializedValue = serializedValue.trim(); - - if (serializedValue === 'true') { - return true; - } - - if (serializedValue === 'false') { - return false; - } - - const m = /^'([^']*)'$/.exec(serializedValue); - if (m) { - return m[1].trim(); - } - - return serializedValue; - } - - private static deserializeRegexValue(serializedValue: string): RegExp | undefined { - if (serializedValue.trim().length === 0) { - console.warn('missing regexp-value for =~-expression'); - return undefined; - } - - const start = serializedValue.indexOf('/'); - const end = serializedValue.lastIndexOf('/'); - if (start === end || start < 0 /* || to < 0 */) { - console.warn(`bad regexp-value '${serializedValue}', missing /-enclosure`); - return undefined; - } - - const value = serializedValue.slice(start + 1, end); - const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : ''; - try { - return new RegExp(value, caseIgnoreFlag); - } catch (e) { - console.warn(`bad regexp-value '${serializedValue}', parse error: ${e}`); - return undefined; - } - } - - public abstract getType(): ContextKeyExprType; - public abstract equals(other: ContextKeyExpr): boolean; - public abstract evaluate(context: Context): boolean; - public abstract normalize(): ContextKeyExpr | undefined; - public abstract serialize(): string; - public abstract keys(): string[]; -} - -function cmp(a: ContextKeyExpr, b: ContextKeyExpr): number { - const aType = a.getType(); - const bType = b.getType(); - if (aType !== bType) { - return aType - bType; - } - switch (aType) { - case ContextKeyExprType.Defined: - return (a).cmp(b); - case ContextKeyExprType.Not: - return (a).cmp(b); - case ContextKeyExprType.Equals: - return (a).cmp(b); - case ContextKeyExprType.NotEquals: - return (a).cmp(b); - case ContextKeyExprType.Regex: - return (a).cmp(b); - default: - throw new Error('Unknown ContextKeyExpr!'); - } -} - -export class ContextKeyDefinedExpr implements ContextKeyExpr { - constructor(protected key: string) { } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.Defined; - } - - public cmp(other: ContextKeyDefinedExpr): number { - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyDefinedExpr) { - return (this.key === other.key); - } - return false; - } - - public evaluate(context: Context): boolean { - return (!!context.getValue(this.key)); - } - - public normalize(): ContextKeyExpr { - return this; - } - - public serialize(): string { - return this.key; - } - - public keys(): string[] { - return [this.key]; - } -} - -export class ContextKeyEqualsExpr implements ContextKeyExpr { - constructor(private key: string, private value: any) { } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.Equals; - } - - public cmp(other: ContextKeyEqualsExpr): number { - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyEqualsExpr) { - return (this.key === other.key && this.value === other.value); - } - return false; - } - - public evaluate(context: Context): boolean { - /* tslint:disable:triple-equals */ - return (context.getValue(this.key) == this.value); - /* tslint:enable:triple-equals */ - } - - public normalize(): ContextKeyExpr { - if (typeof this.value === 'boolean') { - if (this.value) { - return new ContextKeyDefinedExpr(this.key); - } - return new ContextKeyNotExpr(this.key); - } - return this; - } - - public serialize(): string { - if (typeof this.value === 'boolean') { - return this.normalize().serialize(); - } - - return this.key + ' == \'' + this.value + '\''; - } - - public keys(): string[] { - return [this.key]; - } -} - -export class ContextKeyNotEqualsExpr implements ContextKeyExpr { - constructor(private key: string, private value: any) { } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.NotEquals; - } - - public cmp(other: ContextKeyNotEqualsExpr): number { - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyNotEqualsExpr) { - return (this.key === other.key && this.value === other.value); - } - return false; - } - - public evaluate(context: Context): boolean { - /* tslint:disable:triple-equals */ - return (context.getValue(this.key) != this.value); - /* tslint:enable:triple-equals */ - } - - public normalize(): ContextKeyExpr { - if (typeof this.value === 'boolean') { - if (this.value) { - return new ContextKeyNotExpr(this.key); - } - return new ContextKeyDefinedExpr(this.key); - } - return this; - } - - public serialize(): string { - if (typeof this.value === 'boolean') { - return this.normalize().serialize(); - } - - return this.key + ' != \'' + this.value + '\''; - } - - public keys(): string[] { - return [this.key]; - } -} - -export class ContextKeyNotExpr implements ContextKeyExpr { - constructor(private key: string) { } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.Not; - } - - public cmp(other: ContextKeyNotExpr): number { - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyNotExpr) { - return (this.key === other.key); - } - return false; - } - - public evaluate(context: Context): boolean { - return (!context.getValue(this.key)); - } - - public normalize(): ContextKeyExpr { - return this; - } - - public serialize(): string { - return '!' + this.key; - } - - public keys(): string[] { - return [this.key]; - } -} - -export class ContextKeyRegexExpr implements ContextKeyExpr { - constructor(private key: string, private regexp: RegExp | undefined) { } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.Regex; - } - - public cmp(other: ContextKeyRegexExpr): number { - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - const thisSource = this.regexp ? this.regexp.source : ''; - const otherSource = other.regexp ? other.regexp.source : ''; - if (thisSource < otherSource) { - return -1; - } - if (thisSource > otherSource) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyRegexExpr) { - const thisSource = this.regexp ? this.regexp.source : ''; - const otherSource = other.regexp ? other.regexp.source : ''; - return (this.key === other.key && thisSource === otherSource); - } - return false; - } - - public evaluate(context: Context): boolean { - const value = context.getValue(this.key); - return this.regexp ? this.regexp.test(value) : false; - } - - public normalize(): ContextKeyExpr { - return this; - } - - public serialize(): string { - const value = this.regexp - ? `/${this.regexp.source}/${this.regexp.ignoreCase ? 'i' : ''}` - : '/invalid/'; - return `${this.key} =~ ${value}`; - } - - public keys(): string[] { - return [this.key]; - } -} - -export class ContextKeyAndExpr implements ContextKeyExpr { - public readonly expr: ContextKeyExpr[]; - - constructor(expr: (ContextKeyExpr | undefined)[]) { - this.expr = ContextKeyAndExpr.normalizeArr(expr); - } - - public getType(): ContextKeyExprType { - return ContextKeyExprType.And; - } - - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyAndExpr) { - if (this.expr.length !== other.expr.length) { - return false; - } - const length = this.expr.length; - for (let i = 0; i < length; i++) { - if (!this.expr[i].equals(other.expr[i])) { - return false; - } - } - return true; - } - return false; - } - - public evaluate(context: Context): boolean { - const length = this.expr.length; - for (let i = 0; i < length; i++) { - if (!this.expr[i].evaluate(context)) { - return false; - } - } - return true; - } - - private static normalizeArr(arr: (ContextKeyExpr | undefined)[]): ContextKeyExpr[] { - let expr: ContextKeyExpr[] = []; - - if (arr) { - const length = arr.length; - for (let i = 0; i < length; i++) { - let e: ContextKeyExpr | undefined = arr[i]; - if (!e) { - continue; - } - - e = e.normalize(); - if (!e) { - continue; - } - - if (e instanceof ContextKeyAndExpr) { - expr = expr.concat(e.expr); - continue; - } - - expr.push(e); - } - - expr.sort(cmp); - } - - return expr; - } - - public normalize(): ContextKeyExpr | undefined { - if (this.expr.length === 0) { - return undefined; - } - - if (this.expr.length === 1) { - return this.expr[0]; - } - - return this; - } - - public serialize(): string { - if (this.expr.length === 0) { - return ''; - } - if (this.expr.length === 1) { - const normalized = this.normalize(); - if (!normalized) { - return ''; - } - return normalized.serialize(); - } - return this.expr.map(e => e.serialize()).join(' && '); - } - - public keys(): string[] { - const result: string[] = []; - for (const expr of this.expr) { - result.push(...expr.keys()); - } - return result; - } -} - -export class RawContextKey extends ContextKeyDefinedExpr { - - constructor(key: string, private defaultValue: T | undefined) { - super(key); - } - - public bindTo(target: ContextKeyService): ContextKey { - return target.createKey(this.key, this.defaultValue); - } - - public getValue(target: ContextKeyService): T | undefined { - return target.getContextKeyValue(this.key); - } - - public toNegated(): ContextKeyExpr { - return ContextKeyExpr.not(this.key); - } - - public isEqualTo(value: string): ContextKeyExpr { - return ContextKeyExpr.equals(this.key, value); - } - - public notEqualsTo(value: string): ContextKeyExpr { - return ContextKeyExpr.notEquals(this.key, value); - } -} - -export interface Context { - getValue(key: string): T | undefined; -} - -export interface ContextKey { - set(value: T): void; - reset(): void; - get(): T | undefined; -} - -export interface ContextKeyServiceTarget { - parentElement: ContextKeyServiceTarget | null; - setAttribute(attr: string, value: string): void; - removeAttribute(attr: string): void; - hasAttribute(attr: string): boolean; - getAttribute(attr: string): string | null; -} - -export interface ReadableSet { - has(value: T): boolean; -} - -export interface ContextKeyChangeEvent { - affectsSome(keys: ReadableSet): boolean; -} - -export const ContextKeyService = Symbol('ContextKeyService'); -export interface ContextKeyService { - dispose(): void; - - onDidChangeContext: Event; - createKey(key: string, defaultValue: T | undefined): ContextKey; - contextMatchesRules(rules: ContextKeyExpr | undefined): boolean; - getContextKeyValue(key: string): T | undefined; - - createScoped(target?: ContextKeyServiceTarget): ContextKeyService; - getContext(target: ContextKeyServiceTarget | null): Context; -} diff --git a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts index f51d8cda152ea..9b7ddca33dc4d 100644 --- a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts @@ -45,7 +45,7 @@ export class KeybindingsContributionPointHandler { } }); - this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); + this.keybindingRegistry.setKeymap(KeybindingScope.USER, contributions.keybindings); } private handlePartialKeybindings(keybinding: Keybinding, partialKeybindings: Keybinding[]) { diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts index 5650c4de98f64..376016f80d400 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts @@ -26,10 +26,9 @@ import { MockMenuModelRegistry } from '@theia/core/lib/common/test/mock-menu'; import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; import { MenusContributionPointHandler } from './menus-contribution-handler'; -import { ContextKeyService } from '../context-key/context-key'; -import { MockContextKeyService } from '../context-key/mock-context-key-service'; import 'mocha'; import * as sinon from 'sinon'; +import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; disableJSDOM(); @@ -53,7 +52,7 @@ before(() => { bind(MenuModelRegistry).toConstantValue(new MockMenuModelRegistry()); bindContributionProvider(bind, CommandContribution); bind(CommandRegistry).toSelf().inSingletonScope(); - bind(ContextKeyService).toConstantValue(new MockContextKeyService()); + bind(ContextKeyService).toSelf().inSingletonScope(); bind(MenusContributionPointHandler).toSelf(); }); diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index f6980841e0cc5..7c0f5f81d78ab 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -21,8 +21,6 @@ import { MenuModelRegistry } from '@theia/core/lib/common'; import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-views-main'; import { PluginContribution, Menu } from '../../../common'; -import { ContextKeyService, ContextKeyExpr } from '../context-key/context-key'; -import { CommandHandler } from '@theia/core'; @injectable() export class MenusContributionPointHandler { @@ -36,9 +34,6 @@ export class MenusContributionPointHandler { @inject(ILogger) protected readonly logger: ILogger; - @inject(ContextKeyService) - protected readonly contextKeyService: ContextKeyService; - // menu location to command IDs protected readonly registeredMenus: Map> = new Map(); @@ -66,7 +61,6 @@ export class MenusContributionPointHandler { this.registerMenuAction(menuPath, location, menu); } }); - menus.filter(menu => menu.when).forEach(menu => this.registerCommandHandler(menu)); } } } @@ -93,7 +87,8 @@ export class MenusContributionPointHandler { setTimeout(() => { this.menuRegistry.registerMenuAction([...menuPath, group], { commandId: menu.command, - order + order, + when: menu.when }); }, 2000); @@ -104,21 +99,4 @@ export class MenusContributionPointHandler { commands.add(menu.command); this.registeredMenus.set(location, commands); } - - /** Register a handler for the command that should be called by the specified menu item. */ - protected registerCommandHandler(menu: Menu): void { - this.commands.registerHandler(menu.command, this.newHandler(menu)); - } - - /** - * Creates a command handler that executes nothing but allows - * a related menu item be visible depending on the provided rules. - */ - protected newHandler(menu: Menu): CommandHandler { - return { - execute: () => undefined, - isEnabled: () => false, - isVisible: () => this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(menu.when)) - }; - } } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 444d77eb8b276..7a970032ef21a 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -40,8 +40,6 @@ import { PluginExtDeployCommandService } from './plugin-ext-deploy-command'; import { TextEditorService, TextEditorServiceImpl } from './text-editor-service'; import { EditorModelService, EditorModelServiceImpl } from './text-editor-model-service'; import { UntitledResourceResolver } from './editor/untitled-resource'; -import { ContextKeyService } from './context-key/context-key'; -import { ContextKeyServiceImpl } from './context-key/context-key-service'; import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginContributionHandler } from './plugin-contribution-handler'; import { ViewRegistry } from './view/view-registry'; @@ -128,7 +126,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(LanguageClientContributionProvider).toService(LanguageClientContributionProviderImpl); bind(LanguageClientProviderImpl).toSelf().inSingletonScope(); rebind(LanguageClientProvider).toService(LanguageClientProviderImpl); - bind(ContextKeyService).to(ContextKeyServiceImpl).inSingletonScope(); bind(PluginDebugService).toSelf().inSingletonScope(); rebind(DebugService).toService(PluginDebugService); diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx b/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx index f849669915aa4..a99c0988207d9 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx @@ -39,7 +39,7 @@ import { TreeViewItem, TreeViewItemCollapsibleState } from '../../../api/plugin- import { MenuPath } from '@theia/core/lib/common/menu'; import * as ReactDOM from 'react-dom'; import * as React from 'react'; -import { ContextKeyService, ContextKey } from '../context-key/context-key'; +import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; export const TREE_NODE_HYPERLINK = 'theia-TreeNodeHyperlink'; export const VIEW_ITEM_CONTEXT_MENU: MenuPath = ['view-item-context-menu'];