Skip to content

Commit

Permalink
[vscode] support onCommand activation event
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <[email protected]>
  • Loading branch information
akosyakov committed Jul 3, 2019
1 parent 324dcea commit 0d53466
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 22 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Breaking changes:

- [plugin] fixed typo in 'HostedInstanceState' enum from RUNNNING to RUNNING in `plugin-dev` extension
- [plugin] removed member `processOptions` from `AbstractHostedInstanceManager` as it is not initialized or used
- [plugin] added basic support of activation events [#5622](https://github.com/theia-ide/theia/pull/5622)
- [plugin] added support of activation events [#5622](https://github.com/theia-ide/theia/pull/5622)
- `HostedPluginSupport` is refactored to support multiple `PluginManagerExt` properly

## v0.8.0
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/common/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
********************************************************************************/

import { injectable, inject, named } from 'inversify';
import { Event, Emitter } from './event';
import { Disposable, DisposableCollection } from './disposable';
import { ContributionProvider } from './contribution-provider';

Expand Down Expand Up @@ -117,6 +118,12 @@ export interface CommandContribution {
registerCommands(commands: CommandRegistry): void;
}

export interface WillExecuteCommandEvent {
commandId: string;
// tslint:disable-next-line:no-any
waitUntil(thenable: Promise<any>): void;
}

export const commandServicePath = '/services/commands';
export const CommandService = Symbol('CommandService');
/**
Expand All @@ -130,6 +137,12 @@ export interface CommandService {
*/
// tslint:disable-next-line:no-any
executeCommand<T>(command: string, ...args: any[]): Promise<T | undefined>;
/**
* An event is emmited when a command is about to be executed.
*
* It can be used to install or active a command handler.
*/
readonly onWillExecuteCommand: Event<WillExecuteCommandEvent>;
}

/**
Expand All @@ -144,6 +157,9 @@ export class CommandRegistry implements CommandService {
// List of recently used commands.
protected _recent: Command[] = [];

protected readonly onWillExecuteCommandEmitter = new Emitter<WillExecuteCommandEvent>();
readonly onWillExecuteCommand = this.onWillExecuteCommandEmitter.event;

constructor(
@inject(ContributionProvider) @named(CommandContribution)
protected readonly contributionProvider: ContributionProvider<CommandContribution>
Expand Down Expand Up @@ -255,6 +271,7 @@ export class CommandRegistry implements CommandService {
*/
// tslint:disable-next-line:no-any
async executeCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {
await this.fireWillExecuteCommand(commandId);
const handler = this.getActiveHandler(commandId, ...args);
if (handler) {
const result = await handler.execute(...args);
Expand All @@ -268,6 +285,25 @@ export class CommandRegistry implements CommandService {
throw new Error(`The command '${commandId}' cannot be executed. There are no active handlers available for the command.${argsMessage}`);
}

protected async fireWillExecuteCommand(commandId: string): Promise<void> {
const waitables: Promise<void>[] = [];
this.onWillExecuteCommandEmitter.fire({
commandId,
waitUntil: (thenable: Promise<void>) => {
if (Object.isFrozen(waitables)) {
throw new Error('waitUntil cannot be called asynchronously.');
}
waitables.push(thenable);
}
});
if (!waitables.length) {
return;
}
// Asynchronous calls to `waitUntil` should fail.
Object.freeze(waitables);
await Promise.race([Promise.all(waitables), new Promise(resolve => setTimeout(resolve, 30000))]);
}

/**
* Get a visible handler for the given command or `undefined`.
*/
Expand Down
40 changes: 35 additions & 5 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { HostedPluginServer, PluginMetadata, getPluginId } from '../../common/pl
import { HostedPluginWatcher } from './hosted-plugin-watcher';
import { setUpPluginApi } from '../../main/browser/main-context';
import { RPCProtocol, RPCProtocolImpl } from '../../api/rpc-protocol';
import { ILogger, ContributionProvider } from '@theia/core';
import { ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent } from '@theia/core';
import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler';
Expand All @@ -36,6 +36,7 @@ import { KeysToKeysToAnyValue } from '../../common/types';
import { FileStat } from '@theia/filesystem/lib/common/filesystem';
import { PluginManagerExt, MAIN_RPC_CONTEXT } from '../../common';
import { MonacoTextmateService } from '@theia/monaco/lib/browser/textmate';
import { Deferred } from '@theia/core/lib/common/promise-util';

export type PluginHost = 'frontend' | string;

Expand Down Expand Up @@ -80,6 +81,9 @@ export class HostedPluginSupport {
@inject(MonacoTextmateService)
protected readonly monacoTextmateService: MonacoTextmateService;

@inject(CommandRegistry)
protected readonly commands: CommandRegistry;

private theiaReadyPromise: Promise<any>;

protected readonly managers: PluginManagerExt[] = [];
Expand All @@ -98,6 +102,7 @@ export class HostedPluginSupport {
this.activateByLanguage(id);
}
this.monacoTextmateService.onDidActivateLanguage(id => this.activateByLanguage(id));
this.commands.onWillExecuteCommand(event => this.ensureCommandHandlerRegistration(event));
}

checkAndLoadPlugin(container: interfaces.Container): void {
Expand Down Expand Up @@ -196,18 +201,43 @@ export class HostedPluginSupport {
}
}

activateByEvent(activationEvent: string): void {
async activateByEvent(activationEvent: string): Promise<void> {
if (this.activationEvents.has(activationEvent)) {
return;
}
this.activationEvents.add(activationEvent);
const activation: Promise<void>[] = [];
for (const manager of this.managers) {
manager.$activateByEvent(activationEvent);
activation.push(manager.$activateByEvent(activationEvent));
}
await Promise.all(activation);
}

async activateByLanguage(languageId: string): Promise<void> {
await this.activateByEvent(`onLanguage:${languageId}`);
}

async activateByCommand(commandId: string): Promise<void> {
await this.activateByEvent(`onCommand:${commandId}`);
}

activateByLanguage(languageId: string): void {
this.activateByEvent(`onLanguage:${languageId}`);
protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void {
if (!this.contributionHandler.hasCommand(event.commandId) || this.contributionHandler.hasCommandHandler(event.commandId)) {
return;
}
const waitForCommandHandler = new Deferred<void>();
const listener = this.contributionHandler.onDidRegisterCommandHandler(id => {
if (id === event.commandId) {
listener.dispose();
waitForCommandHandler.resolve();
}
});
const p = Promise.all([
this.activateByCommand(event.commandId),
waitForCommandHandler.promise
]);
p.then(() => listener.dispose(), () => listener.dispose());
event.waitUntil(p);
}

}
Expand Down
22 changes: 9 additions & 13 deletions packages/plugin-ext/src/main/browser/command-registry-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,25 @@ 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 { PluginContributionHandler } from './plugin-contribution-handler';

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

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT);
this.delegate = container.get(CommandRegistry);
this.keyBinding = container.get(KeybindingRegistry);
this.contributions = container.get(PluginContributionHandler);
}

$registerCommand(command: theia.CommandDescription): void {
this.commands.set(command.id, this.delegate.registerCommand(command));
this.commands.set(command.id, this.contributions.registerCommand(command));
}
$unregisterCommand(id: string): void {
const command = this.commands.get(id);
Expand All @@ -47,16 +50,9 @@ export class CommandRegistryMainImpl implements CommandRegistryMain {
}

$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; }
}));
this.handlers.set(id, this.contributions.registerCommandHandler(id, (...args) =>
this.proxy.$executeCommand(id, ...args)
));
}
$unregisterHandler(id: string): void {
const handler = this.handlers.get(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/br
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 { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter } from '@theia/core/lib/common/event';

@injectable()
export class PluginContributionHandler {
Expand Down Expand Up @@ -157,7 +159,7 @@ export class PluginContributionHandler {
}
for (const { iconUrl, command, category, title } of contribution.commands) {
const iconClass = iconUrl ? this.style.toIconClass(iconUrl) : undefined;
this.commands.registerCommand({
this.registerCommand({
id: command,
category,
label: title,
Expand All @@ -166,6 +168,41 @@ export class PluginContributionHandler {
}
}

protected readonly registeredCommands = new Set<string>();
protected readonly commandHandlers = new Map<string, CommandHandler['execute']>();
protected readonly onDidRegisterCommandHandlerEmitter = new Emitter<string>();
readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event;
registerCommand(command: Command): Disposable {
const toDispose = new DisposableCollection();
toDispose.push(this.commands.registerCommand(command, {
execute: async (...args) => {
const handler = this.commandHandlers.get(command.id);
if (!handler) {
throw new Error(`command '${command.id}' not found`);
}
return handler(...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; }
}));
this.registeredCommands.add(command.id);
toDispose.push(Disposable.create(() => this.registeredCommands.delete(command.id)));
return toDispose;
}
registerCommandHandler(id: string, execute: CommandHandler['execute']): Disposable {
this.commandHandlers.set(id, execute);
this.onDidRegisterCommandHandlerEmitter.fire(id);
return Disposable.create(() => this.commandHandlers.delete(id));
}
hasCommand(id: string): boolean {
return this.registeredCommands.has(id);
}
hasCommandHandler(id: string): boolean {
return this.commandHandlers.has(id);
}

private updateConfigurationSchema(schema: PreferenceSchema): void {
this.validateConfigurationSchema(schema);
this.preferenceSchemaProvider.setSchema(schema);
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/plugin/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class ActivatedPlugin {

export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {

static SUPPORTED_ACTIVATION_EVENTS = new Set(['*', 'onLanguage']);
static SUPPORTED_ACTIVATION_EVENTS = new Set(['*', 'onLanguage', 'onCommand']);

private readonly registry = new Map<string, Plugin>();
private readonly activations = new Map<string, (() => Promise<void>)[] | undefined>();
Expand Down

0 comments on commit 0d53466

Please sign in to comment.