diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbc1d9deef91f..18b9302cf7ea6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Change Log
+## v0.12.0
+
+Breaking changes:
+
+- [plugin] don't block web socket with many plugins [6252](https://github.com/eclipse-theia/theia/pull/6252)
+ - `PluginModel` does not have anymore `contributes` and `dependencies` to avoid sending unnecessary data.
+ - Use `PluginReader.readContribution` to load contributes.
+ - Use `PluginReader.readDependencies` to load dependencies.
+ - `PluginMetadata` does not have anymore raw package.json model to avoid sending excessive data to the frontend.
+ - `theia.Plugin.packageJSON` throws an unsupported error for frontend plugins as a consequence. Please convert to a backend plugin if you need access to it.
+ - `PluginManagerExt.$init` does not start plugins anymore, but only initialize the manager RPC services to avoid sending excessive initialization data, as all preferences, on each deployment.
+ - Please call `$start` to start plugins.
+ - `PluginDeployerHandler.getPluginMetadata` is replaced with `PluginDeployerHandler.getPluginDependencies` to access plugin dependencies.
+ - `HostedPluginServer.getDeployedMetadata` is replaced with `HostedPluginServer.getDeployedPluginIds` and `HostedPluginServer.getDeployedPlugins`
+ to fetch first only ids of deployed plugins and then deployed metadata for only yet not loaded plugins.
+ - `HostedPluginDeployerHandler.getDeployedFrontendMetadata` and `HostedPluginDeployerHandler.getDeployedBackendMetadata` are replaced with
+ `HostedPluginDeployerHandler.getDeployedFrontendPluginIds`, `HostedPluginDeployerHandlergetDeployedBackendPluginIds` and `HostedPluginDeployerHandler.getDeployedPlugin` to featch first only ids and then deplyoed metadata fro only yet not loaded plugins.
+ - `PluginHost.init` can initialize plugins asynchronous, synchronous initialization is still supported.
+ - `HostedPluginReader.doGetPluginMetadata` is renamed to `HostedPluginReader.getPluginMetadata`.
+ - `PluginDebugAdapterContribution.languages`, `PluginDebugAdapterContribution.getSchemaAttributes` and `PluginDebugAdapterContribution.getConfigurationSnippets` are removed to prevent sending the contributions second time to the frontend. Debug contributions are loaded statically from the deployed plugin metadata instead. The same for corresponding methods in `DebugExtImpl`.
+
## v0.11.0
- [core] added ENTER event handler to the open button in explorer [#6158](https://github.com/eclipse-theia/theia/pull/6158)
diff --git a/packages/core/package.json b/packages/core/package.json
index 95ff7a05d4734..9eff029440be1 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -43,7 +43,7 @@
"vscode-languageserver-types": "^3.15.0-next",
"vscode-uri": "^1.0.8",
"vscode-ws-jsonrpc": "^0.1.1",
- "ws": "^5.2.2",
+ "ws": "^7.1.2",
"yargs": "^11.1.0"
},
"publishConfig": {
diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts
index 90a149dc5e3a9..1ab7cbe683f0d 100644
--- a/packages/core/src/node/messaging/messaging-contribution.ts
+++ b/packages/core/src/node/messaging/messaging-contribution.ts
@@ -84,7 +84,10 @@ export class MessagingContribution implements BackendApplicationContribution, Me
onStart(server: http.Server | https.Server): void {
const wss = new ws.Server({
server,
- perMessageDeflate: false
+ perMessageDeflate: {
+ // don't compress if a message is less than 256kb
+ threshold: 256 * 1024
+ }
});
interface CheckAliveWS extends ws {
alive: boolean;
diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts
index 168d097510fa6..a9404af8371c8 100644
--- a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts
+++ b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts
@@ -23,8 +23,6 @@ import { LanguageGrammarDefinitionContribution, getEncodedLanguageId } from './t
import { createTextmateTokenizer, TokenizerOption } from './textmate-tokenizer';
import { TextmateRegistry } from './textmate-registry';
import { MonacoThemeRegistry } from './monaco-theme-registry';
-import { MonacoEditor } from '../monaco-editor';
-import { EditorManager } from '@theia/editor/lib/browser';
export const OnigasmPromise = Symbol('OnigasmPromise');
export type OnigasmPromise = Promise;
@@ -60,9 +58,6 @@ export class MonacoTextmateService implements FrontendApplicationContribution {
@inject(MonacoThemeRegistry)
protected readonly monacoThemeRegistry: MonacoThemeRegistry;
- @inject(EditorManager)
- private readonly editorManager: EditorManager;
-
initialize(): void {
if (!isBasicWasmSupported) {
console.log('Textmate support deactivated because WebAssembly is not detected.');
@@ -109,7 +104,6 @@ export class MonacoTextmateService implements FrontendApplicationContribution {
for (const { id } of monaco.languages.getLanguages()) {
monaco.languages.onLanguage(id, () => this.activateLanguage(id));
}
- this.detectLanguages();
}
protected readonly toDisposeOnUpdateTheme = new DisposableCollection();
@@ -166,11 +160,4 @@ export class MonacoTextmateService implements FrontendApplicationContribution {
}
}
- detectLanguages(): void {
- for (const editor of MonacoEditor.getAll(this.editorManager)) {
- if (editor.languageAutoDetected) {
- editor.detectLanguage();
- }
- }
- }
}
diff --git a/packages/plugin-dev/src/node/hosted-plugin-reader.ts b/packages/plugin-dev/src/node/hosted-plugin-reader.ts
index a866d3bbcb49f..9de24675a5bb7 100644
--- a/packages/plugin-dev/src/node/hosted-plugin-reader.ts
+++ b/packages/plugin-dev/src/node/hosted-plugin-reader.ts
@@ -34,7 +34,7 @@ export class HostedPluginReader implements BackendApplicationContribution {
protected deployerHandler: HostedPluginDeployerHandler;
async initialize(): Promise {
- this.pluginReader.doGetPluginMetadata(process.env.HOSTED_PLUGIN)
+ this.pluginReader.getPluginMetadata(process.env.HOSTED_PLUGIN)
.then(this.hostedPlugin.resolve.bind(this.hostedPlugin));
const pluginPath = process.env.HOSTED_PLUGIN;
diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
index 7f02a8f511d77..4a049bce4de63 100644
--- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
+++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
@@ -44,7 +44,8 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF
vscode.commands.registerCommand = function (command: theia.CommandDescription | string, handler?: (...args: any[]) => T | Thenable, thisArg?: any): any {
// use of the ID when registering commands
if (typeof command === 'string') {
- const commands = plugin.model.contributes && plugin.model.contributes.commands;
+ const rawCommands = plugin.rawModel.contributes && plugin.rawModel.contributes.commands;
+ const commands = rawCommands ? Array.isArray(rawCommands) ? rawCommands : [rawCommands] : undefined;
if (handler && commands && commands.some(item => item.command === command)) {
return vscode.commands.registerHandler(command, handler, thisArg);
}
diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
index 204f287852230..82407d75674ee 100644
--- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
+++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
@@ -34,6 +34,7 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca
plugin.name = plugin.name.substr(built_prefix.length);
}
const result: PluginModel = {
+ packagePath: plugin.packagePath,
// see id definition: https://github.com/microsoft/vscode/blob/15916055fe0cb9411a5f36119b3b012458fe0a1d/src/vs/platform/extensions/common/extensions.ts#L167-L169
id: `${plugin.publisher.toLowerCase()}.${plugin.name.toLowerCase()}`,
name: plugin.name,
@@ -47,13 +48,26 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca
},
entryPoint: {
backend: plugin.main
- },
- extensionDependencies: this.getDeployableDependencies(plugin.extensionDependencies || [])
+ }
};
- result.contributes = this.readContributions(plugin);
return result;
}
+ /**
+ * Maps extension dependencies to deployable extension dependencies.
+ */
+ getDependencies(plugin: PluginPackage): Map | undefined {
+ if (!plugin.extensionDependencies || !plugin.extensionDependencies.length) {
+ return undefined;
+ }
+ const dependencies = new Map();
+ for (const dependency of plugin.extensionDependencies) {
+ const dependencyId = dependency.toLowerCase();
+ dependencies.set(dependencyId, this.VSCODE_PREFIX + dependencyId);
+ }
+ return dependencies;
+ }
+
getLifecycle(plugin: PluginPackage): PluginLifecycle {
return {
startMethod: 'activate',
@@ -63,11 +77,4 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca
};
}
- /**
- * Converts an array of extension dependencies
- * to an array of deployable extension dependencies
- */
- private getDeployableDependencies(dependencies: string[]): string[] {
- return dependencies.map(dep => this.VSCODE_PREFIX + dep.toLowerCase());
- }
}
diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts
index c85c047d05801..d063df23d43e8 100644
--- a/packages/plugin-ext/src/common/plugin-api-rpc.ts
+++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts
@@ -66,7 +66,6 @@ import {
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { KeysToAnyValues, KeysToKeysToAnyValue } from './types';
import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin';
-import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { DebugProtocol } from 'vscode-debugprotocol';
import { SymbolInformation } from 'vscode-languageserver-types';
@@ -75,16 +74,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { QuickOpenItem, QuickOpenItemOptions } from '@theia/core/lib/common/quick-open-model';
import { QuickTitleButton } from '@theia/core/lib/common/quick-open-model';
-export interface PluginInitData {
- plugins: PluginMetadata[];
- preferences: PreferenceData;
- globalState: KeysToKeysToAnyValue;
- workspaceState: KeysToKeysToAnyValue;
- env: EnvInit;
- extApi?: ExtPluginApi[];
- activationEvents: string[]
-}
-
export interface PreferenceData {
[scope: number]: any;
}
@@ -99,7 +88,7 @@ export interface Plugin {
export interface ConfigStorage {
hostLogPath: string;
- hostStoragePath: string,
+ hostStoragePath?: string,
}
export interface EnvInit {
@@ -141,6 +130,7 @@ export const emptyPlugin: Plugin = {
type: 'empty',
version: 'empty'
},
+ packagePath: 'empty',
entryPoint: {
}
@@ -161,12 +151,35 @@ export const emptyPlugin: Plugin = {
}
};
+export interface PluginManagerInitializeParams {
+ preferences: PreferenceData
+ globalState: KeysToKeysToAnyValue
+ workspaceState: KeysToKeysToAnyValue
+ env: EnvInit
+ extApi?: ExtPluginApi[]
+}
+
+export interface PluginManagerStartParams {
+ plugins: PluginMetadata[]
+ configStorage: ConfigStorage
+ activationEvents: string[]
+}
+
export interface PluginManagerExt {
- $stop(pluginId?: string): PromiseLike;
- $init(pluginInit: PluginInitData, configStorage: ConfigStorage): PromiseLike;
+ /** initialize the manager, should be called only once */
+ $init(params: PluginManagerInitializeParams): Promise;
+
+ /** load and activate plugins */
+ $start(params: PluginManagerStartParams): Promise;
+
+ /** deactivate the plugin */
+ $stop(pluginId: string): Promise;
+
+ /** deactivate all plugins */
+ $stop(): Promise;
- $updateStoragePath(path: string | undefined): PromiseLike;
+ $updateStoragePath(path: string | undefined): Promise;
$activateByEvent(event: string): Promise;
}
@@ -1241,9 +1254,6 @@ export interface DebugExt {
$sessionDidChange(sessionId: string | undefined): void;
$provideDebugConfigurations(debugType: string, workspaceFolder: string | undefined): Promise;
$resolveDebugConfigurations(debugConfiguration: theia.DebugConfiguration, workspaceFolder: string | undefined): Promise;
- $getSupportedLanguages(debugType: string): Promise;
- $getSchemaAttributes(debugType: string): Promise;
- $getConfigurationSnippets(debugType: string): Promise;
$createDebugSession(debugConfiguration: theia.DebugConfiguration): Promise;
$terminateDebugSession(sessionId: string): Promise;
$getTerminalCreationOptions(debugType: string): Promise;
diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts
index 0572e9a94f51f..01528100e7358 100644
--- a/packages/plugin-ext/src/common/plugin-protocol.ts
+++ b/packages/plugin-ext/src/common/plugin-protocol.ts
@@ -228,6 +228,14 @@ export interface PluginScanner {
* @returns {PluginLifecycle}
*/
getLifecycle(plugin: PluginPackage): PluginLifecycle;
+
+ getContribution(plugin: PluginPackage): PluginContribution | undefined;
+
+ /**
+ * A mapping between a dependency as its defined in package.json
+ * and its deployable form, e.g. `publisher.name` -> `vscode:extension/publisher.name`
+ */
+ getDependencies(plugin: PluginPackage): Map | undefined;
}
export const PluginDeployer = Symbol('PluginDeployer');
@@ -374,22 +382,20 @@ export interface PluginModel {
type: PluginEngine;
version: string;
};
- entryPoint: {
- frontend?: string;
- backend?: string;
- };
- contributes?: PluginContribution;
- /**
- * The deployable form of extensionDependencies from package.json,
- * i.e. not `publisher.name`, but `vscode:extension/publisher.name`.
- */
- extensionDependencies?: string[];
+ entryPoint: PluginEntryPoint;
+ packagePath: string;
+}
+
+export interface PluginEntryPoint {
+ frontend?: string;
+ backend?: string;
}
/**
* This interface describes some static plugin contributions.
*/
export interface PluginContribution {
+ activationEvents?: string[];
configuration?: PreferenceSchema[];
configurationDefaults?: PreferenceSchemaProperties;
languages?: LanguageContribution[];
@@ -583,7 +589,6 @@ export interface ExtensionContext {
export interface PluginMetadata {
host: string;
- source: PluginPackage;
model: PluginModel;
lifecycle: PluginLifecycle;
}
@@ -610,20 +615,34 @@ export interface HostedPluginClient {
onDidDeploy(): void;
}
+export interface PluginDependencies {
+ metadata: PluginMetadata
+ mapping?: Map
+}
+
export const PluginDeployerHandler = Symbol('PluginDeployerHandler');
export interface PluginDeployerHandler {
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise;
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise;
- getPluginMetadata(pluginToBeInstalled: PluginDeployerEntry): Promise
+ getPluginDependencies(pluginToBeInstalled: PluginDeployerEntry): Promise
+}
+
+export interface GetDeployedPluginsParams {
+ pluginIds: string[]
+}
+
+export interface DeployedPlugin {
+ metadata: PluginMetadata;
+ contributes?: PluginContribution;
}
export const HostedPluginServer = Symbol('HostedPluginServer');
export interface HostedPluginServer extends JsonRpcServer {
- getDeployedMetadata(): Promise;
- getDeployedFrontendMetadata(): Promise;
- getDeployedBackendMetadata(): Promise;
+ getDeployedPluginIds(): Promise;
+
+ getDeployedPlugins(params: GetDeployedPluginsParams): Promise;
getExtPluginAPI(): Promise;
diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
index 6fb03f665fa5c..e9c554f23cc88 100644
--- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
+++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
@@ -21,9 +21,11 @@
// tslint:disable:no-any
+import debounce = require('lodash.debounce');
+import { UUID } from '@phosphor/coreutils';
import { injectable, inject, interfaces, named, postConstruct } from 'inversify';
import { PluginWorker } from '../../main/browser/plugin-worker';
-import { PluginMetadata, getPluginId, HostedPluginServer } from '../../common/plugin-protocol';
+import { PluginMetadata, getPluginId, HostedPluginServer, DeployedPlugin } from '../../common/plugin-protocol';
import { HostedPluginWatcher } from './hosted-plugin-watcher';
import { MAIN_RPC_CONTEXT, PluginManagerExt } from '../../common/plugin-api-rpc';
import { setUpPluginApi } from '../../main/browser/main-context';
@@ -33,16 +35,14 @@ import {
ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent,
CancellationTokenSource, JsonRpcProxy, ProgressService
} from '@theia/core';
-import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser';
+import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser/preferences';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler';
import { getQueryParameters } from '../../main/browser/env-main';
-import { ExtPluginApi, MainPluginApiProvider } from '../../common/plugin-ext-api-contribution';
+import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution';
import { PluginPathsService } from '../../main/common/plugin-paths-protocol';
import { getPreferences } from '../../main/browser/preference-registry-main';
import { PluginServer } from '../../common/plugin-protocol';
-import { KeysToKeysToAnyValue } from '../../common/types';
-import { FileStat } from '@theia/filesystem/lib/common/filesystem';
import { MonacoTextmateService } from '@theia/monaco/lib/browser/textmate';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
@@ -61,7 +61,10 @@ export const PluginProgressLocation = 'plugin';
@injectable()
export class HostedPluginSupport {
- container: interfaces.Container;
+
+ protected readonly clientId = UUID.uuid4();
+
+ protected container: interfaces.Container;
@inject(ILogger)
protected readonly logger: ILogger;
@@ -155,7 +158,7 @@ export class HostedPluginSupport {
get plugins(): PluginMetadata[] {
const plugins: PluginMetadata[] = [];
- this.contributions.forEach(contributions => plugins.push(contributions.plugin));
+ this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata));
return plugins;
}
@@ -167,38 +170,32 @@ export class HostedPluginSupport {
this.server.onDidOpenConnection(() => this.load());
}
- async load(): Promise {
+ protected loadQueue: Promise = Promise.resolve(undefined);
+ load = debounce(() => this.loadQueue = this.loadQueue.then(async () => {
try {
- await this.progressService.withProgress('', PluginProgressLocation, async () => {
- const roots = this.workspaceService.tryGetRoots();
- const [plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates] = await Promise.all([
- this.server.getDeployedMetadata(),
- this.pluginPathsService.getHostLogPath(),
- this.getStoragePath(),
- this.server.getExtPluginAPI(),
- this.pluginServer.getAllStorageValues(undefined),
- this.pluginServer.getAllStorageValues({ workspace: this.workspaceService.workspace, roots })
- ]);
- await this.doLoad({ plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates, roots }, this.container);
- });
+ await this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad());
} catch (e) {
console.error('Failed to load plugins:', e);
}
- }
+ }), 50, { leading: true });
- protected async doLoad(initData: PluginsInitializationData, container: interfaces.Container): Promise {
+ protected async doLoad(): Promise {
const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
this.server.onDidCloseConnection(() => toDisconnect.dispose());
+ // process empty plugins as well in order to properly remove stale plugin widgets
+ await this.syncPlugins();
+
// make sure that the previous state, including plugin widgets, is restored
// and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell
// but shell is not yet revealed
await this.appState.reachedState('initialized_layout');
+
if (toDisconnect.disposed) {
// if disconnected then don't try to load plugin contributions
return;
}
- const contributionsByHost = this.loadContributions(initData.plugins, toDisconnect);
+ const contributionsByHost = this.loadContributions(toDisconnect);
await this.viewRegistry.initWidgets();
// remove restored plugin widgets which were not registered by contributions
@@ -209,35 +206,73 @@ export class HostedPluginSupport {
// if disconnected then don't try to init plugin code and dynamic contributions
return;
}
- toDisconnect.push(this.startPlugins(contributionsByHost, initData, container));
+ await this.startPlugins(contributionsByHost, toDisconnect);
+ }
+
+ /**
+ * Sync loaded and deployed plugins:
+ * - undeployed plugins are unloaded
+ * - newly deployed plugins are initialized
+ */
+ protected async syncPlugins(): Promise {
+ let initialized = 0;
+ const syncPluginsMeasurement = this.createMeasurement('syncPlugins');
+
+ const toUnload = new Set(this.contributions.keys());
+ try {
+ const pluginIds: string[] = [];
+ const deployedPluginIds = await this.server.getDeployedPluginIds();
+ for (const pluginId of deployedPluginIds) {
+ toUnload.delete(pluginId);
+ if (!this.contributions.has(pluginId)) {
+ pluginIds.push(pluginId);
+ }
+ }
+ for (const pluginId of toUnload) {
+ const contribution = this.contributions.get(pluginId);
+ if (contribution) {
+ contribution.dispose();
+ }
+ }
+ if (pluginIds.length) {
+ const plugins = await this.server.getDeployedPlugins({ pluginIds });
+ for (const plugin of plugins) {
+ const pluginId = plugin.metadata.model.id;
+ const contributions = new PluginContributions(plugin);
+ this.contributions.set(pluginId, contributions);
+ contributions.push(Disposable.create(() => this.contributions.delete(pluginId)));
+ initialized++;
+ }
+ }
+ } finally {
+ if (initialized || toUnload.size) {
+ this.onDidChangePluginsEmitter.fire(undefined);
+ }
+ }
+
+ this.logMeasurement('Sync', initialized, syncPluginsMeasurement);
}
/**
* Always synchronous in order to simplify handling disconnections.
* @throws never
*/
- protected loadContributions(plugins: PluginMetadata[], toDisconnect: DisposableCollection): Map {
+ protected loadContributions(toDisconnect: DisposableCollection): Map {
+ let loaded = 0;
+ const loadPluginsMeasurement = this.createMeasurement('loadPlugins');
+
const hostContributions = new Map();
- const toUnload = new Set(this.contributions.keys());
- let loaded = false;
- for (const plugin of plugins) {
+ for (const contributions of this.contributions.values()) {
+ const plugin = contributions.plugin.metadata;
const pluginId = plugin.model.id;
- toUnload.delete(pluginId);
-
- let contributions = this.contributions.get(pluginId);
- if (!contributions) {
- contributions = new PluginContributions(plugin);
- this.contributions.set(pluginId, contributions);
- contributions.push(Disposable.create(() => this.contributions.delete(pluginId)));
- loaded = true;
- }
if (contributions.state === PluginContributions.State.INITIALIZING) {
contributions.state = PluginContributions.State.LOADING;
- contributions.push(Disposable.create(() => console.log(`[${plugin.model.id}]: Unloaded plugin.`)));
- contributions.push(this.contributionHandler.handleContributions(plugin));
+ contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`)));
+ contributions.push(this.contributionHandler.handleContributions(this.clientId, contributions.plugin));
contributions.state = PluginContributions.State.LOADED;
- console.log(`[${plugin.model.id}]: Loaded contributions.`);
+ console.log(`[${this.clientId}][${pluginId}]: Loaded contributions.`);
+ loaded++;
}
if (contributions.state === PluginContributions.State.LOADED) {
@@ -248,89 +283,111 @@ export class HostedPluginSupport {
hostContributions.set(host, dynamicContributions);
toDisconnect.push(Disposable.create(() => {
contributions!.state = PluginContributions.State.LOADED;
- console.log(`[${plugin.model.id}]: Disconnected.`);
+ console.log(`[${this.clientId}][${pluginId}]: Disconnected.`);
}));
}
}
- for (const pluginId of toUnload) {
- const contribution = this.contributions.get(pluginId);
- if (contribution) {
- contribution.dispose();
- }
- }
- if (loaded || toUnload.size) {
- this.onDidChangePluginsEmitter.fire(undefined);
- }
+
+ this.logMeasurement('Load contributions', loaded, loadPluginsMeasurement);
+
return hostContributions;
}
- protected startPlugins(
- contributionsByHost: Map,
- initData: PluginsInitializationData,
- container: interfaces.Container
- ): Disposable {
- const toDisconnect = new DisposableCollection();
+ protected async startPlugins(contributionsByHost: Map, toDisconnect: DisposableCollection): Promise {
+ let started = 0;
+ const startPluginsMeasurement = this.createMeasurement('startPlugins');
+
+ const [hostLogPath, hostStoragePath] = await Promise.all([
+ this.pluginPathsService.getHostLogPath(),
+ this.getStoragePath()
+ ]);
+ if (toDisconnect.disposed) {
+ return;
+ }
+ const thenable: Promise[] = [];
+ const configStorage = { hostLogPath, hostStoragePath };
for (const [host, hostContributions] of contributionsByHost) {
- const manager = this.obtainManager(host, hostContributions, container, toDisconnect);
- this.initPlugins(manager, {
- ...initData,
- plugins: hostContributions.map(contributions => contributions.plugin)
- }).then(() => {
- if (toDisconnect.disposed) {
- return;
- }
- for (const contributions of hostContributions) {
- const plugin = contributions.plugin;
- const id = plugin.model.id;
- contributions.state = PluginContributions.State.STARTED;
- console.log(`[${id}]: Started plugin.`);
- toDisconnect.push(contributions.push(Disposable.create(() => {
- console.log(`[${id}]: Stopped plugin.`);
- manager.$stop(id);
- })));
-
- this.activateByWorkspaceContains(manager, plugin);
+ const manager = await this.obtainManager(host, hostContributions, toDisconnect);
+ if (!manager) {
+ return;
+ }
+ const plugins = hostContributions.map(contributions => contributions.plugin.metadata);
+ thenable.push((async () => {
+ try {
+ const activationEvents = [...this.activationEvents];
+ await manager.$start({ plugins, configStorage, activationEvents });
+ if (toDisconnect.disposed) {
+ return;
+ }
+ for (const contributions of hostContributions) {
+ started++;
+ const plugin = contributions.plugin;
+ const id = plugin.metadata.model.id;
+ contributions.state = PluginContributions.State.STARTED;
+ console.log(`[${this.clientId}][${id}]: Started plugin.`);
+ toDisconnect.push(contributions.push(Disposable.create(() => {
+ console.log(`[${this.clientId}][${id}]: Stopped plugin.`);
+ manager.$stop(id);
+ })));
+
+ this.activateByWorkspaceContains(manager, plugin);
+ }
+ } catch (e) {
+ console.error(`Failed to start plugins for '${host}' host`, e);
}
- });
+ })());
}
- return toDisconnect;
+ await Promise.all(thenable);
+ if (toDisconnect.disposed) {
+ return;
+ }
+ this.logMeasurement('Start', started, startPluginsMeasurement);
}
- protected obtainManager(host: string, hostContributions: PluginContributions[], container: interfaces.Container, toDispose: DisposableCollection): PluginManagerExt {
+ protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise {
let manager = this.managers.get(host);
if (!manager) {
- const pluginId = getPluginId(hostContributions[0].plugin.model);
- const rpc = this.initRpc(host, pluginId, container);
- toDispose.push(rpc);
+ const pluginId = getPluginId(hostContributions[0].plugin.metadata.model);
+ const rpc = this.initRpc(host, pluginId);
+ toDisconnect.push(rpc);
+
manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT);
this.managers.set(host, manager);
- toDispose.push(Disposable.create(() => this.managers.delete(host)));
+ toDisconnect.push(Disposable.create(() => this.managers.delete(host)));
+
+ const [extApi, globalState, workspaceState] = await Promise.all([
+ this.server.getExtPluginAPI(),
+ this.pluginServer.getAllStorageValues(undefined),
+ this.pluginServer.getAllStorageValues({
+ workspace: this.workspaceService.workspace,
+ roots: this.workspaceService.tryGetRoots()
+ })
+ ]);
+ if (toDisconnect.disposed) {
+ return undefined;
+ }
+
+ await manager.$init({
+ preferences: getPreferences(this.preferenceProviderProvider, this.workspaceService.tryGetRoots()),
+ globalState,
+ workspaceState,
+ env: { queryParams: getQueryParameters(), language: navigator.language },
+ extApi
+ });
+ if (toDisconnect.disposed) {
+ return undefined;
+ }
}
return manager;
}
- protected initRpc(host: PluginHost, pluginId: string, container: interfaces.Container): RPCProtocol {
+ protected initRpc(host: PluginHost, pluginId: string): RPCProtocol {
const rpc = host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(pluginId, host);
- setUpPluginApi(rpc, container);
- this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, container));
+ setUpPluginApi(rpc, this.container);
+ this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container));
return rpc;
}
- protected async initPlugins(manager: PluginManagerExt, data: PluginsInitializationData): Promise {
- await manager.$init({
- plugins: data.plugins,
- preferences: getPreferences(this.preferenceProviderProvider, data.roots),
- globalState: data.globalStates,
- workspaceState: data.workspaceStates,
- env: { queryParams: getQueryParameters(), language: navigator.language },
- extApi: data.pluginAPIs,
- activationEvents: [...this.activationEvents]
- }, {
- hostLogPath: data.logPath,
- hostStoragePath: data.storagePath || ''
- });
- }
-
private createServerRpc(pluginID: string, hostID: string): RPCProtocol {
return new RPCProtocolImpl({
onMessage: this.watcher.onPostMessageEvent,
@@ -420,14 +477,15 @@ export class HostedPluginSupport {
await Promise.all(promises);
}
- protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: PluginMetadata): Promise {
- if (!plugin.source.activationEvents) {
+ protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: DeployedPlugin): Promise {
+ const activationEvents = plugin.contributes && plugin.contributes.activationEvents;
+ if (!activationEvents) {
return;
}
const paths: string[] = [];
const includePatterns: string[] = [];
// should be aligned with https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts#L460-L469
- for (const activationEvent of plugin.source.activationEvents) {
+ for (const activationEvent of activationEvents) {
if (/^workspaceContains:/.test(activationEvent)) {
const fileNameOrGlob = activationEvent.substr('workspaceContains:'.length);
if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
@@ -437,7 +495,7 @@ export class HostedPluginSupport {
}
}
}
- const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.model.id}`);
+ const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.metadata.model.id}`);
const promises: Promise[] = [];
if (paths.length) {
promises.push(this.workspaceService.containsSome(paths));
@@ -472,21 +530,35 @@ export class HostedPluginSupport {
}
}
-}
+ protected createMeasurement(name: string): () => number {
+ const startMarker = `${name}-start`;
+ const endMarker = `${name}-end`;
+ performance.clearMeasures(name);
+ performance.clearMarks(startMarker);
+ performance.clearMarks(endMarker);
+
+ performance.mark(startMarker);
+ return () => {
+ performance.mark(endMarker);
+ performance.measure(name, startMarker, endMarker);
+ const duration = performance.getEntriesByName(name)[0].duration;
+ performance.clearMeasures(name);
+ performance.clearMarks(startMarker);
+ performance.clearMarks(endMarker);
+ return duration;
+ };
+ }
+
+ protected logMeasurement(prefix: string, count: number, measurement: () => number): void {
+ const pluginCount = `${count} plugin${count === 1 ? '' : 's'}`;
+ console.log(`[${this.clientId}] ${prefix} of ${pluginCount} took: ${measurement()} ms`);
+ }
-interface PluginsInitializationData {
- plugins: PluginMetadata[],
- logPath: string,
- storagePath: string | undefined,
- pluginAPIs: ExtPluginApi[],
- globalStates: KeysToKeysToAnyValue,
- workspaceStates: KeysToKeysToAnyValue,
- roots: FileStat[],
}
export class PluginContributions extends DisposableCollection {
constructor(
- readonly plugin: PluginMetadata
+ readonly plugin: DeployedPlugin
) {
super();
}
diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
index d0a04cf39e70f..64927e2a78789 100644
--- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
+++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
@@ -19,7 +19,7 @@ import { RPCProtocolImpl } from '../../../common/rpc-protocol';
import { PluginManagerExtImpl } from '../../../plugin/plugin-manager';
import { MAIN_RPC_CONTEXT, Plugin, emptyPlugin } from '../../../common/plugin-api-rpc';
import { createAPIFactory } from '../../../plugin/plugin-context';
-import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol';
+import { getPluginId, PluginMetadata, PluginPackage } from '../../../common/plugin-protocol';
import * as theia from '@theia/plugin';
import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry';
import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution';
@@ -90,10 +90,12 @@ const pluginManager = new PluginManagerExtImpl({
}
const plugin: Plugin = {
pluginPath: pluginModel.entryPoint.frontend!,
- pluginFolder: plg.source.packagePath,
+ pluginFolder: pluginModel.packagePath,
model: pluginModel,
lifecycle: pluginLifecycle,
- rawModel: plg.source
+ get rawModel(): PluginPackage {
+ throw new Error('not supported');
+ }
};
result.push(plugin);
const apiImpl = apiFactory(plugin);
@@ -102,10 +104,12 @@ const pluginManager = new PluginManagerExtImpl({
} else {
foreign.push({
pluginPath: pluginModel.entryPoint.backend!,
- pluginFolder: plg.source.packagePath,
+ pluginFolder: pluginModel.packagePath,
model: pluginModel,
lifecycle: pluginLifecycle,
- rawModel: plg.source
+ get rawModel(): PluginPackage {
+ throw new Error('not supported');
+ }
});
}
}
diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts
index 216443f25f3e9..28f9726eaa59b 100644
--- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts
+++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts
@@ -16,7 +16,7 @@
import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core';
-import { PluginDeployerHandler, PluginDeployerEntry, PluginMetadata } from '../../common/plugin-protocol';
+import { PluginDeployerHandler, PluginDeployerEntry, PluginEntryPoint, DeployedPlugin, PluginDependencies } from '../../common/plugin-protocol';
import { HostedPluginReader } from './plugin-reader';
import { Deferred } from '@theia/core/lib/common/promise-util';
@@ -32,67 +32,98 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
/**
* Managed plugin metadata backend entries.
*/
- private currentBackendPluginsMetadata: PluginMetadata[] = [];
+ private readonly deployedBackendPlugins = new Map();
/**
* Managed plugin metadata frontend entries.
*/
- private currentFrontendPluginsMetadata: PluginMetadata[] = [];
+ private readonly deployedFrontendPlugins = new Map();
private backendPluginsMetadataDeferred = new Deferred();
private frontendPluginsMetadataDeferred = new Deferred();
- async getDeployedFrontendMetadata(): Promise {
+ async getDeployedFrontendPluginIds(): Promise {
// await first deploy
await this.frontendPluginsMetadataDeferred.promise;
// fetch the last deployed state
- return this.currentFrontendPluginsMetadata;
+ return [...this.deployedFrontendPlugins.keys()];
}
- async getDeployedBackendMetadata(): Promise {
+ async getDeployedBackendPluginIds(): Promise {
// await first deploy
await this.backendPluginsMetadataDeferred.promise;
// fetch the last deployed state
- return this.currentBackendPluginsMetadata;
+ return [...this.deployedBackendPlugins.keys()];
}
- getPluginMetadata(plugin: PluginDeployerEntry): Promise {
- return this.reader.getPluginMetadata(plugin.path());
+ getDeployedPlugin(pluginId: string): DeployedPlugin | undefined {
+ const metadata = this.deployedBackendPlugins.get(pluginId);
+ if (metadata) {
+ return metadata;
+ }
+ return this.deployedFrontendPlugins.get(pluginId);
}
- async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise {
- for (const plugin of frontendPlugins) {
- const metadata = await this.reader.getPluginMetadata(plugin.path());
- if (metadata) {
- if (this.currentFrontendPluginsMetadata.some(value => value.model.id === metadata.model.id)) {
- continue;
- }
-
- this.currentFrontendPluginsMetadata.push(metadata);
- this.logger.info(`Deploying frontend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.frontend || plugin.path()}"`);
+ /**
+ * @throws never! in order to isolate plugin deployment
+ */
+ async getPluginDependencies(entry: PluginDeployerEntry): Promise {
+ const pluginPath = entry.path();
+ try {
+ const manifest = await this.reader.readPackage(pluginPath);
+ if (!manifest) {
+ return undefined;
}
+ const metadata = this.reader.readMetadata(manifest);
+ const dependencies: PluginDependencies = { metadata };
+ dependencies.mapping = this.reader.readDependencies(manifest);
+ return dependencies;
+ } catch (e) {
+ console.error(`Failed to load plugin dependencies from '${pluginPath}' path`, e);
+ return undefined;
}
+ }
+ async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise {
+ for (const plugin of frontendPlugins) {
+ await this.deployPlugin(plugin, 'frontend');
+ }
// resolve on first deploy
this.frontendPluginsMetadataDeferred.resolve(undefined);
}
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise {
for (const plugin of backendPlugins) {
- const metadata = await this.reader.getPluginMetadata(plugin.path());
- if (metadata) {
- if (this.currentBackendPluginsMetadata.some(value => value.model.id === metadata.model.id)) {
- continue;
- }
-
- this.currentBackendPluginsMetadata.push(metadata);
- this.logger.info(`Deploying backend plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint.backend || plugin.path()}"`);
- }
+ await this.deployPlugin(plugin, 'backend');
}
-
// resolve on first deploy
this.backendPluginsMetadataDeferred.resolve(undefined);
}
+ /**
+ * @throws never! in order to isolate plugin deployment
+ */
+ protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise {
+ const pluginPath = entry.path();
+ try {
+ const manifest = await this.reader.readPackage(pluginPath);
+ if (!manifest) {
+ return;
+ }
+
+ const metadata = this.reader.readMetadata(manifest);
+ if (this.deployedBackendPlugins.has(metadata.model.id)) {
+ return;
+ }
+
+ const deployed: DeployedPlugin = { metadata };
+ deployed.contributes = this.reader.readContribution(manifest);
+ this.deployedBackendPlugins.set(metadata.model.id, deployed);
+ this.logger.info(`Deploying ${entryPoint} plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`);
+ } catch (e) {
+ console.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e);
+ }
+ }
+
}
diff --git a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts
index 040f4fa111846..61b8c94bf068e 100644
--- a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts
+++ b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts
@@ -33,7 +33,6 @@ export class MetadataScanner {
const scanner = this.getScanner(plugin);
return {
host: 'main',
- source: plugin,
model: scanner.getModel(plugin),
lifecycle: scanner.getLifecycle(plugin)
};
diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
index 9decabc2591f6..c0b2192ce8432 100644
--- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
+++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
@@ -27,6 +27,7 @@ import { WorkspaceExtImpl } from '../../plugin/workspace';
import { MessageRegistryExt } from '../../plugin/message-registry';
import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext';
import { ClipboardExt } from '../../plugin/clipboard-ext';
+import { loadManifest } from './plugin-manifest-loader';
/**
* Handle the RPC calls.
@@ -128,40 +129,46 @@ export class PluginHostRPC {
console.error(e);
}
},
- init(raw: PluginMetadata[]): [Plugin[], Plugin[]] {
+ async init(raw: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> {
console.log('PLUGIN_HOST(' + process.pid + '): PluginManagerExtImpl/init()');
const result: Plugin[] = [];
const foreign: Plugin[] = [];
for (const plg of raw) {
- const pluginModel = plg.model;
- const pluginLifecycle = plg.lifecycle;
-
- if (pluginModel.entryPoint!.frontend) {
- foreign.push({
- pluginPath: pluginModel.entryPoint.frontend!,
- pluginFolder: plg.source.packagePath,
- model: pluginModel,
- lifecycle: pluginLifecycle,
- rawModel: plg.source
- });
- } else {
- let backendInitPath = pluginLifecycle.backendInitPath;
- // if no init path, try to init as regular Theia plugin
- if (!backendInitPath) {
- backendInitPath = __dirname + '/scanners/backend-init-theia.js';
- }
+ try {
+ const pluginModel = plg.model;
+ const pluginLifecycle = plg.lifecycle;
+
+ const rawModel = await loadManifest(pluginModel.packagePath);
+ rawModel.packagePath = pluginModel.packagePath;
+ if (pluginModel.entryPoint!.frontend) {
+ foreign.push({
+ pluginPath: pluginModel.entryPoint.frontend!,
+ pluginFolder: pluginModel.packagePath,
+ model: pluginModel,
+ lifecycle: pluginLifecycle,
+ rawModel
+ });
+ } else {
+ let backendInitPath = pluginLifecycle.backendInitPath;
+ // if no init path, try to init as regular Theia plugin
+ if (!backendInitPath) {
+ backendInitPath = __dirname + '/scanners/backend-init-theia.js';
+ }
- const plugin: Plugin = {
- pluginPath: pluginModel.entryPoint.backend!,
- pluginFolder: plg.source.packagePath,
- model: pluginModel,
- lifecycle: pluginLifecycle,
- rawModel: plg.source
- };
+ const plugin: Plugin = {
+ pluginPath: pluginModel.entryPoint.backend!,
+ pluginFolder: pluginModel.packagePath,
+ model: pluginModel,
+ lifecycle: pluginLifecycle,
+ rawModel
+ };
- self.initContext(backendInitPath, plugin);
+ self.initContext(backendInitPath, plugin);
- result.push(plugin);
+ result.push(plugin);
+ }
+ } catch (e) {
+ console.error(`Failed to initialize ${plg.model.id} plugin.`, e);
}
}
return [result, foreign];
diff --git a/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts
new file mode 100644
index 0000000000000..6b64f6f8b0ae2
--- /dev/null
+++ b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts
@@ -0,0 +1,71 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+// tslint:disable:no-any
+
+import * as path from 'path';
+import * as fs from 'fs-extra';
+
+const NLS_REGEX = /^%([\w\d.-]+)%$/i;
+
+export async function loadManifest(pluginPath: string): Promise {
+ const [manifest, translations] = await Promise.all([
+ fs.readJson(path.join(pluginPath, 'package.json')),
+ loadTranslations(pluginPath)
+ ]);
+ return manifest && translations && Object.keys(translations).length ?
+ localize(manifest, translations) :
+ manifest;
+}
+
+async function loadTranslations(pluginPath: string): Promise {
+ try {
+ return await fs.readJson(path.join(pluginPath, 'package.nls.json'));
+ } catch (e) {
+ if (e.code !== 'ENOENT') {
+ throw e;
+ }
+ return {};
+ }
+}
+
+function localize(value: any, translations: {
+ [key: string]: string
+}): any {
+ if (typeof value === 'string') {
+ const match = NLS_REGEX.exec(value);
+ return match && translations[match[1]] || value;
+ }
+ if (Array.isArray(value)) {
+ const result = [];
+ for (const item of value) {
+ result.push(localize(item, translations));
+ }
+ return result;
+ }
+ if (value === null) {
+ return value;
+ }
+ if (typeof value === 'object') {
+ const result: { [key: string]: any } = {};
+ // tslint:disable-next-line:forin
+ for (const propertyName in value) {
+ result[propertyName] = localize(value[propertyName], translations);
+ }
+ return result;
+ }
+ return value;
+}
diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts
index d1611818605da..6792a35507ff2 100644
--- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts
+++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts
@@ -14,17 +14,15 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-// tslint:disable:no-any
-
import * as path from 'path';
-import * as fs from 'fs-extra';
import * as express from 'express';
import * as escape_html from 'escape-html';
import { ILogger } from '@theia/core';
import { inject, injectable, optional, multiInject } from 'inversify';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
-import { PluginMetadata, getPluginId, MetadataProcessor } from '../../common/plugin-protocol';
+import { PluginMetadata, getPluginId, MetadataProcessor, PluginPackage, PluginContribution } from '../../common/plugin-protocol';
import { MetadataScanner } from './metadata-scanner';
+import { loadManifest } from './plugin-manifest-loader';
@injectable()
export class HostedPluginReader implements BackendApplicationContribution {
@@ -77,35 +75,36 @@ export class HostedPluginReader implements BackendApplicationContribution {
res.status(404).send(`The plugin with id '${escape_html(pluginId)}' does not exist.`);
}
- async getPluginMetadata(pluginPath: string): Promise {
- return this.doGetPluginMetadata(pluginPath);
- }
-
/**
- * MUST never throw to isolate plugin deployment
+ * @throws never
*/
- async doGetPluginMetadata(pluginPath: string | undefined): Promise {
+ async getPluginMetadata(pluginPath: string | undefined): Promise {
try {
- if (!pluginPath) {
- return undefined;
- }
- pluginPath = path.normalize(pluginPath + '/');
- return await this.loadPluginMetadata(pluginPath);
+ const manifest = await this.readPackage(pluginPath);
+ return manifest && this.readMetadata(manifest);
} catch (e) {
this.logger.error(`Failed to load plugin metadata from "${pluginPath}"`, e);
return undefined;
}
}
- protected async loadPluginMetadata(pluginPath: string): Promise {
- const manifest = await this.loadManifest(pluginPath);
+ async readPackage(pluginPath: string | undefined): Promise {
+ if (!pluginPath) {
+ return undefined;
+ }
+ pluginPath = path.normalize(pluginPath + '/');
+ const manifest = await loadManifest(pluginPath);
if (!manifest) {
return undefined;
}
manifest.packagePath = pluginPath;
- const pluginMetadata = this.scanner.getPluginMetadata(manifest);
+ return manifest;
+ }
+
+ readMetadata(plugin: PluginPackage): PluginMetadata {
+ const pluginMetadata = this.scanner.getPluginMetadata(plugin);
if (pluginMetadata.model.entryPoint.backend) {
- pluginMetadata.model.entryPoint.backend = path.resolve(pluginPath, pluginMetadata.model.entryPoint.backend);
+ pluginMetadata.model.entryPoint.backend = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.backend);
}
if (pluginMetadata) {
// Add post processor
@@ -114,60 +113,19 @@ export class HostedPluginReader implements BackendApplicationContribution {
metadataProcessor.process(pluginMetadata);
});
}
- this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), pluginPath);
+ this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), plugin.packagePath);
}
return pluginMetadata;
}
- protected async loadManifest(pluginPath: string): Promise {
- const [manifest, translations] = await Promise.all([
- fs.readJson(path.join(pluginPath, 'package.json')),
- this.loadTranslations(pluginPath)
- ]);
- return manifest && translations && Object.keys(translations).length ?
- this.localize(manifest, translations) :
- manifest;
+ readContribution(plugin: PluginPackage): PluginContribution | undefined {
+ const scanner = this.scanner.getScanner(plugin);
+ return scanner.getContribution(plugin);
}
- protected async loadTranslations(pluginPath: string): Promise {
- try {
- return await fs.readJson(path.join(pluginPath, 'package.nls.json'));
- } catch (e) {
- if (e.code !== 'ENOENT') {
- throw e;
- }
- return {};
- }
- }
-
- protected localize(value: any, translations: {
- [key: string]: string
- }): any {
- if (typeof value === 'string') {
- const match = HostedPluginReader.NLS_REGEX.exec(value);
- return match && translations[match[1]] || value;
- }
- if (Array.isArray(value)) {
- const result = [];
- for (const item of value) {
- result.push(this.localize(item, translations));
- }
- return result;
- }
- if (value === null) {
- return value;
- }
- if (typeof value === 'object') {
- const result: { [key: string]: any } = {};
- // tslint:disable-next-line:forin
- for (const propertyName in value) {
- result[propertyName] = this.localize(value[propertyName], translations);
- }
- return result;
- }
- return value;
+ readDependencies(plugin: PluginPackage): Map | undefined {
+ const scanner = this.scanner.getScanner(plugin);
+ return scanner.getDependencies(plugin);
}
- static NLS_REGEX = /^%([\w\d.-]+)%$/i;
-
}
diff --git a/packages/plugin-ext/src/hosted/node/plugin-service.ts b/packages/plugin-ext/src/hosted/node/plugin-service.ts
index e39cdfac9f9c0..e385c55acfea0 100644
--- a/packages/plugin-ext/src/hosted/node/plugin-service.ts
+++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts
@@ -14,7 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable, inject, named, postConstruct } from 'inversify';
-import { HostedPluginServer, HostedPluginClient, PluginMetadata, PluginDeployer } from '../../common/plugin-protocol';
+import { HostedPluginServer, HostedPluginClient, PluginMetadata, PluginDeployer, GetDeployedPluginsParams, DeployedPlugin } from '../../common/plugin-protocol';
import { HostedPluginSupport } from './hosted-plugin';
import { ILogger, Disposable } from '@theia/core';
import { ContributionProvider } from '@theia/core';
@@ -63,28 +63,50 @@ export class HostedPluginServerImpl implements HostedPluginServer {
this.hostedPlugin.setClient(client);
}
- getDeployedFrontendMetadata(): Promise {
- return this.deployerHandler.getDeployedFrontendMetadata();
- }
-
- async getDeployedMetadata(): Promise {
- const backendMetadata = await this.deployerHandler.getDeployedBackendMetadata();
+ async getDeployedPluginIds(): Promise {
+ const backendMetadata = await this.deployerHandler.getDeployedBackendPluginIds();
if (backendMetadata.length > 0) {
this.hostedPlugin.runPluginServer();
}
- const allMetadata: PluginMetadata[] = [];
- allMetadata.push(...await this.deployerHandler.getDeployedFrontendMetadata());
- allMetadata.push(...backendMetadata);
-
- // ask remote as well
- const extraBackendPluginsMetadata = await this.hostedPlugin.getExtraPluginMetadata();
- allMetadata.push(...extraBackendPluginsMetadata);
-
- return allMetadata;
+ const plugins = new Set();
+ for (const pluginId of await this.deployerHandler.getDeployedFrontendPluginIds()) {
+ plugins.add(pluginId);
+ }
+ for (const pluginId of backendMetadata) {
+ plugins.add(pluginId);
+ }
+ const extraPluginMetadata = await this.hostedPlugin.getExtraPluginMetadata();
+ for (const plugin of extraPluginMetadata) {
+ plugins.add(plugin.model.id);
+ }
+ return [...plugins.values()];
}
- getDeployedBackendMetadata(): Promise {
- return Promise.resolve(this.deployerHandler.getDeployedBackendMetadata());
+ async getDeployedPlugins({ pluginIds }: GetDeployedPluginsParams): Promise {
+ if (!pluginIds.length) {
+ return [];
+ }
+ const plugins = [];
+ let extraPluginMetadata: Map | undefined;
+ for (const pluginId of pluginIds) {
+ let plugin = this.deployerHandler.getDeployedPlugin(pluginId);
+ if (!plugin) {
+ if (!extraPluginMetadata) {
+ extraPluginMetadata = new Map();
+ for (const extraMetadata of await this.hostedPlugin.getExtraPluginMetadata()) {
+ extraPluginMetadata.set(extraMetadata.model.id, extraMetadata);
+ }
+ }
+ const metadata = extraPluginMetadata.get(pluginId);
+ if (metadata) {
+ plugin = { metadata };
+ }
+ }
+ if (plugin) {
+ plugins.push(plugin);
+ }
+ }
+ return plugins;
}
onMessage(message: string): Promise {
diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
index cde6c5ccbeb39..e8a81f6cec7f7 100644
--- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
+++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
@@ -84,6 +84,7 @@ export class TheiaPluginScanner implements PluginScanner {
getModel(plugin: PluginPackage): PluginModel {
const result: PluginModel = {
+ packagePath: plugin.packagePath,
// see id definition: https://github.com/microsoft/vscode/blob/15916055fe0cb9411a5f36119b3b012458fe0a1d/src/vs/platform/extensions/common/extensions.ts#L167-L169
id: `${plugin.publisher.toLowerCase()}.${plugin.name.toLowerCase()}`,
name: plugin.name,
@@ -98,10 +99,8 @@ export class TheiaPluginScanner implements PluginScanner {
entryPoint: {
frontend: plugin.theiaPlugin!.frontend,
backend: plugin.theiaPlugin!.backend
- },
- extensionDependencies: plugin.extensionDependencies || []
+ }
};
- result.contributes = this.readContributions(plugin);
return result;
}
@@ -115,12 +114,23 @@ export class TheiaPluginScanner implements PluginScanner {
};
}
- protected readContributions(rawPlugin: PluginPackage): PluginContribution | undefined {
- if (!rawPlugin.contributes) {
+ getDependencies(rawPlugin: PluginPackage): Map | undefined {
+ // skip it since there is no way to load transitive dependencies for Theia plugins yet
+ return undefined;
+ }
+
+ getContribution(rawPlugin: PluginPackage): PluginContribution | undefined {
+ if (!rawPlugin.contributes && !rawPlugin.activationEvents) {
return undefined;
}
- const contributions: PluginContribution = {};
+ const contributions: PluginContribution = {
+ activationEvents: rawPlugin.activationEvents
+ };
+
+ if (!rawPlugin.contributes) {
+ return contributions;
+ }
try {
if (rawPlugin.contributes.configuration) {
diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts
index 8c33989e75272..4967d88c9b9de 100644
--- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts
+++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts
@@ -47,7 +47,6 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
import { PluginDebugSessionFactory } from './plugin-debug-session-factory';
import { PluginWebSocketChannel } from '../../../common/connection';
import { PluginDebugAdapterContributionRegistrator, PluginDebugService } from './plugin-debug-service';
-import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater';
import { FileSystem } from '@theia/filesystem/lib/common';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
@@ -66,7 +65,6 @@ export class DebugMainImpl implements DebugMain, Disposable {
private readonly debugPreferences: DebugPreferences;
private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator;
private readonly adapterContributionRegistrator: PluginDebugAdapterContributionRegistrator;
- private readonly debugSchemaUpdater: DebugSchemaUpdater;
private readonly fileSystem: FileSystem;
private readonly pluginService: HostedPluginSupport;
@@ -87,7 +85,6 @@ export class DebugMainImpl implements DebugMain, Disposable {
this.debugPreferences = container.get(DebugPreferences);
this.adapterContributionRegistrator = container.get(PluginDebugService);
this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry);
- this.debugSchemaUpdater = container.get(DebugSchemaUpdater);
this.fileSystem = container.get(FileSystem);
this.pluginService = container.get(HostedPluginSupport);
@@ -146,7 +143,6 @@ export class DebugMainImpl implements DebugMain, Disposable {
);
const toDispose = new DisposableCollection(
- Disposable.create(() => this.debugSchemaUpdater.update()),
Disposable.create(() => this.debuggerContributions.delete(debugType))
);
this.debuggerContributions.set(debugType, toDispose);
@@ -160,8 +156,6 @@ export class DebugMainImpl implements DebugMain, Disposable {
})
]);
this.toDispose.push(Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType)));
-
- this.debugSchemaUpdater.update();
}
async $unregisterDebuggerConfiguration(debugType: string): Promise {
diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts
index d37d41893db80..98fbe0ce8c2ea 100644
--- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts
+++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-adapter-contribution.ts
@@ -16,7 +16,6 @@
import { DebugExt, } from '../../../common/plugin-api-rpc';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
-import { IJSONSchemaSnippet, IJSONSchema } from '@theia/core/lib/common/json-schema';
import { MaybePromise } from '@theia/core/lib/common/types';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
@@ -38,18 +37,6 @@ export class PluginDebugAdapterContribution {
return this.description.label;
}
- get languages(): MaybePromise {
- return this.debugExt.$getSupportedLanguages(this.type);
- }
-
- async getSchemaAttributes(): Promise {
- return this.debugExt.$getSchemaAttributes(this.type);
- }
-
- async getConfigurationSnippets(): Promise {
- return this.debugExt.$getConfigurationSnippets(this.type);
- }
-
async provideDebugConfigurations(workspaceFolderUri: string | undefined): Promise {
return this.debugExt.$provideDebugConfigurations(this.type, workspaceFolderUri);
}
diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts
index 243723cb842d1..6cabe2a706d6c 100644
--- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts
+++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts
@@ -22,6 +22,7 @@ import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribut
import { injectable, inject, postConstruct } from 'inversify';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
+import { DebuggerContribution } from '../../../common/plugin-protocol';
/**
* Debug adapter contribution registrator.
@@ -45,6 +46,8 @@ export interface PluginDebugAdapterContributionRegistrator {
*/
@injectable()
export class PluginDebugService implements DebugService, PluginDebugAdapterContributionRegistrator {
+
+ protected readonly debuggers: DebuggerContribution[] = [];
protected readonly contributors = new Map();
protected readonly toDispose = new DisposableCollection();
@@ -88,8 +91,14 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr
}
async debugTypes(): Promise {
- const debugTypes = await this.delegated.debugTypes();
- return debugTypes.concat(Array.from(this.contributors.keys()));
+ const debugTypes = new Set(await this.delegated.debugTypes());
+ for (const contribution of this.debuggers) {
+ debugTypes.add(contribution.type);
+ }
+ for (const debugType of this.contributors.keys()) {
+ debugTypes.add(debugType);
+ }
+ return [...debugTypes];
}
async provideDebugConfigurations(debugType: string, workspaceFolderUri: string | undefined): Promise {
@@ -123,14 +132,24 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr
return this.delegated.resolveDebugConfiguration(resolved, workspaceFolderUri);
}
+ registerDebugger(contribution: DebuggerContribution): Disposable {
+ this.debuggers.push(contribution);
+ return Disposable.create(() => {
+ const index = this.debuggers.indexOf(contribution);
+ if (index !== -1) {
+ this.debuggers.splice(index, 1);
+ }
+ });
+ }
+
async getDebuggersForLanguage(language: string): Promise {
const debuggers = await this.delegated.getDebuggersForLanguage(language);
- for (const contributor of this.contributors.values()) {
- const languages = await contributor.languages;
+ for (const contributor of this.debuggers) {
+ const languages = contributor.languages;
if (languages && languages.indexOf(language) !== -1) {
- const { type } = contributor;
- debuggers.push({ type, label: await contributor.label || type });
+ const { label, type } = contributor;
+ debuggers.push({ type, label: label || type });
}
}
@@ -138,20 +157,22 @@ export class PluginDebugService implements DebugService, PluginDebugAdapterContr
}
async getSchemaAttributes(debugType: string): Promise {
- const contributor = this.contributors.get(debugType);
- if (contributor) {
- return contributor.getSchemaAttributes && contributor.getSchemaAttributes() || [];
- } else {
- return this.delegated.getSchemaAttributes(debugType);
+ let schemas = await this.delegated.getSchemaAttributes(debugType);
+ for (const contribution of this.debuggers) {
+ if (contribution.configurationAttributes &&
+ (contribution.type === debugType || contribution.type === '*' || debugType === '*')) {
+ schemas = schemas.concat(contribution.configurationAttributes);
+ }
}
+ return schemas;
}
async getConfigurationSnippets(): Promise {
let snippets = await this.delegated.getConfigurationSnippets();
- for (const contributor of this.contributors.values()) {
- if (contributor.getConfigurationSnippets) {
- snippets = snippets.concat(await contributor.getConfigurationSnippets());
+ for (const contribution of this.debuggers) {
+ if (contribution.configurationSnippets) {
+ snippets = snippets.concat(contribution.configurationSnippets);
}
}
diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts
index 4b63aae6a2510..ba8a0b8af8353 100644
--- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts
+++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts
@@ -19,7 +19,7 @@ import { ITokenTypeMap, IEmbeddedLanguagesMap, StandardTokenType } from 'vscode-
import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarDefinition } from '@theia/monaco/lib/browser/textmate';
import { MenusContributionPointHandler } from './menus/menus-contribution-handler';
import { PluginViewRegistry } from './view/plugin-view-registry';
-import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, PluginMetadata } from '../../common';
+import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin } from '../../common';
import { PreferenceSchemaProvider } from '@theia/core/lib/browser';
import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences';
import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler';
@@ -29,6 +29,8 @@ import { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter } from '@theia/core/lib/common/event';
import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser';
+import { PluginDebugService } from './debug/plugin-debug-service';
+import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater';
@injectable()
export class PluginContributionHandler {
@@ -71,27 +73,35 @@ export class PluginContributionHandler {
@inject(ProblemPatternRegistry)
protected readonly problemPatternRegistry: ProblemPatternRegistry;
+ @inject(PluginDebugService)
+ protected readonly debugService: PluginDebugService;
+
+ @inject(DebugSchemaUpdater)
+ protected readonly debugSchema: DebugSchemaUpdater;
+
protected readonly commandHandlers = new Map();
protected readonly onDidRegisterCommandHandlerEmitter = new Emitter();
readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event;
+ protected readonly activatedLanguages = new Set();
+
/**
* Always synchronous in order to simplify handling disconnections.
* @throws never, loading of each contribution should handle errors
* in order to avoid preventing loading of other contibutions or extensions
*/
- handleContributions(plugin: PluginMetadata): Disposable {
- const contributions = plugin.model.contributes;
+ handleContributions(clientId: string, plugin: DeployedPlugin): Disposable {
+ const contributions = plugin.contributes;
if (!contributions) {
return Disposable.NULL;
}
- const toDispose = new DisposableCollection;
+ const toDispose = new DisposableCollection();
const pushContribution = (id: string, contribute: () => Disposable) => {
try {
toDispose.push(contribute());
} catch (e) {
- console.error(`[${plugin.model.id}]: Failed to load '${id}' contribution.`, e);
+ console.error(`[${clientId}][${plugin.metadata.model.id}]: Failed to load '${id}' contribution.`, e);
}
};
@@ -107,8 +117,15 @@ export class PluginContributionHandler {
pushContribution('configurationDefaults', () => this.updateDefaultOverridesSchema(configurationDefaults));
}
- if (contributions.languages) {
- for (const lang of contributions.languages) {
+ const languages = contributions.languages;
+ if (languages && languages.length) {
+ for (const lang of languages) {
+ /*
+ * Monaco guesses a language for opened plain text models on `monaco.languages.register`.
+ * It can trigger language activation before grammars are registered.
+ * Install onLanguage listener earlier in order to catch such activations and activate grammars as well.
+ */
+ monaco.languages.onLanguage(lang.id, () => this.activatedLanguages.add(lang.id));
// it is not possible to unregister a language
monaco.languages.register({
id: lang.id,
@@ -136,7 +153,6 @@ export class PluginContributionHandler {
const grammars = contributions.grammars;
if (grammars && grammars.length) {
- toDispose.push(Disposable.create(() => this.monacoTextmateService.detectLanguages()));
for (const grammar of grammars) {
if (grammar.injectTo) {
for (const injectScope of grammar.injectTo) {
@@ -173,11 +189,10 @@ export class PluginContributionHandler {
tokenTypes: this.convertTokenTypes(grammar.tokenTypes)
}));
pushContribution(`grammar.language.${language}.activation`,
- () => monaco.languages.onLanguage(language, () => this.monacoTextmateService.activateLanguage(language))
+ () => this.onDidActivateLanguage(language, () => this.monacoTextmateService.activateLanguage(language))
);
}
}
- this.monacoTextmateService.detectLanguages();
}
pushContribution('commands', () => this.registerCommands(contributions));
@@ -239,6 +254,16 @@ export class PluginContributionHandler {
}
}
+ if (contributions.debuggers && contributions.debuggers.length) {
+ toDispose.push(Disposable.create(() => this.debugSchema.update()));
+ for (const contribution of contributions.debuggers) {
+ pushContribution(`debuggers.${contribution.type}`,
+ () => this.debugService.registerDebugger(contribution)
+ );
+ }
+ this.debugSchema.update();
+ }
+
return toDispose;
}
@@ -293,6 +318,14 @@ export class PluginContributionHandler {
return !!this.commandHandlers.get(id);
}
+ protected onDidActivateLanguage(language: string, cb: () => {}): Disposable {
+ if (this.activatedLanguages.has(language)) {
+ cb();
+ return Disposable.NULL;
+ }
+ return monaco.languages.onLanguage(language, cb);
+ }
+
private updateConfigurationSchema(schema: PreferenceSchema): Disposable {
this.validateConfigurationSchema(schema);
return this.preferenceSchemaProvider.setSchema(schema);
diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
index 8f3613eed317c..d4bb7b49e0ebf 100644
--- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
+++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
@@ -126,28 +126,22 @@ export class PluginDeployerImpl implements PluginDeployer {
await this.applyFileHandlers(pluginDeployerEntries);
await this.applyDirectoryFileHandlers(pluginDeployerEntries);
for (const deployerEntry of pluginDeployerEntries) {
- const metadata = await this.pluginDeployerHandler.getPluginMetadata(deployerEntry);
- if (metadata && !pluginsToDeploy.has(metadata.model.id)) {
- pluginsToDeploy.set(metadata.model.id, deployerEntry);
- chunk.push(metadata);
+ const dependencies = await this.pluginDeployerHandler.getPluginDependencies(deployerEntry);
+ if (dependencies && !pluginsToDeploy.has(dependencies.metadata.model.id)) {
+ pluginsToDeploy.set(dependencies.metadata.model.id, deployerEntry);
+ if (dependencies.mapping) {
+ chunk.push(dependencies.mapping);
+ }
}
}
} catch (e) {
console.error(`Failed to resolve plugins from '${current}'`, e);
}
}
- for (const metadata of chunk) {
- const extensionDependencies = metadata.source.extensionDependencies;
- const deployableExtensionDependencies = metadata.model.extensionDependencies;
- if (extensionDependencies && deployableExtensionDependencies) {
- for (let dependencyIndex = 0; dependencyIndex < extensionDependencies.length; dependencyIndex++) {
- const dependencyId = extensionDependencies[dependencyIndex].toLowerCase();
- if (!pluginsToDeploy.has(dependencyId)) {
- const deployableDependency = deployableExtensionDependencies[dependencyIndex];
- if (deployableDependency) {
- queue.push(deployableDependency);
- }
- }
+ for (const dependencies of chunk) {
+ for (const [dependency, deployableDependency] of dependencies) {
+ if (!pluginsToDeploy.has(dependency)) {
+ queue.push(deployableDependency);
}
}
}
diff --git a/packages/plugin-ext/src/plugin/node/debug/debug.ts b/packages/plugin-ext/src/plugin/node/debug/debug.ts
index 821958875bf44..e6787709b6ca1 100644
--- a/packages/plugin-ext/src/plugin/node/debug/debug.ts
+++ b/packages/plugin-ext/src/plugin/node/debug/debug.ts
@@ -14,15 +14,14 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { Emitter } from '@theia/core/lib/common/event';
-import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
import { Path } from '@theia/core/lib/common/path';
import { CommunicationProvider } from '@theia/debug/lib/common/debug-model';
import * as theia from '@theia/plugin';
import URI from 'vscode-uri';
import { Breakpoint } from '../../../common/plugin-api-rpc-model';
import { DebugExt, DebugMain, PLUGIN_RPC_CONTEXT as Ext, TerminalOptionsExt } from '../../../common/plugin-api-rpc';
+import { PluginPackageDebuggersContribution } from '../../../common/plugin-protocol';
import { RPCProtocol } from '../../../common/rpc-protocol';
-import { DebuggerContribution } from '../../../common';
import { PluginWebSocketChannel } from '../../../common/connection';
import { CommandRegistryImpl } from '../../command-registry';
import { ConnectionExtImpl } from '../../connection-ext';
@@ -46,7 +45,11 @@ export class DebugExtImpl implements DebugExt {
// providers by type
private configurationProviders = new Map>();
- private debuggersContributions = new Map();
+ /**
+ * Only use internally, don't send it to the frontend. It's expensive!
+ * It's already there as a part of the plugin metadata.
+ */
+ private debuggersContributions = new Map();
private descriptorFactories = new Map();
private trackerFactories: [string, theia.DebugAdapterTrackerFactory][] = [];
private contributionPaths = new Map();
@@ -87,8 +90,8 @@ export class DebugExtImpl implements DebugExt {
* @param pluginFolder plugin folder path
* @param contributions available debuggers contributions
*/
- registerDebuggersContributions(pluginFolder: string, contributions: DebuggerContribution[]): void {
- contributions.forEach((contribution: DebuggerContribution) => {
+ registerDebuggersContributions(pluginFolder: string, contributions: PluginPackageDebuggersContribution[]): void {
+ contributions.forEach(contribution => {
this.contributionPaths.set(contribution.type, pluginFolder);
this.debuggersContributions.set(contribution.type, contribution);
this.proxy.$registerDebuggerContribution({
@@ -229,21 +232,6 @@ export class DebugExtImpl implements DebugExt {
}
}
- async $getSupportedLanguages(debugType: string): Promise {
- const contribution = this.debuggersContributions.get(debugType);
- return contribution && contribution.languages || [];
- }
-
- async $getSchemaAttributes(debugType: string): Promise {
- const contribution = this.debuggersContributions.get(debugType);
- return contribution && contribution.configurationAttributes || [];
- }
-
- async $getConfigurationSnippets(debugType: string): Promise {
- const contribution = this.debuggersContributions.get(debugType);
- return contribution && contribution.configurationSnippets || [];
- }
-
async $getTerminalCreationOptions(debugType: string): Promise {
return this.doGetTerminalCreationOptions(debugType);
}
diff --git a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts
index b3ae5312466e8..a03cc830aaebb 100644
--- a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts
+++ b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-executable-resolver.ts
@@ -16,13 +16,14 @@
import * as path from 'path';
import * as theia from '@theia/plugin';
-import { PlatformSpecificAdapterContribution, DebuggerContribution } from '../../../common';
+import { PlatformSpecificAdapterContribution, PluginPackageDebuggersContribution } from '../../../common';
import { isWindows, isOSX } from '@theia/core/lib/common/os';
/**
* Resolves [DebugAdapterExecutable](#DebugAdapterExecutable) based on contribution.
*/
-export async function resolveDebugAdapterExecutable(pluginPath: string, debuggerContribution: DebuggerContribution): Promise {
+export async function resolveDebugAdapterExecutable(
+ pluginPath: string, debuggerContribution: PluginPackageDebuggersContribution): Promise {
const info = toPlatformInfo(debuggerContribution);
let program = (info && info.program || debuggerContribution.program);
if (!program) {
@@ -43,7 +44,7 @@ export async function resolveDebugAdapterExecutable(pluginPath: string, debugger
};
}
-function toPlatformInfo(executable: DebuggerContribution): PlatformSpecificAdapterContribution | undefined {
+function toPlatformInfo(executable: PluginPackageDebuggersContribution): PlatformSpecificAdapterContribution | undefined {
if (isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
return executable.winx86 || executable.win || executable.windows;
}
diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts
index 32310db739808..b4bea604d8cbe 100644
--- a/packages/plugin-ext/src/plugin/plugin-context.ts
+++ b/packages/plugin-ext/src/plugin/plugin-context.ts
@@ -641,7 +641,7 @@ export function createAPIFactory(
}
};
- const debuggersContributions = plugin.model.contributes && plugin.model.contributes.debuggers || [];
+ const debuggersContributions = plugin.rawModel.contributes && plugin.rawModel.contributes.debuggers || [];
debugExt.assistedInject(connectionExt, commandRegistry);
debugExt.registerDebuggersContributions(plugin.pluginFolder, debuggersContributions);
const debug: typeof theia.debug = {
diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts
index 2f4b432282235..e0fab347d7bc1 100644
--- a/packages/plugin-ext/src/plugin/plugin-manager.ts
+++ b/packages/plugin-ext/src/plugin/plugin-manager.ts
@@ -20,11 +20,12 @@ import {
MainMessageType,
MessageRegistryMain,
PluginManagerExt,
- PluginInitData,
PluginManager,
Plugin,
PluginAPI,
- ConfigStorage
+ ConfigStorage,
+ PluginManagerInitializeParams,
+ PluginManagerStartParams
} from '../common/plugin-api-rpc';
import { PluginMetadata } from '../common/plugin-protocol';
import * as theia from '@theia/plugin';
@@ -43,7 +44,7 @@ export interface PluginHost {
// tslint:disable-next-line:no-any
loadPlugin(plugin: Plugin): any;
- init(data: PluginMetadata[]): [Plugin[], Plugin[]];
+ init(data: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> | [Plugin[], Plugin[]];
initExtApi(extApi: ExtPluginApi[]): void;
@@ -136,46 +137,46 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
}
}
- async $init(pluginInit: PluginInitData, configStorage: ConfigStorage): Promise {
+ async $init(params: PluginManagerInitializeParams): Promise {
this.storageProxy = this.rpc.set(
MAIN_RPC_CONTEXT.STORAGE_EXT,
new KeyValueStorageProxy(this.rpc.getProxy(PLUGIN_RPC_CONTEXT.STORAGE_MAIN),
- pluginInit.globalState,
- pluginInit.workspaceState)
+ params.globalState,
+ params.workspaceState)
);
- // init query parameters
- this.envExt.setQueryParameters(pluginInit.env.queryParams);
- this.envExt.setLanguage(pluginInit.env.language);
+ this.envExt.setQueryParameters(params.env.queryParams);
+ this.envExt.setLanguage(params.env.language);
- this.preferencesManager.init(pluginInit.preferences);
+ this.preferencesManager.init(params.preferences);
- if (pluginInit.extApi) {
- this.host.initExtApi(pluginInit.extApi);
+ if (params.extApi) {
+ this.host.initExtApi(params.extApi);
}
+ }
- const [plugins, foreignPlugins] = this.host.init(pluginInit.plugins);
+ async $start(params: PluginManagerStartParams): Promise {
+ const [plugins, foreignPlugins] = await this.host.init(params.plugins);
// add foreign plugins
for (const plugin of foreignPlugins) {
- this.registerPlugin(plugin, configStorage);
+ this.registerPlugin(plugin, params.configStorage);
}
// add own plugins, before initialization
for (const plugin of plugins) {
- this.registerPlugin(plugin, configStorage);
+ this.registerPlugin(plugin, params.configStorage);
}
// run eager plugins
await this.$activateByEvent('*');
- for (const activationEvent of pluginInit.activationEvents) {
+ for (const activationEvent of params.activationEvents) {
await this.$activateByEvent(activationEvent);
}
if (this.host.loadTests) {
return this.host.loadTests();
}
- this.fireOnDidChange();
- return Promise.resolve();
+ this.fireOnDidChange();
}
protected registerPlugin(plugin: Plugin, configStorage: ConfigStorage): void {
@@ -248,11 +249,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
return loading;
}
- $updateStoragePath(path: string | undefined): PromiseLike {
+ async $updateStoragePath(path: string | undefined): Promise {
this.pluginContextsMap.forEach((pluginContext: theia.PluginContext, pluginId: string) => {
pluginContext.storagePath = path ? join(path, pluginId) : undefined;
});
- return Promise.resolve();
}
async $activateByEvent(activationEvent: string): Promise {
@@ -271,7 +271,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
const subscriptions: theia.Disposable[] = [];
const asAbsolutePath = (relativePath: string): string => join(plugin.pluginFolder, relativePath);
const logPath = join(configStorage.hostLogPath, plugin.model.id); // todo check format
- const storagePath = join(configStorage.hostStoragePath, plugin.model.id);
+ const storagePath = join(configStorage.hostStoragePath || '', plugin.model.id);
const pluginContext: theia.PluginContext = {
extensionPath: plugin.pluginFolder,
globalState: new Memento(plugin.model.id, true, this.storageProxy),
diff --git a/yarn.lock b/yarn.lock
index 8e090dde90702..7a8e33f9ec63f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1584,6 +1584,11 @@ async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+async-limiter@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+ integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
async-limiter@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
@@ -11406,12 +11411,20 @@ write-pkg@^3.1.0:
sort-keys "^2.0.0"
write-json-file "^2.2.0"
-ws@^5.2.0, ws@^5.2.2:
+ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
dependencies:
async-limiter "~1.0.0"
+ws@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73"
+ integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==
+ dependencies:
+ async-limiter "^1.0.0"
+
xdg-basedir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"