Skip to content

Commit

Permalink
fix #3986: support vscode when closure contexts
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Jan 18, 2019
1 parent 6f9a40b commit 179dfdb
Show file tree
Hide file tree
Showing 23 changed files with 184 additions and 1,188 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<ContextKeyChangeEvent>;
export interface ContextKey<T> {
set(value: T | undefined): void;
reset(): void;
get(): T | undefined;
}
export namespace ContextKey {
// tslint:disable-next-line:no-any
export const None: ContextKey<any> = Object.freeze({
set: () => { },
reset: () => { },
get: () => undefined
});
}

@injectable()
export class ContextKeyService {
createKey<T>(key: string, defaultValue: T | undefined): ContextKey<T> {
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<T>(key: string): T | undefined {
return undefined;
}

createScoped(target?: ContextKeyServiceTarget): ContextKeyService {
return this;
}

getContext(target: ContextKeyServiceTarget | null): Context {
return {
getValue: () => undefined
};
}
}
3 changes: 3 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/browser/keybinding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
});

Expand Down
29 changes: 23 additions & 6 deletions packages/core/src/browser/keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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, <HTMLElement>event.target)) {
return false;
}
return true;
}

/**
* Run the command matching to the given keyboard event.
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/browser/menu/browser-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ import {
} from '../../common';
import { KeybindingRegistry, Keybinding } from '../keybinding';
import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application';
import { ContextKeyService } from '../context-key-service';

@injectable()
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,
Expand Down Expand Up @@ -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)
});

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/common/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface MenuAction {
label?: string
icon?: string
order?: string
when?: string
}

export namespace MenuAction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ 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 {

protected _menu: Electron.Menu;
protected _toggledCommands: Set<string> = new Set();

@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

constructor(
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry,
@inject(PreferenceService) protected readonly preferencesService: PreferenceService,
Expand Down Expand Up @@ -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;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/keymaps/src/browser/keymaps-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ export const keymapsSchema = {
},
context: {
type: 'string'
},
when: {
type: 'string'
}
},
required: ['command', 'keybinding'],
optional: ['context'],
optional: ['context', 'when'],
additionalProperties: false
}
};
Expand Down
49 changes: 49 additions & 0 deletions packages/monaco/src/browser/monaco-context-key-service.ts
Original file line number Diff line number Diff line change
@@ -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<T>(key: string, defaultValue: T | undefined): ContextKey<T> {
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<string, monaco.contextkey.ContextKeyExpr>();
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;
}

}
7 changes: 5 additions & 2 deletions packages/monaco/src/browser/monaco-editor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -108,6 +109,7 @@ export class MonacoEditorProvider {

protected async doCreateEditor(factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise<MonacoEditor>): Promise<MonacoEditor> {
const commandService = this.commandServiceFactory();
const contextKeyService = this.contextKeyService.createScoped();
const { codeEditorService, textModelService, contextMenuService } = this;
const IWorkspaceEditService = this.bulkEditService;
const toDispose = new DisposableCollection();
Expand All @@ -116,7 +118,8 @@ export class MonacoEditorProvider {
textModelService,
contextMenuService,
commandService,
IWorkspaceEditService
IWorkspaceEditService,
contextKeyService
}, toDispose);
editor.onDispose(() => toDispose.dispose());

Expand Down
10 changes: 10 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
Expand Down
14 changes: 10 additions & 4 deletions packages/monaco/src/browser/monaco-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,19 @@ export function loadMonaco(vsRequire: any): Promise<void> {
'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;
Expand All @@ -90,6 +94,8 @@ export function loadMonaco(vsRequire: any): Promise<void> {
global.monaco.findController = findController;
global.monaco.rename = rename;
global.monaco.snippetParser = snippetParser;
global.monaco.contextkey = contextKey;
global.monaco.contextKeyService = contextKeyService;
resolve();
});
});
Expand Down
Loading

0 comments on commit 179dfdb

Please sign in to comment.