diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index fdaa10614ad..9a26e5fa431 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -741,8 +741,7 @@ export interface ExtHostExtensionServiceShape { $activateByEvent(activationEvent: string): Promise; $activate(extensionId: ExtensionIdentifier, activationEvent: string): Promise; - $addExtension(extension: IExtensionDescription): Promise; - $removeExtension(extension: ExtensionIdentifier): Promise; + $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; $test_latency(n: number): Promise; $test_up(b: Buffer): Promise; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index fe143a9989f..60d464dc504 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -669,14 +669,9 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { ); } - public $addExtension(extension: IExtensionDescription): Promise { - (extension).extensionLocation = URI.revive(extension.extensionLocation); - this._registry.add(extension); - return Promise.resolve(undefined); - } - - public $removeExtension(extensionId: ExtensionIdentifier): Promise { - this._registry.remove(extensionId); + public $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { + toAdd.forEach((extension) => (extension).extensionLocation = URI.revive(extension.extensionLocation)); + this._registry.deltaExtensions(toAdd, toRemove); return Promise.resolve(undefined); } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index f108a8b8e57..114d60030dc 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -249,12 +249,8 @@ export class ExtensionHostProcessManager extends Disposable { return this._extensionHostProcessProxy.then(proxy => proxy.value.$startExtensionHost(enabledExtensionIds)); } - public addExtension(extension: IExtensionDescription): Promise { - return this._extensionHostProcessProxy.then(proxy => proxy.value.$addExtension(extension)); - } - - public removeExtension(extensionId: ExtensionIdentifier): Promise { - return this._extensionHostProcessProxy.then(proxy => proxy.value.$removeExtension(extensionId)); + public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { + return this._extensionHostProcessProxy.then(proxy => proxy.value.$deltaExtensions(toAdd, toRemove)); } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index dd47be70fc5..173b7faf643 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -49,6 +49,13 @@ function allowProposedApiFromProduct(id: ExtensionIdentifier): boolean { return productAllowProposedApi.has(ExtensionIdentifier.toKey(id)); } +class DeltaExtensionsQueueItem { + constructor( + public readonly toAdd: IExtension[], + public readonly toRemove: string[] + ) { } +} + export class ExtensionService extends Disposable implements IExtensionService { public _serviceBrand: any; @@ -60,6 +67,7 @@ export class ExtensionService extends Disposable implements IExtensionService { private readonly _extensionsMessages: Map; private _allRequestedActivateEvents: { [activationEvent: string]: boolean; }; private readonly _extensionScanner: CachedExtensionScanner; + private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; private readonly _onDidRegisterExtensions: Emitter = this._register(new Emitter()); public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event; @@ -97,6 +105,7 @@ export class ExtensionService extends Disposable implements IExtensionService { this._extensionsMessages = new Map(); this._allRequestedActivateEvents = Object.create(null); this._extensionScanner = this._instantiationService.createInstance(CachedExtensionScanner); + this._deltaExtensionsQueue = []; this._extensionHostProcessManagers = []; this._extensionHostActiveExtensions = new Map(); @@ -115,60 +124,121 @@ export class ExtensionService extends Disposable implements IExtensionService { } this._extensionEnablementService.onEnablementChanged((extensions) => { + let toAdd: IExtension[] = []; + let toRemove: string[] = []; for (const extension of extensions) { if (this._extensionEnablementService.isEnabled(extension)) { // an extension has been enabled - this._addExtension(extension); + toAdd.push(extension); } else { // an extension has been disabled - this._removeExtension(extension.identifier.id); + toRemove.push(extension.identifier.id); } } + this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove)); }); this._extensionManagementService.onDidInstallExtension((event) => { if (event.local) { // an extension has been installed - this._addExtension(event.local); + this._handleDeltaExtensions(new DeltaExtensionsQueueItem([event.local], [])); } }); this._extensionManagementService.onDidUninstallExtension((event) => { if (!event.error) { // an extension has been uninstalled - this._removeExtension(event.identifier.id); + this._handleDeltaExtensions(new DeltaExtensionsQueueItem([], [event.identifier.id])); } }); } - private async _removeExtension(extensionId: string): Promise { - const extensionDescription = this._registry.getExtensionDescription(extensionId); - if (!extensionDescription) { - // ignore disabling an extension which is not running + private _inHandleDeltaExtensions = false; + private async _handleDeltaExtensions(item: DeltaExtensionsQueueItem): Promise { + this._deltaExtensionsQueue.push(item); + if (this._inHandleDeltaExtensions) { + // Let the current item finish, the new one will be picked up return; } - if (!this._canRemoveExtension(extensionDescription)) { - // uses non-dynamic extension point or is activated - return; - } - - // Remove the extension from the local registry and from the extension host - this._registry.remove(extensionDescription.identifier); - - this._rehandleExtensionPoints(extensionDescription); - - if (this._extensionHostProcessManagers.length > 0) { - await this._extensionHostProcessManagers[0].removeExtension(extensionDescription.identifier); + while (this._deltaExtensionsQueue.length > 0) { + const item = this._deltaExtensionsQueue.shift(); + try { + this._inHandleDeltaExtensions = true; + await this._deltaExtensions(item.toAdd, item.toRemove); + } finally { + this._inHandleDeltaExtensions = false; + } } } - private _rehandleExtensionPoints(extensionDescription: IExtensionDescription): void { + private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise { + if (this._windowService.getConfiguration().remoteAuthority) { + return; + } + + let toAdd: IExtensionDescription[] = []; + for (let i = 0, len = _toAdd.length; i < len; i++) { + const extension = _toAdd[i]; + + if (extension.location.scheme !== Schemas.file) { + continue; + } + + const extensionDescription = await this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this.createLogger()); + if (!extensionDescription || !this._usesOnlyDynamicExtensionPoints(extensionDescription)) { + // uses non-dynamic extension point + continue; + } + + toAdd.push(extensionDescription); + } + + let toRemove: IExtensionDescription[] = []; + for (let i = 0, len = _toRemove.length; i < len; i++) { + const extensionId = _toRemove[i]; + const extensionDescription = this._registry.getExtensionDescription(extensionId); + if (!extensionDescription) { + // ignore disabling/uninstalling an extension which is not running + continue; + } + + if (!this._canRemoveExtension(extensionDescription)) { + // uses non-dynamic extension point or is activated + continue; + } + + toRemove.push(extensionDescription); + } + + if (toAdd.length === 0 && toRemove.length === 0) { + return; + } + + // Update the local registry + this._registry.deltaExtensions(toAdd, toRemove.map(e => e.identifier)); + + // Update extension points + this._rehandleExtensionPoints(([]).concat(toAdd).concat(toRemove)); + + // Update the extension host + if (this._extensionHostProcessManagers.length > 0) { + await this._extensionHostProcessManagers[0].deltaExtensions(toAdd, toRemove.map(e => e.identifier)); + } + + for (let i = 0; i < toAdd.length; i++) { + this._activateAddedExtensionIfNeeded(toAdd[i]); + } + } + + private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void { const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null); - if (extensionDescription.contributes) { - for (let extPointName in extensionDescription.contributes) { - if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) { - affectedExtensionPoints[extPointName] = true; + for (let extensionDescription of extensionDescriptions) { + if (extensionDescription.contributes) { + for (let extPointName in extensionDescription.contributes) { + if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) { + affectedExtensionPoints[extPointName] = true; + } } } } @@ -182,7 +252,6 @@ export class ExtensionService extends Disposable implements IExtensionService { ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler); } } - } private _usesOnlyDynamicExtensionPoints(extension: IExtensionDescription): boolean { @@ -210,28 +279,7 @@ export class ExtensionService extends Disposable implements IExtensionService { return this._usesOnlyDynamicExtensionPoints(extension); } - private async _addExtension(extension: IExtension): Promise { - if (this._windowService.getConfiguration().remoteAuthority) { - return; - } - if (extension.location.scheme !== Schemas.file) { - return; - } - - const extensionDescription = await this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this.createLogger()); - if (!extensionDescription || !this._usesOnlyDynamicExtensionPoints(extensionDescription)) { - // uses non-dynamic extension point - return; - } - - // Add the extension to the local registry and to the extension host - this._registry.add(extensionDescription); - - this._rehandleExtensionPoints(extensionDescription); - - if (this._extensionHostProcessManagers.length > 0) { - await this._extensionHostProcessManagers[0].addExtension(extensionDescription); - } + private async _activateAddedExtensionIfNeeded(extensionDescription: IExtensionDescription): Promise { let shouldActivate = false; let shouldActivateReason: string | null = null; diff --git a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts index fc345fcc52c..ad385a196c9 100644 --- a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts @@ -55,13 +55,11 @@ export class ExtensionDescriptionRegistry { this._initialize(); } - public remove(extensionId: ExtensionIdentifier): void { - this._extensionDescriptions = this._extensionDescriptions.filter(extension => !ExtensionIdentifier.equals(extension.identifier, extensionId)); - this._initialize(); - } - - public add(extension: IExtensionDescription): void { - this._extensionDescriptions.push(extension); + public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]) { + this._extensionDescriptions = this._extensionDescriptions.concat(toAdd); + const toRemoveSet = new Set(); + toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId))); + this._extensionDescriptions = this._extensionDescriptions.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier))); this._initialize(); }