Skip to content

Commit

Permalink
fix #4151: register vscode commands/handlers properly
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 29, 2019
1 parent e5bd4f0 commit 3aaa72b
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 104 deletions.
16 changes: 1 addition & 15 deletions packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,12 @@ let pluginApiFactory: PluginAPIFactory;
export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => {
const vscode = apiFactory(plugin);

// register the commands that are in the package.json file
const contributes: any = plugin.rawModel.contributes;
if (contributes && contributes.commands) {
contributes.commands.forEach((commandItem: any) => {
let commandLabel: string;
if (commandItem.category) { // if VS Code command has category we will add it before title, so label will looks like 'category: title'
commandLabel = commandItem.category + ': ' + commandItem.title;
} else {
commandLabel = commandItem.title;
}
vscode.commands.registerCommand({ id: commandItem.command, label: commandLabel });
});
}

// replace command API as it will send only the ID as a string parameter
const registerCommand = vscode.commands.registerCommand;
vscode.commands.registerCommand = function (command: any, handler?: <T>(...args: any[]) => T | Thenable<T>): any {
// use of the ID when registering commands
if (typeof command === 'string' && handler) {
return registerCommand({ id: command }, handler);
return vscode.commands.registerHandler(command, handler);
}
return registerCommand(command, handler);
};
Expand Down
5 changes: 4 additions & 1 deletion packages/plugin-ext/src/api/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ export interface PluginManagerExt {

export interface CommandRegistryMain {
$registerCommand(command: theia.Command): void;

$unregisterCommand(id: string): void;

$registerHandler(id: string): void;
$unregisterHandler(id: string): void;

$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T | undefined>;
$getCommands(): PromiseLike<string[]>;
$getKeyBinding(commandId: string): PromiseLike<theia.CommandKeyBinding[] | undefined>;
Expand Down
48 changes: 28 additions & 20 deletions packages/plugin-ext/src/main/browser/command-registry-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import { Disposable } from '@theia/core/lib/common/disposable';
import { CommandRegistryMain, CommandRegistryExt, MAIN_RPC_CONTEXT } from '../../api/plugin-api';
import { RPCProtocol } from '../../api/rpc-protocol';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';

export class CommandRegistryMainImpl implements CommandRegistryMain {
private proxy: CommandRegistryExt;
private disposables = new Map<string, Disposable>();
private readonly commands = new Map<string, Disposable>();
private readonly handlers = new Map<string, Disposable>();
private delegate: CommandRegistry;
private keyBinding: KeybindingRegistry;

Expand All @@ -36,28 +36,36 @@ export class CommandRegistryMainImpl implements CommandRegistryMain {
}

$registerCommand(command: theia.Command): void {
this.disposables.set(
command.id,
this.delegate.registerCommand(command, {
// tslint:disable-next-line:no-any
execute: (...args: any[]) => {
// plugin command handlers cannot handle Theia URI, only VS Code URI
const resolvedArgs = (args || []).map(arg => arg instanceof URI ? arg['codeUri'] : arg);
this.proxy.$executeCommand(command.id, ...resolvedArgs);
},
// Always enabled - a command can be executed programmatically or via the commands palette.
isEnabled() { return true; },
// Visibility rules are defined via the `menus` contribution point.
isVisible() { return true; }
}));
this.commands.set(command.id, this.delegate.registerCommand(command));
}
$unregisterCommand(id: string): void {
const dis = this.disposables.get(id);
if (dis) {
dis.dispose();
this.disposables.delete(id);
const command = this.commands.get(id);
if (command) {
command.dispose();
this.commands.delete(id);
}
}

$registerHandler(id: string): void {
this.handlers.set(id, this.delegate.registerHandler(id, {
// tslint:disable-next-line:no-any
execute: (...args: any[]) => {
this.proxy.$executeCommand(id, ...args);
},
// Always enabled - a command can be executed programmatically or via the commands palette.
isEnabled() { return true; },
// Visibility rules are defined via the `menus` contribution point.
isVisible() { return true; }
}));
}
$unregisterHandler(id: string): void {
const handler = this.handlers.get(id);
if (handler) {
handler.dispose();
this.handlers.delete(id);
}
}

// tslint:disable-next-line:no-any
$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T | undefined> {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// tslint:disable:no-any

import { injectable, inject } from 'inversify';
import { MenuPath, ILogger, CommandRegistry } from '@theia/core';
import CoreURI from '@theia/core/lib/common/uri';
import { MenuPath, ILogger, CommandRegistry, Command, Mutable, MenuAction } from '@theia/core';
import { EDITOR_CONTEXT_MENU, EditorWidget } from '@theia/editor/lib/browser';
import { MenuModelRegistry } from '@theia/core/lib/common';
import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
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 } from '../view/tree-views-main';
import { PluginContribution, Menu, PluginCommand } from '../../../common';
import { PluginSharedStyle } from '../plugin-shared-style';
import { PluginContribution, Menu } 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';

Expand All @@ -46,9 +47,6 @@ export class MenusContributionPointHandler {
@inject(TabBarToolbarRegistry)
protected readonly tabBarToolbar: TabBarToolbarRegistry;

@inject(PluginSharedStyle)
protected readonly style: PluginSharedStyle;

handle(contributions: PluginContribution): void {
const allMenus = contributions.menus;
if (!allMenus) {
Expand All @@ -62,7 +60,9 @@ export class MenusContributionPointHandler {
}
}
} else if (location === 'editor/title') {
this.registerEditorTitleActions(allMenus[location], contributions);
for (const action of allMenus[location]) {
this.registerEditorTitleAction(action);
}
} else if (allMenus.hasOwnProperty(location)) {
const menuPaths = MenusContributionPointHandler.parseMenuPaths(location);
if (!menuPaths.length) {
Expand All @@ -79,42 +79,23 @@ export class MenusContributionPointHandler {
}
}

protected registerEditorTitleActions(actions: Menu[], contributions: PluginContribution): void {
if (!contributions.commands || !actions.length) {
return;
}
const commands = new Map(contributions.commands.map(c => [c.command, c] as [string, PluginCommand]));
for (const action of actions) {
const pluginCommand = commands.get(action.command);
if (pluginCommand) {
this.registerEditorTitleAction(action, pluginCommand);
}
}
}
protected registerEditorTitleAction(action: Menu): void {
const id = this.createSyntheticCommandId(action, { prefix: '__plugin.editor.title.action.' });
const command: Command = { id };
this.commands.registerCommand(command, {
execute: widget => widget instanceof EditorWidget && this.commands.executeCommand(action.command, widget.editor.uri['codeUri']),
isEnabled: widget => widget instanceof EditorWidget && this.commands.isEnabled(action.command, widget.editor.uri['codeUri']),
isVisible: widget => widget instanceof EditorWidget && this.commands.isVisible(action.command, widget.editor.uri['codeUri'])
});

protected editorTitleActionId = 0;
protected registerEditorTitleAction(action: Menu, pluginCommand: PluginCommand): void {
const id = pluginCommand.command;
const command = '__editor.title.' + id;
const tooltip = pluginCommand.title;
const iconClass = 'plugin-editor-title-action-' + this.editorTitleActionId++;
const { group, when } = action;
const item: Mutable<TabBarToolbarItem> = { id, command: id, group, when };
this.tabBarToolbar.registerItem(item);

const { iconUrl } = pluginCommand;
const darkIconUrl = typeof iconUrl === 'object' ? iconUrl.dark : iconUrl;
const lightIconUrl = typeof iconUrl === 'object' ? iconUrl.light : iconUrl;
this.style.insertRule('.' + iconClass, theme => `
width: 16px;
height: 16px;
background: no-repeat url("${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl}");
`);

this.commands.registerCommand({ id: command, iconClass }, {
execute: widget => widget instanceof EditorWidget && this.commands.executeCommand(id, widget.editor.uri),
isEnabled: widget => widget instanceof EditorWidget,
isVisible: widget => widget instanceof EditorWidget
this.onDidRegisterCommand(action.command, pluginCommand => {
command.iconClass = pluginCommand.iconClass;
item.tooltip = pluginCommand.label;
});
this.tabBarToolbar.registerItem({ id, command, tooltip, group, when });
}

protected static parseMenuPaths(value: string): MenuPath[] {
Expand All @@ -128,17 +109,50 @@ export class MenusContributionPointHandler {
}

protected registerMenuAction(menuPath: MenuPath, menu: Menu): void {
const commandId = this.createSyntheticCommandId(menu, { prefix: '__plugin.menu.action.' });
const command: Command = { id: commandId };
// convert Core URI of a selected resource to a plugin (VS Code) URI format
const resolveArgs = (...args: any[]) => (args || []).map(arg => arg instanceof CoreURI ? arg['codeUri'] : arg);
this.commands.registerCommand(command, {
execute: (...args: any[]) => this.commands.executeCommand(menu.command, ...resolveArgs(...args)),
isEnabled: (...args: any[]) => this.commands.isEnabled(menu.command, ...resolveArgs(...args)),
isVisible: (...args: any[]) => this.commands.isVisible(menu.command, ...resolveArgs(...args))
});

const { when } = menu;
const [group = '', order = undefined] = (menu.group || '').split('@');
// Registering a menu action requires the related command to be already registered.
// But Theia plugin registers the commands dynamically via the Commands API.
// Let's wait for ~2 sec. It should be enough to finish registering all the contributed commands.
// FIXME: remove this workaround (timer) once the https://github.com/theia-ide/theia/issues/3344 is fixed
setTimeout(() => {
this.menuRegistry.registerMenuAction([...menuPath, group], {
commandId: menu.command,
order,
when: menu.when
});
}, 2000);
const action: MenuAction = { commandId, order, when };
this.menuRegistry.registerMenuAction([...menuPath, group], action);

this.onDidRegisterCommand(menu.command, pluginCommand => {
command.category = pluginCommand.category;
action.label = pluginCommand.label;
action.icon = pluginCommand.iconClass;
});
}

protected createSyntheticCommandId(menu: Menu, { prefix }: { prefix: string }): string {
const command = menu.command;
let id = prefix + command;
let index = 0;
while (this.commands.getCommand(id)) {
id = prefix + command + ':' + index;
index++;
}
return id;
}

protected onDidRegisterCommand(id: string, cb: (command: Command) => void): void {
const command = this.commands.getCommand(id);
if (command) {
cb(command);
} else {
// Registering a menu action requires the related command to be already registered.
// But Theia plugin registers the commands dynamically via the Commands API.
// Let's wait for ~2 sec. It should be enough to finish registering all the contributed commands.
// FIXME: remove this workaround (timer) once the https://github.com/theia-ide/theia/issues/3344 is fixed
setTimeout(() => this.onDidRegisterCommand(id, cb), 2000);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { PreferenceSchemaProvider } from '@theia/core/lib/browser';
import { PreferenceSchema } from '@theia/core/lib/browser/preferences';
import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler';
import { MonacoSnippetSuggestProvider } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider';
import { PluginSharedStyle } from './plugin-shared-style';
import { CommandRegistry } from '@theia/core';
import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming';

@injectable()
export class PluginContributionHandler {
Expand Down Expand Up @@ -51,6 +54,12 @@ export class PluginContributionHandler {
@inject(MonacoSnippetSuggestProvider)
protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider;

@inject(CommandRegistry)
protected readonly commands: CommandRegistry;

@inject(PluginSharedStyle)
protected readonly style: PluginSharedStyle;

handleContributions(contributions: PluginContribution): void {
if (contributions.configuration) {
this.updateConfigurationSchema(contributions.configuration);
Expand Down Expand Up @@ -133,6 +142,7 @@ export class PluginContributionHandler {
}
}

this.registerCommands(contributions);
this.menusContributionHandler.handle(contributions);
this.keybindingsContributionHandler.handle(contributions);
if (contributions.snippets) {
Expand All @@ -145,6 +155,29 @@ export class PluginContributionHandler {
}
}

protected pluginCommandIconId = 0;
protected registerCommands(contribution: PluginContribution): void {
if (!contribution.commands) {
return;
}
for (const { iconUrl, command, category, title } of contribution.commands) {
const iconClass = 'plugin-command-icon-' + this.pluginCommandIconId++;
const darkIconUrl = typeof iconUrl === 'object' ? iconUrl.dark : iconUrl;
const lightIconUrl = typeof iconUrl === 'object' ? iconUrl.light : iconUrl;
this.style.insertRule('.' + iconClass, theme => `
width: 16px;
height: 16px;
background: no-repeat url("${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl}");
`);
this.commands.registerCommand({
id: command,
category,
label: title,
iconClass
});
}
}

private updateConfigurationSchema(schema: PreferenceSchema): void {
this.preferenceSchemaProvider.setSchema(schema);
}
Expand Down
Loading

0 comments on commit 3aaa72b

Please sign in to comment.