diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 81598762eace5..b1595faf0c908 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, DisposableCollection } from '../common/disposable'; import { KeyCode, KeySequence, Key } from './keyboard/keys'; import { KeyboardLayoutService } from './keyboard/keyboard-layout-service'; import { ContributionProvider } from '../common/contribution-provider'; @@ -184,8 +185,8 @@ export class KeybindingRegistry { * * @param binding */ - registerKeybinding(binding: Keybinding): void { - this.doRegisterKeybinding(binding, KeybindingScope.DEFAULT); + registerKeybinding(binding: Keybinding): Disposable { + return this.doRegisterKeybinding(binding, KeybindingScope.DEFAULT); } /** @@ -193,8 +194,8 @@ export class KeybindingRegistry { * * @param bindings */ - registerKeybindings(...bindings: Keybinding[]): void { - this.doRegisterKeybindings(bindings, KeybindingScope.DEFAULT); + registerKeybindings(...bindings: Keybinding[]): Disposable { + return this.doRegisterKeybindings(bindings, KeybindingScope.DEFAULT); } /** @@ -222,21 +223,30 @@ export class KeybindingRegistry { }); } - protected doRegisterKeybindings(bindings: Keybinding[], scope: KeybindingScope = KeybindingScope.DEFAULT): void { + protected doRegisterKeybindings(bindings: Keybinding[], scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable { + const toDispose = new DisposableCollection(); for (const binding of bindings) { - this.doRegisterKeybinding(binding, scope); + toDispose.push(this.doRegisterKeybinding(binding, scope)); } + return toDispose; } - protected doRegisterKeybinding(binding: Keybinding, scope: KeybindingScope = KeybindingScope.DEFAULT): void { + protected doRegisterKeybinding(binding: Keybinding, scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable { try { this.resolveKeybinding(binding); if (this.containsKeybinding(this.keymaps[scope], binding)) { throw new Error(`"${binding.keybinding}" is in collision with something else [scope:${scope}]`); } this.keymaps[scope].push(binding); + return Disposable.create(() => { + const index = this.keymaps[scope].indexOf(binding); + if (index !== -1) { + this.keymaps[scope].splice(index, 1); + } + }); } catch (error) { this.logger.warn(`Could not register keybinding:\n ${Keybinding.stringify(binding)}\n${error}`); + return Disposable.NULL; } } @@ -641,16 +651,21 @@ export class KeybindingRegistry { setKeymap(scope: KeybindingScope, bindings: Keybinding[]): void { this.resetKeybindingsForScope(scope); - this.doRegisterKeybindings(bindings, scope); + this.toResetKeymap.set(scope, this.doRegisterKeybindings(bindings, scope)); this.keybindingsChanged.fire(undefined); } + protected readonly toResetKeymap = new Map(); + /** * Reset keybindings for a specific scope * @param scope scope to reset the keybindings for */ resetKeybindingsForScope(scope: KeybindingScope): void { - this.keymaps[scope] = []; + const toReset = this.toResetKeymap.get(scope); + if (toReset) { + toReset.dispose(); + } } /** diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 1e69cbfced9b4..4a0f8850f5dcc 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 { @@ -26,6 +26,7 @@ import { FrontendApplicationConfigProvider } from '../frontend-application-confi import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { bindPreferenceConfigurations, PreferenceConfigurations } from './preference-configurations'; export { PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType }; +import { Mutable } from '../../common/types'; // tslint:disable:no-any // tslint:disable:forin @@ -45,6 +46,12 @@ export interface OverridePreferenceName { preferenceName: string overrideIdentifier: string } +export namespace OverridePreferenceName { + // tslint:disable-next-line:no-any + export function is(arg: any): arg is OverridePreferenceName { + return !!arg && typeof arg === 'object' && 'preferenceName' in arg && 'overrideIdentifier' in arg; + } +} const OVERRIDE_PROPERTY = '\\[(.*)\\]$'; export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); @@ -101,11 +108,12 @@ export class PreferenceSchemaProvider extends PreferenceProvider { this.updateOverridePatternPropertiesKey(); } - protected readonly overridePatternProperties: Required> & PreferenceDataProperty = { + protected readonly overridePatternProperties: Required> & PreferenceDataProperty = { type: 'object', description: 'Configure editor settings to be overridden for a language.', errorMessage: 'Unknown Identifier. Use language identifiers', - properties: {} + properties: {}, + additionalProperties: false }; protected overridePatternPropertiesKey: string | undefined; protected updateOverridePatternPropertiesKey(): void { @@ -134,6 +142,32 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return param.length ? OVERRIDE_PATTERN_WITH_SUBSTITUTION.replace('${0}', param) : undefined; } + protected doUnsetSchema(changes: PreferenceProviderDataChange[]): PreferenceProviderDataChange[] { + const inverseChanges: PreferenceProviderDataChange[] = []; + for (const change of changes) { + const preferenceName = change.preferenceName; + const overridden = this.overriddenPreferenceName(preferenceName); + if (overridden) { + delete this.overridePatternProperties.properties[`[${overridden.overrideIdentifier}]`]; + delete this.combinedSchema.properties[`[${overridden.overrideIdentifier}]`]; + } else { + delete this.combinedSchema.properties[preferenceName]; + } + const newValue = change.oldValue; + const oldValue = change.newValue; + const { scope, domain } = change; + const inverseChange: Mutable = { preferenceName, oldValue, scope, domain }; + if (typeof newValue === undefined) { + delete this.preferences[preferenceName]; + } else { + inverseChange.newValue = newValue; + this.preferences[preferenceName] = newValue; + } + inverseChanges.push(inverseChange); + } + return inverseChanges; + } + protected doSetSchema(schema: PreferenceSchema): PreferenceProviderDataChange[] { const ajv = new Ajv(); const valid = ajv.validateSchema(schema); @@ -234,7 +268,9 @@ export class PreferenceSchemaProvider extends PreferenceProvider { if (this.configurations.isSectionName(name)) { return true; } - const result = this.validateFunction({ [name]: value }) as boolean; + const overridden = this.overriddenPreferenceName(name); + const preferenceName = overridden && overridden.preferenceName || name; + const result = this.validateFunction({ [preferenceName]: value }) as boolean; if (!result && !(name in this.combinedSchema.properties)) { // in order to avoid reporting it on each change if (!this.unsupportedPreferences.has(name)) { @@ -249,10 +285,21 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return this.combinedSchema; } - setSchema(schema: PreferenceSchema): void { + setSchema(schema: PreferenceSchema): Disposable { const changes = this.doSetSchema(schema); + if (!changes.length) { + return Disposable.NULL; + } this.fireDidPreferenceSchemaChanged(); this.emitPreferencesChangedEvent(changes); + return Disposable.create(() => { + const inverseChanges = this.doUnsetSchema(changes); + if (!inverseChanges.length) { + return; + } + this.fireDidPreferenceSchemaChanged(); + this.emitPreferencesChangedEvent(inverseChanges); + }); } getPreferences(): { [name: string]: any } { @@ -264,11 +311,24 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } isValidInScope(preferenceName: string, scope: PreferenceScope): boolean { - const preference = this.getPreferenceProperty(preferenceName); - if (preference) { - return preference.scope! >= scope; + let property; + const overridden = this.overriddenPreferenceName(preferenceName); + if (overridden) { + // try from overriden schema + property = this.overridePatternProperties[`[${overridden.overrideIdentifier}]`]; + property = property && property[overridden.preferenceName]; + if (!property) { + // try from overriden identifier + property = this.overridePatternProperties[overridden.preferenceName]; + } + if (!property) { + // try from overriden value + property = this.combinedSchema.properties[overridden.preferenceName]; + } + } else { + property = this.combinedSchema.properties[preferenceName]; } - return false; + return property && property.scope! >= scope; } *getPreferenceNames(): IterableIterator { @@ -289,11 +349,6 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } } - getPreferenceProperty(preferenceName: string): PreferenceItem | undefined { - const overridden = this.overriddenPreferenceName(preferenceName); - return this.combinedSchema.properties[overridden ? overridden.preferenceName : preferenceName]; - } - overridePreferenceName({ preferenceName, overrideIdentifier }: OverridePreferenceName): string { return `[${overrideIdentifier}].${preferenceName}`; } diff --git a/packages/core/src/browser/preferences/preference-proxy.ts b/packages/core/src/browser/preferences/preference-proxy.ts index 795912a4d6881..34526feaeddf7 100644 --- a/packages/core/src/browser/preferences/preference-proxy.ts +++ b/packages/core/src/browser/preferences/preference-proxy.ts @@ -83,12 +83,11 @@ export function createPreferenceProxy(preferences: PreferenceService, schema: }; const getValue: PreferenceRetrieval['get'] = (arg, defaultValue, resourceUri) => { - const isArgOverridePreferenceName = typeof arg === 'object' && arg.overrideIdentifier; - const preferenceName = isArgOverridePreferenceName ? - preferences.overridePreferenceName(arg) : + const preferenceName = OverridePreferenceName.is(arg) ? + preferences.overridePreferenceName(arg) : arg; const value = preferences.get(preferenceName, defaultValue, resourceUri || opts.resourceUri); - if (preferences.validate(isArgOverridePreferenceName ? (arg).preferenceName : preferenceName, value)) { + if (preferences.validate(preferenceName, value)) { return value; } if (defaultValue !== undefined) { diff --git a/packages/core/src/browser/preferences/preference-service.spec.ts b/packages/core/src/browser/preferences/preference-service.spec.ts index a0c41e19d6f9d..f57cc9dc66b6e 100644 --- a/packages/core/src/browser/preferences/preference-service.spec.ts +++ b/packages/core/src/browser/preferences/preference-service.spec.ts @@ -183,6 +183,68 @@ describe('Preference Service', () => { expect(prefService.get('test.number')).equals(0); }); + it('should unset preference schema', () => { + const events: PreferenceChange[] = []; + prefService.onPreferenceChanged(event => events.push(event)); + + prefSchema.registerOverrideIdentifier('go'); + + const toUnset = prefSchema.setSchema({ + properties: { + 'editor.insertSpaces': { + type: 'boolean', + default: true, + overridable: true + }, + '[go]': { + type: 'object', + default: { + 'editor.insertSpaces': false + } + } + } + }); + + assert.deepStrictEqual([{ + preferenceName: 'editor.insertSpaces', + newValue: true, + oldValue: undefined + }, { + preferenceName: '[go].editor.insertSpaces', + newValue: false, + oldValue: undefined + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue, + oldValue: e.oldValue + })), 'events before'); + assert.strictEqual(prefService.get('editor.insertSpaces'), true, 'get before'); + assert.strictEqual(prefService.get('[go].editor.insertSpaces'), false, 'get before overridden'); + assert.strictEqual(prefSchema.validate('editor.insertSpaces', false), true, 'validate before'); + assert.strictEqual(prefSchema.validate('[go].editor.insertSpaces', true), true, 'validate before overridden'); + + events.length = 0; + toUnset.dispose(); + + assert.deepStrictEqual([{ + preferenceName: 'editor.insertSpaces', + newValue: undefined, + oldValue: true + }, { + preferenceName: '[go].editor.insertSpaces', + newValue: undefined, + oldValue: false + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue, + oldValue: e.oldValue + })), 'events after'); + assert.strictEqual(prefService.get('editor.insertSpaces'), undefined, 'get after'); + assert.strictEqual(prefService.get('[go].editor.insertSpaces'), undefined, 'get after overridden'); + assert.strictEqual(prefSchema.validate('editor.insertSpaces', true), false, 'validate after'); + assert.strictEqual(prefSchema.validate('[go].editor.insertSpaces', true), false, 'validate after overridden'); + }); + describe('overridden preferences', () => { it('get #0', () => { diff --git a/packages/core/src/browser/preferences/preference-service.ts b/packages/core/src/browser/preferences/preference-service.ts index 2b56901f15489..dc848193811fb 100644 --- a/packages/core/src/browser/preferences/preference-service.ts +++ b/packages/core/src/browser/preferences/preference-service.ts @@ -195,6 +195,10 @@ export class PreferenceServiceImpl implements PreferenceService { acceptChange(change); } } + } else if (change.newValue === undefined && change.scope === PreferenceScope.Default) { + // preference is removed + acceptChange(change); + break; } } } 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/core/src/common/preferences/preference-schema.ts b/packages/core/src/common/preferences/preference-schema.ts index 6000643d10b08..4eb4ba4d5bff5 100644 --- a/packages/core/src/common/preferences/preference-schema.ts +++ b/packages/core/src/common/preferences/preference-schema.ts @@ -73,7 +73,7 @@ export interface PreferenceItem { enum?: string[]; items?: PreferenceItem; properties?: { [name: string]: PreferenceItem }; - additionalProperties?: object; + additionalProperties?: object | boolean; [name: string]: any; overridable?: boolean; } diff --git a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts index c43fc58483cf7..396a6d391312e 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,25 +115,36 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } - fromURI(uri: string | URI, options: SnippetLoadOptions): Promise { - const pending = this.loadURI(uri, options); + fromURI(uri: string | URI, options: SnippetLoadOptions): Disposable { + const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); + const pending = this.loadURI(uri, options, toDispose); const { language } = options; const scopes = Array.isArray(language) ? language : !!language ? [language] : ['*']; for (const scope of scopes) { const pendingSnippets = this.pendingSnippets.get(scope) || []; pendingSnippets.push(pending); this.pendingSnippets.set(scope, pendingSnippets); + toDispose.push(Disposable.create(() => { + const index = pendingSnippets.indexOf(pending); + if (index !== -1) { + pendingSnippets.splice(index, 1); + } + })); } - return pending; + return toDispose; } + /** * should NOT throw to prevent load erros on suggest */ - protected async loadURI(uri: string | URI, options: SnippetLoadOptions): Promise { + protected async loadURI(uri: string | URI, options: SnippetLoadOptions, toDispose: DisposableCollection): Promise { try { const { content } = await this.filesystem.resolveContent(uri.toString(), { encoding: 'utf-8' }); + if (toDispose.disposed) { + return; + } const snippets = content && jsoncparser.parse(content, undefined, { disallowComments: false }); - this.fromJSON(snippets, options); + toDispose.push(this.fromJSON(snippets, options)); } catch (e) { if (!FileSystemError.FileNotFound.is(e) && !FileSystemError.FileIsDirectory.is(e)) { console.error(e); @@ -140,7 +152,8 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } - fromJSON(snippets: JsonSerializedSnippets | undefined, { language, source }: SnippetLoadOptions): void { + fromJSON(snippets: JsonSerializedSnippets | undefined, { language, source }: SnippetLoadOptions): Disposable { + const toDispose = new DisposableCollection(); this.parseSnippets(snippets, (name, snippet) => { let { prefix, body, description } = snippet; if (Array.isArray(body)) { @@ -164,15 +177,16 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } } - this.push({ + toDispose.push(this.push({ scopes, name, prefix, description, body, source - }); + })); }); + return toDispose; } protected parseSnippets(snippets: JsonSerializedSnippets | undefined, accept: (name: string, snippet: JsonSerializedSnippet) => void): void { if (typeof snippets === 'object') { @@ -188,14 +202,22 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion } } - push(...snippets: Snippet[]): void { + push(...snippets: Snippet[]): Disposable { + const toDispose = new DisposableCollection(); for (const snippet of snippets) { for (const scope of snippet.scopes) { const languageSnippets = this.snippets.get(scope) || []; languageSnippets.push(snippet); this.snippets.set(scope, languageSnippets); + toDispose.push(Disposable.create(() => { + const index = languageSnippets.indexOf(snippet); + if (index !== -1) { + languageSnippets.splice(index, 1); + } + })); } } + return toDispose; } protected isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean { 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-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index ceccf1a051d8a..99e72066820dd 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -163,7 +163,7 @@ export const emptyPlugin: Plugin = { }; export interface PluginManagerExt { - $stopPlugin(contextPath: string): PromiseLike; + $stop(pluginId?: string): PromiseLike; $init(pluginInit: PluginInitData, configStorage: ConfigStorage): PromiseLike; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 4c71a8ef1acc8..cc5d2cf3edf52 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -23,6 +23,7 @@ import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-sch import { RecursivePartial } from '@theia/core/lib/common/types'; import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/common/preferences/preference-schema'; import { ProblemMatcherContribution, ProblemPatternContribution, TaskDefinition } from '@theia/task/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common'; export const hostedServicePath = '/services/hostedPlugin'; @@ -602,10 +603,6 @@ export function buildFrontendModuleName(plugin: PluginPackage | PluginModel): st export const HostedPluginClient = Symbol('HostedPluginClient'); export interface HostedPluginClient { - setClientId(clientId: number): Promise; - - getClientId(): Promise; - postMessage(message: string): Promise; log(logPart: LogPart): void; @@ -634,6 +631,13 @@ export interface HostedPluginServer extends JsonRpcServer { } +export interface WorkspaceStorageKind { + workspace?: FileStat | undefined; + roots: FileStat[]; +} +export type GlobalStorageKind = undefined; +export type PluginStorageKind = GlobalStorageKind | WorkspaceStorageKind; + /** * The JSON-RPC workspace interface. */ @@ -646,9 +650,9 @@ export interface PluginServer { */ deploy(pluginEntry: string): Promise; - keyValueStorageSet(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise; - keyValueStorageGet(key: string, isGlobal: boolean): Promise; - keyValueStorageGetAll(isGlobal: boolean): Promise; + setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise; + getStorageValue(key: string, kind: PluginStorageKind): Promise; + getAllStorageValues(kind: PluginStorageKind): Promise; } export const ServerPluginRunner = Symbol('ServerPluginRunner'); diff --git a/packages/plugin-ext/src/common/rpc-protocol.ts b/packages/plugin-ext/src/common/rpc-protocol.ts index 4b883727aa05e..443b3574efdaf 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'; @@ -34,7 +35,7 @@ export interface MessageConnection { } export const RPCProtocol = Symbol('RPCProtocol'); -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. */ @@ -61,37 +62,60 @@ 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(() => { /* mark as no disposed */ }) + ); + 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( + 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; } @@ -111,7 +135,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) { @@ -128,7 +152,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; } @@ -168,7 +192,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(); } @@ -183,42 +207,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) { @@ -239,10 +258,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); @@ -262,28 +281,35 @@ function canceled(): Error { * - multiple messages to be sent from one stack get sent in bulk at `process.nextTick`. * - each incoming message is handled in a separate `process.nextTick`. */ -class RPCMultiplexer { +class RPCMultiplexer implements Disposable { private readonly connection: MessageConnection; private readonly sendAccumulatedBound: () => void; private messagesToSend: string[]; + private readonly toDispose = new DisposableCollection(); + constructor(connection: MessageConnection, onMessage: (msg: string) => void, remoteHostId?: string) { this.connection = connection; this.sendAccumulatedBound = this.sendAccumulated.bind(this); + this.toDispose.push(Disposable.create(() => this.messagesToSend = [])); + this.toDispose.push(this.connection.onMessage((data: string[]) => { + const len = data.length; + for (let i = 0; i < len; i++) { + onMessage(data[i]); + } + })); + this.messagesToSend = []; if (remoteHostId) { this.send(`{"setHostID":"${remoteHostId}"}`); } + } - this.connection.onMessage((data: string[]) => { - const len = data.length; - for (let i = 0; i < len; i++) { - onMessage(data[i]); - } - }); + dispose(): void { + this.toDispose.dispose(); } private sendAccumulated(): void { @@ -293,6 +319,9 @@ class RPCMultiplexer { } public send(msg: string): void { + if (this.toDispose.disposed) { + throw new Error('connection is closed'); + } if (this.messagesToSend.length === 0) { if (typeof setImmediate !== 'undefined') { setImmediate(this.sendAccumulatedBound); diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin-watcher.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin-watcher.ts index 2b13be34de9d1..3efc0db7c66ba 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin-watcher.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin-watcher.ts @@ -30,13 +30,7 @@ export class HostedPluginWatcher { getHostedPluginClient(): HostedPluginClient { const messageEmitter = this.onPostMessage; const logEmitter = this.onLogMessage; - let clientId = 0; return { - getClientId: () => Promise.resolve(clientId), - setClientId: (id: number) => { - clientId = id; - return Promise.resolve(); - }, postMessage(message: string): Promise { messageEmitter.fire(JSON.parse(message)); return Promise.resolve(); diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index bd4c86a261fe8..2f1ed0bece88f 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -23,19 +23,18 @@ 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, HostedPluginServer } 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, JsonRpcProxy } 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'; import { getQueryParameters } from '../../main/browser/env-main'; import { ExtPluginApi, MainPluginApiProvider } from '../../common/plugin-ext-api-contribution'; import { PluginPathsService } from '../../main/common/plugin-paths-protocol'; -import { StoragePathService } from '../../main/browser/storage-path-service'; import { getPreferences } from '../../main/browser/preference-registry-main'; import { PluginServer } from '../../common/plugin-protocol'; import { KeysToKeysToAnyValue } from '../../common/types'; @@ -46,7 +45,7 @@ import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-mana import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; import { WaitUntilEvent } from '@theia/core/lib/common/event'; import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; -import { isCancelled } from '@theia/core'; +import { Emitter, isCancelled } from '@theia/core'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry'; import { TaskProviderRegistry, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution'; @@ -62,7 +61,7 @@ export class HostedPluginSupport { protected readonly logger: ILogger; @inject(HostedPluginServer) - private readonly server: HostedPluginServer; + private readonly server: JsonRpcProxy; @inject(HostedPluginWatcher) private readonly watcher: HostedPluginWatcher; @@ -86,9 +85,6 @@ export class HostedPluginSupport { @inject(PluginPathsService) private readonly pluginPathsService: PluginPathsService; - @inject(StoragePathService) - private readonly storagePathService: StoragePathService; - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -121,17 +117,19 @@ export class HostedPluginSupport { private theiaReadyPromise: Promise; - protected readonly managers: PluginManagerExt[] = []; + protected readonly managers = new Map(); - // loaded plugins per #id - private readonly loadedPlugins = new Set(); + private readonly contributions = new Map(); protected readonly activationEvents = new Set(); + protected readonly onDidChangePluginsEmitter = new Emitter(); + readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event; + @postConstruct() protected init(): void { this.theiaReadyPromise = Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]); - this.storagePathService.onStoragePathChanged(path => this.updateStoragePath(path)); + this.workspaceService.onWorkspaceChanged(() => this.updateStoragePath()); for (const id of this.monacoTextmateService.activatedLanguages) { this.activateByLanguage(id); @@ -146,64 +144,158 @@ export class HostedPluginSupport { this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); } - checkAndLoadPlugin(container: interfaces.Container): void { + get plugins(): PluginMetadata[] { + const plugins: PluginMetadata[] = []; + this.contributions.forEach(contributions => plugins.push(contributions.plugin)); + return plugins; + } + + /** do not call it, except from the plugin frontend contribution */ + onStart(container: interfaces.Container): void { this.container = container; - this.initPlugins(); + this.load(); + this.watcher.onDidDeploy(() => this.load()); + 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 roots = this.workspaceService.tryGetRoots(); + const [plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates] = await Promise.all([ + this.server.getDeployedMetadata(), + this.pluginPathsService.getHostLogPath(), + this.getStoragePath(), + this.server.getExtPluginAPI(), + this.pluginServer.getAllStorageValues(undefined), + this.pluginServer.getAllStorageValues({ workspace: this.workspaceService.workspace, roots }) + ]); + await this.doLoad({ plugins, logPath, storagePath, pluginAPIs, globalStates, workspaceStates, roots }, this.container); + } 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 contributions + return; } + const contributionsByHost = this.loadContributions(initData.plugins, toDisconnect); + await this.viewRegistry.initWidgets(); // remove restored plugin widgets which were not registered by contributions this.viewRegistry.removeStaleWidgets(); await this.theiaReadyPromise; - for (const [host, plugins] of hostToPlugins) { - const pluginId = getPluginId(plugins[0].model); - const rpc = this.initRpc(host, pluginId, container); - this.initPluginHostManager(rpc, { ...initData, plugins }); + + if (toDisconnect.disposed) { + // if disconnected then don't try to init plugin code and dynamic contributions + return; } + toDisconnect.push(this.startPlugins(contributionsByHost, initData, container)); + } + + /** + * Always synchronous in order to simplify handling disconnections. + * @throws never + */ + protected loadContributions(plugins: PluginMetadata[], toDisconnect: DisposableCollection): Map { + const hostContributions = new Map(); + const toUnload = new Set(this.contributions.keys()); + let loaded = false; + for (const plugin of plugins) { + const pluginId = plugin.model.id; + toUnload.delete(pluginId); + + let contributions = this.contributions.get(pluginId); + if (!contributions) { + contributions = new PluginContributions(plugin); + this.contributions.set(pluginId, contributions); + contributions.push(Disposable.create(() => this.contributions.delete(pluginId))); + } + + if (contributions.state === PluginContributions.State.INITIALIZING) { + contributions.state = PluginContributions.State.LOADING; + contributions.push(Disposable.create(() => console.log(`[${plugin.model.id}]: Unloaded plugin.`))); + contributions.push(this.contributionHandler.handleContributions(plugin)); + contributions.state = PluginContributions.State.LOADED; + console.log(`[${plugin.model.id}]: Loaded contributions.`); + loaded = true; + } - // update list with loaded plugins - initData.plugins.forEach(value => this.loadedPlugins.add(value.model.id)); + if (contributions.state === PluginContributions.State.LOADED) { + contributions.state = PluginContributions.State.STARTING; + const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host; + const dynamicContributions = hostContributions.get(plugin.host) || []; + dynamicContributions.push(contributions); + hostContributions.set(host, dynamicContributions); + toDisconnect.push(Disposable.create(() => { + contributions!.state = PluginContributions.State.LOADED; + console.log(`[${plugin.model.id}]: Disconnected.`); + })); + } + } + for (const pluginId of toUnload) { + const contribution = this.contributions.get(pluginId); + if (contribution) { + contribution.dispose(); + } + } + if (loaded || toUnload.size) { + this.onDidChangePluginsEmitter.fire(undefined); + } + return hostContributions; + } + + protected startPlugins( + contributionsByHost: Map, + initData: PluginsInitializationData, + container: interfaces.Container + ): Disposable { + const toDisconnect = new DisposableCollection(); + for (const [host, hostContributions] of contributionsByHost) { + const manager = this.obtainManager(host, hostContributions, container, toDisconnect); + this.initPlugins(manager, { + ...initData, + plugins: hostContributions.map(contributions => contributions.plugin) + }).then(() => { + if (toDisconnect.disposed) { + return; + } + for (const contributions of hostContributions) { + const plugin = contributions.plugin; + const id = plugin.model.id; + contributions.state = PluginContributions.State.STARTED; + console.log(`[${id}]: Started plugin.`); + toDisconnect.push(contributions.push(Disposable.create(() => { + console.log(`[${id}]: Stopped plugin.`); + manager.$stop(id); + }))); + + this.activateByWorkspaceContains(manager, plugin); + } + }); + } + return toDisconnect; + } + + protected obtainManager(host: string, hostContributions: PluginContributions[], container: interfaces.Container, toDispose: DisposableCollection): PluginManagerExt { + let manager = this.managers.get(host); + if (!manager) { + const pluginId = getPluginId(hostContributions[0].plugin.model); + const rpc = this.initRpc(host, pluginId, container); + toDispose.push(rpc); + manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); + this.managers.set(host, manager); + toDispose.push(Disposable.create(() => this.managers.delete(host))); + } + return manager; } protected initRpc(host: PluginHost, pluginId: string, container: interfaces.Container): RPCProtocol { @@ -213,10 +305,7 @@ export class HostedPluginSupport { return rpc; } - protected async initPluginHostManager(rpc: RPCProtocol, data: PluginsInitializationData): Promise { - const manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - this.managers.push(manager); - + protected async initPlugins(manager: PluginManagerExt, data: PluginsInitializationData): Promise { await manager.$init({ plugins: data.plugins, preferences: getPreferences(this.preferenceProviderProvider, data.roots), @@ -229,10 +318,6 @@ export class HostedPluginSupport { hostLogPath: data.logPath, hostStoragePath: data.storagePath || '' }); - - for (const plugin of data.plugins) { - this.activateByWorkspaceContains(manager, plugin); - } } private createServerRpc(pluginID: string, hostID: string): RPCProtocol { @@ -247,19 +332,24 @@ export class HostedPluginSupport { }, hostID); } - private updateStoragePath(path: string | undefined): void { - for (const manager of this.managers) { + private async updateStoragePath(): Promise { + const path = await this.getStoragePath(); + for (const manager of this.managers.values()) { manager.$updateStoragePath(path); } } + protected getStoragePath(): Promise { + return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace, this.workspaceService.tryGetRoots()); + } + async activateByEvent(activationEvent: string): Promise { if (this.activationEvents.has(activationEvent)) { return; } this.activationEvents.add(activationEvent); const activation: Promise[] = []; - for (const manager of this.managers) { + for (const manager of this.managers.values()) { activation.push(manager.$activateByEvent(activationEvent)); } await Promise.all(activation); @@ -381,3 +471,21 @@ interface PluginsInitializationData { workspaceStates: KeysToKeysToAnyValue, roots: FileStat[], } + +export class PluginContributions extends DisposableCollection { + constructor( + readonly plugin: PluginMetadata + ) { + super(); + } + state: PluginContributions.State = PluginContributions.State.INITIALIZING; +} +export namespace PluginContributions { + export enum State { + INITIALIZING = 0, + LOADING = 1, + LOADED = 2, + STARTING = 3, + STARTED = 4 + } +} diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts index 25b7be5d9428e..553621bb04fd5 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts @@ -24,7 +24,6 @@ import { HostedPluginClient, ServerPluginRunner, PluginMetadata, PluginHostEnvir import { RPCProtocolImpl } from '../../common/rpc-protocol'; import { MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution'; -import { HostedPluginProcessesCache } from './hosted-plugin-processes-cache'; import * as psTree from 'ps-tree'; export interface IPCConnectionOptions { @@ -43,9 +42,6 @@ export class HostedPluginProcess implements ServerPluginRunner { @inject(HostedPluginCliContribution) protected readonly cli: HostedPluginCliContribution; - @inject(HostedPluginProcessesCache) - protected readonly pluginProcessCache: HostedPluginProcessesCache; - @inject(ContributionProvider) @named(PluginHostEnvironmentVariable) protected readonly pluginHostEnvironmentVariables: ContributionProvider; @@ -54,15 +50,10 @@ export class HostedPluginProcess implements ServerPluginRunner { protected readonly messageService: MessageService; private childProcess: cp.ChildProcess | undefined; - private client: HostedPluginClient; private terminatingPluginServer = false; - private async getClientId(): Promise { - return await this.pluginProcessCache.getLazyClientId(this.client); - } - public setClient(client: HostedPluginClient): void { if (this.client) { if (this.childProcess) { @@ -70,13 +61,6 @@ export class HostedPluginProcess implements ServerPluginRunner { } } this.client = client; - this.getClientId().then(clientId => { - const childProcess = this.pluginProcessCache.retrieveClientChildProcess(clientId); - if (!this.childProcess && childProcess) { - this.childProcess = childProcess; - this.linkClientWithChildProcess(this.childProcess); - } - }); } public clientClosed(): void { @@ -99,12 +83,6 @@ export class HostedPluginProcess implements ServerPluginRunner { } } - public markPluginServerTerminated(): void { - if (this.childProcess) { - this.pluginProcessCache.scheduleChildProcessTermination(this, this.childProcess); - } - } - public terminatePluginServer(): void { if (this.childProcess === undefined) { return; @@ -128,7 +106,7 @@ export class HostedPluginProcess implements ServerPluginRunner { } }); const hostedPluginManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); - hostedPluginManager.$stopPlugin('').then(() => { + hostedPluginManager.$stop().then(() => { emitter.dispose(); this.killProcessTree(cp.pid); }); @@ -153,19 +131,11 @@ export class HostedPluginProcess implements ServerPluginRunner { logger: this.logger, args: [] }); - this.linkClientWithChildProcess(this.childProcess); - - } - - private linkClientWithChildProcess(childProcess: cp.ChildProcess): void { - childProcess.on('message', message => { + this.childProcess.on('message', message => { if (this.client) { this.client.postMessage(message); } }); - this.getClientId().then(clientId => { - this.pluginProcessCache.linkLiveClientAndProcess(clientId, childProcess); - }); } readonly HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION = new RegExp('HOSTED_PLUGIN*'); diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-processes-cache.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-processes-cache.ts deleted file mode 100644 index 7045fdd6b9733..0000000000000 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-processes-cache.ts +++ /dev/null @@ -1,73 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 SAP SE or an SAP affiliate company and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable } from 'inversify'; -import * as cp from 'child_process'; -import { HostedPluginProcess } from './hosted-plugin-process'; -import { HostedPluginClient } from '../../common/plugin-protocol'; - -const DEF_MIN_KEEP_ALIVE_DISCONNECT_TIME = 5 * 1000; // 5 seconds - -@injectable() -export class HostedPluginProcessesCache { - - // child processes are kept for one minute in order to reuse them in case of network disconnections - private cachedCPMap: Map = new Map(); - - // client ids sequence - private clientIdSeq = 1; - - private minKeepAliveDisconnectTime: number = process.env.THEIA_PLUGIN_HOST_MIN_KEEP_ALIVE ? - parseInt(process.env.THEIA_PLUGIN_HOST_MIN_KEEP_ALIVE) : DEF_MIN_KEEP_ALIVE_DISCONNECT_TIME; - - public async getLazyClientId(client: HostedPluginClient): Promise { - let clientId = await client.getClientId(); - if (clientId && clientId <= this.clientIdSeq) { - return clientId; - } - clientId = this.clientIdSeq++; - await client.setClientId(clientId); - return clientId; - } - - public linkLiveClientAndProcess(clientId: number, childProcess: cp.ChildProcess): void { - this.cachedCPMap.set(clientId, { - cp: childProcess, - toBeKilledAfter: Infinity - }); - } - - public retrieveClientChildProcess(clientID: number): cp.ChildProcess | undefined { - const childProcessDatum = this.cachedCPMap.get(clientID); - return childProcessDatum && childProcessDatum.cp; - } - - public scheduleChildProcessTermination(hostedPluginProcess: HostedPluginProcess, childProcess: cp.ChildProcess): void { - for (const cachedChildProcessesDatum of this.cachedCPMap.values()) { - if (cachedChildProcessesDatum.cp === childProcess) { - cachedChildProcessesDatum.toBeKilledAfter = new Date().getTime() + this.minKeepAliveDisconnectTime; - } - } - setTimeout(() => { - this.cachedCPMap.forEach((cachedChildProcessesDatum, clientId) => { - if (cachedChildProcessesDatum.cp === childProcess && cachedChildProcessesDatum.toBeKilledAfter < new Date().getTime()) { - this.cachedCPMap.delete(clientId); - hostedPluginProcess.terminatePluginServer(); - } - }); - }, this.minKeepAliveDisconnectTime * 2); - } -} diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts index 7a16ddeb604df..9da14b926b43c 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts @@ -88,7 +88,7 @@ export class HostedPluginSupport { } private terminatePluginServer(): void { - this.hostedPluginProcess.markPluginServerTerminated(); + this.hostedPluginProcess.terminatePluginServer(); } public runPluginServer(): void { diff --git a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts index f762cd5ca6769..9dd692161896a 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts @@ -30,7 +30,6 @@ import { HostedPluginProcess } from './hosted-plugin-process'; import { ExtPluginApiProvider } from '../../common/plugin-ext-api-contribution'; import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution'; import { HostedPluginDeployerHandler } from './hosted-plugin-deployer-handler'; -import { HostedPluginProcessesCache } from './hosted-plugin-processes-cache'; const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(HostedPluginProcess).toSelf().inSingletonScope(); @@ -49,7 +48,6 @@ const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, b }); export function bindCommonHostedBackend(bind: interfaces.Bind): void { - bind(HostedPluginProcessesCache).toSelf().inSingletonScope(); bind(HostedPluginCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(HostedPluginCliContribution); diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index 59a19938d406b..9decabc2591f6 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -80,16 +80,6 @@ export class PluginHostRPC { } } - /* - * Stop the given context by calling the plug-in manager. - * note: stopPlugin can also be invoked through RPC proxy. - */ - stopContext(): PromiseLike { - const promise = this.pluginManager.$stopPlugin(''); - promise.then(() => delete this.apiFactory); - return promise; - } - // tslint:disable-next-line:no-any createPluginManager(envExt: EnvExtImpl, preferencesManager: PreferenceRegistryExtImpl, rpc: any): PluginManagerExtImpl { const { extensionTestsPath } = process.env; 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..756a204e58019 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 { Disposable, 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,17 @@ 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); + if (!toClose.disposed) { + this.proxy.$deleteConnection(id); + } }); + const toClose = new DisposableCollection(Disposable.create(() => reader.fireClose())); + this.toDispose.push(toClose); + 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..8c33989e75272 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) ), @@ -145,16 +159,15 @@ export class DebugMainImpl implements DebugMain { debugSessionFactory: () => debugSessionFactory }) ]); + this.toDispose.push(Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType))); this.debugSchemaUpdater.update(); } 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..680bf81194efa 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 { injectable, inject, postConstruct } 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,45 +28,44 @@ 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 nextSubscriberId: number; + private readonly subscribers = new Map(); + private nextSubscriberId = 0; - constructor(proxy: WorkspaceExt, container: interfaces.Container) { - this.proxy = proxy; - this.subscribers = new Map(); - this.nextSubscriberId = 0; + @inject(FileSystemWatcher) + private readonly fileSystemWatcher: FileSystemWatcher; - const fileSystemWatcher = container.get(FileSystemWatcher); - fileSystemWatcher.onFilesChanged(event => this.onFilesChangedEventHandler(event)); - fileSystemWatcher.onDidMove(event => this.onDidMoveEventHandler(event)); - fileSystemWatcher.onWillMove(event => this.onWillMoveEventHandler(event)); + @postConstruct() + protected init(): void { + this.fileSystemWatcher.onFilesChanged(event => this.onFilesChangedEventHandler(event)); + this.fileSystemWatcher.onDidMove(event => this.onDidMoveEventHandler(event)); + this.fileSystemWatcher.onWillMove(event => this.onWillMoveEventHandler(event)); } // 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..2cd13363a57af 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 @@ -16,39 +16,30 @@ import { injectable, inject } from 'inversify'; import { PluginContribution, Keybinding as PluginKeybinding } from '../../../common'; -import { Keybinding, KeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding'; -import { ILogger } from '@theia/core/lib/common/logger'; +import { Keybinding } from '@theia/core/lib/common/keybinding'; +import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { OS } from '@theia/core/lib/common/os'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { DisposableCollection } from '@theia/core'; @injectable() export class KeybindingsContributionPointHandler { - @inject(ILogger) - protected readonly logger: ILogger; - @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[] = []; + const toDispose = new DisposableCollection(); for (const raw of contributions.keybindings) { const keybinding = this.toKeybinding(raw); if (keybinding) { - try { - const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(keybinding); - const keybindingResult = this.keybindingRegistry.getKeybindingsForKeySequence(bindingKeySequence); - this.handleShadingKeybindings(keybinding, keybindingResult.shadow); - this.handlePartialKeybindings(keybinding, keybindingResult.partial); - keybindings.push(keybinding); - } catch (e) { - this.logger.error(e.message || e); - } + toDispose.push(this.keybindingRegistry.registerKeybinding(keybinding)); } } - this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings); + return toDispose; } protected toKeybinding(pluginKeybinding: PluginKeybinding): Keybinding | undefined { @@ -72,22 +63,4 @@ export class KeybindingsContributionPointHandler { } return keybinding || pluginKeybinding.keybinding; } - - private handlePartialKeybindings(keybinding: Keybinding, partialKeybindings: Keybinding[]): void { - partialKeybindings.forEach(partial => { - if (keybinding.context === undefined || keybinding.context === partial.context) { - this.logger.warn(`Partial keybinding is ignored; ${Keybinding.stringify(keybinding)} shadows ${Keybinding.stringify(partial)}`); - } - }); - } - - private handleShadingKeybindings(keybinding: Keybinding, shadingKeybindings: Keybinding[]): void { - shadingKeybindings.forEach(shadow => { - if (shadow.context === undefined || shadow.context === keybinding.context) { - this.keybindingRegistry.unregisterKeybinding(shadow); - - this.logger.warn(`Shadowing keybinding is ignored; ${Keybinding.stringify(shadow)}, shadows ${Keybinding.stringify(keybinding)}`); - } - }); - } } 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 d94af4606b558..6687f5e475bfa 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -41,16 +41,17 @@ import { } 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'; @injectable() -export class LanguagesMainImpl implements LanguagesMain { +export class LanguagesMainImpl implements LanguagesMain, Disposable { @inject(MonacoLanguages) private readonly monacoLanguages: MonacoLanguages; @@ -59,12 +60,17 @@ export class LanguagesMainImpl implements LanguagesMain { private readonly problemManager: ProblemManager; private readonly proxy: LanguagesExt; - private readonly disposables = new Map(); + private readonly services = new Map(); + private readonly toDispose = new DisposableCollection(); constructor(@inject(RPCProtocol) rpc: RPCProtocol) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.LANGUAGES_EXT); } + dispose(): void { + this.toDispose.dispose(); + } + $getLanguages(): Promise { return Promise.resolve(monaco.languages.getLanguages().map(l => l.id)); } @@ -83,11 +89,16 @@ export class LanguagesMainImpl implements LanguagesMain { return Promise.resolve(undefined); } + protected register(handle: number, service: Disposable): void { + this.services.set(handle, service); + this.toDispose.push(Disposable.create(() => this.$unregister(handle))); + } + $unregister(handle: number): void { - const disposable = this.disposables.get(handle); + const disposable = this.services.get(handle); if (disposable) { + this.services.delete(handle); disposable.dispose(); - this.disposables.delete(handle); } } @@ -100,11 +111,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.provideCompletionItems(handle, model, position, context, token), resolveCompletionItem: supportsResolveDetails @@ -135,25 +146,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 { @@ -184,9 +189,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 { @@ -205,9 +208,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 { @@ -243,9 +244,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 { @@ -281,9 +280,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 { @@ -300,9 +297,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 { @@ -336,9 +331,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 { @@ -359,9 +352,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 { @@ -404,13 +395,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 { @@ -441,7 +430,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); } @@ -450,10 +439,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 { @@ -555,9 +541,7 @@ export class LanguagesMainImpl implements LanguagesMain { $registerDocumentFormattingSupport(handle: number, selector: SerializedDocumentFilter[]): void { const languageSelector = fromLanguageSelector(selector); const documentFormattingEditSupport = this.createDocumentFormattingSupport(handle); - const disposable = new DisposableCollection(); - disposable.push(monaco.languages.registerDocumentFormattingEditProvider(languageSelector, documentFormattingEditSupport)); - this.disposables.set(handle, disposable); + this.register(handle, monaco.languages.registerDocumentFormattingEditProvider(languageSelector, documentFormattingEditSupport)); } createDocumentFormattingSupport(handle: number): monaco.languages.DocumentFormattingEditProvider { @@ -574,9 +558,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 { @@ -593,9 +575,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( @@ -616,9 +596,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 { @@ -635,9 +613,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 { @@ -683,9 +659,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 { @@ -712,9 +686,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 65d1e1cd0621b..03f286440d188 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -39,6 +39,14 @@ import { FileSystemMainImpl } from './file-system-main'; import { ScmMainImpl } from './scm-main'; import { DecorationsMainImpl } from './decorations/decorations-main'; import { ClipboardMainImpl } from './clipboard-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); @@ -59,9 +67,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/message-registry-main.ts b/packages/plugin-ext/src/main/browser/message-registry-main.ts index 74062a512b328..b57e775cb563b 100644 --- a/packages/plugin-ext/src/main/browser/message-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/message-registry-main.ts @@ -20,7 +20,7 @@ import { MessageRegistryMain, MainMessageType, MainMessageOptions } from '../../ import { ModalNotification, MessageType } from './dialogs/modal-notification'; export class MessageRegistryMainImpl implements MessageRegistryMain { - private messageService: MessageService; + private readonly messageService: MessageService; constructor(container: interfaces.Container) { this.messageService = container.get(MessageService); 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..271151f02a861 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -19,7 +19,7 @@ import { ITokenTypeMap, IEmbeddedLanguagesMap, StandardTokenType } from 'vscode- import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarDefinition } from '@theia/monaco/lib/browser/textmate'; import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginViewRegistry } from './view/plugin-view-registry'; -import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '../../common'; +import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, PluginMetadata } from '../../common'; import { PreferenceSchemaProvider } from '@theia/core/lib/browser'; import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences'; import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler'; @@ -29,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,44 @@ export class PluginContributionHandler { protected readonly onDidRegisterCommandHandlerEmitter = new Emitter(); readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event; - handleContributions(contributions: PluginContribution): void { - if (contributions.configuration) { - if (Array.isArray(contributions.configuration)) { - for (const config of contributions.configuration) { - this.updateConfigurationSchema(config); + /** + * Always synchronous in order to simplify handling disconnections. + * @throws never, loading of each contribution should handle errors + * in order to avoid preventing loading of other contibutions or extensions + */ + handleContributions(plugin: PluginMetadata): Disposable { + const contributions = plugin.model.contributes; + if (!contributions) { + return Disposable.NULL; + } + const toDispose = new DisposableCollection; + const pushContribution = async (id: string, contribute: () => Disposable) => { + try { + toDispose.push(contribute()); + } catch (e) { + console.error(`[${plugin.model.id}]: Failed to load '${id}' contribution.`, e); + } + }; + + const configuration = contributions.configuration; + if (configuration) { + if (Array.isArray(configuration)) { + for (const config of configuration) { + pushContribution('configuration', () => this.updateConfigurationSchema(config)); } } else { - this.updateConfigurationSchema(contributions.configuration); + pushContribution('configuration', () => this.updateConfigurationSchema(configuration)); } } - if (contributions.configurationDefaults) { - this.updateDefaultOverridesSchema(contributions.configurationDefaults); + + const configurationDefaults = contributions.configurationDefaults; + if (configurationDefaults) { + pushContribution('configurationDefaults', () => this.updateDefaultOverridesSchema(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, @@ -106,69 +123,78 @@ export class PluginContributionHandler { firstLine: lang.firstLine, mimetypes: lang.mimetypes }); - if (lang.configuration) { - monaco.languages.setLanguageConfiguration(lang.id, { - wordPattern: this.createRegex(lang.configuration.wordPattern), - autoClosingPairs: lang.configuration.autoClosingPairs, - brackets: lang.configuration.brackets, - comments: lang.configuration.comments, - folding: this.convertFolding(lang.configuration.folding), - surroundingPairs: lang.configuration.surroundingPairs, - indentationRules: this.convertIndentationRules(lang.configuration.indentationRules) - }); + const langConfiguration = lang.configuration; + if (langConfiguration) { + pushContribution(`language.${lang.id}.configuration`, () => monaco.languages.setLanguageConfiguration(lang.id, { + wordPattern: this.createRegex(langConfiguration.wordPattern), + autoClosingPairs: langConfiguration.autoClosingPairs, + brackets: langConfiguration.brackets, + comments: langConfiguration.comments, + folding: this.convertFolding(langConfiguration.folding), + surroundingPairs: langConfiguration.surroundingPairs, + indentationRules: this.convertIndentationRules(langConfiguration.indentationRules) + })); } } } - if (contributions.grammars && contributions.grammars.length) { - for (const grammar of contributions.grammars) { + const grammars = contributions.grammars; + if (grammars && grammars.length) { + toDispose.push(Disposable.create(() => this.monacoTextmateService.detectLanguages())); + for (const grammar of grammars) { if (grammar.injectTo) { for (const injectScope of grammar.injectTo) { - let injections = this.injections.get(injectScope); - if (!injections) { - injections = []; + pushContribution(`grammar.injectTo.${injectScope}`, () => { + const injections = this.injections.get(injectScope) || []; + injections.push(grammar.scope); this.injections.set(injectScope, injections); - } - injections.push(grammar.scope); + return Disposable.create(() => { + const index = injections.indexOf(grammar.scope); + if (index !== -1) { + injections.splice(index, 1); + } + }); + }); } - } - this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { - async getGrammarDefinition(): Promise { - return { - format: grammar.format, - content: grammar.grammar || '', - location: grammar.grammarLocation - }; - }, - getInjections: (scopeName: string) => - this.injections.get(scopeName)! - }); - if (grammar.language) { - this.grammarsRegistry.mapLanguageIdToTextmateGrammar(grammar.language, grammar.scope); - 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(); + pushContribution(`grammar.textmate.scope.${grammar.scope}`, () => this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { + async getGrammarDefinition(): Promise { + return { + format: grammar.format, + content: grammar.grammar || '', + location: grammar.grammarLocation + }; + }, + getInjections: (scopeName: string) => + this.injections.get(scopeName)! + })); + const language = grammar.language; + if (language) { + pushContribution(`grammar.language.${language}.scope`, () => this.grammarsRegistry.mapLanguageIdToTextmateGrammar(language, grammar.scope)); + pushContribution(`grammar.language.${language}.configuration`, () => this.grammarsRegistry.registerGrammarConfiguration(language, { + embeddedLanguages: this.convertEmbeddedLanguages(grammar.embeddedLanguages), + tokenTypes: this.convertTokenTypes(grammar.tokenTypes) + })); + pushContribution(`grammar.language.${language}.activation`, + () => monaco.languages.onLanguage(language, () => this.monacoTextmateService.activateLanguage(language)) + ); + } } } + this.monacoTextmateService.detectLanguages(); } - this.registerCommands(contributions); - this.menusContributionHandler.handle(contributions); - this.keybindingsContributionHandler.handle(contributions); + pushContribution('commands', () => this.registerCommands(contributions)); + pushContribution('menus', () => this.menusContributionHandler.handle(contributions)); + pushContribution('keybindings', () => 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); + pushContribution(`viewContainers.${viewContainer.id}`, + () => this.viewRegistry.registerViewContainer(location, viewContainer) + ); } } } @@ -177,46 +203,71 @@ 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); + pushContribution(`views.${view.id}`, + () => this.viewRegistry.registerView(location, view) + ); } } } if (contributions.snippets) { for (const snippet of contributions.snippets) { - this.snippetSuggestProvider.fromURI(snippet.uri, { + pushContribution(`snippets.${snippet.uri}`, () => this.snippetSuggestProvider.fromURI(snippet.uri, { language: snippet.language, source: snippet.source - }); + })); } } if (contributions.taskDefinitions) { - contributions.taskDefinitions.forEach(def => this.taskDefinitionRegistry.register(def)); + for (const taskDefinition of contributions.taskDefinitions) { + pushContribution(`taskDefinitions.${taskDefinition.taskType}`, + () => this.taskDefinitionRegistry.register(taskDefinition) + ); + } } if (contributions.problemPatterns) { - contributions.problemPatterns.forEach(pattern => this.problemPatternRegistry.register(pattern)); + for (const problemPattern of contributions.problemPatterns) { + pushContribution(`problemPatterns.${problemPattern.name || problemPattern.regexp}`, + () => this.problemPatternRegistry.register(problemPattern) + ); + } } if (contributions.problemMatchers) { - contributions.problemMatchers.forEach(matcher => this.problemMatcherRegistry.register(matcher)); + for (const problemMatcher of contributions.problemMatchers) { + pushContribution(`problemMatchers.${problemMatcher.label}`, + () => this.problemMatcherRegistry.register(problemMatcher) + ); + } } + + 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({ - id: command, - category, - label: title, - iconClass - }); + const value: Command = { id: command, category, label: title }; + if (iconUrl) { + this.style.toIconClass(iconUrl).then(reference => { + if (toDispose.disposed) { + reference.dispose(); + return; + } + toDispose.push(reference); + value.iconClass = reference.object.iconClass; + toDispose.push(this.registerCommand(value)); + }); + } else { + toDispose.push(this.registerCommand(value)); + } } + return toDispose; } registerCommand(command: Command): Disposable { @@ -253,12 +304,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 +327,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 ede346f859d73..5c6dfc110ea9d 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 { @@ -31,11 +32,9 @@ import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRp 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'; @@ -48,7 +47,6 @@ import { LanguageClientProvider } from '@theia/languages/lib/browser/language-cl import { LanguageClientProviderImpl } from './language-provider/plugin-language-client-provider'; import { LanguageClientContributionProviderImpl } from './language-provider/language-client-contribution-provider-impl'; import { LanguageClientContributionProvider } from './language-provider/language-client-contribution-provider'; -import { StoragePathService } from './storage-path-service'; import { DebugSessionContributionRegistry } from '@theia/debug/lib/browser/debug-session-contribution'; import { PluginDebugSessionContributionRegistry } from './debug/plugin-debug-session-contribution-registry'; import { PluginDebugService } from './debug/plugin-debug-service'; @@ -64,6 +62,7 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common'; import { LanguagesMainImpl } from './languages-main'; import { OutputChannelRegistryMainImpl } from './output-channel-registry-main'; +import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -91,15 +90,15 @@ 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 => { @@ -112,7 +111,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { const connection = ctx.container.get(WebSocketConnectionProvider); return connection.createProxy(pluginPathsServicePath); }).inSingletonScope(); - bind(StoragePathService).toSelf().inSingletonScope(); bindViewContribution(bind, PluginFrontendViewContribution); @@ -172,6 +170,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/plugin-ext-widget.tsx b/packages/plugin-ext/src/main/browser/plugin-ext-widget.tsx index 58f4f2c96f09e..e134ab2845ce9 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-widget.tsx +++ b/packages/plugin-ext/src/main/browser/plugin-ext-widget.tsx @@ -17,28 +17,20 @@ import * as React from 'react'; import { injectable, inject, postConstruct } from 'inversify'; import { Message } from '@phosphor/messaging'; -import { DisposableCollection } from '@theia/core'; -import { OpenerService } from '@theia/core/lib/browser'; -import { HostedPluginServer, PluginMetadata } from '../../common/plugin-protocol'; +import { PluginMetadata } from '../../common/plugin-protocol'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message'; -import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher'; +import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; @injectable() export class PluginWidget extends ReactWidget { - protected plugins: PluginMetadata[] = []; - protected readonly toDisposeOnFetch = new DisposableCollection(); - protected readonly toDisposeOnSearch = new DisposableCollection(); - protected ready = false; + protected loaded = false; - @inject(HostedPluginWatcher) - protected readonly watcher: HostedPluginWatcher; + @inject(HostedPluginSupport) + protected readonly pluginService: HostedPluginSupport; - constructor( - @inject(HostedPluginServer) protected readonly hostedPluginServer: HostedPluginServer, - @inject(OpenerService) protected readonly openerService: OpenerService - ) { + constructor() { super(); this.id = 'plugins'; this.title.label = 'Plugins'; @@ -49,12 +41,14 @@ export class PluginWidget extends ReactWidget { this.addClass('theia-plugins'); this.update(); - this.fetchPlugins(); } @postConstruct() protected init(): void { - this.toDispose.push(this.watcher.onDidDeploy(() => this.fetchPlugins())); + this.toDispose.push(this.pluginService.onDidChangePlugins(() => { + this.loaded = true; + this.update(); + })); } protected onActivateRequest(msg: Message): void { @@ -62,23 +56,12 @@ export class PluginWidget extends ReactWidget { this.node.focus(); } - protected fetchPlugins(): Promise { - const promise = this.hostedPluginServer.getDeployedMetadata(); - - promise.then(pluginMetadatas => { - this.plugins = pluginMetadatas; - this.ready = true; - this.update(); - }); - return promise; - } - protected render(): React.ReactNode { - if (this.ready) { - if (!this.plugins.length) { + if (this.loaded) { + if (!this.pluginService.plugins.length) { return ; } - return {this.renderPluginList()}; + return {this.renderPlugins()}; } else { return
@@ -86,15 +69,9 @@ export class PluginWidget extends ReactWidget { } } - protected renderPluginList(): React.ReactNode { - const theList: React.ReactNode[] = []; - this.plugins.forEach(plugin => { - const container = this.renderPlugin(plugin); - theList.push(container); - }); - + protected renderPlugins(): React.ReactNode { return
- {theList} + {this.pluginService.plugins.map(plugin => this.renderPlugin(plugin))}
; } diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts index 220328c714cc4..e007e74f2ee8d 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -18,6 +18,16 @@ import { injectable } from 'inversify'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ThemeService, Theme, BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; import { IconUrl } from '../../common/plugin-protocol'; +import { ReferenceCollection, Reference } from '@theia/core/lib/common/reference'; + +export interface PluginIconKey { + url: IconUrl + size: number +} + +export interface PluginIcon extends Disposable { + readonly iconClass: string +} @injectable() export class PluginSharedStyle { @@ -83,26 +93,31 @@ export class PluginSharedStyle { } } + private readonly icons = new ReferenceCollection(key => this.createPluginIcon(key)); + toIconClass(url: IconUrl, { size }: { size: number } = { size: 16 }): Promise> { + return this.icons.acquire({ url, size }); + } + private iconSequence = 0; - private readonly icons = new Map(); - toIconClass(iconUrl: IconUrl, { size }: { size?: number } = { size: 16 }): string { + protected createPluginIcon(key: PluginIconKey): PluginIcon { + const iconUrl = key.url; + const size = key.size; const darkIconUrl = typeof iconUrl === 'object' ? iconUrl.dark : iconUrl; const lightIconUrl = typeof iconUrl === 'object' ? iconUrl.light : iconUrl; - const key = JSON.stringify({ lightIconUrl, darkIconUrl }); - let iconClass = this.icons.get(key); - if (typeof iconClass !== 'string') { - iconClass = 'plugin-icon-' + this.iconSequence++; - this.insertRule('.' + iconClass, theme => ` - display: inline-block; - background-position: 2px; - width: ${size}px; - height: ${size}px; - background: no-repeat url("${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl}"); - background-size: ${size}px; - `); - this.icons.set(key, iconClass); - } - return iconClass; + const iconClass = 'plugin-icon-' + this.iconSequence++; + const toDispose = new DisposableCollection(); + toDispose.push(this.insertRule('.' + iconClass, theme => ` + display: inline-block; + background-position: 2px; + width: ${size}px; + height: ${size}px; + background: no-repeat url("${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl}"); + background-size: ${size}px; + `)); + return { + iconClass, + dispose: () => toDispose.dispose() + }; } } diff --git a/packages/plugin-ext/src/main/browser/plugin-storage.ts b/packages/plugin-ext/src/main/browser/plugin-storage.ts index 4c757cd85443c..f7487342d4118 100644 --- a/packages/plugin-ext/src/main/browser/plugin-storage.ts +++ b/packages/plugin-ext/src/main/browser/plugin-storage.ts @@ -16,27 +16,40 @@ import { interfaces } from 'inversify'; import { StorageMain } from '../../common/plugin-api-rpc'; -import { PluginServer } from '../../common/plugin-protocol'; +import { PluginServer, PluginStorageKind } from '../../common/plugin-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; export class StorageMainImpl implements StorageMain { - private pluginServer: PluginServer; + private readonly pluginServer: PluginServer; + private readonly workspaceService: WorkspaceService; constructor(container: interfaces.Container) { this.pluginServer = container.get(PluginServer); + this.workspaceService = container.get(WorkspaceService); } $set(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise { - return this.pluginServer.keyValueStorageSet(key, value, isGlobal); + return this.pluginServer.setStorageValue(key, value, this.toKind(isGlobal)); } $get(key: string, isGlobal: boolean): Promise { - return this.pluginServer.keyValueStorageGet(key, isGlobal); + return this.pluginServer.getStorageValue(key, this.toKind(isGlobal)); } $getAll(isGlobal: boolean): Promise { - return this.pluginServer.keyValueStorageGetAll(isGlobal); + return this.pluginServer.getAllStorageValues(this.toKind(isGlobal)); + } + + protected toKind(isGlobal: boolean): PluginStorageKind { + if (isGlobal) { + return undefined; + } + return { + workspace: this.workspaceService.workspace, + roots: this.workspaceService.tryGetRoots() + }; } } 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..aff44b239822f 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); @@ -67,6 +69,10 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { this.sharedStyle = container.get(PluginSharedStyle); } + dispose(): void { + this.toDispose.dispose(); + } + private cleanUp(): void { this.items = undefined; this.acceptor = undefined; @@ -146,7 +152,7 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { return this.quickInput.open(options); } - convertQuickInputButton(quickInputButton: QuickInputButton, index: number): QuickInputTitleButtonHandle { + protected async convertQuickInputButton(quickInputButton: QuickInputButton, index: number, toDispose: DisposableCollection): Promise { const currentIconPath = quickInputButton.iconPath; let newIcon = ''; let newIconClass = ''; @@ -160,7 +166,13 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { light: light.toString(), dark: dark.toString() }; - newIconClass = this.sharedStyle.toIconClass(themedIconClasses); + const reference = await this.sharedStyle.toIconClass(themedIconClasses); + if (toDispose.disposed) { + reference.dispose(); + } else { + toDispose.push(reference); + newIconClass = reference.object.iconClass; + } } const isDefaultQuickInputButton = 'id' in quickInputButton.iconPath && quickInputButton.iconPath.id === 'Back' ? true : false; @@ -190,11 +202,16 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { } } - $showInputBox(inputBox: TransferInputBox, validateInput: boolean): void { + async $showInputBox(inputBox: TransferInputBox, validateInput: boolean): Promise { if (validateInput) { inputBox.validateInput = val => this.proxy.$validateInput(val); } + const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); + const buttons = await Promise.all(inputBox.buttons.map((btn, i) => this.convertQuickInputButton(btn, i, toDispose))); + if (toDispose.disposed) { + return; + } const quickInput = this.quickInput.open({ busy: inputBox.busy, enabled: inputBox.enabled, @@ -203,7 +220,7 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { step: inputBox.step, title: inputBox.title, totalSteps: inputBox.totalSteps, - buttons: inputBox.buttons.map((btn, i) => this.convertQuickInputButton(btn, i)), + buttons, validationMessage: inputBox.validationMessage, placeHolder: inputBox.placeholder, value: inputBox.value, @@ -211,18 +228,21 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { validateInput: inputBox.validateInput }); - const disposableListeners = new DisposableCollection(); - disposableListeners.push(this.quickInput.onDidAccept(() => this.proxy.$acceptOnDidAccept(inputBox.quickInputIndex))); - disposableListeners.push(this.quickInput.onDidChangeValue(changedText => this.proxy.$acceptDidChangeValue(inputBox.quickInputIndex, changedText))); - disposableListeners.push(this.quickTitleBar.onDidTriggerButton(button => { + toDispose.push(this.quickInput.onDidAccept(() => this.proxy.$acceptOnDidAccept(inputBox.quickInputIndex))); + toDispose.push(this.quickInput.onDidChangeValue(changedText => this.proxy.$acceptDidChangeValue(inputBox.quickInputIndex, changedText))); + toDispose.push(this.quickTitleBar.onDidTriggerButton(button => { this.proxy.$acceptOnDidTriggerButton(inputBox.quickInputIndex, button); })); + this.toDispose.push(toDispose); quickInput.then(selection => { + if (toDispose.disposed) { + return; + } if (selection) { this.proxy.$acceptDidChangeSelection(inputBox.quickInputIndex, selection as string); } this.proxy.$acceptOnDidHide(inputBox.quickInputIndex); - disposableListeners.dispose(); + toDispose.dispose(); }); } @@ -281,10 +301,16 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { this.quickInput.refresh(); } - $showCustomQuickPick(options: TransferQuickPick): void { + async $showCustomQuickPick(options: TransferQuickPick): Promise { + const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); + const buttons = await Promise.all(options.buttons.map((btn, i) => this.convertQuickInputButton(btn, i, toDispose))); + if (toDispose.disposed) { + return; + } + const items = this.convertPickOpenItemToQuickOpenItem(options.items); const quickPick = this.quickPick.show(items, { - buttons: options.buttons.map((btn, i) => this.convertQuickInputButton(btn, i)), + buttons, placeholder: options.placeholder, fuzzyMatchDescription: options.matchOnDescription, fuzzyMatchLabel: true, @@ -296,19 +322,22 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { runIfSingle: false, }); - const disposableListeners = new DisposableCollection(); - disposableListeners.push(this.quickPick.onDidAccept(() => this.proxy.$acceptOnDidAccept(options.quickInputIndex))); - disposableListeners.push(this.quickPick.onDidChangeActiveItems(changedItems => this.proxy.$acceptDidChangeActive(options.quickInputIndex, changedItems))); - disposableListeners.push(this.quickPick.onDidChangeValue(value => this.proxy.$acceptDidChangeValue(options.quickInputIndex, value))); - disposableListeners.push(this.quickTitleBar.onDidTriggerButton(button => { + toDispose.push(this.quickPick.onDidAccept(() => this.proxy.$acceptOnDidAccept(options.quickInputIndex))); + toDispose.push(this.quickPick.onDidChangeActiveItems(changedItems => this.proxy.$acceptDidChangeActive(options.quickInputIndex, changedItems))); + toDispose.push(this.quickPick.onDidChangeValue(value => this.proxy.$acceptDidChangeValue(options.quickInputIndex, value))); + toDispose.push(this.quickTitleBar.onDidTriggerButton(button => { this.proxy.$acceptOnDidTriggerButton(options.quickInputIndex, button); })); + this.toDispose.push(toDispose); quickPick.then(selection => { + if (toDispose.disposed) { + return; + } if (selection) { this.proxy.$acceptDidChangeSelection(options.quickInputIndex, selection as string); } this.proxy.$acceptOnDidHide(options.quickInputIndex); - disposableListeners.dispose(); + toDispose.dispose(); }); } 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/storage-path-service.ts b/packages/plugin-ext/src/main/browser/storage-path-service.ts deleted file mode 100644 index 21e32ffc680dc..0000000000000 --- a/packages/plugin-ext/src/main/browser/storage-path-service.ts +++ /dev/null @@ -1,63 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable, inject } from 'inversify'; -import { FileStat } from '@theia/filesystem/lib/common/filesystem'; -import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { PluginPathsService } from '../common/plugin-paths-protocol'; -import { Emitter, Event } from '@theia/core'; - -@injectable() -export class StoragePathService { - - private path: string | undefined; - private pathDeferred = new Deferred(); - - constructor( - @inject(WorkspaceService) private readonly workspaceService: WorkspaceService, - @inject(PluginPathsService) private readonly pluginPathsService: PluginPathsService, - ) { - this.path = undefined; - this.workspaceService.roots.then(roots => this.updateStoragePath(roots)); - } - - async provideHostStoragePath(): Promise { - return this.pathDeferred.promise; - } - - protected readonly onStoragePathChangeEmitter = new Emitter(); - get onStoragePathChanged(): Event { - return this.onStoragePathChangeEmitter.event; - } - - async updateStoragePath(roots: FileStat[]): Promise { - const workspace = this.workspaceService.workspace; - if (!workspace) { - this.path = undefined; - } - - const newPath = await this.pluginPathsService.provideHostStoragePath(workspace, roots); - if (this.path !== newPath) { - this.path = newPath; - this.pathDeferred.resolve(this.path); - this.pathDeferred = new Deferred(); - this.pathDeferred.resolve(this.path); - this.onStoragePathChangeEmitter.fire(this.path); - } - } - -} 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..35bc7b53d1aee 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 { @@ -466,27 +490,33 @@ export class PluginViewRegistry implements FrontendApplicationContribution { console.error(`data provider for '${viewId}' view is already registered`); return Disposable.NULL; } + this.viewDataProviders.set(viewId, provider); + const toDispose = new DisposableCollection(Disposable.create(() => { + this.viewDataProviders.delete(viewId); + this.viewDataState.delete(viewId); + })); this.getView(viewId).then(async view => { + if (toDispose.disposed) { + return; + } if (view) { if (view.isVisible) { await this.prepareView(view); } else { - const toDispose = new DisposableCollection(this.onDidExpandView(async id => { + const toDisposeOnDidExpandView = new DisposableCollection(this.onDidExpandView(async id => { if (id === viewId) { + unsubscribe(); await this.prepareView(view); } })); - const unsubscribe = () => toDispose.dispose(); + const unsubscribe = () => toDisposeOnDidExpandView.dispose(); view.disposed.connect(unsubscribe); - toDispose.push(Disposable.create(() => view.disposed.disconnect(unsubscribe))); + toDisposeOnDidExpandView.push(Disposable.create(() => view.disposed.disconnect(unsubscribe))); + toDispose.push(toDisposeOnDidExpandView); } } }); - this.viewDataProviders.set(viewId, provider); - return Disposable.create(() => { - this.viewDataProviders.delete(viewId); - this.viewDataState.delete(viewId); - }); + return toDispose; } protected async createViewDataWidget(viewId: string): Promise { @@ -499,17 +529,21 @@ export class PluginViewRegistry implements FrontendApplicationContribution { const state = this.viewDataState.get(viewId); const widget = await provider({ state, viewInfo }); if (StatefulWidget.is(widget)) { - const dispose = widget.dispose.bind(widget); - widget.dispose = () => { - this.viewDataState.set(viewId, widget.storeState()); - dispose(); - }; + this.storeViewDataStateOnDispose(viewId, widget); } else { this.viewDataState.delete(viewId); } return widget; } + protected storeViewDataStateOnDispose(viewId: string, widget: Widget & StatefulWidget): void { + const dispose = widget.dispose.bind(widget); + widget.dispose = () => { + this.viewDataState.set(viewId, widget.storeState()); + dispose(); + }; + } + protected trackVisibleWidget(factoryId: string, view: PluginViewRegistry.VisibleView): void { this.doTrackVisibleWidget(this.widgetManager.tryGetWidget(factoryId), view); this.widgetManager.onDidCreateWidget(event => { diff --git a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx index 44608623961a1..f99314c2dbada 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx @@ -90,7 +90,7 @@ export class PluginTree extends TreeImpl { private _proxy: TreeViewsExt | undefined; private _viewInfo: View | undefined; - set proxy(proxy: TreeViewsExt) { + set proxy(proxy: TreeViewsExt | undefined) { this._proxy = proxy; } @@ -103,7 +103,7 @@ export class PluginTree extends TreeImpl { return super.resolveChildren(parent); } const children = await this.fetchChildren(this._proxy, parent); - return children.map(value => this.createTreeNode(value, parent)); + return Promise.all(children.map(value => this.createTreeNode(value, parent))); } protected async fetchChildren(proxy: TreeViewsExt, parent: CompositeTreeNode): Promise { @@ -120,8 +120,8 @@ export class PluginTree extends TreeImpl { } } - protected createTreeNode(item: TreeViewItem, parent: CompositeTreeNode): TreeNode { - const icon = this.toIconClass(item); + protected async createTreeNode(item: TreeViewItem, parent: CompositeTreeNode): Promise { + const icon = await this.toIconClass(item); const update = { name: item.label, icon, @@ -154,12 +154,18 @@ export class PluginTree extends TreeImpl { }, update); } - protected toIconClass(item: TreeViewItem): string | undefined { + protected async toIconClass(item: TreeViewItem): Promise { if (item.icon) { return 'fa ' + item.icon; } if (item.iconUrl) { - return this.sharedStyle.toIconClass(item.iconUrl); + const reference = await this.sharedStyle.toIconClass(item.iconUrl); + if (this.toDispose.disposed) { + reference.dispose(); + return undefined; + } + this.toDispose.push(reference); + return reference.object.iconClass; } if (item.themeIconId) { return item.themeIconId === 'folder' ? FOLDER_ICON : FILE_ICON; @@ -178,7 +184,7 @@ export class PluginTreeModel extends TreeModelImpl { @inject(PluginTree) protected readonly tree: PluginTree; - set proxy(proxy: TreeViewsExt) { + set proxy(proxy: TreeViewsExt | undefined) { this.tree.proxy = proxy; } 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..f9864c0d3a35f 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,10 @@ export class TreeViewsMainImpl implements TreeViewsMain { private readonly treeViewProviders = new Map(); + private readonly toDispose = new DisposableCollection( + Disposable.create(() => { /* mark as not disposed */ }) + ); + constructor(rpc: RPCProtocol, private container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT); this.viewRegistry = container.get(PluginViewRegistry); @@ -42,6 +46,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 }); @@ -50,7 +58,7 @@ export class TreeViewsMainImpl implements TreeViewsMain { widget.restoreState(state); // ensure that state is completely restored await widget.model.refresh(); - } else { + } else if (!widget.model.root) { const root: CompositeTreeNode & ExpandableTreeNode = { id: '', parent: undefined, @@ -61,11 +69,17 @@ export class TreeViewsMainImpl implements TreeViewsMain { }; widget.model.root = root; } - widget.model.proxy = this.proxy; + if (this.toDispose.disposed) { + widget.model.proxy = undefined; + } else { + widget.model.proxy = this.proxy; + this.toDispose.push(Disposable.create(() => widget.model.proxy = undefined)); + this.handleTreeEvents(widget.id, widget); + } await widget.model.refresh(); - this.handleTreeEvents(widget.id, widget); return widget; })); + this.toDispose.push(Disposable.create(() => this.$unregisterTreeDataProvider(treeViewId))); } async $unregisterTreeDataProvider(treeViewId: string): Promise { @@ -74,10 +88,6 @@ export class TreeViewsMainImpl implements TreeViewsMain { this.treeViewProviders.delete(treeViewId); treeDataProvider.dispose(); } - const treeViewWidget = await this.widgetManager.getWidget(PLUGIN_VIEW_DATA_FACTORY_ID, { id: treeViewId }); - if (treeViewWidget) { - treeViewWidget.dispose(); - } } async $refresh(treeViewId: string): Promise { @@ -101,11 +111,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 +131,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..3a16e08078232 100644 --- a/packages/plugin-ext/src/main/browser/workspace-main.ts +++ b/packages/plugin-ext/src/main/browser/workspace-main.ts @@ -27,14 +27,14 @@ 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; @@ -54,10 +54,10 @@ export class WorkspaceMainImpl implements WorkspaceMain { private workspaceService: WorkspaceService; - private storagePathService: StoragePathService; - 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); @@ -66,27 +66,30 @@ export class WorkspaceMainImpl implements WorkspaceMain { this.resourceResolver = container.get(TextContentResourceResolver); this.pluginServer = container.get(PluginServer); 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); - }); + })); } - async processWorkspaceFoldersChanged(roots: FileStat[]): Promise { + dispose(): void { + this.toDispose.dispose(); + } + + protected async processWorkspaceFoldersChanged(roots: FileStat[]): Promise { if (this.isAnyRootChanged(roots) === false) { return; } this.roots = roots; this.proxy.$onWorkspaceFoldersChanged({ roots }); - await this.storagePathService.updateStoragePath(roots); - - const keyValueStorageWorkspacesData = await this.pluginServer.keyValueStorageGetAll(false); + const keyValueStorageWorkspacesData = await this.pluginServer.getAllStorageValues({ + workspace: this.workspaceService.workspace, + roots: this.workspaceService.tryGetRoots() + }); this.storageProxy.$updatePluginsWorkspaceData(keyValueStorageWorkspacesData); } @@ -192,8 +195,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 +207,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 +255,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/plugin-ext/src/main/common/plugin-paths-protocol.ts b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts index e7d9bad0e1d81..10f736bd6b934 100644 --- a/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts +++ b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts @@ -21,12 +21,10 @@ export const pluginPathsServicePath = '/services/plugin-paths'; // Service to create plugin configuration folders for different purpose. export const PluginPathsService = Symbol('PluginPathsService'); export interface PluginPathsService { - // Builds hosted log path. Create directory by this path if it is not exist on the file system. - provideHostLogPath(): Promise; - // Builds storage path for given workspace - provideHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise; - // Returns last resolved storage path - getLastStoragePath(): Promise; - // Returns Theia data directory (one for all Theia workspaces, so doesn't change) + /** Returns hosted log path. Create directory by this path if it is not exist on the file system. */ + getHostLogPath(): Promise; + /** Returns storage path for given workspace */ + getHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise; + /** Returns Theia data directory (one for all Theia workspaces, so doesn't change) */ getTheiaDirPath(): Promise; } diff --git a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts index 668de37aa8265..58c1b9c1e19be 100644 --- a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts +++ b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts @@ -19,7 +19,6 @@ import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import * as path from 'path'; import * as crypto from 'crypto'; import URI from '@theia/core/lib/common/uri'; -import { Deferred } from '@theia/core/lib/common/promise-util'; import { isWindows } from '@theia/core'; import { PluginPaths } from './const'; import { PluginPathsService } from '../../common/plugin-paths-protocol'; @@ -29,22 +28,12 @@ import { THEIA_EXT, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '@theia/work @injectable() export class PluginPathsServiceImpl implements PluginPathsService { - private windowsDataFolders = [PluginPaths.WINDOWS_APP_DATA_DIR, PluginPaths.WINDOWS_ROAMING_DIR]; - // storage path is undefined when no workspace is opened - private cachedStoragePath: string | undefined; - // is returned when storage path requested before initialization - private deferredStoragePath: Deferred; - // shows if storage path is initialized - private storagePathInitialized: boolean; - - constructor( - @inject(FileSystem) readonly fileSystem: FileSystem, - ) { - this.deferredStoragePath = new Deferred(); - this.storagePathInitialized = false; - } + private readonly windowsDataFolders = [PluginPaths.WINDOWS_APP_DATA_DIR, PluginPaths.WINDOWS_ROAMING_DIR]; + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; - async provideHostLogPath(): Promise { + async getHostLogPath(): Promise { const parentLogsDir = await this.getLogsDirPath(); if (!parentLogsDir) { @@ -57,7 +46,7 @@ export class PluginPathsServiceImpl implements PluginPathsService { return new URI(pluginDirPath).path.toString(); } - async provideHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise { + async getHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise { const parentStorageDir = await this.getWorkspaceStorageDirPath(); if (!parentStorageDir) { @@ -65,11 +54,7 @@ export class PluginPathsServiceImpl implements PluginPathsService { } if (!workspace) { - if (!this.storagePathInitialized) { - this.deferredStoragePath.resolve(undefined); - this.storagePathInitialized = true; - } - return this.cachedStoragePath = undefined; + return undefined; } if (!await this.fileSystem.exists(parentStorageDir)) { @@ -82,24 +67,10 @@ export class PluginPathsServiceImpl implements PluginPathsService { await this.fileSystem.createFolder(storageDirPath); } - const storagePathString = new URI(storageDirPath).path.toString(); - if (!this.storagePathInitialized) { - this.deferredStoragePath.resolve(storagePathString); - this.storagePathInitialized = true; - } - - return this.cachedStoragePath = storagePathString; - } - - async getLastStoragePath(): Promise { - if (this.storagePathInitialized) { - return this.cachedStoragePath; - } else { - return this.deferredStoragePath.promise; - } + return new URI(storageDirPath).path.toString(); } - async buildWorkspaceId(workspace: FileStat, roots: FileStat[]): Promise { + protected async buildWorkspaceId(workspace: FileStat, roots: FileStat[]): Promise { const homeDir = await this.getUserHomeDir(); const untitledWorkspace = getTemporaryWorkspaceFileUri(new URI(homeDir)); diff --git a/packages/plugin-ext/src/main/node/plugin-server-handler.ts b/packages/plugin-ext/src/main/node/plugin-server-handler.ts index 647e3d1b34247..0251579fdccee 100644 --- a/packages/plugin-ext/src/main/node/plugin-server-handler.ts +++ b/packages/plugin-ext/src/main/node/plugin-server-handler.ts @@ -17,7 +17,7 @@ import { injectable, inject } from 'inversify'; import { PluginDeployerImpl } from './plugin-deployer-impl'; import { PluginsKeyValueStorage } from './plugins-key-value-storage'; -import { PluginServer, PluginDeployer } from '../../common/plugin-protocol'; +import { PluginServer, PluginDeployer, PluginStorageKind } from '../../common/plugin-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; @injectable() @@ -33,16 +33,16 @@ export class PluginServerHandler implements PluginServer { return this.pluginDeployer.deploy(pluginEntry); } - keyValueStorageSet(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise { - return this.pluginsKeyValueStorage.set(key, value, isGlobal); + setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise { + return this.pluginsKeyValueStorage.set(key, value, kind); } - keyValueStorageGet(key: string, isGlobal: boolean): Promise { - return this.pluginsKeyValueStorage.get(key, isGlobal); + getStorageValue(key: string, kind: PluginStorageKind): Promise { + return this.pluginsKeyValueStorage.get(key, kind); } - keyValueStorageGetAll(isGlobal: boolean = false): Promise { - return this.pluginsKeyValueStorage.getAll(isGlobal); + getAllStorageValues(kind: PluginStorageKind): Promise { + return this.pluginsKeyValueStorage.getAll(kind); } } diff --git a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts index 269c4e3fa6436..86459038602be 100644 --- a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts +++ b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts @@ -14,47 +14,48 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; -import * as fs from 'fs'; +import { injectable, inject, postConstruct } from 'inversify'; +import * as fs from 'fs-extra'; import * as path from 'path'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { FileSystem } from '@theia/filesystem/lib/common'; import { PluginPaths } from './paths/const'; import { PluginPathsService } from '../common/plugin-paths-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; +import { PluginStorageKind } from '../../common'; @injectable() export class PluginsKeyValueStorage { - private theiaDirPath: string | undefined; - private globalDataPath: string | undefined; - private deferredTheiaDirPath = new Deferred(); + private readonly deferredGlobalDataPath = new Deferred(); - constructor( - @inject(PluginPathsService) private readonly pluginPathsService: PluginPathsService, - @inject(FileSystem) protected readonly fileSystem: FileSystem - ) { - this.setupDirectories(); - } - - private async setupDirectories(): Promise { - const theiaDirPath = await this.pluginPathsService.getTheiaDirPath(); - await this.fileSystem.createFolder(theiaDirPath); - this.theiaDirPath = theiaDirPath; + @inject(PluginPathsService) + private readonly pluginPathsService: PluginPathsService; - this.globalDataPath = path.join(this.theiaDirPath, PluginPaths.PLUGINS_GLOBAL_STORAGE_DIR, 'global-state.json'); - await this.fileSystem.createFolder(path.dirname(this.globalDataPath)); + @inject(FileSystem) + protected readonly fileSystem: FileSystem; - this.deferredTheiaDirPath.resolve(this.theiaDirPath); + @postConstruct() + protected async init(): Promise { + try { + const theiaDirPath = await this.pluginPathsService.getTheiaDirPath(); + await this.fileSystem.createFolder(theiaDirPath); + const globalDataPath = path.join(theiaDirPath, PluginPaths.PLUGINS_GLOBAL_STORAGE_DIR, 'global-state.json'); + await this.fileSystem.createFolder(path.dirname(globalDataPath)); + this.deferredGlobalDataPath.resolve(globalDataPath); + } catch (e) { + console.error('Faild to initialize global state path: ', e); + this.deferredGlobalDataPath.resolve(undefined); + } } - async set(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise { - const dataPath = await this.getDataPath(isGlobal); + async set(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise { + const dataPath = await this.getDataPath(kind); if (!dataPath) { throw new Error('Cannot save data: no opened workspace'); } - const data = this.readFromFile(dataPath); + const data = await this.readFromFile(dataPath); if (value === undefined || value === {}) { delete data[key]; @@ -62,65 +63,50 @@ export class PluginsKeyValueStorage { data[key] = value; } - this.writeToFile(dataPath, data); + await this.writeToFile(dataPath, data); return true; } - async get(key: string, isGlobal: boolean): Promise { - const dataPath = await this.getDataPath(isGlobal); + async get(key: string, kind: PluginStorageKind): Promise { + const dataPath = await this.getDataPath(kind); if (!dataPath) { return {}; } - - const data = this.readFromFile(dataPath); + const data = await this.readFromFile(dataPath); return data[key]; } - async getAll(isGlobal: boolean): Promise { - const dataPath = await this.getDataPath(isGlobal); + async getAll(kind: PluginStorageKind): Promise { + const dataPath = await this.getDataPath(kind); if (!dataPath) { return {}; } - - const data = this.readFromFile(dataPath); - return data; + return this.readFromFile(dataPath); } - private async getDataPath(isGlobal: boolean): Promise { - if (this.theiaDirPath === undefined) { - // wait for Theia data directory path if it hasn't been initialized yet - await this.deferredTheiaDirPath.promise; - } - - if (isGlobal) { - return this.globalDataPath!; - } else { - const storagePath = await this.pluginPathsService.getLastStoragePath(); - return storagePath ? path.join(storagePath, 'workspace-state.json') : undefined; + private async getDataPath(kind: PluginStorageKind): Promise { + if (!kind) { + return this.deferredGlobalDataPath.promise; } + const storagePath = await this.pluginPathsService.getHostStoragePath(kind.workspace, kind.roots); + return storagePath ? path.join(storagePath, 'workspace-state.json') : undefined; } - private readFromFile(pathToFile: string): KeysToKeysToAnyValue { - if (!fs.existsSync(pathToFile)) { + private async readFromFile(pathToFile: string): Promise { + if (!await fs.pathExists(pathToFile)) { return {}; } - - const rawData = fs.readFileSync(pathToFile, 'utf8'); try { - return JSON.parse(rawData); + return await fs.readJSON(pathToFile); } catch (error) { console.error('Failed to parse data from "', pathToFile, '". Reason:', error); return {}; } } - private writeToFile(pathToFile: string, data: KeysToKeysToAnyValue): void { - if (!fs.existsSync(path.dirname(pathToFile))) { - fs.mkdirSync(path.dirname(pathToFile)); - } - - const rawData = JSON.stringify(data); - fs.writeFileSync(pathToFile, rawData, 'utf8'); + private async writeToFile(pathToFile: string, data: KeysToKeysToAnyValue): Promise { + await fs.ensureDir(path.dirname(pathToFile)); + await fs.writeJSON(pathToFile, data); } } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index ac2a80260301c..cb90edfd2e219 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -78,8 +78,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { /** promises to whether loading each plugin has been successful */ private readonly loadedPlugins = new Map>(); private readonly activatedPlugins = new Map(); - private pluginActivationPromises = new Map>(); - private pluginContextsMap: Map = new Map(); + private readonly pluginActivationPromises = new Map>(); + private readonly pluginContextsMap = new Map(); private storageProxy: KeyValueStorageProxy; private onDidChangeEmitter = new Emitter(); @@ -97,25 +97,44 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { this.messageRegistryProxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN); } - $stopPlugin(contextPath: string): PromiseLike { - this.activatedPlugins.forEach(plugin => { - if (plugin.stopFn) { - plugin.stopFn(); - } + async $stop(pluginId?: string): Promise { + if (!pluginId) { + this.stopAll(); + return; + } + // TODO what to do about transitive extensions? + this.registry.delete(pluginId); + this.pluginActivationPromises.delete(pluginId); + this.pluginContextsMap.delete(pluginId); + this.loadedPlugins.delete(pluginId); + const plugin = this.activatedPlugins.get(pluginId); + if (!plugin) { + return; + } + this.activatedPlugins.delete(pluginId); + this.stopPlugin(plugin); + } - // dispose any objects - const pluginContext = plugin.pluginContext; - if (pluginContext) { - dispose(pluginContext.subscriptions); - } - }); + protected stopAll(): void { + this.activatedPlugins.forEach(plugin => this.stopPlugin(plugin)); - // clean map + this.registry.clear(); + this.loadedPlugins.clear(); this.activatedPlugins.clear(); this.pluginActivationPromises.clear(); this.pluginContextsMap.clear(); + } - return Promise.resolve(); + protected stopPlugin(plugin: ActivatedPlugin): void { + if (plugin.stopFn) { + plugin.stopFn(); + } + + // dispose any objects + const pluginContext = plugin.pluginContext; + if (pluginContext) { + dispose(pluginContext.subscriptions); + } } async $init(pluginInit: PluginInitData, configStorage: ConfigStorage): Promise { @@ -285,7 +304,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { this.pluginActivationPromises.get(plugin.model.id)!.reject(err); } this.messageRegistryProxy.$showMessage(MainMessageType.Error, `Activating extension ${id} failed: ${err.message}.`, {}, []); - console.error(`Error on activation of ${plugin.model.name} - ${err}`); + console.error(`Error on activation of ${plugin.model.name}`, err); return false; } } else { 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