diff --git a/packages/isolated-extension-api/compile.tsconfig.json b/packages/isolated-extension-api/compile.tsconfig.json index b8b72b49c8822..797c60148d07b 100644 --- a/packages/isolated-extension-api/compile.tsconfig.json +++ b/packages/isolated-extension-api/compile.tsconfig.json @@ -2,7 +2,12 @@ "extends": "../../configs/base.tsconfig", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "lib": [ + "es6", + "dom", + "webworker" + ] }, "include": [ "src" diff --git a/packages/isolated-extension-api/src/api/extension-api.ts b/packages/isolated-extension-api/src/api/extension-api.ts new file mode 100644 index 0000000000000..60d5b88221670 --- /dev/null +++ b/packages/isolated-extension-api/src/api/extension-api.ts @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +import { createProxyIdentifier, ProxyIdentifier } from './rpc-protocol'; +import * as theia from 'theia'; +/** + * A command handler is an implementation of a command. + * + * A command can have multiple handlers + * but they should be active in different contexts, + * otherwise first active will be executed. + */ +export interface CommandHandler { + /** + * Execute this handler. + */ + execute(...args: any[]): any; + /** + * Test whether this handler is enabled (active). + */ + isEnabled?(...args: any[]): boolean; + /** + * Test whether menu items for this handler should be visible. + */ + isVisible?(...args: any[]): boolean; +} + +export interface CommandRegistryMain { + /** + * Register the given command and handler if present. + * + * Throw if a command is already registered for the given command identifier. + */ + registerCommand(command: theia.Command): void; + + unregisterCommand(id: string): void; + executeCommand(id: string, args: any[]): PromiseLike; + getCommands(): PromiseLike; +} + +export interface CommandRegistryExt { + executeContributedCommand(id: string): PromiseLike; +} + +export const EXTENSION_RPC_CONTEXT = { + COMMAND_REGISTRY_MAIN: >createProxyIdentifier("CommandRegistryMain") +}; + +export const MAIN_RPC_CONTEXT = { + COMMAND_REGISTRY_EXT: createProxyIdentifier("CommandRegistryExt") +}; diff --git a/packages/isolated-extension-api/src/api/rpc-protocol.ts b/packages/isolated-extension-api/src/api/rpc-protocol.ts index 94df23eb79d65..9d1157b05e174 100644 --- a/packages/isolated-extension-api/src/api/rpc-protocol.ts +++ b/packages/isolated-extension-api/src/api/rpc-protocol.ts @@ -23,6 +23,11 @@ export interface RPCProtocol { */ getProxy(proxyId: ProxyIdentifier): T; + /** + * Register manually created instance. + */ + set(identifier: ProxyIdentifier, instance: R): R; + } export class ProxyIdentifier { @@ -33,15 +38,12 @@ export class ProxyIdentifier { } } -export function createMainProxyIdentifier(identifier: string): ProxyIdentifier { - return new ProxyIdentifier(true, 'm' + identifier); -} - -export function createExtensionProxyIdentifier(identifier: string): ProxyIdentifier { - return new ProxyIdentifier(false, 'e' + identifier); +export function createProxyIdentifier(identifier: string): ProxyIdentifier { + return new ProxyIdentifier(false, identifier); } export class RPCProtocolImpl implements RPCProtocol { + private isDisposed: boolean; private readonly locals: { [id: string]: any; }; private readonly proxies: { [id: string]: any; }; @@ -66,10 +68,15 @@ export class RPCProtocolImpl implements RPCProtocol { return this.proxies[proxyId.id]; } + set(identifier: ProxyIdentifier, instance: R): R { + this.locals[identifier.id] = instance; + return instance; + } + private createProxy(proxyId: string): T { const handler = { get: (target: any, name: string) => { - if (!target[name] && name.charCodeAt(0) === 36 /* CharCode.DollarSign */) { + if (!target[name] /*&& name.charCodeAt(0) === 36 */ /* CharCode.DollarSign */) { target[name] = (...myArgs: any[]) => this.remoteCall(proxyId, name, myArgs); } diff --git a/packages/isolated-extension-api/src/browser/command-registry-main.ts b/packages/isolated-extension-api/src/browser/command-registry-main.ts new file mode 100644 index 0000000000000..9c65b57970e8c --- /dev/null +++ b/packages/isolated-extension-api/src/browser/command-registry-main.ts @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +import { CommandRegistryExt, MAIN_RPC_CONTEXT, CommandRegistryMain } from '../api/extension-api'; +import { injectable, inject } from "inversify"; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { ExtensionWorker } from './extension-worker'; +import * as theia from 'theia'; +import { Disposable } from '@theia/core/lib/common/disposable'; + +@injectable() +export class CommandRegistryMainImpl implements CommandRegistryMain { + private proxy: CommandRegistryExt; + private disposables = new Map(); + + constructor( @inject(CommandRegistry) private readonly delegate: CommandRegistry, + @inject(ExtensionWorker) worker: ExtensionWorker) { + this.proxy = worker.rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT); + } + + registerCommand(command: theia.Command): void { + this.disposables.set( + command.id, + this.delegate.registerCommand(command, { + execute: (...args: any[]) => { + this.proxy.executeContributedCommand(command.id); + }, + isEnabled() { return true; }, + isVisible() { return true; } + })); + } + unregisterCommand(id: string): void { + const dis = this.disposables.get(id); + if (dis) { + dis.dispose(); + this.disposables.delete(id); + } + } + executeCommand(id: string, args: any[]): PromiseLike { + throw new Error("Method not implemented."); + } + getCommands(): PromiseLike { + throw new Error("Method not implemented."); + } + +} diff --git a/packages/isolated-extension-api/src/browser/extension-api-frontend-module.ts b/packages/isolated-extension-api/src/browser/extension-api-frontend-module.ts index 3fc17a7919890..6b28eef5fe3a6 100644 --- a/packages/isolated-extension-api/src/browser/extension-api-frontend-module.ts +++ b/packages/isolated-extension-api/src/browser/extension-api-frontend-module.ts @@ -11,15 +11,19 @@ import { ContainerModule } from "inversify"; import { FrontendApplicationContribution, FrontendApplication } from "@theia/core/lib/browser"; import { MaybePromise } from "@theia/core/lib/common"; +import { ExtensionWorker } from './extension-worker'; +import { CommandRegistryMainImpl } from './command-registry-main'; +import { EXTENSION_RPC_CONTEXT } from '../api/extension-api'; export default new ContainerModule(bind => { + bind(ExtensionWorker).toSelf().inSingletonScope(); + bind(CommandRegistryMainImpl).toSelf().inSingletonScope(); + // bind(FrontendApplicationContribution).toService(ExtensionWorker); bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ onStart(app: FrontendApplication): MaybePromise { - - const worker: Worker = new (require('../worker/worker-main')); - worker.addEventListener('message', message => { - console.log('message is', message); - }); + const worker = ctx.container.get(ExtensionWorker); + const commandRegistryMain = ctx.container.get(CommandRegistryMainImpl); + worker.rpc.set(EXTENSION_RPC_CONTEXT.COMMAND_REGISTRY_MAIN, commandRegistryMain); } })); }); diff --git a/packages/isolated-extension-api/src/browser/extension-worker.ts b/packages/isolated-extension-api/src/browser/extension-worker.ts new file mode 100644 index 0000000000000..e165863677a69 --- /dev/null +++ b/packages/isolated-extension-api/src/browser/extension-worker.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +import { injectable } from "inversify"; +import { RPCProtocolImpl, RPCProtocol } from '../api/rpc-protocol'; +import { Emitter } from '@theia/core/lib/common/event'; + +@injectable() +export class ExtensionWorker { + + private worker: Worker; + public readonly rpc: RPCProtocol; + constructor() { + const emmitter = new Emitter(); + this.worker = new (require('../worker/worker-main')); + this.worker.onmessage = (message) => { + emmitter.fire(message.data); + }; + + this.rpc = new RPCProtocolImpl({ + onMessage: emmitter.event, + send: (m: {}) => this.worker.postMessage(m) + }); + + } +} diff --git a/packages/isolated-extension-api/src/extension/comand-registry.ts b/packages/isolated-extension-api/src/extension/comand-registry.ts new file mode 100644 index 0000000000000..024df391f48a0 --- /dev/null +++ b/packages/isolated-extension-api/src/extension/comand-registry.ts @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +import { CommandRegistryExt, EXTENSION_RPC_CONTEXT as Ext, CommandRegistryMain } from '../api/extension-api'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { RPCProtocol } from '../api/rpc-protocol'; +import * as theia from 'theia'; + +export type Handler = (...args: any[]) => T | PromiseLike; + +export class CommandRegistryImpl implements CommandRegistryExt { + + private proxy: CommandRegistryMain; + private commands = new Map(); + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN); + } + registerCommand(command: theia.Command, handler: Handler): Disposable { + if (this.commands.has(command.id)) { + throw new Error(`Command ${command.id} already exist`); + } + this.commands.set(command.id, handler); + this.proxy.registerCommand(command); + + return Disposable.create(() => { + this.proxy.unregisterCommand(command.id); + }); + + } + + dispose(): void { + throw new Error("Method not implemented."); + } + + executeContributedCommand(id: string): PromiseLike { + const handler = this.commands.get(id); + if (handler) { + return Promise.resolve(handler()); + } else { + return Promise.reject(new Error(`Command ${id} doesn't exist`)); + } + } + +} diff --git a/packages/isolated-extension-api/src/extension/extension-context.ts b/packages/isolated-extension-api/src/extension/extension-context.ts new file mode 100644 index 0000000000000..e04044e02fc9e --- /dev/null +++ b/packages/isolated-extension-api/src/extension/extension-context.ts @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +import { MAIN_RPC_CONTEXT } from '../api/extension-api'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { RPCProtocol } from '../api/rpc-protocol'; +import * as theia from 'theia'; +import { CommandRegistryImpl } from './comand-registry'; + +export function createAPI(rpc: RPCProtocol): typeof theia { + const commandRegistryExt = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); + + const commands: typeof theia.commands = { + registerCommand(command: theia.Command, callback: (...args: any[]) => T | Thenable): Disposable { + return commandRegistryExt.registerCommand(command, callback); + } + }; + return { + commands + }; + +} diff --git a/packages/isolated-extension-api/src/theia.d.ts b/packages/isolated-extension-api/src/theia.d.ts new file mode 100644 index 0000000000000..06725d37083dd --- /dev/null +++ b/packages/isolated-extension-api/src/theia.d.ts @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +//import { Disposable } from '@theia/core/lib/common/disposable'; + +declare module 'theia' { + /** + * A command is a unique identifier of a function + * which can be executed by a user via a keyboard shortcut, + * a menu action or directly. + */ + export interface Command { + /** + * A unique identifier of this command. + */ + id: string; + /** + * A label of this command. + */ + label?: string; + /** + * An icon class of this command. + */ + iconClass?: string; + } + export namespace commands { + export function registerCommand(command: Command, callback: (...args: any[]) => any): Disposable + } +} \ No newline at end of file diff --git a/packages/isolated-extension-api/src/worker/worker-main.ts b/packages/isolated-extension-api/src/worker/worker-main.ts index d846ab540c4cb..7c3bbcdcc0b65 100644 --- a/packages/isolated-extension-api/src/worker/worker-main.ts +++ b/packages/isolated-extension-api/src/worker/worker-main.ts @@ -9,17 +9,11 @@ * Red Hat, Inc. - initial API and implementation */ -import { RPCProtocolImpl, createExtensionProxyIdentifier } from '../api/rpc-protocol'; +import { RPCProtocolImpl } from '../api/rpc-protocol'; import { Emitter } from '@theia/core/lib/common/event'; -import { Disposable } from '@theia/core/lib/common/disposable'; -export interface MainThreadCommandsShape extends Disposable { - $registerCommand(id: string): void; - $unregisterCommand(id: string): void; - $executeCommand(id: string, args: any[]): Thenable; - $getCommands(): Thenable; -} +import { createAPI } from '../extension/extension-context'; -const ctx: Worker = self as any; +const ctx: DedicatedWorkerGlobalScope = self as any; const emmitter = new Emitter(); const rpc = new RPCProtocolImpl({ @@ -32,8 +26,7 @@ addEventListener('message', (message: any) => { emmitter.fire(message.data); }); -const ExtHostCommands = createExtensionProxyIdentifier("MainThreadCommands"); - -const proxy = rpc.getProxy(ExtHostCommands); - -proxy.$registerCommand('foo'); +const api = createAPI(rpc); +api.commands.registerCommand({ id: 'fooBar', label: 'Command From Extension' }, () => { + console.log("Hello from WebWorker Command"); +});