diff --git a/news/3 Code Health/1747.md b/news/3 Code Health/1747.md new file mode 100644 index 000000000000..7d098b4bf0a8 --- /dev/null +++ b/news/3 Code Health/1747.md @@ -0,0 +1 @@ +Prompt user to reload Visual Studio Code when toggling between the analysis engines. diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts new file mode 100644 index 000000000000..1f8ff2d754ff --- /dev/null +++ b/src/client/activation/activationService.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { isPythonAnalysisEngineTest, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import '../common/extensions'; +import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from './types'; + +const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; + +type ActivatorInfo = { jedi: boolean; activator: IExtensionActivator }; + +@injectable() +export class ExtensionActivationService implements IExtensionActivationService, Disposable { + private currentActivator?: ActivatorInfo; + private readonly workspaceService: IWorkspaceService; + private readonly output: OutputChannel; + private readonly appShell: IApplicationShell; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.appShell = this.serviceContainer.get(IApplicationShell); + + const disposables = serviceContainer.get(IDisposableRegistry); + disposables.push(this); + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + public async activate(): Promise { + if (this.currentActivator) { + return; + } + + const jedi = this.useJedi(); + + const engineName = jedi ? 'classic analysis engine' : 'analysis engine'; + this.output.appendLine(`Starting the ${engineName}.`); + const activatorName = jedi ? ExtensionActivators.Jedi : ExtensionActivators.DotNet; + const activator = this.serviceContainer.get(IExtensionActivator, activatorName); + this.currentActivator = { jedi, activator }; + + await activator.activate(); + } + public dispose() { + if (this.currentActivator) { + this.currentActivator.activator.deactivate().ignoreErrors(); + } + } + private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; + if (workspacesUris.findIndex(uri => event.affectsConfiguration(`python.${jediEnabledSetting}`, uri)) === -1) { + return; + } + const jedi = this.useJedi(); + if (this.currentActivator && this.currentActivator.jedi === jedi) { + return; + } + + const item = await this.appShell.showInformationMessage('Please reload the window switching between the analysis engines.', 'Reload'); + if (item === 'Reload') { + this.serviceContainer.get(ICommandManager).executeCommand('workbench.action.reloadWindow'); + } + } + private useJedi(): boolean { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(item => item.uri) : [undefined]; + const configuraionService = this.serviceContainer.get(IConfigurationService); + const jediEnabledForAnyWorkspace = workspacesUris.filter(uri => configuraionService.getSettings(uri).jediEnabled).length > 0; + return !isPythonAnalysisEngineTest() && jediEnabledForAnyWorkspace; + } +} diff --git a/src/client/activation/analysis.ts b/src/client/activation/analysis.ts index bf8fd4c9e587..b628159409e6 100644 --- a/src/client/activation/analysis.ts +++ b/src/client/activation/analysis.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ExtensionContext, OutputChannel } from 'vscode'; import { Message } from 'vscode-jsonrpc'; @@ -10,7 +11,7 @@ import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { createDeferred, Deferred } from '../common/helpers'; import { IFileSystem, IPlatformService } from '../common/platform/types'; import { StopWatch } from '../common/stopWatch'; -import { IConfigurationService, IOutputChannel, IPythonSettings } from '../common/types'; +import { IConfigurationService, IExtensionContext, IOutputChannel } from '../common/types'; import { IEnvironmentVariablesProvider } from '../common/variables/types'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; @@ -43,6 +44,7 @@ class LanguageServerStartupErrorHandler implements ErrorHandler { } } +@injectable() export class AnalysisExtensionActivator implements IExtensionActivator { private readonly configuration: IConfigurationService; private readonly appShell: IApplicationShell; @@ -53,10 +55,11 @@ export class AnalysisExtensionActivator implements IExtensionActivator { private readonly interpreterService: IInterpreterService; private readonly disposables: Disposable[] = []; private languageClient: LanguageClient | undefined; - private context: ExtensionContext | undefined; + private readonly context: ExtensionContext; private interpreterHash: string = ''; - constructor(private readonly services: IServiceContainer, pythonSettings: IPythonSettings) { + constructor(@inject(IServiceContainer) private readonly services: IServiceContainer) { + this.context = this.services.get(IExtensionContext); this.configuration = this.services.get(IConfigurationService); this.appShell = this.services.get(IApplicationShell); this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); @@ -65,15 +68,14 @@ export class AnalysisExtensionActivator implements IExtensionActivator { this.interpreterService = this.services.get(IInterpreterService); } - public async activate(context: ExtensionContext): Promise { + public async activate(): Promise { this.sw.reset(); - this.context = context; - const clientOptions = await this.getAnalysisOptions(context); + const clientOptions = await this.getAnalysisOptions(this.context); if (!clientOptions) { return false; } this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.restartLanguageServer())); - return this.startLanguageServer(context, clientOptions); + return this.startLanguageServer(this.context, clientOptions); } public async deactivate(): Promise { @@ -94,7 +96,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator { if (!idata || idata.hash !== this.interpreterHash) { this.interpreterHash = idata ? idata.hash : ''; await this.deactivate(); - await this.activate(this.context); + await this.activate(); } } diff --git a/src/client/activation/classic.ts b/src/client/activation/classic.ts index 76a25a426415..5ca9ee04c216 100644 --- a/src/client/activation/classic.ts +++ b/src/client/activation/classic.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { DocumentFilter, ExtensionContext, languages, OutputChannel } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { ILogger, IOutputChannel, IPythonSettings } from '../common/types'; +import { inject, injectable } from 'inversify'; +import { DocumentFilter, languages, OutputChannel } from 'vscode'; +import { PYTHON, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { IConfigurationService, IExtensionContext, ILogger, IOutputChannel } from '../common/types'; import { IShebangCodeLensProvider } from '../interpreter/contracts'; import { IServiceManager } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; @@ -19,15 +20,22 @@ import { PythonSymbolProvider } from '../providers/symbolProvider'; import { IUnitTestManagementService } from '../unittests/types'; import { IExtensionActivator } from './types'; +@injectable() export class ClassicExtensionActivator implements IExtensionActivator { - constructor(private serviceManager: IServiceManager, private pythonSettings: IPythonSettings, private documentSelector: DocumentFilter[]) { + private readonly context: IExtensionContext; + private jediFactory?: JediFactory; + private readonly documentSelector: DocumentFilter[]; + constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { + this.context = this.serviceManager.get(IExtensionContext); + this.documentSelector = PYTHON; } - public async activate(context: ExtensionContext): Promise { + public async activate(): Promise { + const context = this.context; const standardOutputChannel = this.serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); activateSimplePythonRefactorProvider(context, standardOutputChannel, this.serviceManager); - const jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager); + const jediFactory = this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager); context.subscriptions.push(jediFactory); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); @@ -44,7 +52,8 @@ export class ClassicExtensionActivator implements IExtensionActivator { const symbolProvider = new PythonSymbolProvider(jediFactory); context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider)); - if (this.pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { + const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); + if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { context.subscriptions.push(languages.registerSignatureHelpProvider(this.documentSelector, new PythonSignatureProvider(jediFactory), '(', ',')); } @@ -56,6 +65,9 @@ export class ClassicExtensionActivator implements IExtensionActivator { return true; } - // tslint:disable-next-line:no-empty - public async deactivate(): Promise { } + public async deactivate(): Promise { + if (this.jediFactory) { + this.jediFactory.dispose(); + } + } } diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts new file mode 100644 index 000000000000..b07ba82d6218 --- /dev/null +++ b/src/client/activation/serviceRegistry.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IServiceManager } from '../ioc/types'; +import { ExtensionActivationService } from './activationService'; +import { AnalysisExtensionActivator } from './analysis'; +import { ClassicExtensionActivator } from './classic'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IExtensionActivationService, ExtensionActivationService); + serviceManager.add(IExtensionActivator, ClassicExtensionActivator, ExtensionActivators.Jedi); + serviceManager.add(IExtensionActivator, AnalysisExtensionActivator, ExtensionActivators.DotNet); +} diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index f8366a6ce5dd..714f51378251 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -1,9 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as vscode from 'vscode'; +export const IExtensionActivationService = Symbol('IExtensionActivationService'); +export interface IExtensionActivationService { + activate(): Promise; +} + +export enum ExtensionActivators { + Jedi = 'Jedi', + DotNet = 'DotNet' +} +export const IExtensionActivator = Symbol('IExtensionActivator'); export interface IExtensionActivator { - activate(context: vscode.ExtensionContext): Promise; + activate(): Promise; deactivate(): Promise; } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 52533d6642f1..6affcbcf1617 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -71,8 +71,7 @@ export namespace LinterErrors { export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; export function isTestExecution(): boolean { - // tslint:disable-next-line:interface-name no-string-literal - return process.env['VSC_PYTHON_CI_TEST'] === '1'; + return process.env.VSC_PYTHON_CI_TEST === '1'; } export function isPythonAnalysisEngineTest(): boolean { return process.env.VSC_PYTHON_ANALYSIS === '1'; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 547ccc8f6401..71d39e8e4497 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -3,14 +3,16 @@ // Licensed under the MIT License. import { Socket } from 'net'; -import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri } from 'vscode'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, ExtensionContext, OutputChannel, Uri } from 'vscode'; import { EnvironmentVariables } from './variables/types'; export const IOutputChannel = Symbol('IOutputChannel'); +export interface IOutputChannel extends OutputChannel { } export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export const IsWindows = Symbol('IS_WINDOWS'); export const Is64Bit = Symbol('Is64Bit'); export const IDisposableRegistry = Symbol('IDiposableRegistry'); +export type IDisposableRegistry = Disposable[]; export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); @@ -233,3 +235,6 @@ export interface ISocketServer extends Disposable { readonly client: Promise; Start(options?: { port?: number; host?: string }): Promise; } + +export const IExtensionContext = Symbol('ExtensionContext'); +export interface IExtensionContext extends ExtensionContext { } diff --git a/src/client/extension.ts b/src/client/extension.ts index 70499bd31280..05a15fa5db55 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -11,11 +11,10 @@ import { extensions, IndentAction, languages, Memento, OutputChannel, window } from 'vscode'; -import { AnalysisExtensionActivator } from './activation/analysis'; -import { ClassicExtensionActivator } from './activation/classic'; -import { IExtensionActivator } from './activation/types'; +import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; +import { IExtensionActivationService } from './activation/types'; import { PythonSettings } from './common/configSettings'; -import { isPythonAnalysisEngineTest, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; +import { PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { FeatureDeprecationManager } from './common/featureDeprecationManager'; import { createDeferred } from './common/helpers'; import { PythonInstaller } from './common/installer/pythonInstallation'; @@ -25,7 +24,7 @@ import { registerTypes as processRegisterTypes } from './common/process/serviceR import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; import { StopWatch } from './common/stopWatch'; import { ITerminalHelper } from './common/terminal/types'; -import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; +import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, IExtensionContext, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; import { AttachRequestArguments, LaunchRequestArguments } from './debugger/Common/Contracts'; import { BaseConfigurationProvider } from './debugger/configProviders/baseProvider'; @@ -37,7 +36,7 @@ import { ICondaService, IInterpreterService } from './interpreter/contracts'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; -import { IServiceContainer } from './ioc/types'; +import { IServiceContainer, IServiceManager } from './ioc/types'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { ILintingEngine } from './linters/types'; @@ -76,18 +75,14 @@ export async function activate(context: ExtensionContext) { const configuration = serviceManager.get(IConfigurationService); const pythonSettings = configuration.getSettings(); - const activator: IExtensionActivator = isPythonAnalysisEngineTest() || !pythonSettings.jediEnabled - ? new AnalysisExtensionActivator(serviceManager, pythonSettings) - : new ClassicExtensionActivator(serviceManager, pythonSettings, PYTHON); - - await activator.activate(context); + const activationService = serviceContainer.get(IExtensionActivationService); + await activationService.activate(); const standardOutputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); sortImports.activate(context, standardOutputChannel, serviceManager); serviceManager.get(ICodeExecutionManager).registerCommands(); - // tslint:disable-next-line:no-floating-promises - sendStartupTelemetry(activated, serviceContainer); + sendStartupTelemetry(activated, serviceContainer).ignoreErrors(); const pythonInstaller = new PythonInstaller(serviceContainer); pythonInstaller.checkPythonInstallation(PythonSettings.getInstance()) @@ -160,15 +155,18 @@ export async function activate(context: ExtensionContext) { function registerServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance(IServiceManager, serviceManager); serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(IExtensionContext, context); const standardOutputChannel = window.createOutputChannel('Python'); const unitTestOutChannel = window.createOutputChannel('Python Test Log'); serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + activationRegisterTypes(serviceManager); commonRegisterTypes(serviceManager); processRegisterTypes(serviceManager); variableRegisterTypes(serviceManager); diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index d72edac84532..21a0ca067145 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -41,7 +41,6 @@ export abstract class BaseFormatter { return vscode.Uri.file(__dirname); } protected async provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, args: string[], cwd?: string): Promise { - this.outputChannel.clear(); if (typeof cwd !== 'string' || cwd.length === 0) { cwd = this.getWorkspaceUri(document).fsPath; } diff --git a/src/client/ioc/serviceManager.ts b/src/client/ioc/serviceManager.ts index 792358da06dd..bcd4331dfd13 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Container, interfaces } from 'inversify'; +import { Container, injectable, interfaces } from 'inversify'; import { Abstract, IServiceManager, Newable } from './types'; type identifier = string | symbol | Newable | Abstract; +@injectable() export class ServiceManager implements IServiceManager { constructor(private container: Container) { } // tslint:disable-next-line:no-any diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts index 93b424780278..722dc61c830e 100644 --- a/src/client/linters/lintingEngine.ts +++ b/src/client/linters/lintingEngine.ts @@ -92,7 +92,6 @@ export class LintingEngine implements ILintingEngine { }); this.pendingLintings.set(document.uri.fsPath, cancelToken); - this.outputChannel.clear(); const promises: Promise[] = this.linterManager.getActiveLinters(document.uri) .map(info => { diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts new file mode 100644 index 000000000000..24c5d2841522 --- /dev/null +++ b/src/test/activation/activationService.unit.test.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import * as TypeMoq from 'typemoq'; +import { ConfigurationChangeEvent, Disposable } from 'vscode'; +import { ExtensionActivationService } from '../../client/activation/activationService'; +import { ExtensionActivators, IExtensionActivationService, IExtensionActivator } from '../../client/activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { isPythonAnalysisEngineTest } from '../../client/common/constants'; +import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Activation - ActivationService', () => { + [true, false].forEach(jediIsEnabled => { + suite(`Jedi is ${jediIsEnabled ? 'dnabled' : 'disabled'}`, () => { + let serviceContainer: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let cmdManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + setup(function () { + if (isPythonAnalysisEngineTest()) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + serviceContainer = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + cmdManager = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + + workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup(w => w.workspaceFolders).returns(() => []); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + const output = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())).returns(() => output.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); + }); + + async function testActivation(activationService: IExtensionActivationService, activator: TypeMoq.IMock) { + activator + .setup(a => a.activate()).returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + const activatorName = jediIsEnabled ? ExtensionActivators.Jedi : ExtensionActivators.DotNet; + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IExtensionActivator), TypeMoq.It.isValue(activatorName))) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.once()); + + await activationService.activate(); + + activator.verifyAll(); + serviceContainer.verifyAll(); + } + test('Activatory must be activated', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + await testActivation(activationService, activator); + }); + test('Activatory must be deactivated', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + await testActivation(activationService, activator); + + activator + .setup(a => a.deactivate()).returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + activationService.dispose(); + activator.verifyAll(); + }); + test('Prompt user to reload VS Code and reload, when setting is toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + let jediIsEnabledValueInSetting = jediIsEnabled; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve('Reload')) + .verifiable(TypeMoq.Times.once()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.once()); + + // Toggle the value in the setting and invoke the callback. + jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Prompt user to reload VS Code and do not reload, when setting is toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + let jediIsEnabledValueInSetting = jediIsEnabled; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Toggle the value in the setting and invoke the callback. + jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Do not prompt user to reload VS Code when setting is not toggled', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('Do not prompt user to reload VS Code when setting is not changed', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + workspaceService + .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => callbackHandler = cb) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator = TypeMoq.Mock.ofType(); + const activationService = new ExtensionActivationService(serviceContainer.object); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + }); + }); +});