diff --git a/src/vs/platform/actions/test/common/menuService.test.ts b/src/vs/platform/actions/test/common/menuService.test.ts index 9602dcd6bf5..fc50290e74b 100644 --- a/src/vs/platform/actions/test/common/menuService.test.ts +++ b/src/vs/platform/actions/test/common/menuService.test.ts @@ -10,30 +10,37 @@ import { MenuService } from 'vs/platform/actions/common/menuService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { NullCommandService } from 'vs/platform/commands/common/commands'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { AbstractExtensionService, ActivatedExtension } from 'vs/platform/extensions/common/abstractExtensionService'; import { IExtensionPoint } from "vs/platform/extensions/common/extensionsRegistry"; import { TPromise } from "vs/base/common/winjs.base"; -import { ExtensionPointContribution, IExtensionDescription } from "vs/platform/extensions/common/extensions"; +import { ExtensionPointContribution, IExtensionDescription, IExtensionsStatus, IExtensionService } from "vs/platform/extensions/common/extensions"; // --- service instances -const extensionService = new class extends AbstractExtensionService { - protected _showMessage(): void { - console.log(arguments); +class MockExtensionService implements IExtensionService { + public _serviceBrand: any; + + public activateByEvent(activationEvent: string): TPromise { + throw new Error('Not implemented'); } - protected _createFailedExtension() { - return null; - } - protected _actualActivateExtension() { - return null; + + public onReady(): TPromise { + return TPromise.as(true); } + public getExtensions(): TPromise { throw new Error('Not implemented'); } + public readExtensionPointContributions(extPoint: IExtensionPoint): TPromise[]> { throw new Error('Not implemented'); } -}(true); + + public getExtensionsStatus(): { [id: string]: IExtensionsStatus } { + throw new Error('Not implemented'); + } +} + +const extensionService = new MockExtensionService(); const contextKeyService = new class extends MockContextKeyService { contextMatchesRules() { diff --git a/src/vs/platform/extensions/common/abstractExtensionService.ts b/src/vs/platform/extensions/common/abstractExtensionService.ts index ac65223c76e..a952d903b45 100644 --- a/src/vs/platform/extensions/common/abstractExtensionService.ts +++ b/src/vs/platform/extensions/common/abstractExtensionService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IExtensionDescription, IExtensionsStatus } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; const hasOwnProperty = Object.hasOwnProperty; @@ -252,10 +252,6 @@ export abstract class AbstractExtensionService { return this._onReady; } - public getExtensionsStatus(): { [id: string]: IExtensionsStatus } { - return null; - } - public isActivated(extensionId: string): boolean { return this._manager.isActivated(extensionId); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts b/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts index 7fc4fa04952..a076c013d00 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts @@ -25,9 +25,7 @@ export class MainProcessExtensionServiceAPI extends MainProcessExtensionServiceS this._extensionService._localShowMessage(severity, msg); } $onExtensionActivated(extensionId: string): void { - this._extensionService._onExtensionActivated(extensionId); } $onExtensionActivationFailed(extensionId: string): void { - this._extensionService._onExtensionActivationFailed(extensionId); } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index f031648f17c..f6e794f2e00 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -436,7 +436,7 @@ export abstract class ExtHostWorkspaceShape { } export abstract class ExtHostExtensionServiceShape { - $activateExtension(extensionDescription: IExtensionDescription): TPromise { throw ni(); } + $activateByEvent(activationEvent: string): TPromise { throw ni(); } } export interface FileSystemEvents { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 429b20540d6..5c4d229ca87 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -331,10 +331,9 @@ export class ExtHostExtensionService extends AbstractExtensionService { - return this._manager._activateExtension(extensionDescription); + public $activateByEvent(activationEvent: string): TPromise { + return this._manager.activateByEvent(activationEvent); } - } function loadCommonJSModule(modulePath: string): TPromise { diff --git a/src/vs/workbench/node/extensionPoints.ts b/src/vs/workbench/node/extensionPoints.ts index c1b76d345ec..8f1f53391e8 100644 --- a/src/vs/workbench/node/extensionPoints.ts +++ b/src/vs/workbench/node/extensionPoints.ts @@ -32,7 +32,13 @@ const nlsConfig: NlsConfiguration = { pseudo: Platform.locale === 'pseudo' }; -export class MessagesCollector { +export interface IMessagesCollector { + error(source: string, message: string): void; + warn(source: string, message: string): void; + info(source: string, message: string): void; +} + +export class MessagesCollector implements IMessagesCollector { private _messages: IMessage[]; @@ -70,12 +76,12 @@ export class MessagesCollector { abstract class ExtensionManifestHandler { protected _ourVersion: string; - protected _collector: MessagesCollector; + protected _collector: IMessagesCollector; protected _absoluteFolderPath: string; protected _isBuiltin: boolean; protected _absoluteManifestPath: string; - constructor(ourVersion: string, collector: MessagesCollector, absoluteFolderPath: string, isBuiltin: boolean) { + constructor(ourVersion: string, collector: IMessagesCollector, absoluteFolderPath: string, isBuiltin: boolean) { this._ourVersion = ourVersion; this._collector = collector; this._absoluteFolderPath = absoluteFolderPath; @@ -190,7 +196,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { * This routine makes the following assumptions: * The root element is an object literal */ - private static _replaceNLStrings(literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string }, collector: MessagesCollector, messageScope: string): void { + private static _replaceNLStrings(literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string }, collector: IMessagesCollector, messageScope: string): void { function processEntry(obj: any, key: string | number, command?: boolean) { let value = obj[key]; if (Types.isString(value)) { @@ -283,7 +289,7 @@ export class ExtensionScanner { */ public static scanExtension( version: string, - collector: MessagesCollector, + collector: IMessagesCollector, absoluteFolderPath: string, isBuiltin: boolean ): TPromise { @@ -312,7 +318,7 @@ export class ExtensionScanner { */ public static scanExtensions( version: string, - collector: MessagesCollector, + collector: IMessagesCollector, absoluteFolderPath: string, isBuiltin: boolean ): TPromise { @@ -371,7 +377,7 @@ export class ExtensionScanner { */ public static scanOneOrMultipleExtensions( version: string, - collector: MessagesCollector, + collector: IMessagesCollector, absoluteFolderPath: string, isBuiltin: boolean ): TPromise { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index a8118730acf..6325fa29569 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -10,7 +10,7 @@ import pkg from 'vs/platform/node/package'; import { localize } from 'vs/nls'; import * as path from 'path'; import URI from 'vs/base/common/uri'; -import { AbstractExtensionService, ActivatedExtension } from 'vs/platform/extensions/common/abstractExtensionService'; +import { ExtensionDescriptionRegistry } from 'vs/platform/extensions/common/abstractExtensionService'; import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution } from 'vs/platform/extensions/common/extensions'; import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, getGloballyDisabledExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -28,72 +28,82 @@ import { MainThreadService } from "vs/workbench/services/thread/electron-browser const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); -/** - * Represents a failed extension in the ext host. - */ -class MainProcessFailedExtension extends ActivatedExtension { - constructor() { - super(true); - } -} - -/** - * Represents an extension that was successfully loaded or an - * empty extension in the ext host. - */ -class MainProcessSuccessExtension extends ActivatedExtension { - constructor() { - super(false); - } -} - function messageWithSource(msg: IMessage): string { return (msg.source ? '[' + msg.source + ']: ' : '') + msg.message; } const hasOwnProperty = Object.hasOwnProperty; +const NO_OP_VOID_PROMISE = TPromise.as(void 0); -export class MainProcessExtensionService extends AbstractExtensionService implements IThreadService, IExtensionService { +export abstract class AbstractExtensionService { + public _serviceBrand: any; - private _proxy: ExtHostExtensionServiceShape; - private _isDev: boolean; - private _extensionsStatus: { [id: string]: IExtensionsStatus }; - private _threadService: IThreadService; + private _onReady: TPromise; + private _onReadyC: (v: boolean) => void; + private _isReady: boolean; + protected _registry: ExtensionDescriptionRegistry; + constructor() { + this._isReady = false; + this._onReady = new TPromise((c, e, p) => { + this._onReadyC = c; + }, () => { + console.warn('You should really not try to cancel this ready promise!'); + }); + this._registry = new ExtensionDescriptionRegistry(); + } + + protected _triggerOnReady(): void { + this._isReady = true; + this._onReadyC(true); + } + + public onReady(): TPromise { + return this._onReady; + } + + public activateByEvent(activationEvent: string): TPromise { + if (this._isReady) { + return this._activateByEvent(activationEvent); + } else { + return this._onReady.then(() => this._activateByEvent(activationEvent)); + } + } + + protected abstract _activateByEvent(activationEvent: string): TPromise; +} + +export class MainProcessExtensionService extends AbstractExtensionService implements IThreadService, IExtensionService { + + private readonly _isDev: boolean; + private readonly _extensionsStatus: { [id: string]: IExtensionsStatus }; /** - * This class is constructed manually because it is a service, so it doesn't use any ctor injection + * A map of already activated events to speed things up if the same activation event is triggered multiple times. */ + private readonly _alreadyActivatedEvents: { [activationEvent: string]: boolean; }; + private readonly _threadService: IThreadService; + private readonly _proxy: ExtHostExtensionServiceShape; + constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IMessageService private readonly _messageService: IMessageService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IExtensionEnablementService extensionEnablementService: IExtensionEnablementService, - @IStorageService storageService: IStorageService, + @IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService, + @IStorageService private readonly _storageService: IStorageService, ) { - super(false); - this._isDev = !environmentService.isBuilt || environmentService.isExtensionDevelopment; + super(); + this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment; this._extensionsStatus = {}; + this._alreadyActivatedEvents = Object.create(null); const extensionHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker); this._threadService = this._instantiationService.createInstance(MainThreadService, extensionHostProcessWorker.messagingProtocol); this._proxy = this._threadService.get(ExtHostContext.ExtHostExtensionService); extensionHostProcessWorker.start(this); - this.scanExtensions().done(extensionDescriptions => { - const disabledExtensions = [ - ...getGloballyDisabledExtensions(extensionEnablementService, storageService, extensionDescriptions), - ...extensionEnablementService.getWorkspaceDisabledExtensions() - ]; - - _telemetryService.publicLog('extensionsScanned', { - totalCount: extensionDescriptions.length, - disabledCount: disabledExtensions.length - }); - - this._onExtensionDescriptions(disabledExtensions.length ? extensionDescriptions.filter(e => disabledExtensions.every(id => !areSameExtensions({ id }, e))) : extensionDescriptions); - }); + this._initialize(); } // ---- begin IThreadService @@ -108,41 +118,22 @@ export class MainProcessExtensionService extends AbstractExtensionService { + // } + + protected _activateByEvent(activationEvent: string): TPromise { + if (this._alreadyActivatedEvents[activationEvent]) { + return NO_OP_VOID_PROMISE; } + return this._proxy.$activateByEvent(activationEvent).then(() => { + this._alreadyActivatedEvents[activationEvent] = true; + }); } - public _localShowMessage(severity: Severity, msg: string, useMessageService: boolean = this._isDev): void { - // Only show nasty intrusive messages if doing extension development - // and print all other messages to the console - if (useMessageService && (severity === Severity.Error || severity === Severity.Warning)) { - this._messageService.show(severity, msg); - } else if (severity === Severity.Error) { - console.error(msg); - } else if (severity === Severity.Warning) { - console.warn(msg); - } else { - console.log(msg); - } - } - - // -- overwriting AbstractExtensionService + // public onReady(): TPromise { + // } public getExtensions(): TPromise { return this.onReady().then(() => { @@ -171,25 +162,89 @@ export class MainProcessExtensionService extends AbstractExtensionService { + private _initialize(): void { - // redirect extension activation to the extension host - return this._proxy.$activateExtension(extensionDescription).then(_ => { + MainProcessExtensionService._scanInstalledExtensions(this._environmentService).done(([installedExtensions, messages]) => { + messages.forEach(entry => this._localShowMessage(entry.type, this._isDev ? (entry.source ? '[' + entry.source + ']: ' : '') + entry.message : entry.message)); - // the extension host calls $onExtensionActivated, where we write to `_activatedExtensions` - return this._manager.getActivatedExtension(extensionDescription.id); + const disabledExtensions = [ + ...getGloballyDisabledExtensions(this._extensionEnablementService, this._storageService, installedExtensions), + ...this._extensionEnablementService.getWorkspaceDisabledExtensions() + ]; + + this._telemetryService.publicLog('extensionsScanned', { + totalCount: installedExtensions.length, + disabledCount: disabledExtensions.length + }); + + this._onExtensionDescriptions(disabledExtensions.length ? installedExtensions.filter(e => disabledExtensions.every(id => !areSameExtensions({ id }, e))) : installedExtensions); }); } - // -- called by extension host + private static _scanInstalledExtensions(environmentService: IEnvironmentService): TPromise<[IExtensionDescription[], IMessage[]]> { + const collector = new MessagesCollector(); + const version = pkg.version; + const builtinExtensions = ExtensionScanner.scanExtensions(version, collector, SystemExtensionsRoot, true); + const userExtensions = environmentService.disableExtensions || !environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, collector, environmentService.extensionsPath, false); + const developedExtensions = environmentService.disableExtensions || !environmentService.isExtensionDevelopment ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, collector, environmentService.extensionDevelopmentPath, false); + + return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { + const builtinExtensions = extensionDescriptions[0]; + const userExtensions = extensionDescriptions[1]; + const developedExtensions = extensionDescriptions[2]; + + let result: { [extensionId: string]: IExtensionDescription; } = {}; + builtinExtensions.forEach((builtinExtension) => { + result[builtinExtension.id] = builtinExtension; + }); + userExtensions.forEach((userExtension) => { + if (result.hasOwnProperty(userExtension.id)) { + collector.warn(userExtension.extensionFolderPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath)); + } + result[userExtension.id] = userExtension; + }); + developedExtensions.forEach(developedExtension => { + collector.info('', localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath)); + if (result.hasOwnProperty(developedExtension.id)) { + collector.warn(developedExtension.extensionFolderPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionFolderPath, developedExtension.extensionFolderPath)); + } + result[developedExtension.id] = developedExtension; + }); + + return Object.keys(result).map(name => result[name]); + }).then(null, err => { + collector.error('', err); + return []; + }).then<[IExtensionDescription[], IMessage[]]>(extensions => { + const messages = collector.getMessages(); + const result: [IExtensionDescription[], IMessage[]] = [extensions, messages]; + return result; + }); + } private _onExtensionDescriptions(extensionDescriptions: IExtensionDescription[]): void { this._registry.registerExtensions(extensionDescriptions); @@ -223,51 +278,19 @@ export class MainProcessExtensionService extends AbstractExtensionService { - const collector = new MessagesCollector(); - const version = pkg.version; - const builtinExtensions = ExtensionScanner.scanExtensions(version, collector, SystemExtensionsRoot, true); - const userExtensions = this.environmentService.disableExtensions || !this.environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, collector, this.environmentService.extensionsPath, false); - const developedExtensions = this.environmentService.disableExtensions || !this.environmentService.isExtensionDevelopment ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, collector, this.environmentService.extensionDevelopmentPath, false); - - return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { - let builtinExtensions = extensionDescriptions[0]; - let userExtensions = extensionDescriptions[1]; - let developedExtensions = extensionDescriptions[2]; - - let result: { [extensionId: string]: IExtensionDescription; } = {}; - builtinExtensions.forEach((builtinExtension) => { - result[builtinExtension.id] = builtinExtension; - }); - userExtensions.forEach((userExtension) => { - if (result.hasOwnProperty(userExtension.id)) { - collector.warn(userExtension.extensionFolderPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath)); - } - result[userExtension.id] = userExtension; - }); - developedExtensions.forEach(developedExtension => { - collector.info('', localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath)); - if (result.hasOwnProperty(developedExtension.id)) { - collector.warn(developedExtension.extensionFolderPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionFolderPath, developedExtension.extensionFolderPath)); - } - result[developedExtension.id] = developedExtension; - }); - - return Object.keys(result).map(name => result[name]); - }).then(null, err => { - collector.error('', err); - return []; - }).then(extensions => { - collector.getMessages().forEach(entry => this._localShowMessage(entry.type, this._isDev ? (entry.source ? '[' + entry.source + ']: ' : '') + entry.message : entry.message)); - return extensions; - }); + public _localShowMessage(severity: Severity, msg: string, useMessageService: boolean = this._isDev): void { + // Only show nasty intrusive messages if doing extension development + // and print all other messages to the console + if (useMessageService && (severity === Severity.Error || severity === Severity.Warning)) { + this._messageService.show(severity, msg); + } else if (severity === Severity.Error) { + console.error(msg); + } else if (severity === Severity.Warning) { + console.warn(msg); + } else { + console.log(msg); + } } }