Skip to content

Commit

Permalink
Add submenu plugin contribution, fix context-keys ScmProvider update
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
  • Loading branch information
vinokurig committed Feb 4, 2021
1 parent 3e0c9e8 commit b4270a3
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 46 deletions.
13 changes: 12 additions & 1 deletion packages/core/src/browser/shell/tab-bar-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,16 @@ export class TabBarToolbar extends ReactWidget {
const menuPath = ['TAB_BAR_TOOLBAR_CONTEXT_MENU'];
const toDisposeOnHide = new DisposableCollection();
for (const [, item] of this.more) {
toDisposeOnHide.push(this.menus.registerMenuAction([...menuPath, item.group!], {
// Register a submenu for the item, if the group is in format `<submenu group>/<submenu name>/.../<item group>`
if (item.group!.indexOf('/') !== -1) {
const split = item.group!.split('/');
const paths: string[] = [];
for (let i = 0; i < split.length - 1; i += 2) {
paths.push(split[i], split[i + 1]);
toDisposeOnHide.push(this.menus.registerSubmenu([...menuPath, ...paths], split[i + 1]));
}
}
toDisposeOnHide.push(this.menus.registerMenuAction([...menuPath, ...item.group!.split('/')], {
label: item.tooltip,
commandId: item.id,
when: item.when
Expand Down Expand Up @@ -291,6 +300,8 @@ export interface TabBarToolbarItem {
/**
* Optional group for the item. Default `navigation`.
* `navigation` group will be inlined, while all the others will be within the `...` dropdown.
* A group in format `submenu_group_1/submenu 1/.../submenu_group_n/ submenu n/item_group` means that the item will be located in a submenu(s) of the `...` dropdown.
* The submenu's title is named by the submenu section name, e.g. `group/<submenu name>/subgroup`.
*/
readonly group?: string;

Expand Down
18 changes: 16 additions & 2 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface PluginPackageContribution {
viewsWelcome?: PluginPackageViewWelcome[];
commands?: PluginPackageCommand | PluginPackageCommand[];
menus?: { [location: string]: PluginPackageMenu[] };
submenus?: PluginPackageSubmenu[];
keybindings?: PluginPackageKeybinding | PluginPackageKeybinding[];
debuggers?: PluginPackageDebuggersContribution[];
snippets?: PluginPackageSnippetsContribution[];
Expand Down Expand Up @@ -116,12 +117,18 @@ export interface PluginPackageCommand {
}

export interface PluginPackageMenu {
command: string;
command?: string;
submenu?: string;
alt?: string;
group?: string;
when?: string;
}

export interface PluginPackageSubmenu {
id: string;
label: string;
}

export interface PluginPackageKeybinding {
key?: string;
command: string;
Expand Down Expand Up @@ -487,6 +494,7 @@ export interface PluginContribution {
viewsWelcome?: ViewWelcome[];
commands?: PluginCommand[]
menus?: { [location: string]: Menu[] };
submenus?: Submenu[];
keybindings?: Keybinding[];
debuggers?: DebuggerContribution[];
snippets?: SnippetContribution[];
Expand Down Expand Up @@ -647,12 +655,18 @@ export type IconUrl = string | { light: string; dark: string; };
* Menu contribution
*/
export interface Menu {
command: string;
command?: string;
submenu?: string
alt?: string;
group?: string;
when?: string;
}

export interface Submenu {
id: string;
label: string;
}

/**
* Keybinding contribution
*/
Expand Down
77 changes: 52 additions & 25 deletions packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,42 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject } from 'inversify';
import { inject, injectable } from 'inversify';
import {
AutoClosingPair,
AutoClosingPairConditional,
buildFrontendModuleName,
DebuggerContribution,
IconThemeContribution,
IconUrl,
Keybinding,
LanguageConfiguration,
LanguageContribution,
Menu,
PluginCommand,
PluginContribution,
PluginEngine,
PluginLifecycle,
PluginModel,
PluginPackage,
PluginScanner,
PluginLifecycle,
buildFrontendModuleName,
PluginContribution,
PluginPackageCommand,
PluginPackageDebuggersContribution,
PluginPackageKeybinding,
PluginPackageLanguageContribution,
LanguageContribution,
PluginPackageLanguageContributionConfiguration,
LanguageConfiguration,
PluginTaskDefinitionContribution,
AutoClosingPairConditional,
AutoClosingPair,
ViewContainer,
Keybinding,
PluginPackageKeybinding,
PluginPackageViewContainer,
View,
PluginPackageMenu,
PluginPackageSubmenu,
PluginPackageView,
ViewWelcome,
PluginPackageViewContainer,
PluginPackageViewWelcome,
Menu,
PluginPackageMenu,
PluginPackageDebuggersContribution,
DebuggerContribution,
PluginScanner,
PluginTaskDefinitionContribution,
SnippetContribution,
PluginPackageCommand,
PluginCommand,
IconUrl,
Submenu,
ThemeContribution,
IconThemeContribution
View,
ViewContainer,
ViewWelcome
} from '../../../common/plugin-protocol';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -60,7 +62,11 @@ import { deepClone } from '@theia/core/lib/common/objects';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/common/preferences/preference-schema';
import { RecursivePartial } from '@theia/core/lib/common/types';
import { ProblemMatcherContribution, ProblemPatternContribution, TaskDefinition } from '@theia/task/lib/common/task-protocol';
import {
ProblemMatcherContribution,
ProblemPatternContribution,
TaskDefinition
} from '@theia/task/lib/common/task-protocol';
import { ColorDefinition } from '@theia/core/lib/browser/color-registry';
import { ResourceLabelFormatter } from '@theia/core/lib/common/label-protocol';

Expand Down Expand Up @@ -168,6 +174,14 @@ export class TheiaPluginScanner implements PluginScanner {
console.error(`Could not read '${rawPlugin.name}' contribution 'languages'.`, rawPlugin.contributes!.languages, err);
}

try {
if (rawPlugin.contributes!.submenus) {
contributions.submenus = this.readSubmenus(rawPlugin.contributes.submenus!);
}
} catch (err) {
console.error(`Could not read '${rawPlugin.name}' contribution 'submenus'.`, rawPlugin.contributes!.submenus, err);
}

try {
if (rawPlugin.contributes!.grammars) {
const grammars = this.grammarsReader.readGrammars(rawPlugin.contributes.grammars!, rawPlugin.packagePath);
Expand Down Expand Up @@ -527,6 +541,7 @@ export class TheiaPluginScanner implements PluginScanner {
private readMenu(rawMenu: PluginPackageMenu): Menu {
const result: Menu = {
command: rawMenu.command,
submenu: rawMenu.submenu,
alt: rawMenu.alt,
group: rawMenu.group,
when: rawMenu.when
Expand All @@ -538,6 +553,18 @@ export class TheiaPluginScanner implements PluginScanner {
return rawLanguages.map(language => this.readLanguage(language, pluginPath));
}

private readSubmenus(rawSubmenus: PluginPackageSubmenu[]): Submenu[] {
return rawSubmenus.map(submenu => this.readSubmenu(submenu));
}

private readSubmenu(rawSubmenu: PluginPackageSubmenu): Submenu {
return {
id: rawSubmenu.id,
label: rawSubmenu.label
};

}

private readLanguage(rawLang: PluginPackageLanguageContribution, pluginPath: string): LanguageContribution {
// TODO: add validation to all parameters
const result: LanguageContribution = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browse
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { QuickCommandService } from '@theia/core/lib/browser/quick-open/quick-command-service';
import { VIEW_ITEM_CONTEXT_MENU, TreeViewWidget, VIEW_ITEM_INLINE_MENU } from '../view/tree-view-widget';
import { DeployedPlugin, Menu, ScmCommandArg, TimelineCommandArg, TreeViewSelection } from '../../../common';
import { DeployedPlugin, Menu, ScmCommandArg, Submenu, TimelineCommandArg, TreeViewSelection } from '../../../common';
import { DebugStackFramesWidget } from '@theia/debug/lib/browser/view/debug-stack-frames-widget';
import { DebugThreadsWidget } from '@theia/debug/lib/browser/view/debug-threads-widget';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
Expand Down Expand Up @@ -98,32 +98,39 @@ export class MenusContributionPointHandler {
if (!allMenus) {
return Disposable.NULL;
}
const allSubmenus = plugin.contributes && plugin.contributes.submenus;
const toDispose = new DisposableCollection();
for (const location in allMenus) {
if (location === 'commandPalette') {
for (const menu of allMenus[location]) {
if (menu.when) {
if (menu.command && menu.when) {
toDispose.push(this.quickCommandService.pushCommandContext(menu.command, menu.when));
}
}
} else if (location === 'editor/title') {
for (const action of allMenus[location]) {
if (!action.command) {
continue;
}
toDispose.push(this.registerTitleAction(location, action, {
execute: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.executeCommand(action.command, this.codeEditorWidgetUtil.getResourceUri(widget)),
isEnabled: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.isEnabled(action.command, this.codeEditorWidgetUtil.getResourceUri(widget)),
isVisible: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.isVisible(action.command, this.codeEditorWidgetUtil.getResourceUri(widget))
execute: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.executeCommand(action.command!, this.codeEditorWidgetUtil.getResourceUri(widget)),
isEnabled: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.isEnabled(action.command!, this.codeEditorWidgetUtil.getResourceUri(widget)),
isVisible: widget => this.codeEditorWidgetUtil.is(widget) && this.commands.isVisible(action.command!, this.codeEditorWidgetUtil.getResourceUri(widget))
}));
}
} else if (location === 'view/title') {
for (const action of allMenus[location]) {
if (!action.command) {
continue;
}
toDispose.push(this.registerTitleAction(location, { ...action, when: undefined }, {
execute: widget => widget instanceof PluginViewWidget && this.commands.executeCommand(action.command),
execute: widget => widget instanceof PluginViewWidget && this.commands.executeCommand(action.command!),
isEnabled: widget => widget instanceof PluginViewWidget &&
this.viewContextKeys.with({ view: widget.options.viewId }, () =>
this.commands.isEnabled(action.command) && this.viewContextKeys.match(action.when)),
this.commands.isEnabled(action.command!) && this.viewContextKeys.match(action.when)),
isVisible: widget => widget instanceof PluginViewWidget &&
this.viewContextKeys.with({ view: widget.options.viewId }, () =>
this.commands.isVisible(action.command) && this.viewContextKeys.match(action.when))
this.commands.isVisible(action.command!) && this.viewContextKeys.match(action.when))
}));
}
} else if (location === 'view/item/context') {
Expand All @@ -133,9 +140,20 @@ export class MenusContributionPointHandler {
toDispose.push(this.registerTreeMenuAction(menuPath, menu));
}
} else if (location === 'scm/title') {
for (const action of allMenus[location]) {
toDispose.push(this.registerScmTitleAction(location, action));
}
const registerActions = (menus: Menu[], group: string | undefined) => {
for (const action of menus) {
if (group) {
action.group = group + (action.group ? '/' + action.group.split('@')[0] : '/_');
}
if (action.submenu) {
const submenu: Submenu = allSubmenus!.find(s => s.id === action.submenu)!;
registerActions(allMenus[action.submenu], action.group!.split('@')[0] + '/' + submenu.label);
} else {
toDispose.push(this.registerScmTitleAction(location, action));
}
}
};
registerActions(allMenus[location], undefined);
} else if (location === 'scm/resourceGroup/context') {
for (const menu of allMenus[location]) {
const inline = menu.group && /^inline/.test(menu.group) || false;
Expand Down Expand Up @@ -256,6 +274,9 @@ export class MenusContributionPointHandler {
}

protected registerTitleAction(location: string, action: Menu, handler: CommandHandler): Disposable {
if (!action.command) {
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
const id = this.createSyntheticCommandId(action.command, { prefix: `__plugin.${location.replace('/', '.')}.action.` });
const command: Command = { id };
Expand Down Expand Up @@ -301,11 +322,14 @@ export class MenusContributionPointHandler {
}

protected registerScmTitleAction(location: string, action: Menu): Disposable {
if (!action.command) {
return Disposable.NULL;
}
const selectedRepository = () => this.toScmArgs(this.scmService.selectedRepository);
return this.registerTitleAction(location, action, {
execute: widget => widget instanceof ScmWidget && this.commands.executeCommand(action.command, selectedRepository()),
isEnabled: widget => widget instanceof ScmWidget && this.commands.isEnabled(action.command, selectedRepository()),
isVisible: widget => widget instanceof ScmWidget && this.commands.isVisible(action.command, selectedRepository())
execute: widget => widget instanceof ScmWidget && this.commands.executeCommand(action.command!, selectedRepository()),
isEnabled: widget => widget instanceof ScmWidget && this.commands.isEnabled(action.command!, selectedRepository()),
isVisible: widget => widget instanceof ScmWidget && this.commands.isVisible(action.command!, selectedRepository())
});
}
protected registerScmMenuAction(menuPath: MenuPath, menu: Menu): Disposable {
Expand Down Expand Up @@ -403,6 +427,9 @@ export class MenusContributionPointHandler {
}

protected registerMenuAction(menuPath: MenuPath, menu: Menu, handler: (command: string) => CommandHandler): Disposable {
if (!menu.command) {
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
const commandId = this.createSyntheticCommandId(menu.command, { prefix: '__plugin.menu.action.' });
const command: Command = { id: commandId };
Expand Down
1 change: 1 addition & 0 deletions packages/scm/src/browser/scm-groups-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class ScmGroupsTreeModel extends ScmTreeModel {

protected changeRepository(provider: ScmProvider | undefined): void {
this.toDisposeOnRepositoryChange.dispose();
this.contextKeys.scmProvider.set(provider ? provider.id : undefined);
this.provider = provider;
if (provider) {
this.toDisposeOnRepositoryChange.push(provider.onDidChange(() => {
Expand Down
4 changes: 0 additions & 4 deletions packages/scm/src/browser/scm-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,11 @@ export abstract class ScmTreeModel extends TreeModelImpl {
return;
}

const currentScmProviderId = this.contextKeys.scmProvider.get();
const currentScmResourceGroup = this.contextKeys.scmResourceGroup.get();
this.contextKeys.scmProvider.set(this.provider.id);
this.contextKeys.scmResourceGroup.set(groupId);
try {
callback();
} finally {
this.contextKeys.scmProvider.set(currentScmProviderId);
this.contextKeys.scmResourceGroup.set(currentScmResourceGroup);
}
}

Expand Down

0 comments on commit b4270a3

Please sign in to comment.