Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'menus' contribution point for plugins #2955

Merged
merged 2 commits into from
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/common/test/mock-menu.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions packages/plugin-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface PluginPackageContribution {
grammars?: PluginPackageGrammarsContribution[];
viewsContainers?: { [location: string]: PluginPackageViewContainer[] };
views?: { [location: string]: PluginPackageView[] };
menus?: { [location: string]: PluginPackageMenu[] };
}

export interface PluginPackageViewContainer {
Expand All @@ -68,6 +69,11 @@ export interface PluginPackageView {
name: string;
}

export interface PluginPackageMenu {
command: string;
group?: string;
}

export interface PluginPackageGrammarsContribution {
language?: string;
scopeName: string;
Expand Down Expand Up @@ -291,6 +297,7 @@ export interface PluginContribution {
grammars?: GrammarsContribution[];
viewsContainers?: { [location: string]: ViewContainer[] };
views?: { [location: string]: View[] };
menus?: { [location: string]: Menu[] };
}

export interface GrammarsContribution {
Expand Down Expand Up @@ -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.
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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],
<MenuAction>{
commandId: testCommandId,
order: order || undefined
});
}
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -107,6 +111,8 @@ export class PluginContributionHandler {
}
}
}

this.menusContributionHandler.handle(contributions);
}

private createRegex(value: string | undefined): RegExp | undefined {
Expand Down
Loading