From b8fca1e8efda6251b4690e3dfe58623139a86d80 Mon Sep 17 00:00:00 2001 From: Artem Zatsarynnyi Date: Mon, 24 Sep 2018 12:18:37 +0300 Subject: [PATCH 1/2] Add 'menus' contribution point for Plugin API Signed-off-by: Artem Zatsarynnyi --- packages/core/src/common/test/mock-menu.ts | 35 ++++ packages/plugin-ext/package.json | 2 + .../plugin-ext/src/common/plugin-protocol.ts | 15 ++ .../src/hosted/node/scanners/scanner-theia.ts | 25 ++- .../menus/menus-contribution-handler.spec.ts | 151 ++++++++++++++++++ .../menus/menus-contribution-handler.ts | 63 ++++++++ .../browser/plugin-contribution-handler.ts | 10 +- .../browser/plugin-ext-frontend-module.ts | 2 + 8 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/common/test/mock-menu.ts create mode 100644 packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts create mode 100644 packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts diff --git a/packages/core/src/common/test/mock-menu.ts b/packages/core/src/common/test/mock-menu.ts new file mode 100644 index 0000000000000..da48e934a7f32 --- /dev/null +++ b/packages/core/src/common/test/mock-menu.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +import { Disposable } from '../disposable'; +import { CommandRegistry } from '../command'; +import { MenuModelRegistry, MenuPath, MenuAction } from '../menu'; + +export class MockMenuModelRegistry extends MenuModelRegistry { + + constructor() { + const commands = new CommandRegistry({ getContributions: () => [] }); + super({ getContributions: () => [] }, commands); + } + + registerMenuAction(menuPath: MenuPath, item: MenuAction): Disposable { + return Disposable.NULL; + } + + registerSubmenu(menuPath: MenuPath, label: string): Disposable { + return Disposable.NULL; + } +} diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index ed7a7003e2a2a..c751067ccd4ad 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -6,8 +6,10 @@ "typings": "lib/common/index.d.ts", "dependencies": { "@theia/core": "^0.3.14", + "@theia/editor": "^0.3.14", "@theia/filesystem": "^0.3.14", "@theia/monaco": "^0.3.14", + "@theia/navigator": "^0.3.14", "@theia/plugin": "^0.3.14", "@theia/workspace": "^0.3.14", "decompress": "^4.2.0", diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index aed2f1ee1ae37..19fe48978a777 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -55,6 +55,7 @@ export interface PluginPackageContribution { grammars?: PluginPackageGrammarsContribution[]; viewsContainers?: { [location: string]: PluginPackageViewContainer[] }; views?: { [location: string]: PluginPackageView[] }; + menus?: { [location: string]: PluginPackageMenu[] }; } export interface PluginPackageViewContainer { @@ -68,6 +69,11 @@ export interface PluginPackageView { name: string; } +export interface PluginPackageMenu { + command: string; + group?: string; +} + export interface PluginPackageGrammarsContribution { language?: string; scopeName: string; @@ -291,6 +297,7 @@ export interface PluginContribution { grammars?: GrammarsContribution[]; viewsContainers?: { [location: string]: ViewContainer[] }; views?: { [location: string]: View[] }; + menus?: { [location: string]: Menu[] }; } export interface GrammarsContribution { @@ -369,6 +376,14 @@ export interface View { name: string; } +/** + * Menu contribution + */ +export interface Menu { + command: string; + group?: string; +} + /** * This interface describes a plugin lifecycle object. */ 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 cefb46a61de85..73237e598f7ba 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -32,7 +32,9 @@ import { ViewContainer, PluginPackageViewContainer, View, - PluginPackageView + PluginPackageView, + Menu, + PluginPackageMenu } from '../../../common/plugin-protocol'; import * as fs from 'fs'; import * as path from 'path'; @@ -124,6 +126,15 @@ export class TheiaPluginScanner implements PluginScanner { }); } + if (rawPlugin.contributes!.menus) { + contributions.menus = {}; + + Object.keys(rawPlugin.contributes.menus!).forEach(location => { + const menus = this.readMenus(rawPlugin.contributes!.menus![location]); + contributions.menus![location] = menus; + }); + } + return contributions; } @@ -154,6 +165,18 @@ export class TheiaPluginScanner implements PluginScanner { return result; } + private readMenus(rawMenus: PluginPackageMenu[]): Menu[] { + return rawMenus.map(rawMenu => this.readMenu(rawMenu)); + } + + private readMenu(rawMenu: PluginPackageMenu): Menu { + const result: Menu = { + command: rawMenu.command, + group: rawMenu.group + }; + return result; + } + private readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): LanguageContribution[] { return rawLanguages.map(language => this.readLanguage(language, pluginPath)); } 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 new file mode 100644 index 0000000000000..9a13d77eb8c99 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.spec.ts @@ -0,0 +1,151 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; + +const disableJSDOM = enableJSDOM(); + +import { Container, ContainerModule } from 'inversify'; +import { ILogger, MessageClient, MessageService, MenuPath, MenuAction } from '@theia/core'; +import { MenuModelRegistry } from '@theia/core/lib/common'; +import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; +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 'mocha'; +import * as sinon from 'sinon'; + +disableJSDOM(); + +let testContainer: Container; +let handler: MenusContributionPointHandler; + +let notificationWarnSpy: sinon.SinonSpy; +let registerMenuSpy: sinon.SinonSpy; + +const testCommandId = 'core.about'; + +before(() => { + testContainer = new Container(); + + const module = new ContainerModule((bind, unbind, isBound, rebind) => { + bind(ILogger).to(MockLogger).inSingletonScope(); + bind(MessageClient).toSelf().inSingletonScope(); + bind(MessageService).toSelf().inSingletonScope(); + bind(MenuModelRegistry).toConstantValue(new MockMenuModelRegistry()); + bind(MenusContributionPointHandler).toSelf(); + }); + + testContainer.load(module); +}); + +beforeEach(() => { + handler = testContainer.get(MenusContributionPointHandler); + + const messageService = testContainer.get(MessageService); + notificationWarnSpy = sinon.spy(messageService, 'warn'); + + const menuRegistry = testContainer.get(MenuModelRegistry); + registerMenuSpy = sinon.spy(menuRegistry, 'registerMenuAction'); +}); + +afterEach(function () { + notificationWarnSpy.restore(); + registerMenuSpy.restore(); +}); + +describe('MenusContributionHandler', () => { + describe('should register an item in the supported menus', () => { + it('editor context menu', () => { + handler.handle({ + menus: { + 'editor/context': [{ + command: testCommandId + }] + } + }); + + assertItemIsRegistered(EDITOR_CONTEXT_MENU); + }); + + it('navigator context menu', () => { + handler.handle({ + menus: { + 'explorer/context': [{ + command: testCommandId + }] + } + }); + + assertItemIsRegistered(NAVIGATOR_CONTEXT_MENU); + }); + }); + + it('should register an item in a menu\'s group', () => { + handler.handle({ + menus: { + 'explorer/context': [{ + command: testCommandId, + group: 'navigation' + }] + } + }); + + assertItemIsRegistered(NAVIGATOR_CONTEXT_MENU, 'navigation'); + }); + + it('should register an item in a menu\'s group with a position', () => { + handler.handle({ + menus: { + 'explorer/context': [{ + command: testCommandId, + group: 'navigation@7' + }] + } + }); + + assertItemIsRegistered(NAVIGATOR_CONTEXT_MENU, 'navigation', '7'); + }); + + it('should do nothing when no \'menus\' contribution provided', () => { + handler.handle({}); + + sinon.assert.notCalled(notificationWarnSpy); + sinon.assert.notCalled(registerMenuSpy); + }); + + it('should warn when invalid menu identifier', () => { + handler.handle({ + menus: { + 'non-existent location': [{ + command: testCommandId + }] + } + }); + + sinon.assert.called(notificationWarnSpy); + }); + + function assertItemIsRegistered(menuPath: MenuPath, menuGroup: string = '', order?: string) { + sinon.assert.calledWithExactly(registerMenuSpy, + [...menuPath, menuGroup], + { + commandId: testCommandId, + order: order || undefined + }); + } +}); 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 new file mode 100644 index 0000000000000..f62e02ecf2cbd --- /dev/null +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -0,0 +1,63 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { MenuPath, MessageService } from '@theia/core'; +import { MenuModelRegistry } from '@theia/core/lib/common'; +import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; +import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; +import { PluginContribution } from '../../../common'; + +@injectable() +export class MenusContributionPointHandler { + + @inject(MenuModelRegistry) + protected readonly menuRegistry: MenuModelRegistry; + + @inject(MessageService) + protected readonly messageService: MessageService; + + handle(contributions: PluginContribution): void { + if (!contributions.menus) { + return; + } + + for (const location in contributions.menus) { + if (contributions.menus.hasOwnProperty(location)) { + const menuPath = this.parseMenuPath(location); + if (!menuPath) { + this.messageService.warn(`Plugin contributes items to a menu with invalid identifier: ${location}`); + continue; + } + const menus = contributions.menus[location]; + menus.forEach(menu => { + const [group = '', order = undefined] = (menu.group || '').split('@'); + this.menuRegistry.registerMenuAction([...menuPath, group], { + commandId: menu.command, + order + }); + }); + } + } + } + + protected parseMenuPath(value: string): MenuPath | undefined { + switch (value) { + case 'editor/context': return EDITOR_CONTEXT_MENU; + case 'explorer/context': return NAVIGATOR_CONTEXT_MENU; + } + } +} diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 13383a13cb4af..c2e99b87cd8c4 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -15,10 +15,11 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '../../common'; -import { TextmateRegistry, getEncodedLanguageId } from '@theia/monaco/lib/browser/textmate'; import { ITokenTypeMap, IEmbeddedLanguagesMap, StandardTokenType } from 'vscode-textmate'; +import { TextmateRegistry, getEncodedLanguageId } from '@theia/monaco/lib/browser/textmate'; +import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { ViewRegistry } from './view/view-registry'; +import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '../../common'; @injectable() export class PluginContributionHandler { @@ -31,6 +32,9 @@ export class PluginContributionHandler { @inject(ViewRegistry) private readonly viewRegistry: ViewRegistry; + @inject(MenusContributionPointHandler) + private readonly menusContributionHandler: MenusContributionPointHandler; + handleContributions(contributions: PluginContribution): void { if (contributions.languages) { for (const lang of contributions.languages) { @@ -107,6 +111,8 @@ export class PluginContributionHandler { } } } + + this.menusContributionHandler.handle(contributions); } private createRegex(value: string | undefined): RegExp | undefined { 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 2a86281e3f322..d26bce1b1c55d 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 @@ -39,6 +39,7 @@ 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 { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginContributionHandler } from './plugin-contribution-handler'; import { ViewRegistry } from './view/view-registry'; @@ -90,6 +91,7 @@ export default new ContainerModule(bind => { }).inSingletonScope(); bind(ViewRegistry).toSelf().inSingletonScope(); + bind(MenusContributionPointHandler).toSelf().inSingletonScope(); bind(PluginContributionHandler).toSelf().inSingletonScope(); }); From de256c3cfb29d123f63c6493fc9a2d291668c5ab Mon Sep 17 00:00:00 2001 From: Artem Zatsarynnyi Date: Mon, 24 Sep 2018 12:26:49 +0300 Subject: [PATCH 2/2] Update Changelog Signed-off-by: Artem Zatsarynnyi --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c403cf10ae2..f6a8c3a44ed07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v0.3.15 +- [plug-in] added `menus` contribution point + ## v0.3.13 - [cpp] Add a status bar button to select an active cpp build configuration - Recently opened workspaces history