diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 81598762eace5..136b324dc4ce0 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -18,6 +18,7 @@ import { injectable, inject, named } from 'inversify'; import { isOSX } from '../common/os'; import { Emitter, Event } from '../common/event'; import { CommandRegistry } from '../common/command'; +import { Disposable } from '../common/disposable'; import { KeyCode, KeySequence, Key } from './keyboard/keys'; import { KeyboardLayoutService } from './keyboard/keyboard-layout-service'; import { ContributionProvider } from '../common/contribution-provider'; @@ -639,10 +640,13 @@ export class KeybindingRegistry { return commandId === KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND; } - setKeymap(scope: KeybindingScope, bindings: Keybinding[]): void { + setKeymap(scope: KeybindingScope, bindings: Keybinding[]): Disposable { this.resetKeybindingsForScope(scope); this.doRegisterKeybindings(bindings, scope); this.keybindingsChanged.fire(undefined); + return Disposable.create(() => { + // TODO + }); } /** diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 1e69cbfced9b4..c7a59bca01af7 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -16,7 +16,7 @@ import * as Ajv from 'ajv'; import { inject, injectable, interfaces, named, postConstruct } from 'inversify'; -import { ContributionProvider, bindContributionProvider, escapeRegExpCharacters, Emitter, Event } from '../../common'; +import { ContributionProvider, bindContributionProvider, escapeRegExpCharacters, Emitter, Event, Disposable } from '../../common'; import { PreferenceScope } from './preference-scope'; import { PreferenceProvider, PreferenceProviderDataChange } from './preference-provider'; import { @@ -249,10 +249,15 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return this.combinedSchema; } - setSchema(schema: PreferenceSchema): void { + setSchema(schema: PreferenceSchema): Disposable { const changes = this.doSetSchema(schema); this.fireDidPreferenceSchemaChanged(); this.emitPreferencesChangedEvent(changes); + return { + dispose: () => { + // TODO: unset schema + } + }; } getPreferences(): { [name: string]: any } { diff --git a/packages/core/src/browser/quick-open/quick-command-service.ts b/packages/core/src/browser/quick-open/quick-command-service.ts index 8d749fac4bb09..0e176638f2bb2 100644 --- a/packages/core/src/browser/quick-open/quick-command-service.ts +++ b/packages/core/src/browser/quick-open/quick-command-service.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { Command, CommandRegistry } from '../../common'; +import { Command, CommandRegistry, Disposable } from '../../common'; import { Keybinding, KeybindingRegistry } from '../keybinding'; import { QuickOpenModel, QuickOpenItem, QuickOpenMode, QuickOpenGroupItem, QuickOpenGroupItemOptions } from './quick-open-model'; import { QuickOpenOptions } from './quick-open-service'; @@ -51,10 +51,16 @@ export class QuickCommandService implements QuickOpenModel, QuickOpenHandler { protected readonly corePreferences: CorePreferences; protected readonly contexts = new Map(); - pushCommandContext(commandId: string, when: string): void { + pushCommandContext(commandId: string, when: string): Disposable { const contexts = this.contexts.get(commandId) || []; contexts.push(when); this.contexts.set(commandId, contexts); + return Disposable.create(() => { + const index = contexts.indexOf(when); + if (index !== -1) { + contexts.splice(index, 1); + } + }); } /** Initialize this quick open model with the commands. */ diff --git a/packages/core/src/common/disposable.ts b/packages/core/src/common/disposable.ts index 80c192ff111c5..af29a3bac50d4 100644 --- a/packages/core/src/common/disposable.ts +++ b/packages/core/src/common/disposable.ts @@ -23,6 +23,10 @@ export interface Disposable { } export namespace Disposable { + // tslint:disable-next-line:no-any + export function is(arg: any): arg is Disposable { + return !!arg && typeof arg === 'object' && 'dispose' in arg && typeof arg['dispose'] === 'function'; + } export function create(func: () => void): Disposable { return { dispose: func @@ -56,7 +60,7 @@ export class DisposableCollection implements Disposable { private disposingElements = false; dispose(): void { - if (this.disposed || this.disposingElements) { + if (this.disposed || this.disposingElements) { return; } this.disposingElements = true; diff --git a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts index c43fc58483cf7..77367c824ef58 100644 --- a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts +++ b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts @@ -21,6 +21,7 @@ import * as jsoncparser from 'jsonc-parser'; import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; import { CompletionTriggerKind } from '@theia/languages/lib/browser'; @@ -114,7 +115,8 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } - fromURI(uri: string | URI, options: SnippetLoadOptions): Promise { + fromURI(uri: string | URI, options: SnippetLoadOptions): Disposable { + const toDispose = new DisposableCollection(); const pending = this.loadURI(uri, options); const { language } = options; const scopes = Array.isArray(language) ? language : !!language ? [language] : ['*']; @@ -123,7 +125,8 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion pendingSnippets.push(pending); this.pendingSnippets.set(scope, pendingSnippets); } - return pending; + // TODO unload snippets + return toDispose; } /** * should NOT throw to prevent load erros on suggest diff --git a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts index e76e26970725b..168d097510fa6 100644 --- a/packages/monaco/src/browser/textmate/monaco-textmate-service.ts +++ b/packages/monaco/src/browser/textmate/monaco-textmate-service.ts @@ -23,6 +23,8 @@ 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; @@ -58,6 +60,9 @@ 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.'); @@ -104,6 +109,7 @@ 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(); @@ -159,4 +165,12 @@ export class MonacoTextmateService implements FrontendApplicationContribution { this.onDidActivateLanguageEmitter.fire(languageId); } } + + detectLanguages(): void { + for (const editor of MonacoEditor.getAll(this.editorManager)) { + if (editor.languageAutoDetected) { + editor.detectLanguage(); + } + } + } } diff --git a/packages/monaco/src/browser/textmate/textmate-registry.ts b/packages/monaco/src/browser/textmate/textmate-registry.ts index 7c79ec8e6c00e..792bf3c3a0d70 100644 --- a/packages/monaco/src/browser/textmate/textmate-registry.ts +++ b/packages/monaco/src/browser/textmate/textmate-registry.ts @@ -17,6 +17,7 @@ import { injectable } from 'inversify'; import { IGrammarConfiguration } from 'vscode-textmate'; import { TokenizerOption } from './textmate-tokenizer'; +import { Disposable } from '@theia/core/lib/common/disposable'; export interface TextmateGrammarConfiguration extends IGrammarConfiguration { @@ -45,7 +46,7 @@ export class TextmateRegistry { readonly languageToConfig = new Map(); readonly languageIdToScope = new Map(); - registerTextmateGrammarScope(scope: string, description: GrammarDefinitionProvider): void { + registerTextmateGrammarScope(scope: string, description: GrammarDefinitionProvider): Disposable { const existingProvider = this.scopeToProvider.get(scope); if (existingProvider) { Promise.all([existingProvider.getGrammarDefinition(), description.getGrammarDefinition()]).then(([a, b]) => { @@ -55,18 +56,30 @@ export class TextmateRegistry { }); } this.scopeToProvider.set(scope, description); + return Disposable.create(() => { + this.scopeToProvider.delete(scope); + if (existingProvider) { + this.scopeToProvider.set(scope, existingProvider); + } + }); } getProvider(scope: string): GrammarDefinitionProvider | undefined { return this.scopeToProvider.get(scope); } - mapLanguageIdToTextmateGrammar(languageId: string, scope: string): void { + mapLanguageIdToTextmateGrammar(languageId: string, scope: string): Disposable { const existingScope = this.getScope(languageId); if (typeof existingScope === 'string') { console.warn(new Error(`'${languageId}' language is remapped from '${existingScope}' to '${scope}' scope`)); } this.languageIdToScope.set(languageId, scope); + return Disposable.create(() => { + this.languageIdToScope.delete(languageId); + if (typeof existingScope === 'string') { + this.languageIdToScope.set(languageId, existingScope); + } + }); } getScope(languageId: string): string | undefined { @@ -82,11 +95,18 @@ export class TextmateRegistry { return undefined; } - registerGrammarConfiguration(languageId: string, config: TextmateGrammarConfiguration): void { - if (this.languageToConfig.has(languageId)) { + registerGrammarConfiguration(languageId: string, config: TextmateGrammarConfiguration): Disposable { + const existignConfig = this.languageToConfig.get(languageId); + if (existignConfig) { console.warn(new Error(`a registered grammar configuration for '${languageId}' language is overridden`)); } this.languageToConfig.set(languageId, config); + return Disposable.create(() => { + this.languageToConfig.delete(languageId); + if (existignConfig) { + this.languageToConfig.set(languageId, existignConfig); + } + }); } getGrammarConfiguration(languageId: string): TextmateGrammarConfiguration { diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 4c71a8ef1acc8..ed4cf9a291309 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { JsonRpcServer, JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; import { RPCProtocol } from './rpc-protocol'; import { Disposable } from '@theia/core/lib/common/disposable'; import { LogPart, KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -634,6 +634,9 @@ export interface HostedPluginServer extends JsonRpcServer { } +export const HostedPluginServerProxy = Symbol('HostedPluginServerProxy'); +export type HostedPluginServerProxy = JsonRpcProxy; + /** * The JSON-RPC workspace interface. */ diff --git a/packages/plugin-ext/src/common/rpc-protocol.ts b/packages/plugin-ext/src/common/rpc-protocol.ts index 9a96b35c6d990..e7459912070c9 100644 --- a/packages/plugin-ext/src/common/rpc-protocol.ts +++ b/packages/plugin-ext/src/common/rpc-protocol.ts @@ -23,6 +23,7 @@ /* tslint:disable:no-any */ import { Event } from '@theia/core/lib/common/event'; +import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { Deferred } from '@theia/core/lib/common/promise-util'; import VSCodeURI from 'vscode-uri'; import URI from '@theia/core/lib/common/uri'; @@ -33,7 +34,7 @@ export interface MessageConnection { onMessage: Event<{}>; } -export interface RPCProtocol { +export interface RPCProtocol extends Disposable { /** * Returns a proxy to an object addressable/named in the plugin process or in the main process. */ @@ -60,37 +61,58 @@ export function createProxyIdentifier(identifier: string): ProxyIdentifier export class RPCProtocolImpl implements RPCProtocol { - private isDisposed: boolean; - private readonly locals: { [id: string]: any; }; - private readonly proxies: { [id: string]: any; }; - private lastMessageId: number; - private readonly invokedHandlers: { [req: string]: Promise; }; - private readonly cancellationTokenSources: { [req: string]: CancellationTokenSource } = {}; - private readonly pendingRPCReplies: { [msgId: string]: Deferred; }; + private readonly locals = new Map(); + private readonly proxies = new Map(); + private lastMessageId = 0; + private readonly cancellationTokenSources = new Map(); + private readonly pendingRPCReplies = new Map>(); private readonly multiplexor: RPCMultiplexer; private messageToSendHostId: string | undefined; + private readonly toDispose = new DisposableCollection( + Disposable.create(() => {/* no-op, see isDisposed */ }) + ); + constructor(connection: MessageConnection, readonly remoteHostID?: string) { - this.isDisposed = false; - // tslint:disable-next-line:no-null-keyword - this.locals = Object.create(null); - // tslint:disable-next-line:no-null-keyword - this.proxies = Object.create(null); - this.lastMessageId = 0; - // tslint:disable-next-line:no-null-keyword - this.invokedHandlers = Object.create(null); - this.pendingRPCReplies = {}; this.multiplexor = new RPCMultiplexer(connection, msg => this.receiveOneMessage(msg), remoteHostID); + this.toDispose.push(Disposable.create(() => { + this.proxies.clear(); + for (const reply of this.pendingRPCReplies.values()) { + reply.reject(new Error('connection is closed')); + } + this.pendingRPCReplies.clear(); + })); } + + private get isDisposed(): boolean { + return this.toDispose.disposed; + } + + dispose(): void { + this.toDispose.dispose(); + } + getProxy(proxyId: ProxyIdentifier): T { - if (!this.proxies[proxyId.id]) { - this.proxies[proxyId.id] = this.createProxy(proxyId.id); + if (this.isDisposed) { + throw new Error('connection is closed'); + } + let proxy = this.proxies.get(proxyId.id); + if (!proxy) { + proxy = this.createProxy(proxyId.id); + this.proxies.set(proxyId.id, proxy); } - return this.proxies[proxyId.id]; + return proxy; } set(identifier: ProxyIdentifier, instance: R): R { - this.locals[identifier.id] = instance; + if (this.isDisposed) { + throw new Error('connection is closed'); + } + this.locals.set(identifier.id, instance); + if (Disposable.is(instance)) { + this.toDispose.push(instance); + } + this.toDispose.push(Disposable.create(() => this.locals.delete(identifier.id))); return instance; } @@ -110,7 +132,7 @@ export class RPCProtocolImpl implements RPCProtocol { private remoteCall(proxyId: string, methodName: string, args: any[]): Promise { if (this.isDisposed) { - return Promise.reject(canceled()); + return Promise.reject(new Error('connection is closed')); } const cancellationToken: CancellationToken | undefined = args.length && CancellationToken.is(args[args.length - 1]) ? args.pop() : undefined; if (cancellationToken && cancellationToken.isCancellationRequested) { @@ -127,7 +149,7 @@ export class RPCProtocolImpl implements RPCProtocol { ); } - this.pendingRPCReplies[callId] = result; + this.pendingRPCReplies.set(callId, result); this.multiplexor.send(MessageFactory.request(callId, proxyId, methodName, args, this.messageToSendHostId)); return result.promise; } @@ -167,7 +189,7 @@ export class RPCProtocolImpl implements RPCProtocol { } private receiveCancel(msg: CancelMessage): void { - const cancellationTokenSource = this.cancellationTokenSources[msg.id]; + const cancellationTokenSource = this.cancellationTokenSources.get(msg.id); if (cancellationTokenSource) { cancellationTokenSource.cancel(); } @@ -182,42 +204,37 @@ export class RPCProtocolImpl implements RPCProtocol { const addToken = args.length && args[args.length - 1] === 'add.cancellation.token' ? args.pop() : false; if (addToken) { const tokenSource = new CancellationTokenSource(); - this.cancellationTokenSources[callId] = tokenSource; + this.cancellationTokenSources.set(callId, tokenSource); args.push(tokenSource.token); } - this.invokedHandlers[callId] = this.invokeHandler(proxyId, msg.method, args); - - this.invokedHandlers[callId].then(r => { - delete this.invokedHandlers[callId]; - delete this.cancellationTokenSources[callId]; - this.multiplexor.send(MessageFactory.replyOK(callId, r, this.messageToSendHostId)); - }, err => { - delete this.invokedHandlers[callId]; - delete this.cancellationTokenSources[callId]; - this.multiplexor.send(MessageFactory.replyErr(callId, err, this.messageToSendHostId)); + const invocation = this.invokeHandler(proxyId, msg.method, args); + + invocation.then(result => { + this.cancellationTokenSources.delete(callId); + this.multiplexor.send(MessageFactory.replyOK(callId, result, this.messageToSendHostId)); + }, error => { + this.cancellationTokenSources.delete(callId); + this.multiplexor.send(MessageFactory.replyErr(callId, error, this.messageToSendHostId)); }); } private receiveReply(msg: ReplyMessage): void { const callId = msg.id; - if (!this.pendingRPCReplies.hasOwnProperty(callId)) { + const pendingReply = this.pendingRPCReplies.get(callId); + if (!pendingReply) { return; } - - const pendingReply = this.pendingRPCReplies[callId]; - delete this.pendingRPCReplies[callId]; - + this.pendingRPCReplies.delete(callId); pendingReply.resolve(msg.res); } private receiveReplyErr(msg: ReplyErrMessage): void { const callId = msg.id; - if (!this.pendingRPCReplies.hasOwnProperty(callId)) { + const pendingReply = this.pendingRPCReplies.get(callId); + if (!pendingReply) { return; } - - const pendingReply = this.pendingRPCReplies[callId]; - delete this.pendingRPCReplies[callId]; + this.pendingRPCReplies.delete(callId); let err: Error | undefined = undefined; if (msg.err && msg.err.$isError) { @@ -238,10 +255,10 @@ export class RPCProtocolImpl implements RPCProtocol { } private doInvokeHandler(proxyId: string, methodName: string, args: any[]): any { - if (!this.locals[proxyId]) { + const actor = this.locals.get(proxyId); + if (!actor) { throw new Error('Unknown actor ' + proxyId); } - const actor = this.locals[proxyId]; const method = actor[methodName]; if (typeof method !== 'function') { throw new Error('Unknown method ' + methodName + ' on actor ' + proxyId); diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index bd4c86a261fe8..46f876113253d 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -23,12 +23,12 @@ import { injectable, inject, interfaces, named, postConstruct } from 'inversify'; import { PluginWorker } from '../../main/browser/plugin-worker'; -import { HostedPluginServer, PluginMetadata, getPluginId } from '../../common/plugin-protocol'; +import { PluginMetadata, getPluginId, HostedPluginServerProxy } 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'; import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol'; -import { ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent, CancellationTokenSource } from '@theia/core'; +import { Disposable, DisposableCollection, ILogger, ContributionProvider, CommandRegistry, WillExecuteCommandEvent, CancellationTokenSource } from '@theia/core'; import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler'; @@ -61,8 +61,8 @@ export class HostedPluginSupport { @inject(ILogger) protected readonly logger: ILogger; - @inject(HostedPluginServer) - private readonly server: HostedPluginServer; + @inject(HostedPluginServerProxy) + private readonly server: HostedPluginServerProxy; @inject(HostedPluginWatcher) private readonly watcher: HostedPluginWatcher; @@ -123,8 +123,7 @@ export class HostedPluginSupport { protected readonly managers: PluginManagerExt[] = []; - // loaded plugins per #id - private readonly loadedPlugins = new Set(); + private readonly loadedPlugins = new Map(); protected readonly activationEvents = new Set(); @@ -146,64 +145,92 @@ export class HostedPluginSupport { this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); } - checkAndLoadPlugin(container: interfaces.Container): void { + /** do not call it, except from the plugin frontend contribution */ + async onStart(container: interfaces.Container): Promise { this.container = container; - this.initPlugins(); + await this.load(); + this.watcher.onDidDeploy(() => this.load()); + // TODO fire didDeploy event instead for each connected client? + this.server.onDidOpenConnection(() => this.load()); } - public initPlugins(): void { - Promise.all([ - this.server.getDeployedMetadata(), - this.pluginPathsService.provideHostLogPath(), - this.storagePathService.provideHostStoragePath(), - this.server.getExtPluginAPI(), - this.pluginServer.keyValueStorageGetAll(true), - this.pluginServer.keyValueStorageGetAll(false), - this.workspaceService.roots, - ]).then(metadata => { - const pluginsInitData: PluginsInitializationData = { - plugins: metadata['0'], - logPath: metadata['1'], - storagePath: metadata['2'], - pluginAPIs: metadata['3'], - globalStates: metadata['4'], - workspaceStates: metadata['5'], - roots: metadata['6'] - }; - this.loadPlugins(pluginsInitData, this.container); - }).catch(e => console.error(e)); + async load(): Promise { + try { + const [plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates, roots] = await Promise.all([ + this.server.getDeployedMetadata(), + this.pluginPathsService.provideHostLogPath(), + this.storagePathService.provideHostStoragePath(), + this.server.getExtPluginAPI(), + this.pluginServer.keyValueStorageGetAll(true), + this.pluginServer.keyValueStorageGetAll(false), + this.workspaceService.roots, + ]); + await this.doLoad({ plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates, roots }, this.container); + } catch (e) { + console.error('Failed to load plugins:', e); + } } - async loadPlugins(initData: PluginsInitializationData, container: interfaces.Container): Promise { - // don't load plugins twice - initData.plugins = initData.plugins.filter(value => !this.loadedPlugins.has(value.model.id)); + protected async doLoad(initData: PluginsInitializationData, container: interfaces.Container): Promise { + const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ })); + this.server.onDidCloseConnection(() => toDisconnect.dispose()); // 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'); - const hostToPlugins = new Map(); - for (const plugin of initData.plugins) { - const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host; - const plugins = hostToPlugins.get(plugin.host) || []; - plugins.push(plugin); - hostToPlugins.set(host, plugins); - if (plugin.model.contributes) { - this.contributionHandler.handleContributions(plugin.model.contributes); - } + if (toDisconnect.disposed) { + // if disconnected then don't try to load plugin static contributions + return; } + + const hostToPlugins = this.loadStaticContributions(initData.plugins); await this.viewRegistry.initWidgets(); // remove restored plugin widgets which were not registered by contributions this.viewRegistry.removeStaleWidgets(); await this.theiaReadyPromise; + if (toDisconnect.disposed) { + // if disconnected then don't try to load plugin code and dynamic contributions + return; + } + for (const [host, plugins] of hostToPlugins) { const pluginId = getPluginId(plugins[0].model); const rpc = this.initRpc(host, pluginId, container); + toDisconnect.push(rpc); this.initPluginHostManager(rpc, { ...initData, plugins }); + // TODO toUnload.push(manager.$stop(pluginId)) } + } - // update list with loaded plugins - initData.plugins.forEach(value => this.loadedPlugins.add(value.model.id)); + protected loadStaticContributions(pluginsToLoad: PluginMetadata[]): Map { + const hostToPlugins = new Map(); + const pluginsToUnload = new Set(this.loadedPlugins.keys()); + for (const plugin of pluginsToLoad) { + const pluginId = plugin.model.id; + pluginsToUnload.delete(pluginId); + if (this.loadedPlugins.has(pluginId)) { + continue; + } + const toUnload = new DisposableCollection(); + this.loadedPlugins.set(pluginId, toUnload); + toUnload.push(Disposable.create(() => this.loadedPlugins.delete(pluginId))); + + const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host; + const hostPlugins = hostToPlugins.get(plugin.host) || []; + hostPlugins.push(plugin); + hostToPlugins.set(host, hostPlugins); + if (plugin.model.contributes) { + toUnload.push(this.contributionHandler.handleContributions(plugin.model.contributes)); + } + } + for (const pluginId of pluginsToUnload) { + const toUnload = this.loadedPlugins.get(pluginId); + if (toUnload) { + toUnload.dispose(); + } + } + return hostToPlugins; } protected initRpc(host: PluginHost, pluginId: string, container: interfaces.Container): RPCProtocol { diff --git a/packages/plugin-ext/src/main/browser/command-registry-main.ts b/packages/plugin-ext/src/main/browser/command-registry-main.ts index cac05348dcbdd..d3a9b1c66a31c 100644 --- a/packages/plugin-ext/src/main/browser/command-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/command-registry-main.ts @@ -17,20 +17,22 @@ import { interfaces } from 'inversify'; import { CommandRegistry } from '@theia/core/lib/common/command'; import * as theia from '@theia/plugin'; -import { Disposable } from '@theia/core/lib/common/disposable'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { CommandRegistryMain, CommandRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { KeybindingRegistry } from '@theia/core/lib/browser'; import { PluginContributionHandler } from './plugin-contribution-handler'; -export class CommandRegistryMainImpl implements CommandRegistryMain { - private proxy: CommandRegistryExt; +export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable { + private readonly proxy: CommandRegistryExt; private readonly commands = new Map(); private readonly handlers = new Map(); private readonly delegate: CommandRegistry; private readonly keyBinding: KeybindingRegistry; private readonly contributions: PluginContributionHandler; + protected readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT); this.delegate = container.get(CommandRegistry); @@ -38,8 +40,14 @@ export class CommandRegistryMainImpl implements CommandRegistryMain { this.contributions = container.get(PluginContributionHandler); } + dispose(): void { + this.toDispose.dispose(); + } + $registerCommand(command: theia.CommandDescription): void { - this.commands.set(command.id, this.contributions.registerCommand(command)); + const id = command.id; + this.commands.set(id, this.contributions.registerCommand(command)); + this.toDispose.push(Disposable.create(() => this.$unregisterCommand(id))); } $unregisterCommand(id: string): void { const command = this.commands.get(id); @@ -53,6 +61,7 @@ export class CommandRegistryMainImpl implements CommandRegistryMain { this.handlers.set(id, this.contributions.registerCommandHandler(id, (...args) => this.proxy.$executeCommand(id, ...args) )); + this.toDispose.push(Disposable.create(() => this.$unregisterHandler(id))); } $unregisterHandler(id: string): void { const handler = this.handlers.get(id); diff --git a/packages/plugin-ext/src/main/browser/connection-main.ts b/packages/plugin-ext/src/main/browser/connection-main.ts index 9ef6aa1ce8521..2dbb95018239d 100644 --- a/packages/plugin-ext/src/main/browser/connection-main.ts +++ b/packages/plugin-ext/src/main/browser/connection-main.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { MAIN_RPC_CONTEXT, ConnectionMain, ConnectionExt } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { PluginConnection } from '../../common/connection'; @@ -26,12 +27,17 @@ import { PluginMessageWriter } from '../../common/plugin-message-writer'; */ export class ConnectionMainImpl implements ConnectionMain { - private proxy: ConnectionExt; - private connections = new Map(); + private readonly proxy: ConnectionExt; + private readonly connections = new Map(); + private readonly toDispose = new DisposableCollection(); constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.CONNECTION_EXT); } + dispose(): void { + this.toDispose.dispose(); + } + /** * Gets the connection between plugin by id and sends string message to it. * @@ -85,12 +91,14 @@ export class ConnectionMainImpl implements ConnectionMain { protected async doCreateConnection(id: string): Promise { const reader = new PluginMessageReader(); const writer = new PluginMessageWriter(id, this.proxy); - return new PluginConnection( + const connection = new PluginConnection( reader, writer, () => { this.connections.delete(id); this.proxy.$deleteConnection(id); }); + this.toDispose.push(connection); + return connection; } } 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 a00a0e57f4e30..f4f6c05572dd2 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -43,7 +43,7 @@ import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { DebugPreferences } from '@theia/debug/lib/browser/debug-preferences'; import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution'; import { PluginDebugSessionContributionRegistrator, PluginDebugSessionContributionRegistry } from './plugin-debug-session-contribution-registry'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { PluginDebugSessionFactory } from './plugin-debug-session-factory'; import { PluginWebSocketChannel } from '../../../common/connection'; import { PluginDebugAdapterContributionRegistrator, PluginDebugService } from './plugin-debug-service'; @@ -51,7 +51,7 @@ import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-update import { FileSystem } from '@theia/filesystem/lib/common'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; -export class DebugMainImpl implements DebugMain { +export class DebugMainImpl implements DebugMain, Disposable { private readonly debugExt: DebugExt; private readonly sessionManager: DebugSessionManager; @@ -70,7 +70,8 @@ export class DebugMainImpl implements DebugMain { private readonly fileSystem: FileSystem; private readonly pluginService: HostedPluginSupport; - private readonly toDispose = new Map(); + private readonly debuggerContributions = new Map(); + private readonly toDispose = new DisposableCollection(); constructor(rpc: RPCProtocol, readonly connectionMain: ConnectionMainImpl, container: interfaces.Container) { this.debugExt = rpc.getProxy(MAIN_RPC_CONTEXT.DEBUG_EXT); @@ -90,7 +91,7 @@ export class DebugMainImpl implements DebugMain { this.fileSystem = container.get(FileSystem); this.pluginService = container.get(HostedPluginSupport); - this.breakpointsManager.onDidChangeBreakpoints(({ added, removed, changed }) => { + this.toDispose.push(this.breakpointsManager.onDidChangeBreakpoints(({ added, removed, changed }) => { // TODO can we get rid of all to reduce amount of data set each time, should not it be possible to recover on another side from deltas? const all = this.breakpointsManager.getBreakpoints(); this.debugExt.$breakpointsDidChange( @@ -99,12 +100,18 @@ export class DebugMainImpl implements DebugMain { this.toTheiaPluginApiBreakpoints(removed), this.toTheiaPluginApiBreakpoints(changed) ); - }); + })); - this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)); - this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)); - this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)); - this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)); + this.toDispose.pushAll([ + this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)), + this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)), + this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)), + this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)) + ]); + } + + dispose(): void { + this.toDispose.dispose(); } async $appendToDebugConsole(value: string): Promise { @@ -116,9 +123,11 @@ export class DebugMainImpl implements DebugMain { } async $registerDebuggerContribution(description: DebuggerDescription): Promise { - const disposable = new DisposableCollection(); - this.toDispose.set(description.type, disposable); - const terminalOptionsExt = await this.debugExt.$getTerminalCreationOptions(description.type); + const debugType = description.type; + const terminalOptionsExt = await this.debugExt.$getTerminalCreationOptions(debugType); + if (this.toDispose.disposed) { + return; + } const debugSessionFactory = new PluginDebugSessionFactory( this.terminalService, @@ -136,7 +145,12 @@ export class DebugMainImpl implements DebugMain { terminalOptionsExt ); - disposable.pushAll([ + const toDispose = new DisposableCollection( + Disposable.create(() => this.debugSchemaUpdater.update()), + Disposable.create(() => this.debuggerContributions.delete(debugType)) + ); + this.debuggerContributions.set(debugType, toDispose); + toDispose.pushAll([ this.adapterContributionRegistrator.registerDebugAdapterContribution( new PluginDebugAdapterContribution(description, this.debugExt, this.pluginService) ), @@ -150,11 +164,9 @@ export class DebugMainImpl implements DebugMain { } async $unregisterDebuggerConfiguration(debugType: string): Promise { - const disposable = this.toDispose.get(debugType); + const disposable = this.debuggerContributions.get(debugType); if (disposable) { disposable.dispose(); - this.toDispose.delete(debugType); - this.debugSchemaUpdater.update(); } } diff --git a/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts b/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts index 596c5b8afc20b..184adc2b2d45a 100644 --- a/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts +++ b/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts @@ -22,31 +22,42 @@ import { } from '../../../common/plugin-api-rpc'; import { interfaces } from 'inversify'; -import { Emitter } from '@theia/core'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Tree, TreeDecoration } from '@theia/core/lib/browser'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { ScmDecorationsService } from '@theia/scm/lib/browser/decorations/scm-decorations-service'; -export class DecorationsMainImpl implements DecorationsMain { +export class DecorationsMainImpl implements DecorationsMain, Disposable { private readonly proxy: DecorationsExt; + // TODO: why it is SCM specific? VS Code apis about any decorations for the explorer private readonly scmDecorationsService: ScmDecorationsService; protected readonly emitter = new Emitter<(tree: Tree) => Map>(); + protected readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT); this.scmDecorationsService = container.get(ScmDecorationsService); } - readonly providersMap: Map = new Map(); + dispose(): void { + this.toDispose.dispose(); + } + + // TODO: why it is never used? + protected readonly providers = new Map(); async $dispose(id: number): Promise { - this.providersMap.delete(id); + // TODO: What about removing decorations when a provider is gone? + this.providers.delete(id); } async $registerDecorationProvider(id: number, provider: DecorationProvider): Promise { - this.providersMap.set(id, provider); + this.providers.set(id, provider); + this.toDispose.push(Disposable.create(() => this.$dispose(id))); return id; } @@ -61,6 +72,7 @@ export class DecorationsMainImpl implements DecorationsMain { } this.scmDecorationsService.fireNavigatorDecorationsChanged(result); } else if (arg) { + // TODO: why to make a remote call instead of sending decoration to `$fireDidChangeDecorations` in first place? this.proxy.$provideDecoration(id, arg); } } diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index df808e632c406..5d1218ae1908a 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -78,32 +78,30 @@ export class ModelReferenceCollection { } } -export class DocumentsMainImpl implements DocumentsMain { +export class DocumentsMainImpl implements DocumentsMain, Disposable { - private proxy: DocumentsExt; - private toDispose = new DisposableCollection(); - private modelToDispose = new Map(); - private modelIsSynced = new Map(); - private modelService: EditorModelService; - private modelReferenceCache = new ModelReferenceCollection(); + private readonly proxy: DocumentsExt; + private readonly syncedModels = new Map(); + private readonly modelReferenceCache = new ModelReferenceCollection(); protected saveTimeout = 1750; + private readonly toDispose = new DisposableCollection(this.modelReferenceCache); + constructor( editorsAndDocuments: EditorsAndDocumentsMain, - modelService: EditorModelService, + private readonly modelService: EditorModelService, rpc: RPCProtocol, private editorManager: EditorManager, private openerService: OpenerService, private shell: ApplicationShell ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT); - this.modelService = modelService; + this.toDispose.push(editorsAndDocuments); this.toDispose.push(editorsAndDocuments.onDocumentAdd(documents => documents.forEach(this.onModelAdded, this))); this.toDispose.push(editorsAndDocuments.onDocumentRemove(documents => documents.forEach(this.onModelRemoved, this))); this.toDispose.push(modelService.onModelModeChanged(this.onModelChanged, this)); - this.toDispose.push(this.modelReferenceCache); this.toDispose.push(modelService.onModelSaved(m => { this.proxy.$acceptModelSaved(m.textEditorModel.uri); @@ -127,48 +125,45 @@ export class DocumentsMainImpl implements DocumentsMain { } dispose(): void { - this.modelToDispose.forEach(val => val.dispose()); - this.modelToDispose = new Map(); this.toDispose.dispose(); } private onModelChanged(event: { model: MonacoEditorModel, oldModeId: string }): void { const modelUrl = event.model.textEditorModel.uri; - if (!this.modelIsSynced.get(modelUrl.toString())) { - return; + if (this.syncedModels.has(modelUrl.toString())) { + this.proxy.$acceptModelModeChanged(modelUrl, event.oldModeId, event.model.languageId); } - - this.proxy.$acceptModelModeChanged(modelUrl, event.oldModeId, event.model.languageId); } private onModelAdded(model: MonacoEditorModel): void { - const modelUrl = model.textEditorModel.uri; - this.modelIsSynced.set(modelUrl.toString(), true); - this.modelToDispose.set(modelUrl.toString(), model.textEditorModel.onDidChangeContent(e => { - this.proxy.$acceptModelChanged(modelUrl, { - eol: e.eol, - versionId: e.versionId, - changes: e.changes.map(c => - ({ - text: c.text, - range: c.range, - rangeLength: c.rangeLength, - rangeOffset: c.rangeOffset - })) - }, model.dirty); - })); - + const modelUri = model.textEditorModel.uri; + const key = modelUri.toString(); + + const toDispose = new DisposableCollection( + model.textEditorModel.onDidChangeContent(e => + this.proxy.$acceptModelChanged(modelUri, { + eol: e.eol, + versionId: e.versionId, + changes: e.changes.map(c => + ({ + text: c.text, + range: c.range, + rangeLength: c.rangeLength, + rangeOffset: c.rangeOffset + })) + }, model.dirty) + ), + Disposable.create(() => this.syncedModels.delete(key)) + ); + this.syncedModels.set(key, toDispose); + this.toDispose.push(toDispose); } private onModelRemoved(url: monaco.Uri): void { - const modelUrl = url.toString(); - if (!this.modelIsSynced.get(modelUrl)) { - return; + const model = this.syncedModels.get(url.toString()); + if (model) { + model.dispose(); } - - this.modelIsSynced.delete(modelUrl); - this.modelToDispose.get(modelUrl)!.dispose(); - this.modelToDispose.delete(modelUrl); } async $tryCreateDocument(options?: { language?: string; content?: string; }): Promise { diff --git a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts index b04bb0f4c6693..3f89db9ccb57d 100644 --- a/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts +++ b/packages/plugin-ext/src/main/browser/editors-and-documents-main.ts @@ -23,68 +23,55 @@ import { EditorsAndDocumentsDelta, ModelAddedData, TextEditorAddData, - EditorPosition, - PLUGIN_RPC_CONTEXT + EditorPosition } from '../../common/plugin-api-rpc'; import { Disposable } from '@theia/core/lib/common/disposable'; import { EditorModelService } from './text-editor-model-service'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { TextEditorMain } from './text-editor-main'; -import { Emitter, Event } from '@theia/core'; +import { Emitter } from '@theia/core'; import { DisposableCollection } from '@theia/core'; -import { DocumentsMainImpl } from './documents-main'; -import { TextEditorsMainImpl } from './text-editors-main'; -import { EditorManager } from '@theia/editor/lib/browser'; -import { OpenerService } from '@theia/core/lib/browser/opener-service'; -import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service'; -import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; -import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; - -export class EditorsAndDocumentsMain { - private toDispose = new DisposableCollection(); - private stateComputer: EditorAndDocumentStateComputer; - private proxy: EditorsAndDocumentsExt; - private textEditors = new Map(); + +export class EditorsAndDocumentsMain implements Disposable { + + private readonly proxy: EditorsAndDocumentsExt; + + private readonly stateComputer: EditorAndDocumentStateComputer; + private readonly textEditors = new Map(); private readonly modelService: EditorModelService; - private onTextEditorAddEmitter = new Emitter(); - private onTextEditorRemoveEmitter = new Emitter(); - private onDocumentAddEmitter = new Emitter(); - private onDocumentRemoveEmitter = new Emitter(); + private readonly onTextEditorAddEmitter = new Emitter(); + private readonly onTextEditorRemoveEmitter = new Emitter(); + private readonly onDocumentAddEmitter = new Emitter(); + private readonly onDocumentRemoveEmitter = new Emitter(); - readonly onTextEditorAdd: Event = this.onTextEditorAddEmitter.event; + readonly onTextEditorAdd = this.onTextEditorAddEmitter.event; readonly onTextEditorRemove = this.onTextEditorRemoveEmitter.event; readonly onDocumentAdd = this.onDocumentAddEmitter.event; readonly onDocumentRemove = this.onDocumentRemoveEmitter.event; + private readonly toDispose = new DisposableCollection( + Disposable.create(() => this.textEditors.clear()) + ); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT); - const editorService = container.get(TextEditorService); - this.modelService = container.get(EditorModelService); - const editorManager = container.get(EditorManager); - const openerService = container.get(OpenerService); - const bulkEditService = container.get(MonacoBulkEditService); - const monacoEditorService = container.get(MonacoEditorService); - const shell = container.get(ApplicationShell); - - const documentsMain = new DocumentsMainImpl(this, this.modelService, rpc, editorManager, openerService, shell); - rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); - - const editorsMain = new TextEditorsMainImpl(this, rpc, bulkEditService, monacoEditorService); - rpc.set(PLUGIN_RPC_CONTEXT.TEXT_EDITORS_MAIN, editorsMain); + const editorService = container.get(TextEditorService); + this.modelService = container.get(EditorModelService); this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d), editorService, this.modelService); - this.toDispose.push(documentsMain); - this.toDispose.push(editorsMain); this.toDispose.push(this.stateComputer); this.toDispose.push(this.onTextEditorAddEmitter); this.toDispose.push(this.onTextEditorRemoveEmitter); this.toDispose.push(this.onDocumentAddEmitter); this.toDispose.push(this.onDocumentRemoveEmitter); + } + dispose(): void { + this.toDispose.dispose(); } private onDelta(delta: EditorAndDocumentStateDelta): void { @@ -96,6 +83,7 @@ export class EditorsAndDocumentsMain { for (const editor of delta.addedEditors) { const textEditorMain = new TextEditorMain(editor.id, editor.editor.getControl().getModel()!, editor.editor); this.textEditors.set(editor.id, textEditorMain); + this.toDispose.push(textEditorMain); addedEditors.push(textEditorMain); } @@ -178,27 +166,35 @@ export class EditorsAndDocumentsMain { } } -class EditorAndDocumentStateComputer { - private toDispose = new Array(); - private disposeOnEditorRemove = new Map(); - private currentState: EditorAndDocumentState; +class EditorAndDocumentStateComputer implements Disposable { + private currentState: EditorAndDocumentState | undefined; + private readonly editors = new Map(); + private readonly toDispose = new DisposableCollection( + Disposable.create(() => this.currentState = undefined) + ); - constructor(private callback: (delta: EditorAndDocumentStateDelta) => void, private editorService: TextEditorService, private modelService: EditorModelService) { - editorService.onTextEditorAdd(e => { + constructor( + private callback: (delta: EditorAndDocumentStateDelta) => void, + private readonly editorService: TextEditorService, + private readonly modelService: EditorModelService + ) { + this.toDispose.push(editorService.onTextEditorAdd(e => { this.onTextEditorAdd(e); - }); - editorService.onTextEditorRemove(e => { + })); + this.toDispose.push(editorService.onTextEditorRemove(e => { this.onTextEditorRemove(e); - }); - modelService.onModelAdded(this.onModelAdded, this, this.toDispose); - modelService.onModelRemoved(e => { - this.update(); - }); + })); + this.toDispose.push(modelService.onModelAdded(this.onModelAdded, this)); + this.toDispose.push(modelService.onModelRemoved(() => this.update())); editorService.listTextEditors().forEach(e => this.onTextEditorAdd(e)); this.update(); } + dispose(): void { + this.toDispose.dispose(); + } + private onModelAdded(model: MonacoEditorModel): void { if (!this.currentState) { this.update(); @@ -219,18 +215,21 @@ class EditorAndDocumentStateComputer { )); } - private onTextEditorAdd(e: MonacoEditor): void { - const disposables: Disposable[] = []; - disposables.push(e.onFocusChanged(_ => this.update())); - this.disposeOnEditorRemove.set(e.getControl().getId(), disposables); + private onTextEditorAdd(editor: MonacoEditor): void { + const id = editor.getControl().getId(); + const toDispose = new DisposableCollection( + editor.onFocusChanged(_ => this.update()), + Disposable.create(() => this.editors.delete(id)) + ); + this.editors.set(id, toDispose); + this.toDispose.push(toDispose); this.update(); } private onTextEditorRemove(e: MonacoEditor): void { - const dis = this.disposeOnEditorRemove.get(e.getControl().getId()); - if (dis) { - this.disposeOnEditorRemove.delete(e.getControl().getId()); - dis.forEach(d => d.dispose()); + const toDispose = this.editors.get(e.getControl().getId()); + if (toDispose) { + toDispose.dispose(); this.update(); } } @@ -274,12 +273,6 @@ class EditorAndDocumentStateComputer { } } - dispose(): void { - this.toDispose.forEach(element => { - element.dispose(); - }); - } - } class EditorAndDocumentStateDelta { @@ -309,7 +302,7 @@ class EditorAndDocumentState { readonly activeEditor: string | undefined) { } - static compute(before: EditorAndDocumentState, after: EditorAndDocumentState): EditorAndDocumentStateDelta { + static compute(before: EditorAndDocumentState | undefined, after: EditorAndDocumentState): EditorAndDocumentStateDelta { if (!before) { return new EditorAndDocumentStateDelta( [], diff --git a/packages/plugin-ext/src/main/browser/file-system-main.ts b/packages/plugin-ext/src/main/browser/file-system-main.ts index cabd4b5f28e15..3ecdbd3d6875f 100644 --- a/packages/plugin-ext/src/main/browser/file-system-main.ts +++ b/packages/plugin-ext/src/main/browser/file-system-main.ts @@ -26,28 +26,34 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { private readonly proxy: FileSystemExt; private readonly resourceResolver: FSResourceResolver; - private readonly disposables = new Map(); + private readonly providers = new Map(); + private readonly toDispose = new DisposableCollection(); constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT); this.resourceResolver = container.get(FSResourceResolver); } + dispose(): void { + this.toDispose.dispose(); + } + async $registerFileSystemProvider(handle: number, scheme: string): Promise { - this.disposables.set(handle, await this.resourceResolver.registerResourceProvider(handle, scheme, this.proxy)); + const toDispose = new DisposableCollection( + this.resourceResolver.registerResourceProvider(handle, scheme, this.proxy), + Disposable.create(() => this.providers.delete(handle)) + ); + this.providers.set(handle, toDispose); + this.toDispose.push(toDispose); } $unregisterProvider(handle: number): void { - const disposable = this.disposables.get(handle); + const disposable = this.providers.get(handle); if (disposable) { disposable.dispose(); - this.disposables.delete(handle); } } - dispose(): void { - this.disposables.forEach(d => d.dispose()); - } } @injectable() diff --git a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts b/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts index 0e897735300e9..a29bb1484fb2f 100644 --- a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts +++ b/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { interfaces } from 'inversify'; +import { interfaces, injectable } from 'inversify'; import { FileSystemWatcher, FileChangeEvent, FileChangeType, FileChange, FileMoveEvent, FileWillMoveEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { WorkspaceExt } from '../../common/plugin-api-rpc'; import { FileWatcherSubscriberOptions } from '../../common/plugin-api-rpc-model'; @@ -28,14 +28,13 @@ import { theiaUritoUriComponents } from '../../common/uri-components'; * and process all file system events in all workspace roots whether they matches to any subscription. * Only if event matches it will be sent into plugin side to specific subscriber. */ +@injectable() export class InPluginFileSystemWatcherManager { - private proxy: WorkspaceExt; - private subscribers: Map; + private readonly subscribers: Map; private nextSubscriberId: number; - constructor(proxy: WorkspaceExt, container: interfaces.Container) { - this.proxy = proxy; + constructor(container: interfaces.Container) { this.subscribers = new Map(); this.nextSubscriberId = 0; @@ -46,27 +45,27 @@ export class InPluginFileSystemWatcherManager { } // Filter file system changes according to subscribers settings here to avoid unneeded traffic. - onFilesChangedEventHandler(changes: FileChangeEvent): void { + protected onFilesChangedEventHandler(changes: FileChangeEvent): void { for (const change of changes) { switch (change.type) { case FileChangeType.UPDATED: for (const [id, subscriber] of this.subscribers) { if (!subscriber.ignoreChangeEvents && this.uriMatches(subscriber, change)) { - this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'updated' }); + subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'updated' }); } } break; case FileChangeType.ADDED: for (const [id, subscriber] of this.subscribers) { if (!subscriber.ignoreCreateEvents && this.uriMatches(subscriber, change)) { - this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'created' }); + subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'created' }); } } break; case FileChangeType.DELETED: for (const [id, subscriber] of this.subscribers) { if (!subscriber.ignoreDeleteEvents && this.uriMatches(subscriber, change)) { - this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'deleted' }); + subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'deleted' }); } } break; @@ -75,9 +74,9 @@ export class InPluginFileSystemWatcherManager { } // Filter file system changes according to subscribers settings here to avoid unneeded traffic. - onDidMoveEventHandler(change: FileMoveEvent): void { - for (const [id] of this.subscribers) { - this.proxy.$onFileRename({ + protected onDidMoveEventHandler(change: FileMoveEvent): void { + for (const [id, subscriber] of this.subscribers) { + subscriber.proxy.$onFileRename({ subscriberId: id, oldUri: theiaUritoUriComponents(change.sourceUri), newUri: theiaUritoUriComponents(change.targetUri) @@ -86,9 +85,9 @@ export class InPluginFileSystemWatcherManager { } // Filter file system changes according to subscribers settings here to avoid unneeded traffic. - onWillMoveEventHandler(change: FileWillMoveEvent): void { - for (const [id] of this.subscribers) { - this.proxy.$onWillRename({ + protected onWillMoveEventHandler(change: FileWillMoveEvent): void { + for (const [id, subscriber] of this.subscribers) { + subscriber.proxy.$onWillRename({ subscriberId: id, oldUri: theiaUritoUriComponents(change.sourceUri), newUri: theiaUritoUriComponents(change.targetUri) @@ -106,7 +105,7 @@ export class InPluginFileSystemWatcherManager { * @param options subscription options * @returns generated subscriber id */ - registerFileWatchSubscription(options: FileWatcherSubscriberOptions): string { + registerFileWatchSubscription(options: FileWatcherSubscriberOptions, proxy: WorkspaceExt): string { const subscriberId = this.getNextId(); let globPatternMatcher: ParsedPattern; @@ -122,7 +121,8 @@ export class InPluginFileSystemWatcherManager { mather: globPatternMatcher, ignoreCreateEvents: options.ignoreCreateEvents === true, ignoreChangeEvents: options.ignoreChangeEvents === true, - ignoreDeleteEvents: options.ignoreDeleteEvents === true + ignoreDeleteEvents: options.ignoreDeleteEvents === true, + proxy }; this.subscribers.set(subscriberId, subscriber); @@ -145,4 +145,5 @@ interface FileWatcherSubscriber { ignoreCreateEvents: boolean; ignoreChangeEvents: boolean; ignoreDeleteEvents: boolean; + proxy: WorkspaceExt } diff --git a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts index b8d9f707de875..c4ea7f7bba285 100644 --- a/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/keybindings/keybindings-contribution-handler.ts @@ -19,6 +19,7 @@ import { PluginContribution, Keybinding as PluginKeybinding } from '../../../com import { Keybinding, KeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding'; import { ILogger } from '@theia/core/lib/common/logger'; import { OS } from '@theia/core/lib/common/os'; +import { Disposable } from '@theia/core/lib/common/disposable'; @injectable() export class KeybindingsContributionPointHandler { @@ -29,9 +30,9 @@ export class KeybindingsContributionPointHandler { @inject(KeybindingRegistry) private readonly keybindingRegistry: KeybindingRegistry; - handle(contributions: PluginContribution): void { + handle(contributions: PluginContribution): Disposable { if (!contributions || !contributions.keybindings) { - return; + return Disposable.NULL; } const keybindings: Keybinding[] = []; for (const raw of contributions.keybindings) { @@ -48,7 +49,7 @@ export class KeybindingsContributionPointHandler { } } } - this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); + return this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); } protected toKeybinding(pluginKeybinding: PluginKeybinding): Keybinding | undefined { diff --git a/packages/plugin-ext/src/main/browser/languages-contribution-main.ts b/packages/plugin-ext/src/main/browser/languages-contribution-main.ts index b50fe3b7e57ad..00bf35301a811 100644 --- a/packages/plugin-ext/src/main/browser/languages-contribution-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-contribution-main.ts @@ -24,19 +24,22 @@ import { Workspace, Languages, MessageReader, MessageWriter } from '@theia/langu import { LanguageClientFactory, BaseLanguageClientContribution } from '@theia/languages/lib/browser'; import { MessageService, CommandRegistry } from '@theia/core'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { DisposableCollection, Disposable } from '@theia/core'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { createMessageConnection, MessageConnection } from 'vscode-jsonrpc'; import { ConnectionMainImpl } from './connection-main'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { LanguageClientContributionProvider } from './language-provider/language-client-contribution-provider'; /** * Implementation of languages contribution system of the plugin API. * Uses for registering new language server which was described in the plug-in. */ -export class LanguagesContributionMainImpl implements LanguagesContributionMain { +export class LanguagesContributionMainImpl implements LanguagesContributionMain, Disposable { + private readonly languageClientContributionProvider: LanguageClientContributionProvider; + private readonly toDispose = new DisposableCollection(); + constructor(protected readonly rpc: RPCProtocol, protected readonly container: interfaces.Container, protected readonly connectionMain: ConnectionMainImpl) { @@ -44,6 +47,10 @@ export class LanguagesContributionMainImpl implements LanguagesContributionMain this.languageClientContributionProvider = container.get(LanguageClientContributionProvider); } + dispose(): void { + this.toDispose.dispose(); + } + /** * Creates new client contribution for the language server and register it. * @@ -68,6 +75,7 @@ export class LanguagesContributionMainImpl implements LanguagesContributionMain newLanguageContribution.patterns = languageServerInfo.globPatterns; this.languageClientContributionProvider.registerLanguageClientContribution(newLanguageContribution); + this.toDispose.push(Disposable.create(() => this.$stop(languageServerInfo.id))); } /** diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 06ef9182b9789..cf0b1be4c1945 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -38,27 +38,34 @@ import { interfaces } from 'inversify'; import { SerializedDocumentFilter, MarkerData, Range, WorkspaceSymbolProvider, RelatedInformation, MarkerSeverity, DocumentLink } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { fromLanguageSelector } from '../../plugin/type-converters'; -import { DisposableCollection, Emitter } from '@theia/core'; import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import URI from 'vscode-uri/lib/umd'; import CoreURI from '@theia/core/lib/common/uri'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Emitter } from '@theia/core/lib/common/event'; import { ProblemManager } from '@theia/markers/lib/browser'; import * as vst from 'vscode-languageserver-types'; import * as theia from '@theia/plugin'; -export class LanguagesMainImpl implements LanguagesMain { +export class LanguagesMainImpl implements LanguagesMain, Disposable { private readonly monacoLanguages: MonacoLanguages; private readonly problemManager: ProblemManager; private readonly proxy: LanguagesExt; - private readonly disposables = new Map(); + private readonly services = new Map(); + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.LANGUAGES_EXT); this.monacoLanguages = container.get(MonacoLanguages); this.problemManager = container.get(ProblemManager); } + dispose(): void { + this.toDispose.dispose(); + } + $getLanguages(): Promise { return Promise.resolve(monaco.languages.getLanguages().map(l => l.id)); } @@ -77,11 +84,20 @@ export class LanguagesMainImpl implements LanguagesMain { return Promise.resolve(undefined); } + protected register(handle: number, service: Disposable): void { + const dispose = service.dispose.bind(service); + service.dispose = () => { + this.services.delete(handle); + dispose(); + }; + this.services.set(handle, service); + this.toDispose.push(service); + } + $unregister(handle: number): void { - const disposable = this.disposables.get(handle); + const disposable = this.services.get(handle); if (disposable) { disposable.dispose(); - this.disposables.delete(handle); } } @@ -94,11 +110,11 @@ export class LanguagesMainImpl implements LanguagesMain { onEnterRules: reviveOnEnterRules(configuration.onEnterRules), }; - this.disposables.set(handle, monaco.languages.setLanguageConfiguration(languageId, config)); + this.register(handle, monaco.languages.setLanguageConfiguration(languageId, config)); } $registerCompletionSupport(handle: number, selector: SerializedDocumentFilter[], triggerCharacters: string[], supportsResolveDetails: boolean): void { - this.disposables.set(handle, monaco.modes.CompletionProviderRegistry.register(fromLanguageSelector(selector), { + this.register(handle, monaco.modes.CompletionProviderRegistry.register(fromLanguageSelector(selector), { triggerCharacters, provideCompletionItems: (model, position, context, token) => this.proxy.$provideCompletionItems(handle, model.uri, position, context, token).then(result => { @@ -120,25 +136,19 @@ export class LanguagesMainImpl implements LanguagesMain { $registerDefinitionProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const definitionProvider = this.createDefinitionProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerDefinitionProvider(languageSelector, definitionProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerDefinitionProvider(languageSelector, definitionProvider)); } $registerDeclarationProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const declarationProvider = this.createDeclarationProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerDeclarationProvider(languageSelector, declarationProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerDeclarationProvider(languageSelector, declarationProvider)); } $registerReferenceProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const referenceProvider = this.createReferenceProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerReferenceProvider(languageSelector, referenceProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerReferenceProvider(languageSelector, referenceProvider)); } protected createReferenceProvider(handle: number): monaco.languages.ReferenceProvider { @@ -165,9 +175,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerSignatureHelpProvider(handle: number, selector: SerializedDocumentFilter[], metadata: theia.SignatureHelpProviderMetadata): void { const languageSelector = fromLanguageSelector(selector); const signatureHelpProvider = this.createSignatureHelpProvider(handle, metadata); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerSignatureHelpProvider(languageSelector, signatureHelpProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerSignatureHelpProvider(languageSelector, signatureHelpProvider)); } $clearDiagnostics(id: string): void { @@ -186,9 +194,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerImplementationProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const implementationProvider = this.createImplementationProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerImplementationProvider(languageSelector, implementationProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerImplementationProvider(languageSelector, implementationProvider)); } protected createImplementationProvider(handle: number): monaco.languages.ImplementationProvider { @@ -220,9 +226,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerTypeDefinitionProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const typeDefinitionProvider = this.createTypeDefinitionProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerTypeDefinitionProvider(languageSelector, typeDefinitionProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerTypeDefinitionProvider(languageSelector, typeDefinitionProvider)); } protected createTypeDefinitionProvider(handle: number): monaco.languages.TypeDefinitionProvider { @@ -254,9 +258,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerHoverProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const hoverProvider = this.createHoverProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerHoverProvider(languageSelector, hoverProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerHoverProvider(languageSelector, hoverProvider)); } protected createHoverProvider(handle: number): monaco.languages.HoverProvider { @@ -269,9 +271,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerDocumentHighlightProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const documentHighlightProvider = this.createDocumentHighlightProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerDocumentHighlightProvider(languageSelector, documentHighlightProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerDocumentHighlightProvider(languageSelector, documentHighlightProvider)); } protected createDocumentHighlightProvider(handle: number): monaco.languages.DocumentHighlightProvider { @@ -301,9 +301,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerWorkspaceSymbolProvider(handle: number): void { const workspaceSymbolProvider = this.createWorkspaceSymbolProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(this.monacoLanguages.registerWorkspaceSymbolProvider(workspaceSymbolProvider)); - this.disposables.set(handle, disposable); + this.register(handle, this.monacoLanguages.registerWorkspaceSymbolProvider(workspaceSymbolProvider)); } protected createWorkspaceSymbolProvider(handle: number): WorkspaceSymbolProvider { @@ -316,9 +314,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerDocumentLinkProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const linkProvider = this.createLinkProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerLinkProvider(languageSelector, linkProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerLinkProvider(languageSelector, linkProvider)); } protected createLinkProvider(handle: number): monaco.languages.LinkProvider { @@ -355,13 +351,11 @@ export class LanguagesMainImpl implements LanguagesMain { if (typeof eventHandle === 'number') { const emitter = new Emitter(); - this.disposables.set(eventHandle, emitter); + this.register(eventHandle, emitter); lensProvider.onDidChange = emitter.event; } - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerCodeLensProvider(languageSelector, lensProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerCodeLensProvider(languageSelector, lensProvider)); } protected createCodeLensProvider(handle: number): monaco.languages.CodeLensProvider { @@ -385,7 +379,7 @@ export class LanguagesMainImpl implements LanguagesMain { // tslint:disable-next-line:no-any $emitCodeLensEvent(eventHandle: number, event?: any): void { - const obj = this.disposables.get(eventHandle); + const obj = this.services.get(eventHandle); if (obj instanceof Emitter) { obj.fire(event); } @@ -394,10 +388,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerOutlineSupport(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const symbolProvider = this.createDocumentSymbolProvider(handle); - - const disposable = new DisposableCollection(); - disposable.push(monaco.modes.DocumentSymbolProviderRegistry.register(languageSelector, symbolProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.modes.DocumentSymbolProviderRegistry.register(languageSelector, symbolProvider)); } protected createDocumentSymbolProvider(handle: number): monaco.languages.DocumentSymbolProvider { @@ -485,7 +476,7 @@ export class LanguagesMainImpl implements LanguagesMain { const documentFormattingEditSupport = this.createDocumentFormattingSupport(handle); const disposable = new DisposableCollection(); disposable.push(monaco.languages.registerDocumentFormattingEditProvider(languageSelector, documentFormattingEditSupport)); - this.disposables.set(handle, disposable); + this.services.set(handle, disposable); } createDocumentFormattingSupport(handle: number): monaco.languages.DocumentFormattingEditProvider { @@ -498,9 +489,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerRangeFormattingProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const rangeFormattingEditProvider = this.createRangeFormattingProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerDocumentRangeFormattingEditProvider(languageSelector, rangeFormattingEditProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerDocumentRangeFormattingEditProvider(languageSelector, rangeFormattingEditProvider)); } createRangeFormattingProvider(handle: number): monaco.languages.DocumentRangeFormattingEditProvider { @@ -513,9 +502,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerOnTypeFormattingProvider(handle: number, selector: SerializedDocumentFilter[], autoFormatTriggerCharacters: string[]): void { const languageSelector = fromLanguageSelector(selector); const onTypeFormattingProvider = this.createOnTypeFormattingProvider(handle, autoFormatTriggerCharacters); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerOnTypeFormattingEditProvider(languageSelector, onTypeFormattingProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerOnTypeFormattingEditProvider(languageSelector, onTypeFormattingProvider)); } protected createOnTypeFormattingProvider( @@ -532,9 +519,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerFoldingRangeProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const provider = this.createFoldingRangeProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerFoldingRangeProvider(languageSelector, provider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerFoldingRangeProvider(languageSelector, provider)); } createFoldingRangeProvider(handle: number): monaco.languages.FoldingRangeProvider { @@ -547,9 +532,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerDocumentColorProvider(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const colorProvider = this.createColorProvider(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerColorProvider(languageSelector, colorProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerColorProvider(languageSelector, colorProvider)); } createColorProvider(handle: number): monaco.languages.DocumentColorProvider { @@ -587,9 +570,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerQuickFixProvider(handle: number, selector: SerializedDocumentFilter[], codeActionKinds?: string[]): void { const languageSelector = fromLanguageSelector(selector); const quickFixProvider = this.createQuickFixProvider(handle, codeActionKinds); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerCodeActionProvider(languageSelector, quickFixProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerCodeActionProvider(languageSelector, quickFixProvider)); } protected createQuickFixProvider(handle: number, providedCodeActionKinds?: string[]): monaco.languages.CodeActionProvider { @@ -612,9 +593,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerRenameProvider(handle: number, selector: SerializedDocumentFilter[], supportsResolveLocation: boolean): void { const languageSelector = fromLanguageSelector(selector); const renameProvider = this.createRenameProvider(handle, supportsResolveLocation); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerRenameProvider(languageSelector, renameProvider)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerRenameProvider(languageSelector, renameProvider)); } protected createRenameProvider(handle: number, supportsResolveLocation: boolean): monaco.languages.RenameProvider { diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 72e606cf3ca57..5b4369133dfde 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -40,6 +40,14 @@ import { DebugMainImpl } from './debug/debug-main'; import { FileSystemMainImpl } from './file-system-main'; import { ScmMainImpl } from './scm-main'; import { DecorationsMainImpl } from './decorations/decorations-main'; +import { DocumentsMainImpl } from './documents-main'; +import { TextEditorsMainImpl } from './text-editors-main'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { EditorModelService } from './text-editor-model-service'; +import { OpenerService } from '@theia/core/lib/browser/opener-service'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; +import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service'; +import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -60,9 +68,18 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const preferenceRegistryMain = new PreferenceRegistryMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN, preferenceRegistryMain); - /* tslint:disable */ - new EditorsAndDocumentsMain(rpc, container); - /* tslint:enable */ + const editorsAndDocuments = new EditorsAndDocumentsMain(rpc, container); + const modelService = container.get(EditorModelService); + const editorManager = container.get(EditorManager); + const openerService = container.get(OpenerService); + const shell = container.get(ApplicationShell); + const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell); + rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); + + const bulkEditService = container.get(MonacoBulkEditService); + const monacoEditorService = container.get(MonacoEditorService); + const editorsMain = new TextEditorsMainImpl(editorsAndDocuments, rpc, bulkEditService, monacoEditorService); + rpc.set(PLUGIN_RPC_CONTEXT.TEXT_EDITORS_MAIN, editorsMain); const statusBarMessageRegistryMain = new StatusBarMessageRegistryMainImpl(container); rpc.set(PLUGIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_MAIN, statusBarMessageRegistryMain); diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 191438c66eb91..fcd92dda94edd 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -18,7 +18,7 @@ import CodeUri from 'vscode-uri'; import { injectable, inject } from 'inversify'; -import { MenuPath, ILogger, CommandRegistry, Command, Mutable, MenuAction, SelectionService, CommandHandler } from '@theia/core'; +import { MenuPath, ILogger, CommandRegistry, Command, Mutable, MenuAction, SelectionService, CommandHandler, Disposable, DisposableCollection } from '@theia/core'; import { EDITOR_CONTEXT_MENU, EditorWidget } from '@theia/editor/lib/browser'; import { MenuModelRegistry } from '@theia/core/lib/common'; import { TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; @@ -80,29 +80,30 @@ export class MenusContributionPointHandler { @inject(ViewContextKeyService) protected readonly viewContextKeys: ViewContextKeyService; - handle(contributions: PluginContribution): void { + handle(contributions: PluginContribution): Disposable { const allMenus = contributions.menus; if (!allMenus) { - return; + return Disposable.NULL; } + const toDispose = new DisposableCollection(); for (const location in allMenus) { if (location === 'commandPalette') { for (const menu of allMenus[location]) { if (menu.when) { - this.quickCommandService.pushCommandContext(menu.command, menu.when); + toDispose.push(this.quickCommandService.pushCommandContext(menu.command, menu.when)); } } } else if (location === 'editor/title') { for (const action of allMenus[location]) { - this.registerTitleAction(location, action, { + toDispose.push(this.registerTitleAction(location, action, { execute: widget => CodeEditorWidget.is(widget) && this.commands.executeCommand(action.command, CodeEditorWidget.getResourceUri(widget)), isEnabled: widget => CodeEditorWidget.is(widget) && this.commands.isEnabled(action.command, CodeEditorWidget.getResourceUri(widget)), isVisible: widget => CodeEditorWidget.is(widget) && this.commands.isVisible(action.command, CodeEditorWidget.getResourceUri(widget)) - }); + })); } } else if (location === 'view/title') { for (const action of allMenus[location]) { - this.registerTitleAction(location, { ...action, when: undefined }, { + toDispose.push(this.registerTitleAction(location, { ...action, when: undefined }, { execute: widget => widget instanceof PluginViewWidget && this.commands.executeCommand(action.command), isEnabled: widget => widget instanceof PluginViewWidget && this.viewContextKeys.with({ view: widget.options.viewId }, () => @@ -110,38 +111,38 @@ export class MenusContributionPointHandler { isVisible: widget => widget instanceof PluginViewWidget && this.viewContextKeys.with({ view: widget.options.viewId }, () => this.commands.isVisible(action.command) && this.viewContextKeys.match(action.when)) - }); + })); } } else if (location === 'view/item/context') { for (const menu of allMenus[location]) { const inline = menu.group && /^inline/.test(menu.group) || false; const menuPath = inline ? VIEW_ITEM_INLINE_MNUE : VIEW_ITEM_CONTEXT_MENU; - this.registerTreeMenuAction(menuPath, menu); + toDispose.push(this.registerTreeMenuAction(menuPath, menu)); } } else if (location === 'scm/title') { for (const action of allMenus[location]) { - this.registerScmTitleAction(location, action); + toDispose.push(this.registerScmTitleAction(location, action)); } } else if (location === 'scm/resourceGroup/context') { for (const menu of allMenus[location]) { const inline = menu.group && /^inline/.test(menu.group) || false; const menuPath = inline ? ScmWidget.RESOURCE_GROUP_INLINE_MENU : ScmWidget.RESOURCE_GROUP_CONTEXT_MENU; - this.registerScmMenuAction(menuPath, menu); + toDispose.push(this.registerScmMenuAction(menuPath, menu)); } } else if (location === 'scm/resourceState/context') { for (const menu of allMenus[location]) { const inline = menu.group && /^inline/.test(menu.group) || false; const menuPath = inline ? ScmWidget.RESOURCE_INLINE_MENU : ScmWidget.RESOURCE_CONTEXT_MENU; - this.registerScmMenuAction(menuPath, menu); + toDispose.push(this.registerScmMenuAction(menuPath, menu)); } } else if (location === 'debug/callstack/context') { for (const menu of allMenus[location]) { for (const menuPath of [DebugStackFramesWidget.CONTEXT_MENU, DebugThreadsWidget.CONTEXT_MENU]) { - this.registerMenuAction(menuPath, menu, command => ({ + toDispose.push(this.registerMenuAction(menuPath, menu, command => ({ execute: (...args) => this.commands.executeCommand(command, args[0]), isEnabled: (...args) => this.commands.isEnabled(command, args[0]), isVisible: (...args) => this.commands.isVisible(command, args[0]) - })); + }))); } } } else if (allMenus.hasOwnProperty(location)) { @@ -153,11 +154,12 @@ export class MenusContributionPointHandler { const menus = allMenus[location]; menus.forEach(menu => { for (const menuPath of menuPaths) { - this.registerGlobalMenuAction(menuPath, menu); + toDispose.push(this.registerGlobalMenuAction(menuPath, menu)); } }); } } + return toDispose; } protected static parseMenuPaths(value: string): MenuPath[] { @@ -168,8 +170,8 @@ export class MenusContributionPointHandler { return []; } - protected registerTreeMenuAction(menuPath: MenuPath, menu: Menu): void { - this.registerMenuAction(menuPath, menu, command => ({ + protected registerTreeMenuAction(menuPath: MenuPath, menu: Menu): Disposable { + return this.registerMenuAction(menuPath, menu, command => ({ execute: (...args) => this.commands.executeCommand(command, ...this.toTreeArgs(...args)), isEnabled: (...args) => this.commands.isEnabled(command, ...this.toTreeArgs(...args)), isVisible: (...args) => this.commands.isVisible(command, ...this.toTreeArgs(...args)) @@ -185,10 +187,11 @@ export class MenusContributionPointHandler { return treeArgs; } - protected registerTitleAction(location: string, action: Menu, handler: CommandHandler): void { + protected registerTitleAction(location: string, action: Menu, handler: CommandHandler): Disposable { + const toDispose = new DisposableCollection(); const id = this.createSyntheticCommandId(action.command, { prefix: `__plugin.${location.replace('/', '.')}.action.` }); const command: Command = { id }; - this.commands.registerCommand(command, handler); + toDispose.push(this.commands.registerCommand(command, handler)); const { when } = action; // handle group and priority @@ -200,27 +203,28 @@ export class MenusContributionPointHandler { // navigation@test => ['navigation', 0] const [group, sort] = (action.group || 'navigation').split('@'); const item: Mutable = { id, command: id, group: group.trim() || 'navigation', priority: ~~sort || undefined, when }; - this.tabBarToolbar.registerItem(item); + toDispose.push(this.tabBarToolbar.registerItem(item)); - this.onDidRegisterCommand(action.command, pluginCommand => { + toDispose.push(this.onDidRegisterCommand(action.command, pluginCommand => { command.category = pluginCommand.category; item.tooltip = pluginCommand.label; if (group === 'navigation') { command.iconClass = pluginCommand.iconClass; } - }); + })); + return toDispose; } - protected registerScmTitleAction(location: string, action: Menu): void { + protected registerScmTitleAction(location: string, action: Menu): Disposable { const selectedRepository = () => this.toScmArgs(this.scmService.selectedRepository); - this.registerTitleAction(location, action, { + return this.registerTitleAction(location, action, { execute: widget => widget instanceof ScmWidget && this.commands.executeCommand(action.command, selectedRepository()), isEnabled: widget => widget instanceof ScmWidget && this.commands.isEnabled(action.command, selectedRepository()), isVisible: widget => widget instanceof ScmWidget && this.commands.isVisible(action.command, selectedRepository()) }); } - protected registerScmMenuAction(menuPath: MenuPath, menu: Menu): void { - this.registerMenuAction(menuPath, menu, command => ({ + protected registerScmMenuAction(menuPath: MenuPath, menu: Menu): Disposable { + return this.registerMenuAction(menuPath, menu, command => ({ execute: (...args) => this.commands.executeCommand(command, ...this.toScmArgs(...args)), isEnabled: (...args) => this.commands.isEnabled(command, ...this.toScmArgs(...args)), isVisible: (...args) => this.commands.isVisible(command, ...this.toScmArgs(...args)) @@ -257,7 +261,7 @@ export class MenusContributionPointHandler { } } - protected registerGlobalMenuAction(menuPath: MenuPath, menu: Menu): void { + protected registerGlobalMenuAction(menuPath: MenuPath, menu: Menu): Disposable { const selectedResource = () => { const selection = this.selectionService.selection; if (TreeWidgetSelection.is(selection) && selection.source instanceof TreeViewWidget && selection[0]) { @@ -266,32 +270,33 @@ export class MenusContributionPointHandler { const uri = this.resourceContextKey.get(); return uri ? uri['codeUri'] : undefined; }; - this.registerMenuAction(menuPath, menu, command => ({ + return this.registerMenuAction(menuPath, menu, command => ({ execute: () => this.commands.executeCommand(command, selectedResource()), isEnabled: () => this.commands.isEnabled(command, selectedResource()), isVisible: () => this.commands.isVisible(command, selectedResource()) })); } - protected registerMenuAction(menuPath: MenuPath, menu: Menu, handler: (command: string) => CommandHandler): void { + protected registerMenuAction(menuPath: MenuPath, menu: Menu, handler: (command: string) => CommandHandler): Disposable { + const toDispose = new DisposableCollection(); const commandId = this.createSyntheticCommandId(menu.command, { prefix: '__plugin.menu.action.' }); const command: Command = { id: commandId }; - this.commands.registerCommand(command, handler(menu.command)); - this.quickCommandService.pushCommandContext(commandId, 'false'); + toDispose.push(this.commands.registerCommand(command, handler(menu.command))); + toDispose.push(this.quickCommandService.pushCommandContext(commandId, 'false')); let altId: string | undefined; if (menu.alt) { altId = this.createSyntheticCommandId(menu.alt, { prefix: '__plugin.menu.action.' }); const alt: Command = { id: altId }; - this.commands.registerCommand(alt, handler(menu.alt)); - this.quickCommandService.pushCommandContext(altId, 'false'); - this.onDidRegisterCommand(menu.alt, pluginCommand => { + toDispose.push(this.commands.registerCommand(alt, handler(menu.alt))); + toDispose.push(this.quickCommandService.pushCommandContext(altId, 'false')); + toDispose.push(this.onDidRegisterCommand(menu.alt, pluginCommand => { alt.category = pluginCommand.category; alt.label = pluginCommand.label; if (inline) { alt.iconClass = pluginCommand.iconClass; } - }); + })); } const { when } = menu; @@ -299,15 +304,16 @@ export class MenusContributionPointHandler { const action: MenuAction = { commandId, alt: altId, order, when }; const inline = /^inline/.test(group); menuPath = inline ? menuPath : [...menuPath, group]; - this.menuRegistry.registerMenuAction(menuPath, action); + toDispose.push(this.menuRegistry.registerMenuAction(menuPath, action)); - this.onDidRegisterCommand(menu.command, pluginCommand => { + toDispose.push(this.onDidRegisterCommand(menu.command, pluginCommand => { command.category = pluginCommand.category; command.label = pluginCommand.label; if (inline) { command.iconClass = pluginCommand.iconClass; } - }); + })); + return toDispose; } protected createSyntheticCommandId(command: string, { prefix }: { prefix: string }): string { @@ -320,17 +326,20 @@ export class MenusContributionPointHandler { return id; } - protected onDidRegisterCommand(id: string, cb: (command: Command) => void): void { + protected onDidRegisterCommand(id: string, cb: (command: Command) => void): Disposable { const command = this.commands.getCommand(id); if (command) { cb(command); - } else { - // Registering a menu action requires the related command to be already registered. - // But Theia plugin registers the commands dynamically via the Commands API. - // Let's wait for ~2 sec. It should be enough to finish registering all the contributed commands. - // FIXME: remove this workaround (timer) once the https://github.com/eclipse-theia/theia/issues/3344 is fixed - setTimeout(() => this.onDidRegisterCommand(id, cb), 2000); + return Disposable.NULL; } + const toDispose = new DisposableCollection(); + // Registering a menu action requires the related command to be already registered. + // But Theia plugin registers the commands dynamically via the Commands API. + // Let's wait for ~2 sec. It should be enough to finish registering all the contributed commands. + // FIXME: remove this workaround (timer) once the https://github.com/theia-ide/theia/issues/3344 is fixed + const handle = setTimeout(() => toDispose.push(this.onDidRegisterCommand(id, cb)), 2000); + toDispose.push(Disposable.create(() => clearTimeout(handle))); + return toDispose; } } diff --git a/packages/plugin-ext/src/main/browser/notification-main.ts b/packages/plugin-ext/src/main/browser/notification-main.ts index a14d87b7b2c96..1b8e256a038a6 100644 --- a/packages/plugin-ext/src/main/browser/notification-main.ts +++ b/packages/plugin-ext/src/main/browser/notification-main.ts @@ -18,23 +18,38 @@ import { NotificationMain } from '../../common/plugin-api-rpc'; import { ProgressService, Progress, ProgressMessage } from '@theia/core/lib/common'; import { interfaces } from 'inversify'; import { RPCProtocol } from '../../common/rpc-protocol'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -export class NotificationMainImpl implements NotificationMain { +export class NotificationMainImpl implements NotificationMain, Disposable { private readonly progressService: ProgressService; private readonly progressMap = new Map(); private readonly progress2Work = new Map(); + protected readonly toDispose = new DisposableCollection( + Disposable.create(() => { /* mark as not disposed */ }) + ); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.progressService = container.get(ProgressService); } + dispose(): void { + this.toDispose.dispose(); + } + async $startProgress(options: string | NotificationMain.StartProgressOptions): Promise { const progressMessage = this.mapOptions(options); const progress = await this.progressService.showProgress(progressMessage); - this.progressMap.set(progress.id, progress); - this.progress2Work.set(progress.id, 0); - return progress.id; + const id = progress.id; + this.progressMap.set(id, progress); + this.progress2Work.set(id, 0); + if (this.toDispose.disposed) { + this.$stopProgress(id); + } else { + this.toDispose.push(Disposable.create(() => this.$stopProgress(id))); + } + return id; } protected mapOptions(options: string | NotificationMain.StartProgressOptions): ProgressMessage { const text = typeof options === 'string' ? options : options.title; 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 2f3d4c044db67..5efd772839e09 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -29,17 +29,12 @@ 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 { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; -import { EditorManager } from '@theia/editor/lib/browser'; @injectable() export class PluginContributionHandler { private injections = new Map(); - @inject(EditorManager) - private readonly editorManager: EditorManager; - @inject(TextmateRegistry) private readonly grammarsRegistry: TextmateRegistry; @@ -81,22 +76,24 @@ export class PluginContributionHandler { protected readonly onDidRegisterCommandHandlerEmitter = new Emitter(); readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event; - handleContributions(contributions: PluginContribution): void { + handleContributions(contributions: PluginContribution): Disposable { + const toDispose = new DisposableCollection; if (contributions.configuration) { if (Array.isArray(contributions.configuration)) { for (const config of contributions.configuration) { - this.updateConfigurationSchema(config); + toDispose.push(this.updateConfigurationSchema(config)); } } else { - this.updateConfigurationSchema(contributions.configuration); + toDispose.push(this.updateConfigurationSchema(contributions.configuration)); } } if (contributions.configurationDefaults) { - this.updateDefaultOverridesSchema(contributions.configurationDefaults); + toDispose.push(this.updateDefaultOverridesSchema(contributions.configurationDefaults)); } if (contributions.languages) { for (const lang of contributions.languages) { + // it is not possible to unregister a language monaco.languages.register({ id: lang.id, aliases: lang.aliases, @@ -107,7 +104,7 @@ export class PluginContributionHandler { mimetypes: lang.mimetypes }); if (lang.configuration) { - monaco.languages.setLanguageConfiguration(lang.id, { + toDispose.push(monaco.languages.setLanguageConfiguration(lang.id, { wordPattern: this.createRegex(lang.configuration.wordPattern), autoClosingPairs: lang.configuration.autoClosingPairs, brackets: lang.configuration.brackets, @@ -115,25 +112,29 @@ export class PluginContributionHandler { folding: this.convertFolding(lang.configuration.folding), surroundingPairs: lang.configuration.surroundingPairs, indentationRules: this.convertIndentationRules(lang.configuration.indentationRules) - }); + })); } } } if (contributions.grammars && contributions.grammars.length) { + toDispose.push(Disposable.create(() => this.monacoTextmateService.detectLanguages())); for (const grammar of contributions.grammars) { if (grammar.injectTo) { for (const injectScope of grammar.injectTo) { - let injections = this.injections.get(injectScope); - if (!injections) { - injections = []; - this.injections.set(injectScope, injections); - } + const injections = this.injections.get(injectScope) || []; injections.push(grammar.scope); + this.injections.set(injectScope, injections); + toDispose.push(Disposable.create(() => { + const index = injections.indexOf(grammar.scope); + if (index !== -1) { + injections.splice(index, 1); + } + })); } } - this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { + toDispose.push(this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { async getGrammarDefinition(): Promise { return { format: grammar.format, @@ -143,32 +144,28 @@ export class PluginContributionHandler { }, getInjections: (scopeName: string) => this.injections.get(scopeName)! - }); + })); if (grammar.language) { - this.grammarsRegistry.mapLanguageIdToTextmateGrammar(grammar.language, grammar.scope); - this.grammarsRegistry.registerGrammarConfiguration(grammar.language, { + toDispose.push(this.grammarsRegistry.mapLanguageIdToTextmateGrammar(grammar.language, grammar.scope)); + toDispose.push(this.grammarsRegistry.registerGrammarConfiguration(grammar.language, { embeddedLanguages: this.convertEmbeddedLanguages(grammar.embeddedLanguages), tokenTypes: this.convertTokenTypes(grammar.tokenTypes) - }); - monaco.languages.onLanguage(grammar.language, () => this.monacoTextmateService.activateLanguage(grammar.language!)); - } - } - for (const editor of MonacoEditor.getAll(this.editorManager)) { - if (editor.languageAutoDetected) { - editor.detectLanguage(); + })); + toDispose.push(monaco.languages.onLanguage(grammar.language, () => this.monacoTextmateService.activateLanguage(grammar.language!))); } } + this.monacoTextmateService.detectLanguages(); } - this.registerCommands(contributions); - this.menusContributionHandler.handle(contributions); - this.keybindingsContributionHandler.handle(contributions); + toDispose.push(this.registerCommands(contributions)); + toDispose.push(this.menusContributionHandler.handle(contributions)); + toDispose.push(this.keybindingsContributionHandler.handle(contributions)); if (contributions.viewsContainers) { for (const location in contributions.viewsContainers) { if (contributions.viewsContainers!.hasOwnProperty(location)) { for (const viewContainer of contributions.viewsContainers[location]) { - this.viewRegistry.registerViewContainer(location, viewContainer); + toDispose.push(this.viewRegistry.registerViewContainer(location, viewContainer)); } } } @@ -177,46 +174,49 @@ export class PluginContributionHandler { // tslint:disable-next-line:forin for (const location in contributions.views) { for (const view of contributions.views[location]) { - this.viewRegistry.registerView(location, view); + toDispose.push(this.viewRegistry.registerView(location, view)); } } } if (contributions.snippets) { for (const snippet of contributions.snippets) { - this.snippetSuggestProvider.fromURI(snippet.uri, { + toDispose.push(this.snippetSuggestProvider.fromURI(snippet.uri, { language: snippet.language, source: snippet.source - }); + })); } } if (contributions.taskDefinitions) { - contributions.taskDefinitions.forEach(def => this.taskDefinitionRegistry.register(def)); + contributions.taskDefinitions.forEach(def => toDispose.push(this.taskDefinitionRegistry.register(def))); } if (contributions.problemPatterns) { - contributions.problemPatterns.forEach(pattern => this.problemPatternRegistry.register(pattern)); + contributions.problemPatterns.forEach(pattern => toDispose.push(this.problemPatternRegistry.register(pattern))); } if (contributions.problemMatchers) { - contributions.problemMatchers.forEach(matcher => this.problemMatcherRegistry.register(matcher)); + contributions.problemMatchers.forEach(matcher => toDispose.push(this.problemMatcherRegistry.register(matcher))); } + return toDispose; } - protected registerCommands(contribution: PluginContribution): void { + protected registerCommands(contribution: PluginContribution): Disposable { if (!contribution.commands) { - return; + return Disposable.NULL; } + const toDispose = new DisposableCollection(); for (const { iconUrl, command, category, title } of contribution.commands) { const iconClass = iconUrl ? this.style.toIconClass(iconUrl) : undefined; - this.registerCommand({ + toDispose.push(this.registerCommand({ id: command, category, label: title, iconClass - }); + })); } + return toDispose; } registerCommand(command: Command): Disposable { @@ -253,12 +253,12 @@ export class PluginContributionHandler { return !!this.commandHandlers.get(id); } - private updateConfigurationSchema(schema: PreferenceSchema): void { + private updateConfigurationSchema(schema: PreferenceSchema): Disposable { this.validateConfigurationSchema(schema); - this.preferenceSchemaProvider.setSchema(schema); + return this.preferenceSchemaProvider.setSchema(schema); } - protected updateDefaultOverridesSchema(configurationDefaults: PreferenceSchemaProperties): void { + protected updateDefaultOverridesSchema(configurationDefaults: PreferenceSchemaProperties): Disposable { const defaultOverrides: PreferenceSchema = { id: 'defaultOverrides', title: 'Default Configuration Overrides', @@ -276,8 +276,9 @@ export class PluginContributionHandler { } } if (Object.keys(defaultOverrides.properties).length) { - this.preferenceSchemaProvider.setSchema(defaultOverrides); + return this.preferenceSchemaProvider.setSchema(defaultOverrides); } + return Disposable.NULL; } private createRegex(value: string | undefined): RegExp | undefined { diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-deploy-command.ts b/packages/plugin-ext/src/main/browser/plugin-ext-deploy-command.ts index e02fbf4f8cb71..60a764cb65484 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-deploy-command.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-deploy-command.ts @@ -99,8 +99,7 @@ export class DeployQuickOpenItem extends QuickOpenItem { if (mode !== QuickOpenMode.OPEN) { return false; } - const promise = this.pluginServer.deploy(this.name); - promise.then(() => this.hostedPluginSupport.initPlugins()); + this.pluginServer.deploy(this.name); return true; } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 173a38b6501be..d8756fc118656 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import '../../../src/main/style/status-bar.css'; +import '../../../src/main/browser/style/index.css'; import { ContainerModule } from 'inversify'; import { @@ -27,15 +28,13 @@ import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher'; import { OpenUriCommandHandler } from './commands'; import { PluginApiFrontendContribution } from './plugin-frontend-contribution'; -import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRpcPath } from '../../common/plugin-protocol'; +import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRpcPath, HostedPluginServerProxy } from '../../common/plugin-protocol'; import { ModalNotification } from './dialogs/modal-notification'; import { PluginWidget } from './plugin-ext-widget'; import { PluginFrontendViewContribution } from './plugin-frontend-view-contribution'; - -import '../../../src/main/browser/style/index.css'; import { PluginExtDeployCommandService } from './plugin-ext-deploy-command'; -import { TextEditorService, TextEditorServiceImpl } from './text-editor-service'; -import { EditorModelService, EditorModelServiceImpl } from './text-editor-model-service'; +import { TextEditorService } from './text-editor-service'; +import { EditorModelService } from './text-editor-model-service'; import { UntitledResourceResolver } from './editor/untitled-resource'; import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginContributionHandler } from './plugin-contribution-handler'; @@ -60,6 +59,7 @@ import { ViewColumnService } from './view-column-service'; import { ViewContextKeyService } from './view/view-context-key-service'; import { PluginViewWidget, PluginViewWidgetIdentifier } from './view/plugin-view-widget'; import { TreeViewWidgetIdentifier, VIEW_ITEM_CONTEXT_MENU, PluginTree, TreeViewWidget, PluginTreeModel } from './view/tree-view-widget'; +import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -74,22 +74,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(PluginApiFrontendContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(PluginApiFrontendContribution); - bind(TextEditorService).to(TextEditorServiceImpl).inSingletonScope(); - bind(EditorModelService).to(EditorModelServiceImpl).inSingletonScope(); + bind(TextEditorService).toSelf().inSingletonScope(); + bind(EditorModelService).toSelf().inSingletonScope(); bind(UntitledResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(UntitledResourceResolver); bind(FrontendApplicationContribution).toDynamicValue(ctx => ({ onStart(app: FrontendApplication): MaybePromise { - ctx.container.get(HostedPluginSupport).checkAndLoadPlugin(ctx.container); + ctx.container.get(HostedPluginSupport).onStart(ctx.container); } })); - bind(HostedPluginServer).toDynamicValue(ctx => { + bind(HostedPluginServerProxy).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); const hostedWatcher = ctx.container.get(HostedPluginWatcher); return connection.createProxy(hostedServicePath, hostedWatcher.getHostedPluginClient()); }).inSingletonScope(); + bind(HostedPluginServer).toService(HostedPluginServerProxy); bind(PluginPathsService).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); @@ -155,6 +156,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(PluginContributionHandler).toSelf().inSingletonScope(); + bind(InPluginFileSystemWatcherManager).toSelf().inSingletonScope(); bind(TextContentResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(TextContentResourceResolver); bind(FSResourceResolver).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/preference-registry-main.ts b/packages/plugin-ext/src/main/browser/preference-registry-main.ts index d3dbb622c6a8d..abe9afad27af2 100644 --- a/packages/plugin-ext/src/main/browser/preference-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/preference-registry-main.ts @@ -32,6 +32,7 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { ConfigurationTarget } from '../../plugin/types-impl'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { FileStat } from '@theia/filesystem/lib/common/filesystem'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData { const folders = rootFolders.map(root => root.uri.toString()); @@ -51,30 +52,35 @@ export function getPreferences(preferenceProviderProvider: PreferenceProviderPro }, {} as PreferenceData); } -export class PreferenceRegistryMainImpl implements PreferenceRegistryMain { - private proxy: PreferenceRegistryExt; - private preferenceService: PreferenceService; - private readonly preferenceProviderProvider: PreferenceProviderProvider; +export class PreferenceRegistryMainImpl implements PreferenceRegistryMain, Disposable { + private readonly proxy: PreferenceRegistryExt; + private readonly preferenceService: PreferenceService; + + protected readonly toDispose = new DisposableCollection(); constructor(prc: RPCProtocol, container: interfaces.Container) { this.proxy = prc.getProxy(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT); this.preferenceService = container.get(PreferenceService); - this.preferenceProviderProvider = container.get(PreferenceProviderProvider); + const preferenceProviderProvider = container.get(PreferenceProviderProvider); const preferenceServiceImpl = container.get(PreferenceServiceImpl); const workspaceService = container.get(WorkspaceService); - preferenceServiceImpl.onPreferencesChanged(changes => { + this.toDispose.push(preferenceServiceImpl.onPreferencesChanged(changes => { // it HAS to be synchronous to propagate changes before update/remove response const roots = workspaceService.tryGetRoots(); - const data = getPreferences(this.preferenceProviderProvider, roots); + const data = getPreferences(preferenceProviderProvider, roots); const eventData: PreferenceChangeExt[] = []; for (const preferenceName of Object.keys(changes)) { const { newValue } = changes[preferenceName]; eventData.push({ preferenceName, newValue }); } this.proxy.$acceptConfigurationChanged(data, eventData); - }); + })); + } + + dispose(): void { + this.toDispose.dispose(); } // tslint:disable-next-line:no-any diff --git a/packages/plugin-ext/src/main/browser/quick-open-main.ts b/packages/plugin-ext/src/main/browser/quick-open-main.ts index 6ed3d2268fe04..ece53ccff7801 100644 --- a/packages/plugin-ext/src/main/browser/quick-open-main.ts +++ b/packages/plugin-ext/src/main/browser/quick-open-main.ts @@ -40,10 +40,10 @@ import URI from 'vscode-uri'; import { ThemeIcon, QuickInputButton } from '../../plugin/types-impl'; import { QuickPickService, QuickPickItem, QuickPickValue } from '@theia/core/lib/common/quick-pick-service'; import { QuickTitleBar } from '@theia/core/lib/browser/quick-open/quick-title-bar'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { QuickTitleButtonSide } from '@theia/core/lib/common/quick-open-model'; -export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { +export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel, Disposable { private quickInput: QuickInputService; private quickPick: QuickPickService; @@ -58,6 +58,8 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { private activeElement: HTMLElement | undefined; + protected readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT); this.delegate = container.get(MonacoQuickOpenService); @@ -65,6 +67,11 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { this.quickTitleBar = container.get(QuickTitleBar); this.quickPick = container.get(QuickPickService); this.sharedStyle = container.get(PluginSharedStyle); + this.toDispose.push(Disposable.create(() => this.$hide())); + } + + dispose(): void { + this.toDispose.dispose(); } private cleanUp(): void { @@ -217,7 +224,11 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { disposableListeners.push(this.quickTitleBar.onDidTriggerButton(button => { this.proxy.$acceptOnDidTriggerButton(inputBox.quickInputIndex, button); })); + this.toDispose.push(disposableListeners); quickInput.then(selection => { + if (disposableListeners.disposed) { + return; + } if (selection) { this.proxy.$acceptDidChangeSelection(inputBox.quickInputIndex, selection as string); } @@ -303,7 +314,11 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { disposableListeners.push(this.quickTitleBar.onDidTriggerButton(button => { this.proxy.$acceptOnDidTriggerButton(options.quickInputIndex, button); })); + this.toDispose.push(disposableListeners); quickPick.then(selection => { + if (disposableListeners.disposed) { + return; + } if (selection) { this.proxy.$acceptDidChangeSelection(options.quickInputIndex, selection as string); } diff --git a/packages/plugin-ext/src/main/browser/scm-main.ts b/packages/plugin-ext/src/main/browser/scm-main.ts index cbe4ef8f9dff9..2a649a857aecd 100644 --- a/packages/plugin-ext/src/main/browser/scm-main.ts +++ b/packages/plugin-ext/src/main/browser/scm-main.ts @@ -27,24 +27,31 @@ import { ScmRepository } from '@theia/scm/lib/browser/scm-repository'; import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { RPCProtocol } from '../../common/rpc-protocol'; import { interfaces } from 'inversify'; -import { CancellationToken, DisposableCollection, Emitter, Event } from '@theia/core'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import URI from '@theia/core/lib/common/uri'; import { LabelProvider } from '@theia/core/lib/browser'; import { ScmNavigatorDecorator } from '@theia/scm/lib/browser/decorations/scm-navigator-decorator'; -export class ScmMainImpl implements ScmMain { +export class ScmMainImpl implements ScmMain, Disposable { private readonly proxy: ScmExt; private readonly scmService: ScmService; - private readonly scmRepositoryMap: Map; + private readonly scmRepositoryMap = new Map(); private readonly labelProvider: LabelProvider; private lastSelectedSourceControlHandle: number | undefined; + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.SCM_EXT); this.scmService = container.get(ScmService); - this.scmRepositoryMap = new Map(); this.labelProvider = container.get(LabelProvider); - this.scmService.onDidChangeSelectedRepository(repository => this.updateSelectedRepository(repository)); + this.toDispose.push(this.scmService.onDidChangeSelectedRepository(repository => this.updateSelectedRepository(repository))); + } + + dispose(): void { + this.toDispose.dispose(); } protected updateSelectedRepository(repository: ScmRepository | undefined): void { @@ -66,7 +73,7 @@ export class ScmMainImpl implements ScmMain { } async $registerSourceControl(sourceControlHandle: number, id: string, label: string, rootUri: string): Promise { - const provider: ScmProvider = new PluginScmProvider(this.proxy, sourceControlHandle, id, label, rootUri, this.labelProvider); + const provider = new PluginScmProvider(this.proxy, sourceControlHandle, id, label, rootUri, this.labelProvider); const repository = this.scmService.registerScmProvider(provider); repository.input.onDidChange(() => this.proxy.$updateInputBox(sourceControlHandle, repository.input.value) @@ -75,6 +82,7 @@ export class ScmMainImpl implements ScmMain { if (this.scmService.repositories.length === 1) { this.updateSelectedRepository(repository); } + this.toDispose.push(Disposable.create(() => this.$unregisterSourceControl(sourceControlHandle))); } async $updateSourceControl(sourceControlHandle: number, features: SourceControlProviderFeatures): Promise { diff --git a/packages/plugin-ext/src/main/browser/status-bar-message-registry-main.ts b/packages/plugin-ext/src/main/browser/status-bar-message-registry-main.ts index 1838d7ee23e8e..07578a7e0e435 100644 --- a/packages/plugin-ext/src/main/browser/status-bar-message-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/status-bar-message-registry-main.ts @@ -14,19 +14,26 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { interfaces } from 'inversify'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import * as types from '../../plugin/types-impl'; import { StatusBarMessageRegistryMain } from '../../common/plugin-api-rpc'; import { StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar'; -export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistryMain { - private delegate: StatusBar; - - private entries: Map = new Map(); +export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistryMain, Disposable { + private readonly delegate: StatusBar; + private readonly entries = new Map(); + private readonly toDispose = new DisposableCollection( + Disposable.create(() => { /* mark as not disposed */ }) + ); constructor(container: interfaces.Container) { this.delegate = container.get(StatusBar); } + dispose(): void { + this.toDispose.dispose(); + } + async $setMessage(id: string, text: string | undefined, priority: number, @@ -45,6 +52,11 @@ export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistr this.entries.set(id, entry); await this.delegate.setElement(id, entry); + if (this.toDispose.disposed) { + this.$dispose(id); + } else { + this.toDispose.push(Disposable.create(() => this.$dispose(id))); + } } $update(id: string, message: string): void { diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts index f7e1e233749a3..b2f98ccb9e3ed 100644 --- a/packages/plugin-ext/src/main/browser/tasks-main.ts +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -22,82 +22,82 @@ import { TaskDto } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; -import { DisposableCollection } from '@theia/core'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common'; import { TaskProviderRegistry, TaskResolverRegistry, TaskProvider, TaskResolver } from '@theia/task/lib/browser/task-contribution'; import { interfaces } from 'inversify'; -import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { TaskInfo, TaskExitedEvent, TaskConfiguration } from '@theia/task/lib/common/task-protocol'; import { TaskWatcher } from '@theia/task/lib/common/task-watcher'; import { TaskService } from '@theia/task/lib/browser/task-service'; import { TaskDefinitionRegistry } from '@theia/task/lib/browser'; -export class TasksMainImpl implements TasksMain { - private workspaceRootUri: string | undefined = undefined; - +export class TasksMainImpl implements TasksMain, Disposable { private readonly proxy: TasksExt; - private readonly disposables = new Map(); private readonly taskProviderRegistry: TaskProviderRegistry; private readonly taskResolverRegistry: TaskResolverRegistry; private readonly taskWatcher: TaskWatcher; private readonly taskService: TaskService; - private readonly workspaceService: WorkspaceService; private readonly taskDefinitionRegistry: TaskDefinitionRegistry; - constructor(rpc: RPCProtocol, container: interfaces.Container, ) { + private readonly taskProviders = new Map(); + private readonly toDispose = new DisposableCollection(); + + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TASKS_EXT); this.taskProviderRegistry = container.get(TaskProviderRegistry); this.taskResolverRegistry = container.get(TaskResolverRegistry); - this.workspaceService = container.get(WorkspaceService); this.taskWatcher = container.get(TaskWatcher); this.taskService = container.get(TaskService); this.taskDefinitionRegistry = container.get(TaskDefinitionRegistry); - this.workspaceService.roots.then(roots => { - const root = roots[0]; - if (root) { - this.workspaceRootUri = root.uri; - } - }); - - this.taskWatcher.onTaskCreated((event: TaskInfo) => { - if (event.ctx === this.workspaceRootUri) { - this.proxy.$onDidStartTask({ - id: event.taskId, - task: event.config - }); - } - }); + this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => { + this.proxy.$onDidStartTask({ + id: event.taskId, + task: event.config + }); + })); - this.taskWatcher.onTaskExit((event: TaskExitedEvent) => { - if (event.ctx === this.workspaceRootUri) { - this.proxy.$onDidEndTask(event.taskId); - } - }); + this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => { + this.proxy.$onDidEndTask(event.taskId); + })); - this.taskWatcher.onDidStartTaskProcess((event: TaskInfo) => { - if (event.ctx === this.workspaceRootUri && event.processId !== undefined) { + this.toDispose.push(this.taskWatcher.onDidStartTaskProcess((event: TaskInfo) => { + if (event.processId !== undefined) { this.proxy.$onDidStartTaskProcess(event.processId, { id: event.taskId, task: event.config }); } - }); + })); - this.taskWatcher.onDidEndTaskProcess((event: TaskExitedEvent) => { - if (event.ctx === this.workspaceRootUri && event.code !== undefined) { + this.toDispose.push(this.taskWatcher.onDidEndTaskProcess((event: TaskExitedEvent) => { + if (event.code !== undefined) { this.proxy.$onDidEndTaskProcess(event.code, event.taskId); } - }); + })); + } + + dispose(): void { + this.toDispose.dispose(); } $registerTaskProvider(handle: number, type: string): void { const taskProvider = this.createTaskProvider(handle); const taskResolver = this.createTaskResolver(handle); - const disposable = new DisposableCollection(); - disposable.push(this.taskProviderRegistry.register(type, taskProvider, handle)); - disposable.push(this.taskResolverRegistry.register(type, taskResolver)); - this.disposables.set(handle, disposable); + const toDispose = new DisposableCollection( + this.taskProviderRegistry.register(type, taskProvider, handle), + this.taskResolverRegistry.register(type, taskResolver), + Disposable.create(() => this.taskProviders.delete(handle)) + ); + this.taskProviders.set(handle, toDispose); + this.toDispose.push(toDispose); + } + + $unregister(handle: number): void { + const disposable = this.taskProviders.get(handle); + if (disposable) { + disposable.dispose(); + } } async $fetchTasks(taskVersion: string | undefined, taskType: string | undefined): Promise { @@ -154,14 +154,6 @@ export class TasksMainImpl implements TasksMain { } } - $unregister(handle: number): void { - const disposable = this.disposables.get(handle); - if (disposable) { - disposable.dispose(); - this.disposables.delete(handle); - } - } - async $taskExecutions(): Promise<{ id: number; task: TaskConfiguration; diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 54f7bf3f47d0e..0297dbd292e00 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -21,28 +21,35 @@ import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; /** * Plugin api service allows working with terminal emulator. */ -export class TerminalServiceMainImpl implements TerminalServiceMain { +export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable { private readonly terminals: TerminalService; private readonly shell: ApplicationShell; private readonly extProxy: TerminalServiceExt; + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.terminals = container.get(TerminalService); this.shell = container.get(ApplicationShell); this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT); - this.terminals.onDidCreateTerminal(terminal => this.trackTerminal(terminal)); + this.toDispose.push(this.terminals.onDidCreateTerminal(terminal => this.trackTerminal(terminal))); for (const terminal of this.terminals.all) { this.trackTerminal(terminal); } - this.terminals.onDidChangeCurrentTerminal(() => this.updateCurrentTerminal()); + this.toDispose.push(this.terminals.onDidChangeCurrentTerminal(() => this.updateCurrentTerminal())); this.updateCurrentTerminal(); } + dispose(): void { + this.toDispose.dispose(); + } + protected updateCurrentTerminal(): void { const { currentTerminal } = this.terminals; this.extProxy.$currentTerminalChanged(currentTerminal && currentTerminal.id); @@ -51,19 +58,22 @@ export class TerminalServiceMainImpl implements TerminalServiceMain { protected async trackTerminal(terminal: TerminalWidget): Promise { let name = terminal.title.label; this.extProxy.$terminalCreated(terminal.id, name); - terminal.title.changed.connect(() => { + const updateTitle = () => { if (name !== terminal.title.label) { name = terminal.title.label; this.extProxy.$terminalNameChanged(terminal.id, name); } - }); + }; + terminal.title.changed.connect(updateTitle); + this.toDispose.push(Disposable.create(() => terminal.title.changed.disconnect(updateTitle))); + const updateProcessId = () => terminal.processId.then( processId => this.extProxy.$terminalOpened(terminal.id, processId), () => {/*no-op*/ } ); updateProcessId(); - terminal.onDidOpen(() => updateProcessId()); - terminal.onTerminalDidClose(() => this.extProxy.$terminalClosed(terminal.id)); + this.toDispose.push(terminal.onDidOpen(() => updateProcessId())); + this.toDispose.push(terminal.onTerminalDidClose(() => this.extProxy.$terminalClosed(terminal.id))); } async $createTerminal(id: string, options: TerminalOptions): Promise { diff --git a/packages/plugin-ext/src/main/browser/text-editor-main.ts b/packages/plugin-ext/src/main/browser/text-editor-main.ts index f0e4afb45c3e7..07aa30e35f58d 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-main.ts @@ -13,6 +13,8 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ + +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { TextEditorConfiguration, @@ -26,32 +28,38 @@ import { DecorationOptions } from '../../common/plugin-api-rpc'; import { Range } from '../../common/plugin-api-rpc-model'; -import { DisposableCollection, Emitter, Event } from '@theia/core'; +import { Emitter, Event } from '@theia/core'; import { TextEditorCursorStyle, cursorStyleToString } from '../../common/editor-options'; import { TextEditorLineNumbersStyle, EndOfLine } from '../../plugin/types-impl'; -export class TextEditorMain { +export class TextEditorMain implements Disposable { private properties: TextEditorPropertiesMain | undefined; - private modelListeners: DisposableCollection = new DisposableCollection(); private editor: MonacoEditor | undefined; - private editorListeners = new DisposableCollection(); private readonly onPropertiesChangedEmitter = new Emitter(); + private readonly toDispose = new DisposableCollection( + Disposable.create(() => this.properties = undefined), + this.onPropertiesChangedEmitter + ); + constructor( private id: string, private model: monaco.editor.IModel, editor: MonacoEditor ) { - this.properties = undefined; - this.modelListeners.push(this.model.onDidChangeOptions(e => { - this.updateProperties(undefined); - })); + this.toDispose.push(this.model.onDidChangeOptions(() => + this.updateProperties(undefined) + )); this.setEditor(editor); this.updateProperties(undefined); } + dispose(): void { + this.toDispose.dispose(); + } + private updateProperties(source?: string): void { this.setProperties(TextEditorPropertiesMain.readFromEditor(this.properties, this.model, this.editor!), source); } @@ -64,33 +72,35 @@ export class TextEditorMain { } } + protected readonly toDisposeOnEditor = new DisposableCollection(); + private setEditor(editor?: MonacoEditor): void { if (this.editor === editor) { return; } - - this.editorListeners.dispose(); - this.editorListeners = new DisposableCollection(); + this.toDisposeOnEditor.dispose(); + this.toDispose.push(this.toDisposeOnEditor); this.editor = editor; + this.toDisposeOnEditor.push(Disposable.create(() => this.editor = undefined)); if (this.editor) { const monaco = this.editor.getControl(); - this.editorListeners.push(this.editor.onSelectionChanged(_ => { + this.toDisposeOnEditor.push(this.editor.onSelectionChanged(_ => { this.updateProperties(); })); - this.editorListeners.push(monaco.onDidChangeModel(() => { + this.toDisposeOnEditor.push(monaco.onDidChangeModel(() => { this.setEditor(undefined); })); - this.editorListeners.push(monaco.onDidChangeCursorSelection(e => { + this.toDisposeOnEditor.push(monaco.onDidChangeCursorSelection(e => { this.updateProperties(e.source); })); - this.editorListeners.push(monaco.onDidChangeConfiguration(() => { + this.toDisposeOnEditor.push(monaco.onDidChangeConfiguration(() => { this.updateProperties(); })); - this.editorListeners.push(monaco.onDidLayoutChange(() => { + this.toDisposeOnEditor.push(monaco.onDidLayoutChange(() => { this.updateProperties(); })); - this.editorListeners.push(monaco.onDidScrollChange(() => { + this.toDisposeOnEditor.push(monaco.onDidScrollChange(() => { this.updateProperties(); })); @@ -99,14 +109,6 @@ export class TextEditorMain { } } - dispose(): void { - this.modelListeners.dispose(); - delete this.model; - - this.editorListeners.dispose(); - delete this.editor; - } - getId(): string { return this.id; } diff --git a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts index df064c0d45b12..1630176e44141 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts @@ -22,23 +22,8 @@ import { Schemes } from '../../common/uri-components'; import URI from '@theia/core/lib/common/uri'; import { Reference } from '@theia/core/lib/common/reference'; -export const EditorModelService = Symbol('EditorModelService'); -export interface EditorModelService { - onModelAdded: Event; - onModelRemoved: Event; - onModelModeChanged: Event<{ model: MonacoEditorModel, oldModeId: string }>; - - onModelWillSave: Event; - onModelDirtyChanged: Event; - onModelSaved: Event; - - getModels(): MonacoEditorModel[]; - createModelReference(uri: URI): Promise> - saveAll(includeUntitled?: boolean): Promise; -} - @injectable() -export class EditorModelServiceImpl implements EditorModelService { +export class EditorModelService { private monacoModelService: MonacoTextModelService; private modelModeChangedEmitter = new Emitter<{ model: MonacoEditorModel, oldModeId: string }>(); @@ -47,11 +32,11 @@ export class EditorModelServiceImpl implements EditorModelService { private modelSavedEmitter = new Emitter(); private onModelWillSavedEmitter = new Emitter(); - onModelDirtyChanged: Event = this.modelDirtyEmitter.event; - onModelSaved: Event = this.modelSavedEmitter.event; - onModelModeChanged = this.modelModeChangedEmitter.event; - onModelRemoved = this.onModelRemovedEmitter.event; - onModelWillSave = this.onModelWillSavedEmitter.event; + readonly onModelDirtyChanged = this.modelDirtyEmitter.event; + readonly onModelSaved = this.modelSavedEmitter.event; + readonly onModelModeChanged = this.modelModeChangedEmitter.event; + readonly onModelRemoved = this.onModelRemovedEmitter.event; + readonly onModelWillSave = this.onModelWillSavedEmitter.event; constructor(@inject(MonacoTextModelService) monacoModelService: MonacoTextModelService, @inject(MonacoWorkspace) monacoWorkspace: MonacoWorkspace) { diff --git a/packages/plugin-ext/src/main/browser/text-editor-service.ts b/packages/plugin-ext/src/main/browser/text-editor-service.ts index edf7e35f6c56b..e1424580fb335 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-service.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-service.ts @@ -13,30 +13,18 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Emitter, Event } from '@theia/core'; +import { Emitter } from '@theia/core'; import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { inject, injectable } from 'inversify'; -export const TextEditorService = Symbol('TextEditorService'); -/** - * Stores TextEditor and handles lifecycle - */ -export interface TextEditorService { - onTextEditorAdd: Event; - onTextEditorRemove: Event; - listTextEditors(): MonacoEditor[]; - - getActiveEditor(): EditorWidget | undefined; -} - @injectable() -export class TextEditorServiceImpl implements TextEditorService { - private onTextEditorAddEmitter = new Emitter(); - private onTextEditorRemoveEmitter = new Emitter(); +export class TextEditorService { + private readonly onTextEditorAddEmitter = new Emitter(); + readonly onTextEditorAdd = this.onTextEditorAddEmitter.event; - onTextEditorAdd: Event = this.onTextEditorAddEmitter.event; - onTextEditorRemove: Event = this.onTextEditorRemoveEmitter.event; + private readonly onTextEditorRemoveEmitter = new Emitter(); + readonly onTextEditorRemove = this.onTextEditorRemoveEmitter.event; constructor(@inject(EditorManager) private editorManager: EditorManager) { editorManager.onCreated(w => this.onEditorCreated(w)); diff --git a/packages/plugin-ext/src/main/browser/text-editors-main.ts b/packages/plugin-ext/src/main/browser/text-editors-main.ts index 1963d3baf3627..31e8eb37e0c0f 100644 --- a/packages/plugin-ext/src/main/browser/text-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editors-main.ts @@ -31,7 +31,7 @@ import { import { Range } from '../../common/plugin-api-rpc-model'; import { EditorsAndDocumentsMain } from './editors-and-documents-main'; import { RPCProtocol } from '../../common/rpc-protocol'; -import { DisposableCollection } from '@theia/core'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { TextEditorMain } from './text-editor-main'; import { disposed } from '../../common/errors'; import { reviveWorkspaceEditDto } from './languages-main'; @@ -40,32 +40,36 @@ import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-ser export class TextEditorsMainImpl implements TextEditorsMain { - private toDispose = new DisposableCollection(); - private proxy: TextEditorsExt; - private editorsToDispose = new Map(); + private readonly proxy: TextEditorsExt; + private readonly toDispose = new DisposableCollection(); + private readonly editorsToDispose = new Map(); - constructor(private readonly editorsAndDocuments: EditorsAndDocumentsMain, + constructor( + private readonly editorsAndDocuments: EditorsAndDocumentsMain, rpc: RPCProtocol, private readonly bulkEditService: MonacoBulkEditService, - private readonly monacoEditorService: MonacoEditorService) { + private readonly monacoEditorService: MonacoEditorService + ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT); + this.toDispose.push(editorsAndDocuments); this.toDispose.push(editorsAndDocuments.onTextEditorAdd(editors => editors.forEach(this.onTextEditorAdd, this))); this.toDispose.push(editorsAndDocuments.onTextEditorRemove(editors => editors.forEach(this.onTextEditorRemove, this))); } dispose(): void { - this.editorsToDispose.forEach(val => val.dispose()); - this.editorsToDispose = new Map(); this.toDispose.dispose(); } private onTextEditorAdd(editor: TextEditorMain): void { const id = editor.getId(); - const toDispose = new DisposableCollection(); - toDispose.push(editor.onPropertiesChangedEvent(e => { - this.proxy.$acceptEditorPropertiesChanged(id, e); - })); + const toDispose = new DisposableCollection( + editor.onPropertiesChangedEvent(e => { + this.proxy.$acceptEditorPropertiesChanged(id, e); + }), + Disposable.create(() => this.editorsToDispose.delete(id)) + ); this.editorsToDispose.set(id, toDispose); + this.toDispose.push(toDispose); } private onTextEditorRemove(id: string): void { @@ -73,7 +77,6 @@ export class TextEditorsMainImpl implements TextEditorsMain { if (disposables) { disposables.dispose(); } - this.editorsToDispose.delete(id); } $trySetOptions(id: string, options: TextEditorConfigurationUpdate): Promise { @@ -125,6 +128,7 @@ export class TextEditorsMainImpl implements TextEditorsMain { $registerTextEditorDecorationType(key: string, options: DecorationRenderOptions): void { this.monacoEditorService.registerDecorationType(key, options); + this.toDispose.push(Disposable.create(() => this.$removeTextEditorDecorationType(key))); } $removeTextEditorDecorationType(key: string): void { diff --git a/packages/plugin-ext/src/main/browser/view-column-service.ts b/packages/plugin-ext/src/main/browser/view-column-service.ts index e32d2c44d13f8..4c0384e2b7399 100644 --- a/packages/plugin-ext/src/main/browser/view-column-service.ts +++ b/packages/plugin-ext/src/main/browser/view-column-service.ts @@ -15,24 +15,20 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { Emitter, Event } from '@theia/core'; +import { Emitter, Event } from '@theia/core/lib/common/event'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; -import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel'; import { toArray } from '@phosphor/algorithm'; @injectable() export class ViewColumnService { - private mainPanel: TheiaDockPanel; - private columnValues = new Map(); - private viewColumnIds = new Map(); + private readonly columnValues = new Map(); + private readonly viewColumnIds = new Map(); protected readonly onViewColumnChangedEmitter = new Emitter<{ id: string, viewColumn: number }>(); constructor( @inject(ApplicationShell) private readonly shell: ApplicationShell, ) { - this.mainPanel = this.shell.mainPanel; - let oldColumnValues = new Map(); const update = async () => { await new Promise((resolve => setTimeout(() => resolve()))); @@ -46,8 +42,8 @@ export class ViewColumnService { }); oldColumnValues = new Map(this.columnValues.entries()); }; - this.mainPanel.widgetAdded.connect(() => update()); - this.mainPanel.widgetRemoved.connect(() => update()); + this.shell.mainPanel.widgetAdded.connect(() => update()); + this.shell.mainPanel.widgetRemoved.connect(() => update()); } get onViewColumnChanged(): Event<{ id: string, viewColumn: number }> { @@ -56,7 +52,7 @@ export class ViewColumnService { updateViewColumns(): void { const positionIds = new Map(); - toArray(this.mainPanel.tabBars()).forEach(tabBar => { + toArray(this.shell.mainPanel.tabBars()).forEach(tabBar => { if (!tabBar.node.style.left) { return; } diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts index 383a318b3bfbf..885fdb6b26783 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts @@ -182,27 +182,31 @@ export class PluginViewRegistry implements FrontendApplicationContribution { return view.when === undefined || this.contextKeyService.match(view.when); } - registerViewContainer(location: string, viewContainer: ViewContainer): void { + registerViewContainer(location: string, viewContainer: ViewContainer): Disposable { if (this.viewContainers.has(viewContainer.id)) { console.warn('view container such id already registered: ', JSON.stringify(viewContainer)); - return; + return Disposable.NULL; } + const toDispose = new DisposableCollection(); const iconClass = 'plugin-view-container-icon-' + viewContainer.id; - this.style.insertRule('.' + iconClass, () => ` + toDispose.push(this.style.insertRule('.' + iconClass, () => ` mask: url('${viewContainer.iconUrl}') no-repeat 50% 50%; -webkit-mask: url('${viewContainer.iconUrl}') no-repeat 50% 50%; - `); - this.doRegisterViewContainer(viewContainer.id, location, { + `)); + toDispose.push(this.doRegisterViewContainer(viewContainer.id, location, { label: viewContainer.title, iconClass, closeable: true - }); + })); + return toDispose; } - protected doRegisterViewContainer(id: string, location: string, options: ViewContainerTitleOptions): void { + protected doRegisterViewContainer(id: string, location: string, options: ViewContainerTitleOptions): Disposable { + const toDispose = new DisposableCollection(); this.viewContainers.set(id, [location, options]); + toDispose.push(Disposable.create(() => this.viewContainers.delete(id))); const toggleCommandId = `plugin.view-container.${id}.toggle`; - this.commands.registerCommand({ + toDispose.push(this.commands.registerCommand({ id: toggleCommandId, label: 'Toggle ' + options.label + ' View' }, { @@ -217,26 +221,45 @@ export class PluginViewRegistry implements FrontendApplicationContribution { } } } - }); - this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, { + })); + toDispose.push(this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, { commandId: toggleCommandId, label: options.label - }); + })); + toDispose.push(Disposable.create(async () => { + const widget = await this.getPluginViewContainer(id); + if (widget) { + widget.dispose(); + } + })); + return toDispose; } - registerView(viewContainerId: string, view: View): void { + registerView(viewContainerId: string, view: View): Disposable { if (this.views.has(view.id)) { console.warn('view with such id already registered: ', JSON.stringify(view)); - return; + return Disposable.NULL; } + const toDispose = new DisposableCollection(); + this.views.set(view.id, [viewContainerId, view]); + toDispose.push(Disposable.create(() => this.views.delete(view.id))); + const containerViews = this.containerViews.get(viewContainerId) || []; containerViews.push(view.id); this.containerViews.set(viewContainerId, containerViews); + toDispose.push(Disposable.create(() => { + const index = containerViews.indexOf(view.id); + if (index !== -1) { + containerViews.splice(index, 1); + } + })); + if (view.when) { this.viewClauseContexts.set(view.id, this.contextKeyService.parseKeys(view.when)); + toDispose.push(Disposable.create(() => this.viewClauseContexts.delete(view.id))); } - this.quickView.registerItem({ + toDispose.push(this.quickView.registerItem({ label: view.name, open: async () => { const widget = await this.openView(view.id); @@ -244,7 +267,8 @@ export class PluginViewRegistry implements FrontendApplicationContribution { this.shell.activateWidget(widget.id); } } - }); + })); + return toDispose; } async getView(viewId: string): Promise { diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts index 62fca9cc20005..6ccae3d26cd63 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts @@ -20,10 +20,10 @@ import { RPCProtocol } from '../../../common/rpc-protocol'; import { PluginViewRegistry, PLUGIN_VIEW_DATA_FACTORY_ID } from './plugin-view-registry'; import { SelectableTreeNode, ExpandableTreeNode, CompositeTreeNode, WidgetManager } from '@theia/core/lib/browser'; import { ViewContextKeyService } from './view-context-key-service'; -import { CommandRegistry, Disposable } from '@theia/core'; +import { CommandRegistry, Disposable, DisposableCollection } from '@theia/core'; import { TreeViewWidget, TreeViewNode } from './tree-view-widget'; -export class TreeViewsMainImpl implements TreeViewsMain { +export class TreeViewsMainImpl implements TreeViewsMain, Disposable { private readonly proxy: TreeViewsExt; private readonly viewRegistry: PluginViewRegistry; @@ -33,6 +33,8 @@ export class TreeViewsMainImpl implements TreeViewsMain { private readonly treeViewProviders = new Map(); + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, private container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT); this.viewRegistry = container.get(PluginViewRegistry); @@ -42,6 +44,10 @@ export class TreeViewsMainImpl implements TreeViewsMain { this.widgetManager = this.container.get(WidgetManager); } + dispose(): void { + this.toDispose.dispose(); + } + async $registerTreeDataProvider(treeViewId: string): Promise { this.treeViewProviders.set(treeViewId, this.viewRegistry.registerViewDataProvider(treeViewId, async ({ state, viewInfo }) => { const widget = await this.widgetManager.getOrCreateWidget(PLUGIN_VIEW_DATA_FACTORY_ID, { id: treeViewId }); @@ -66,6 +72,7 @@ export class TreeViewsMainImpl implements TreeViewsMain { this.handleTreeEvents(widget.id, widget); return widget; })); + this.toDispose.push(Disposable.create(() => this.$unregisterTreeDataProvider(treeViewId))); } async $unregisterTreeDataProvider(treeViewId: string): Promise { @@ -101,11 +108,11 @@ export class TreeViewsMainImpl implements TreeViewsMain { } protected handleTreeEvents(treeViewId: string, treeViewWidget: TreeViewWidget): void { - treeViewWidget.model.onExpansionChanged(event => { + this.toDispose.push(treeViewWidget.model.onExpansionChanged(event => { this.proxy.$setExpanded(treeViewId, event.id, event.expanded); - }); + })); - treeViewWidget.model.onSelectionChanged(event => { + this.toDispose.push(treeViewWidget.model.onSelectionChanged(event => { if (event.length === 1) { const { contextValue } = event[0] as TreeViewNode; this.contextKeys.viewItem.set(contextValue); @@ -121,11 +128,11 @@ export class TreeViewsMainImpl implements TreeViewsMain { if (treeNode && treeNode.command) { this.commands.executeCommand(treeNode.command.id, ...(treeNode.command.arguments || [])); } - }); + })); const updateVisible = () => this.proxy.$setVisible(treeViewId, treeViewWidget.isVisible); updateVisible(); - treeViewWidget.onDidChangeVisibility(() => updateVisible()); + this.toDispose.push(treeViewWidget.onDidChangeVisibility(() => updateVisible())); } } diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index b2b8f45f2dfb1..26155a48099da 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -24,13 +24,13 @@ import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { WebviewWidget } from './webview/webview'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeRulesService } from './webview/theme-rules-service'; -import { DisposableCollection } from '@theia/core'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ViewColumnService } from './view-column-service'; import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker'; import debounce = require('lodash.debounce'); -export class WebviewsMainImpl implements WebviewsMain { +export class WebviewsMainImpl implements WebviewsMain, Disposable { private readonly revivers = new Set(); private readonly proxy: WebviewsExt; protected readonly shell: ApplicationShell; @@ -51,6 +51,8 @@ export class WebviewsMainImpl implements WebviewsMain { protected readonly mouseTracker: ApplicationShellMouseTracker; + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT); this.shell = container.get(ApplicationShell); @@ -62,9 +64,13 @@ export class WebviewsMainImpl implements WebviewsMain { this.checkViewOptions(key); } }, 100); - this.shell.activeChanged.connect(() => this.updateViewOptions()); - this.shell.currentChanged.connect(() => this.updateViewOptions()); - this.viewColumnService.onViewColumnChanged(() => this.updateViewOptions()); + this.toDispose.push(this.shell.onDidChangeActiveWidget(() => this.updateViewOptions())); + this.toDispose.push(this.shell.onDidChangeCurrentWidget(() => this.updateViewOptions())); + this.toDispose.push(this.viewColumnService.onViewColumnChanged(() => this.updateViewOptions())); + } + + dispose(): void { + this.toDispose.dispose(); } $createWebviewPanel( @@ -76,6 +82,7 @@ export class WebviewsMainImpl implements WebviewsMain { extensionLocation: UriComponents ): void { const toDispose = new DisposableCollection(); + const toDisposeOnLoad = new DisposableCollection(); const view = new WebviewWidget(title, { allowScripts: options ? options.enableScripts : false }, { @@ -88,11 +95,12 @@ export class WebviewsMainImpl implements WebviewsMain { onLoad: contentDocument => { const styleId = 'webview-widget-theme'; let styleElement: HTMLStyleElement | null | undefined; - if (!toDispose.disposed) { + if (!toDisposeOnLoad.disposed) { // if reload the frame - toDispose.dispose(); + toDisposeOnLoad.dispose(); styleElement = contentDocument.getElementById(styleId); } + toDispose.push(toDisposeOnLoad); if (!styleElement) { const parent = contentDocument.head ? contentDocument.head : contentDocument.body; styleElement = this.themeRulesService.createStyleSheet(parent); @@ -102,7 +110,7 @@ export class WebviewsMainImpl implements WebviewsMain { this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules()); contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`; - toDispose.push(this.themeService.onThemeChange(() => { + toDisposeOnLoad.push(this.themeService.onThemeChange(() => { this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules()); contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`; })); @@ -111,11 +119,18 @@ export class WebviewsMainImpl implements WebviewsMain { this.mouseTracker); view.disposed.connect(() => { toDispose.dispose(); - this.onCloseView(panelId); + this.proxy.$onDidDisposeWebviewPanel(viewId); }); + const viewId = view.id; + this.toDispose.push(Disposable.create(() => this.themeRulesService.setIconPath(viewId, undefined))); this.views.set(panelId, view); - this.viewsOptions.set(view.id, { panelOptions: showOptions, options: options, panelId, visible: false, active: false }); + this.toDispose.push(Disposable.create(() => this.views.delete(panelId))); + + this.viewsOptions.set(viewId, { panelOptions: showOptions, options: options, panelId, visible: false, active: false }); + this.toDispose.push(Disposable.create(() => this.viewsOptions.delete(viewId))); + this.toDispose.push(this.toDispose); + this.addOrReattachWidget(panelId, showOptions); } private addOrReattachWidget(handler: string, showOptions: WebviewPanelShowOptions): void { @@ -234,6 +249,7 @@ export class WebviewsMainImpl implements WebviewsMain { } $registerSerializer(viewType: string): void { this.revivers.add(viewType); + this.toDispose.push(Disposable.create(() => this.$unregisterSerializer(viewType))); } $unregisterSerializer(viewType: string): void { this.revivers.delete(viewType); @@ -271,15 +287,4 @@ export class WebviewsMainImpl implements WebviewsMain { return webview; } - private onCloseView(viewId: string): void { - const view = this.views.get(viewId); - if (view) { - this.themeRulesService.setIconPath(view.id, undefined); - } - const cleanUp = () => { - this.views.delete(viewId); - this.viewsOptions.delete(viewId); - }; - this.proxy.$onDidDisposeWebviewPanel(viewId).then(cleanUp, cleanUp); - } } diff --git a/packages/plugin-ext/src/main/browser/window-state-main.ts b/packages/plugin-ext/src/main/browser/window-state-main.ts index 36e7307f1f1aa..7d199850b0d36 100644 --- a/packages/plugin-ext/src/main/browser/window-state-main.ts +++ b/packages/plugin-ext/src/main/browser/window-state-main.ts @@ -19,20 +19,32 @@ import { interfaces } from 'inversify'; import { WindowStateExt, MAIN_RPC_CONTEXT, WindowMain } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; -export class WindowStateMain implements WindowMain { +export class WindowStateMain implements WindowMain, Disposable { private readonly proxy: WindowStateExt; private readonly windowService: WindowService; + private readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT); this.windowService = container.get(WindowService); - window.addEventListener('focus', () => this.onFocusChanged(true)); - window.addEventListener('blur', () => this.onFocusChanged(false)); + const fireDidFocus = () => this.onFocusChanged(true); + window.addEventListener('focus', fireDidFocus); + this.toDispose.push(Disposable.create(() => window.removeEventListener('focus', fireDidFocus))); + + const fireDidBlur = () => this.onFocusChanged(false); + window.addEventListener('blur', fireDidBlur); + this.toDispose.push(Disposable.create(() => window.removeEventListener('blur', fireDidBlur))); + } + + dispose(): void { + this.toDispose.dispose(); } private onFocusChanged(focused: boolean): void { diff --git a/packages/plugin-ext/src/main/browser/workspace-main.ts b/packages/plugin-ext/src/main/browser/workspace-main.ts index 2ff68008b20d5..06f05f42c9fc6 100644 --- a/packages/plugin-ext/src/main/browser/workspace-main.ts +++ b/packages/plugin-ext/src/main/browser/workspace-main.ts @@ -27,14 +27,15 @@ import { FileSearchService } from '@theia/file-search/lib/common/file-search-ser import URI from '@theia/core/lib/common/uri'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { Resource } from '@theia/core/lib/common/resource'; -import { Emitter, Event, Disposable, ResourceResolver } from '@theia/core'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Emitter, Event, ResourceResolver } from '@theia/core'; import { FileWatcherSubscriberOptions } from '../../common/plugin-api-rpc-model'; import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager'; import { StoragePathService } from './storage-path-service'; import { PluginServer } from '../../common/plugin-protocol'; import { FileSystemPreferences } from '@theia/filesystem/lib/browser'; -export class WorkspaceMainImpl implements WorkspaceMain { +export class WorkspaceMainImpl implements WorkspaceMain, Disposable { private readonly proxy: WorkspaceExt; @@ -58,6 +59,8 @@ export class WorkspaceMainImpl implements WorkspaceMain { private fsPreferences: FileSystemPreferences; + protected readonly toDispose = new DisposableCollection(); + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WORKSPACE_EXT); this.storageProxy = rpc.getProxy(MAIN_RPC_CONTEXT.STORAGE_EXT); @@ -68,13 +71,16 @@ export class WorkspaceMainImpl implements WorkspaceMain { this.workspaceService = container.get(WorkspaceService); this.storagePathService = container.get(StoragePathService); this.fsPreferences = container.get(FileSystemPreferences); - - this.inPluginFileSystemWatcherManager = new InPluginFileSystemWatcherManager(this.proxy, container); + this.inPluginFileSystemWatcherManager = container.get(InPluginFileSystemWatcherManager); this.processWorkspaceFoldersChanged(this.workspaceService.tryGetRoots()); - this.workspaceService.onWorkspaceChanged(roots => { + this.toDispose.push(this.workspaceService.onWorkspaceChanged(roots => { this.processWorkspaceFoldersChanged(roots); - }); + })); + } + + dispose(): void { + this.toDispose.dispose(); } async processWorkspaceFoldersChanged(roots: FileStat[]): Promise { @@ -192,8 +198,10 @@ export class WorkspaceMainImpl implements WorkspaceMain { return uriStrs.map(uriStr => Uri.parse(uriStr)); } - $registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise { - return Promise.resolve(this.inPluginFileSystemWatcherManager.registerFileWatchSubscription(options)); + async $registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise { + const handle = this.inPluginFileSystemWatcherManager.registerFileWatchSubscription(options, this.proxy); + this.toDispose.push(Disposable.create(() => this.inPluginFileSystemWatcherManager.unregisterFileWatchSubscription(handle))); + return handle; } $unregisterFileSystemWatcher(watcherId: string): Promise { @@ -202,7 +210,8 @@ export class WorkspaceMainImpl implements WorkspaceMain { } async $registerTextDocumentContentProvider(scheme: string): Promise { - return this.resourceResolver.registerContentProvider(scheme, this.proxy); + this.resourceResolver.registerContentProvider(scheme, this.proxy); + this.toDispose.push(Disposable.create(() => this.resourceResolver.unregisterContentProvider(scheme))); } $unregisterTextDocumentContentProvider(scheme: string): void { @@ -249,7 +258,7 @@ export class TextContentResourceResolver implements ResourceResolver { throw new Error(`Unable to find Text Content Resource Provider for scheme '${uri.scheme}'`); } - async registerContentProvider(scheme: string, proxy: WorkspaceExt): Promise { + registerContentProvider(scheme: string, proxy: WorkspaceExt): void { if (this.providers.has(scheme)) { throw new Error(`Text Content Resource Provider for scheme '${scheme}' is already registered`); } diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 91c00716a612e..5331ac9f133b0 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -121,9 +121,10 @@ export class TaskConfigurations implements Disposable { @postConstruct() protected init(): void { this.reorgnizeTasks(); - this.toDispose.push( - this.taskDefinitionRegistry.onDidRegisterTaskDefinition(() => this.reorgnizeTasks()) - ); + this.toDispose.pushAll([ + this.taskDefinitionRegistry.onDidRegisterTaskDefinition(() => this.reorgnizeTasks()), + this.taskDefinitionRegistry.onDidUnregisterTaskDefinition(() => this.reorgnizeTasks()) + ]); } setClient(client: TaskConfigurationClient): void { diff --git a/packages/task/src/browser/task-definition-registry.ts b/packages/task/src/browser/task-definition-registry.ts index 03d074828a4ed..b47248c9afc86 100644 --- a/packages/task/src/browser/task-definition-registry.ts +++ b/packages/task/src/browser/task-definition-registry.ts @@ -18,6 +18,7 @@ import { injectable } from 'inversify'; import { Event, Emitter } from '@theia/core/lib/common'; import { TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; import URI from '@theia/core/lib/common/uri'; +import { Disposable } from '@theia/core/lib/common/disposable'; @injectable() export class TaskDefinitionRegistry { @@ -30,6 +31,11 @@ export class TaskDefinitionRegistry { return this.onDidRegisterTaskDefinitionEmitter.event; } + protected readonly onDidUnregisterTaskDefinitionEmitter = new Emitter(); + get onDidUnregisterTaskDefinition(): Event { + return this.onDidUnregisterTaskDefinitionEmitter.event; + } + /** * Finds the task definition(s) from the registry with the given `taskType`. * @@ -74,10 +80,19 @@ export class TaskDefinitionRegistry { * * @param definition the task definition to be added. */ - register(definition: TaskDefinition): void { + register(definition: TaskDefinition): Disposable { const taskType = definition.taskType; - this.definitions.set(taskType, [...this.getDefinitions(taskType), definition]); + const definitions = this.definitions.get(taskType) || []; + definitions.push(definition); + this.definitions.set(taskType, definitions); this.onDidRegisterTaskDefinitionEmitter.fire(undefined); + return Disposable.create(() => { + const index = definitions.indexOf(definition); + if (index !== -1) { + definitions.splice(index, 1); + } + this.onDidUnregisterTaskDefinitionEmitter.fire(undefined); + }); } compareTasks(one: TaskConfiguration, other: TaskConfiguration): boolean { diff --git a/packages/task/src/browser/task-problem-matcher-registry.ts b/packages/task/src/browser/task-problem-matcher-registry.ts index 56fb06e6472b8..ecad9e8fa6b77 100644 --- a/packages/task/src/browser/task-problem-matcher-registry.ts +++ b/packages/task/src/browser/task-problem-matcher-registry.ts @@ -20,6 +20,7 @@ *--------------------------------------------------------------------------------------------*/ import { inject, injectable, postConstruct } from 'inversify'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ApplyToKind, FileLocationKind, NamedProblemMatcher, Severity, ProblemPattern, ProblemMatcher, ProblemMatcherContribution, WatchingMatcher @@ -29,7 +30,7 @@ import { ProblemPatternRegistry } from './task-problem-pattern-registry'; @injectable() export class ProblemMatcherRegistry { - private matchers: { [name: string]: NamedProblemMatcher }; + private readonly matchers = new Map(); private readyPromise: Promise; @inject(ProblemPatternRegistry) @@ -37,8 +38,6 @@ export class ProblemMatcherRegistry { @postConstruct() protected init(): void { - // tslint:disable-next-line:no-null-keyword - this.matchers = Object.create(null); this.problemPatternRegistry.onReady().then(() => { this.fillDefaults(); this.readyPromise = new Promise((res, rej) => res(undefined)); @@ -54,13 +53,21 @@ export class ProblemMatcherRegistry { * * @param definition the problem matcher to be added. */ - async register(matcher: ProblemMatcherContribution): Promise { + register(matcher: ProblemMatcherContribution): Disposable { if (!matcher.name) { console.error('Only named Problem Matchers can be registered.'); - return; + return Disposable.NULL; } + const toDispose = new DisposableCollection(Disposable.create(() => {/* mark as not disposed */ })); + this.doRegister(matcher, toDispose); + return toDispose; + } + protected async doRegister(matcher: ProblemMatcherContribution, toDispose: DisposableCollection): Promise { const problemMatcher = await this.getProblemMatcherFromContribution(matcher); - this.add(problemMatcher as NamedProblemMatcher); + if (toDispose.disposed) { + return; + } + toDispose.push(this.add(problemMatcher as NamedProblemMatcher)); } /** @@ -71,9 +78,9 @@ export class ProblemMatcherRegistry { */ get(name: string): NamedProblemMatcher | undefined { if (name.startsWith('$')) { - return this.matchers[name.slice(1)]; + return this.matchers.get(name.slice(1)); } - return this.matchers[name]; + return this.matchers.get(name); } /** @@ -127,8 +134,9 @@ export class ProblemMatcherRegistry { return problemMatcher; } - private add(matcher: NamedProblemMatcher): void { - this.matchers[matcher.name] = matcher; + private add(matcher: NamedProblemMatcher): Disposable { + this.matchers.set(matcher.name, matcher); + return Disposable.create(() => this.matchers.delete(matcher.name)); } private getFileLocationKindAndPrefix(matcher: ProblemMatcherContribution): { fileLocation: FileLocationKind, filePrefix: string } { diff --git a/packages/task/src/browser/task-problem-pattern-registry.ts b/packages/task/src/browser/task-problem-pattern-registry.ts index 72cf4d2550833..63b36b0518802 100644 --- a/packages/task/src/browser/task-problem-pattern-registry.ts +++ b/packages/task/src/browser/task-problem-pattern-registry.ts @@ -21,16 +21,15 @@ import { injectable, postConstruct } from 'inversify'; import { NamedProblemPattern, ProblemLocationKind, ProblemPattern, ProblemPatternContribution } from '../common'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; @injectable() export class ProblemPatternRegistry { - private patterns: { [name: string]: NamedProblemPattern | NamedProblemPattern[] }; + private readonly patterns = new Map(); private readyPromise: Promise; @postConstruct() protected init(): void { - // tslint:disable-next-line:no-null-keyword - this.patterns = Object.create(null); this.fillDefaults(); this.readyPromise = new Promise((res, rej) => res(undefined)); } @@ -44,17 +43,18 @@ export class ProblemPatternRegistry { * * @param definition the problem pattern to be added. */ - register(value: ProblemPatternContribution | ProblemPatternContribution[]): void { + register(value: ProblemPatternContribution | ProblemPatternContribution[]): Disposable { if (Array.isArray(value)) { - value.forEach(problemPatternContribution => this.register(problemPatternContribution)); - } else { - if (!value.name) { - console.error('Only named Problem Patterns can be registered.'); - return; - } - const problemPattern = ProblemPattern.fromProblemPatternContribution(value); - this.add(problemPattern.name!, problemPattern); + const toDispose = new DisposableCollection(); + value.forEach(problemPatternContribution => toDispose.push(this.register(problemPatternContribution))); + return toDispose; + } + if (!value.name) { + console.error('Only named Problem Patterns can be registered.'); + return Disposable.NULL; } + const problemPattern = ProblemPattern.fromProblemPatternContribution(value); + return this.add(problemPattern.name!, problemPattern); } /** @@ -64,17 +64,18 @@ export class ProblemPatternRegistry { * @return a problem pattern or an array of the problem patterns associated with the name. If no problem patterns are found, `undefined` is returned. */ get(key: string): undefined | NamedProblemPattern | NamedProblemPattern[] { - return this.patterns[key]; + return this.patterns.get(key); } - private add(key: string, value: ProblemPattern | ProblemPattern[]): void { + private add(key: string, value: ProblemPattern | ProblemPattern[]): Disposable { let toAdd: NamedProblemPattern | NamedProblemPattern[]; if (Array.isArray(value)) { toAdd = value.map(v => Object.assign(v, { name: key })); } else { toAdd = Object.assign(value, { name: key }); } - this.patterns[key] = toAdd; + this.patterns.set(key, toAdd); + return Disposable.create(() => this.patterns.delete(key)); } // copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts