diff --git a/news/2 Fixes/3008.md b/news/2 Fixes/3008.md new file mode 100644 index 000000000000..a1a3ee4bdd54 --- /dev/null +++ b/news/2 Fixes/3008.md @@ -0,0 +1 @@ +Add support for multi root workspaces with the new language server server diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index a114595f1343..de9f3567509e 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -77,6 +77,15 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } } protected onWorkspaceFoldersChanged() { + //If an activated workspace folder was removed, delete its key + const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspaceKey(workspaceFolder.uri)); + const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + this.activatedWorkspaces.delete(folder); + } + } this.addRemoveDocOpenedHandlers(); } protected hasMultipleWorkspaces() { diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 5dad35d07073..b8f4cbf30219 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -17,12 +17,14 @@ import { EventName } from '../telemetry/constants'; import { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types'; const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; +const workspacePathNameForGlobalWorkspaces = ''; type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator }; @injectable() export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable { + private lsActivatedWorkspaces = new Map(); private currentActivator?: ActivatorInfo; - private activatedOnce: boolean = false; + private jediActivatedOnce: boolean = false; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; private readonly appShell: IApplicationShell; @@ -40,45 +42,54 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); } public async activate(resource: Resource): Promise { - if (this.currentActivator || this.activatedOnce) { - return; - } - this.resource = resource; - this.activatedOnce = true; - let jedi = this.useJedi(); if (!jedi) { + if (this.lsActivatedWorkspaces.has(this.getWorkspacePathKey(resource))) { + return; + } const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined); this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); if (diagnostic.length) { sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED); jedi = true; } + } else { + if (this.jediActivatedOnce) { + return; + } + this.jediActivatedOnce = true; } + this.resource = resource; await this.logStartup(jedi); - let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; let activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); this.currentActivator = { jedi, activator }; try { - await activator.activate(); - return; + await activator.activate(resource); + if (!jedi) { + this.lsActivatedWorkspaces.set(this.getWorkspacePathKey(resource), activator); + } } catch (ex) { if (jedi) { return; } //Language server fails, reverting to jedi + if (this.jediActivatedOnce) { + return; + } + this.jediActivatedOnce = true; jedi = true; await this.logStartup(jedi); activatorName = LanguageServerActivator.Jedi; activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); this.currentActivator = { jedi, activator }; - await activator.activate(); + await activator.activate(resource); } } @@ -88,6 +99,19 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv } } + protected onWorkspaceFoldersChanged() { + //If an activated workspace folder was removed, dispose its activator + const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspacePathKey(workspaceFolder.uri)); + const activatedWkspcKeys = Array.from(this.lsActivatedWorkspaces.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + this.lsActivatedWorkspaces.get(folder).dispose(); + this.lsActivatedWorkspaces.delete(folder); + } + } + } + private async logStartup(isJedi: boolean): Promise { const outputLine = isJedi ? 'Starting Jedi Python language engine.' @@ -119,4 +143,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv const configurationService = this.serviceContainer.get(IConfigurationService); return configurationService.getSettings(this.resource).jediEnabled; } + private getWorkspacePathKey(resource: Resource): string { + return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); + } } diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index 1773dc38dcff..4c5ab167689f 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -4,7 +4,7 @@ import { inject, injectable } from 'inversify'; import { DocumentFilter, languages } from 'vscode'; import { PYTHON } from '../common/constants'; -import { IConfigurationService, IExtensionContext, ILogger } from '../common/types'; +import { IConfigurationService, IExtensionContext, ILogger, Resource } from '../common/types'; import { IShebangCodeLensProvider } from '../interpreter/contracts'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; @@ -33,7 +33,10 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.documentSelector = PYTHON; } - public async activate(): Promise { + public async activate(resource: Resource): Promise { + if (this.jediFactory) { + throw new Error('Jedi already started'); + } const context = this.context; const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager)); diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index 5bb8a2608a10..750de3a2acce 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -34,14 +34,16 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService - ) {} + ) { } @traceDecorators.error('Failed to activate language server') - public async activate(): Promise { - const mainWorkspaceUri = this.workspace.hasWorkspaceFolders - ? this.workspace.workspaceFolders![0].uri - : undefined; - await this.ensureLanguageServerIsAvailable(mainWorkspaceUri); - await this.manager.start(mainWorkspaceUri); + public async activate(resource: Resource): Promise { + if (!resource) { + resource = this.workspace.hasWorkspaceFolders + ? this.workspace.workspaceFolders![0].uri + : undefined; + } + await this.ensureLanguageServerIsAvailable(resource); + await this.manager.start(resource); } public dispose(): void { this.manager.dispose(); diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts index 42a33e35d036..785753379d9a 100644 --- a/src/client/activation/languageServer/analysisOptions.ts +++ b/src/client/activation/languageServer/analysisOptions.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { CancellationToken, CompletionContext, ConfigurationChangeEvent, Disposable, Event, EventEmitter, OutputChannel, Position, TextDocument } from 'vscode'; import { LanguageClientOptions, ProvideCompletionItemsSignature } from 'vscode-languageclient'; import { IWorkspaceService } from '../../common/application/types'; -import { isTestExecution, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import { isTestExecution, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import { traceDecorators, traceError } from '../../common/logger'; import { BANNER_NAME_PROPOSE_LS, IConfigurationService, IExtensionContext, IOutputChannel, IPathUtils, IPythonExtensionBanner, Resource } from '../../common/types'; import { debounce } from '../../common/utils/decorators'; @@ -79,7 +79,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt properties['DatabasePath'] = path.join(this.context.extensionPath, this.languageServerFolder); let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : []; - const settings = this.configuration.getSettings(); + const settings = this.configuration.getSettings(this.resource); if (settings.autoComplete) { const extraPaths = settings.autoComplete.extraPaths; if (extraPaths && extraPaths.length > 0) { @@ -99,11 +99,20 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt this.excludedFiles = this.getExcludedFiles(); this.typeshedPaths = this.getTypeshedPaths(); - + const workspaceFolder = this.workspace.getWorkspaceFolder(this.resource); + const documentSelector = [ + { scheme: 'file', language: PYTHON_LANGUAGE }, + { scheme: 'untitled', language: PYTHON_LANGUAGE } + ]; + if (workspaceFolder){ + // tslint:disable-next-line:no-any + (documentSelector[0] as any).pattern = `${workspaceFolder.uri.fsPath}/**/*`; + } // Options to control the language client return { // Register the server for Python documents - documentSelector: PYTHON, + documentSelector, + workspaceFolder, synchronize: { configurationSection: PYTHON_LANGUAGE }, diff --git a/src/client/activation/languageServer/languageServerExtension.ts b/src/client/activation/languageServer/languageServerExtension.ts new file mode 100644 index 000000000000..5e5a886dda5a --- /dev/null +++ b/src/client/activation/languageServer/languageServerExtension.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { ICommandManager } from '../../common/application/types'; +import '../../common/extensions'; +import { IDisposable } from '../../common/types'; +import { ILanguageServerExtension } from '../types'; + +const loadExtensionCommand = 'python._loadLanguageServerExtension'; + +@injectable() +export class LanguageServerExtension implements ILanguageServerExtension { + public loadExtensionArgs?: {}; + protected readonly _invoked = new EventEmitter(); + private disposable?: IDisposable; + constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) { } + public dispose() { + if (this.disposable) { + this.disposable.dispose(); + } + } + public register(): Promise { + if (this.disposable) { + return; + } + this.disposable = this.commandManager.registerCommand(loadExtensionCommand, args => { + this.loadExtensionArgs = args; + this._invoked.fire(); + }); + } + public get invoked(): Event { + return this._invoked.event; + } +} diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 4486ff2661ac..50f3aa3b2b62 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -4,7 +4,6 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ICommandManager } from '../../common/application/types'; import '../../common/extensions'; import { traceDecorators } from '../../common/logger'; import { IDisposable, Resource } from '../../common/types'; @@ -12,21 +11,18 @@ import { debounce } from '../../common/utils/decorators'; import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types'; - -const loadExtensionCommand = 'python._loadLanguageServerExtension'; +import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension, ILanguageServerManager } from '../types'; @injectable() export class LanguageServerManager implements ILanguageServerManager { - protected static loadExtensionArgs?: {}; private languageServer?: ILanguageServer; private resource!: Resource; private disposables: IDisposable[] = []; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions - ) {} + @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, + @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension + ) { } public dispose() { if (this.languageServer) { this.languageServer.dispose(); @@ -46,15 +42,11 @@ export class LanguageServerManager implements ILanguageServerManager { await this.startLanguageServer(); } protected registerCommandHandler() { - const disposable = this.commandManager.registerCommand(loadExtensionCommand, args => { - LanguageServerManager.loadExtensionArgs = args; - this.loadExtensionIfNecessary(); - }); - this.disposables.push(disposable); + this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); } protected loadExtensionIfNecessary() { - if (this.languageServer && LanguageServerManager.loadExtensionArgs) { - this.languageServer.loadExtension(LanguageServerManager.loadExtensionArgs); + if (this.languageServer && this.lsExtension.loadExtensionArgs) { + this.languageServer.loadExtension(this.lsExtension.loadExtensionArgs); } } @debounce(1000) diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 0190bbb3b897..297d3620ab97 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -21,16 +21,18 @@ import { InterpreterDataService } from './languageServer/interpreterDataService' import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from './languageServer/languageClientFactory'; import { LanguageServer } from './languageServer/languageServer'; import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService'; +import { LanguageServerExtension } from './languageServer/languageServerExtension'; import { LanguageServerFolderService } from './languageServer/languageServerFolderService'; import { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } from './languageServer/languageServerPackageRepository'; import { LanguageServerPackageService } from './languageServer/languageServerPackageService'; import { LanguageServerManager } from './languageServer/manager'; import { PlatformData } from './languageServer/platformData'; -import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IInterpreterDataService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types'; +import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IInterpreterDataService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IExtensionActivationManager, ExtensionActivationManager); serviceManager.addSingleton(IExtensionActivationService, LanguageServerExtensionActivationService); + serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension); + serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); serviceManager.add(ILanguageServerActivator, JediExtensionActivator, LanguageServerActivator.Jedi); serviceManager.add(ILanguageServerActivator, LanguageServerExtensionActivator, LanguageServerActivator.DotNet); serviceManager.addSingleton(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 1e85c3cad556..e14b0d1d43e5 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -28,7 +28,7 @@ export enum LanguageServerActivator { export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); export interface ILanguageServerActivator extends IDisposable { - activate(): Promise; + activate(resource: Resource): Promise; } export const IHttpClient = Symbol('IHttpClient'); @@ -88,6 +88,12 @@ export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { start(resource: Resource): Promise; } +export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); +export interface ILanguageServerExtension extends IDisposable { + readonly invoked: Event; + loadExtensionArgs?: {}; + register(): void; +} export const ILanguageServer = Symbol('ILanguageServer'); export interface ILanguageServer extends IDisposable { start(resource: Resource, options: LanguageClientOptions): Promise; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index e0016a6919c8..57fea6d413c3 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -373,6 +373,17 @@ export class PythonSettings implements IPythonSettings { protected getPythonExecutable(pythonPath: string) { return getPythonExecutable(pythonPath); } + protected onWorkspaceFoldersChanged() { + //If an activated workspace folder was removed, delete its key + const workspaceKeys = this.workspace.workspaceFolders!.map(workspaceFolder => workspaceFolder.uri.fsPath); + const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + PythonSettings.pythonSettings.delete(folder); + } + } + } protected initialize(): void { const onDidChange = () => { const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot); @@ -382,6 +393,7 @@ export class PythonSettings implements IPythonSettings { // Let's defer the change notification. this.debounceChangeNotification(); }; + this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); this.disposables.push(this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { if (event.affectsConfiguration('python')) { diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b3bf641bf670..8ba0e1fdd3c5 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,5 +1,7 @@ +import { DocumentFilter } from 'vscode'; + export const PYTHON_LANGUAGE = 'python'; -export const PYTHON = [ +export const PYTHON: DocumentFilter[] = [ { scheme: 'file', language: PYTHON_LANGUAGE }, { scheme: 'untitled', language: PYTHON_LANGUAGE } ]; diff --git a/src/client/extension.ts b/src/client/extension.ts index b43834a9099d..5f6f6bd945e6 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -33,7 +33,7 @@ import { } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; -import { IExtensionActivationManager } from './activation/types'; +import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types'; import { buildApi, IExtensionApi } from './api'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; @@ -294,6 +294,7 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa serviceContainer.get(IApplicationDiagnostics).register(); serviceContainer.get(ITestCodeNavigatorCommandHandler).register(); serviceContainer.get(ITestExplorerCommandHandler).register(); + serviceContainer.get(ILanguageServerExtension).register(); } // tslint:disable-next-line:no-any diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 43df84602a92..87dfc045d3ad 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -32,6 +32,7 @@ import { ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitT @injectable() export class UnitTestManagementService implements IUnitTestManagementService, Disposable { private readonly outputChannel: OutputChannel; + private activatedOnce: boolean = false; private readonly disposableRegistry: Disposable[]; private workspaceTestManagerService?: IWorkspaceTestManagerService; private documentManager: IDocumentManager; @@ -59,6 +60,10 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di return this._onDidStatusChange.event; } public async activate(symbolProvider: DocumentSymbolProvider): Promise { + if (this.activatedOnce) { + return; + } + this.activatedOnce = true; this.workspaceTestManagerService = this.serviceContainer.get(IWorkspaceTestManagerService); const disposablesRegistry = this.serviceContainer.get(IDisposableRegistry); diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index d1e295afa07c..bec7fa120060 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -174,7 +174,7 @@ suite('Activation - ActivationManager', () => { documentManager.verifyAll(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); - verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); verify(workspaceService.hasWorkspaceFolders).once(); verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); verify(activationService1.activate(resource)).once(); @@ -230,7 +230,7 @@ suite('Activation - ActivationManager', () => { documentManager.verifyAll(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); - verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); verify(workspaceService.hasWorkspaceFolders).once(); //Removed no. of folders to one @@ -240,7 +240,7 @@ suite('Activation - ActivationManager', () => { workspaceFoldersChangedHandler.call(managerTest); - verify(workspaceService.workspaceFolders).twice(); + verify(workspaceService.workspaceFolders).atLeast(1); verify(workspaceService.hasWorkspaceFolders).twice(); }); }); diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 1e820152fe30..b39d1023a672 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -5,15 +5,15 @@ // tslint:disable:max-func-body-length +import { expect } from 'chai'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { ConfigurationChangeEvent, Disposable } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, Uri } from 'vscode'; import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; import { FolderVersionPair, IExtensionActivationService, ILanguageServerActivator, - ILanguageServerCompatibilityService, ILanguageServerFolderService, LanguageServerActivator } from '../../client/activation/types'; @@ -21,7 +21,7 @@ import { LSNotSupportedDiagnosticServiceId } from '../../client/application/diag import { IDiagnosticsService } from '../../client/application/diagnostics/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; import { IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IConfigurationService, IDisposable, IDisposableRegistry, IOutputChannel, IPythonSettings, Resource } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; suite('Activation - ActivationService', () => { @@ -33,7 +33,6 @@ suite('Activation - ActivationService', () => { let cmdManager: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; let platformService: TypeMoq.IMock; - let lanagueServerSupportedService: TypeMoq.IMock; let lsNotSupportedDiagnosticService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); @@ -48,7 +47,6 @@ suite('Activation - ActivationService', () => { path: '', version: new SemVer('1.2.3') }; - lanagueServerSupportedService = TypeMoq.Mock.ofType(); lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType(); workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); workspaceService.setup(w => w.workspaceFolders).returns(() => []); @@ -96,7 +94,7 @@ suite('Activation - ActivationService', () => { lsSupported: boolean = true ) { activator - .setup(a => a.activate()) + .setup(a => a.activate(undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); let activatorName = LanguageServerActivator.Jedi; @@ -127,7 +125,6 @@ suite('Activation - ActivationService', () => { } test('LS is supported', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); @@ -135,7 +132,6 @@ suite('Activation - ActivationService', () => { await testActivation(activationService, activator, true); }); test('LS is not supported', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(false)); pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); @@ -144,7 +140,6 @@ suite('Activation - ActivationService', () => { }); test('Activatory must be activated', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); @@ -152,7 +147,6 @@ suite('Activation - ActivationService', () => { await testActivation(activationService, activator); }); test('Activatory must be deactivated', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); @@ -168,7 +162,6 @@ suite('Activation - ActivationService', () => { activator.verifyAll(); }); test('Prompt user to reload VS Code and reload, when setting is toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; let jediIsEnabledValueInSetting = jediIsEnabled; workspaceService @@ -206,7 +199,6 @@ suite('Activation - ActivationService', () => { cmdManager.verifyAll(); }); test('Prompt user to reload VS Code and do not reload, when setting is toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; let jediIsEnabledValueInSetting = jediIsEnabled; workspaceService @@ -244,7 +236,6 @@ suite('Activation - ActivationService', () => { cmdManager.verifyAll(); }); test('Do not prompt user to reload VS Code when setting is not toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; workspaceService .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -280,7 +271,6 @@ suite('Activation - ActivationService', () => { cmdManager.verifyAll(); }); test('Do not prompt user to reload VS Code when setting is not changed', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; workspaceService .setup(w => w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -317,7 +307,6 @@ suite('Activation - ActivationService', () => { }); if (!jediIsEnabled) { test('Revert to jedi when LS activation fails', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activatorDotNet = TypeMoq.Mock.ofType(); const activatorJedi = TypeMoq.Mock.ofType(); @@ -339,7 +328,7 @@ suite('Activation - ActivationService', () => { .returns(() => activatorDotNet.object) .verifiable(TypeMoq.Times.once()); activatorDotNet - .setup(a => a.activate()) + .setup(a => a.activate(undefined)) .returns(() => Promise.reject(new Error(''))) .verifiable(TypeMoq.Times.once()); serviceContainer @@ -352,7 +341,7 @@ suite('Activation - ActivationService', () => { .returns(() => activatorJedi.object) .verifiable(TypeMoq.Times.once()); activatorJedi - .setup(a => a.activate()) + .setup(a => a.activate(undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -362,6 +351,114 @@ suite('Activation - ActivationService', () => { activatorJedi.verifyAll(); serviceContainer.verifyAll(); }); + async function testActivationOfResource( + activationService: IExtensionActivationService, + activator: TypeMoq.IMock, + resource: Resource + ) { + activator + .setup(a => a.activate(resource)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + lsNotSupportedDiagnosticService + .setup(l => l.diagnose(undefined)) + .returns(() => Promise.resolve([])); + lsNotSupportedDiagnosticService + .setup(l => l.handle(TypeMoq.It.isValue([]))) + .returns(() => Promise.resolve()); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.DotNet))) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup(w => w.getWorkspaceFolderIdentifier(resource, '')) + .returns(() => resource.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await activationService.activate(resource); + + activator.verifyAll(); + serviceContainer.verifyAll(); + workspaceService.verifyAll(); + } + test('Activator is disposed if activated workspace is removed', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + let workspaceFoldersChangedHandler!: Function; + workspaceService + .setup(w => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => (workspaceFoldersChangedHandler = cb)) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); + workspaceService.verifyAll(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + const folder3 = { name: 'three', uri: Uri.parse('three'), index: 3 }; + + const activator1 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator1, folder1.uri); + const activator2 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator2, folder2.uri); + const activator3 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator3, folder3.uri); + + //Now remove folder3 + workspaceService.reset(); + workspaceService.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspaceService + .setup(w => w.getWorkspaceFolderIdentifier(folder1.uri, '')) + .returns(() => folder1.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup(w => w.getWorkspaceFolderIdentifier(folder2.uri, '')) + .returns(() => folder2.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + activator1 + .setup(d => d.dispose()) + .verifiable(TypeMoq.Times.never()); + activator2 + .setup(d => d.dispose()) + .verifiable(TypeMoq.Times.never()); + activator3 + .setup(d => d.dispose()) + .verifiable(TypeMoq.Times.once()); + workspaceFoldersChangedHandler.call(activationService); + workspaceService.verifyAll(); + activator3.verifyAll(); + }); + } else { + test('Jedi is only activated once', async () => { + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activator1 = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService(serviceContainer.object); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.Jedi))) + .returns(() => activator1.object) + .verifiable(TypeMoq.Times.once()); + activator1 + .setup(a => a.activate(folder1.uri)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await activationService.activate(folder1.uri); + activator1.verifyAll(); + serviceContainer.verifyAll(); + + const activator2 = TypeMoq.Mock.ofType(); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.Jedi))) + .returns(() => activator2.object) + .verifiable(TypeMoq.Times.once()); + activator2 + .setup(a => a.activate(folder2.uri)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + await activationService.activate(folder2.uri); + serviceContainer.verifyAll(); + activator2.verifyAll(); + }); } }); }); diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts index 592e1da0c42c..cd7811fdcf26 100644 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ b/src/test/activation/languageServer/activator.unit.test.ts @@ -61,7 +61,7 @@ suite('Language Server - Activator', () => { when(manager.start(undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(); + await activator.activate(undefined); verify(manager.start(undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -76,7 +76,7 @@ suite('Language Server - Activator', () => { when(manager.start(undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(); + await activator.activate(undefined); verify(manager.start(undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -94,7 +94,7 @@ suite('Language Server - Activator', () => { when(lsFolderService.getLanguageServerFolderName()).thenResolve(languageServerFolder); when(fs.fileExists(mscorlib)).thenResolve(true); - await activator.activate(); + await activator.activate(undefined); verify(manager.start(undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -114,7 +114,7 @@ suite('Language Server - Activator', () => { when(fs.fileExists(mscorlib)).thenResolve(false); when(lsDownloader.downloadLanguageServer(languageServerFolderPath)).thenReturn(deferred.promise); - const promise = activator.activate(); + const promise = activator.activate(undefined); await sleep(1); verify(workspaceService.hasWorkspaceFolders).once(); verify(lsFolderService.getLanguageServerFolderName()).once(); @@ -135,7 +135,7 @@ suite('Language Server - Activator', () => { when(manager.start(uri)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(); + await activator.activate(undefined); verify(manager.start(uri)).once(); verify(workspaceService.hasWorkspaceFolders).once(); diff --git a/src/test/activation/languageServer/languageServerExtension.unit.test.ts b/src/test/activation/languageServer/languageServerExtension.unit.test.ts new file mode 100644 index 000000000000..3c7739a690dd --- /dev/null +++ b/src/test/activation/languageServer/languageServerExtension.unit.test.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; + +use(chaiAsPromised); + +// tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression + +const loadExtensionCommand = 'python._loadLanguageServerExtension'; + +suite('Language Server - Language Server Extension', () => { + class LanguageServerExtensionTest extends LanguageServerExtension { + // tslint:disable-next-line:no-unnecessary-override + public async register(): Promise { + return super.register(); + } + public clearLoadExtensionArgs() { + super.loadExtensionArgs = undefined; + } + } + let extension: LanguageServerExtensionTest; + let cmdManager: ICommandManager; + let commandRegistrationDisposable: typemoq.IMock; + setup(() => { + cmdManager = mock(CommandManager); + commandRegistrationDisposable = typemoq.Mock.ofType(); + extension = new LanguageServerExtensionTest(instance(cmdManager)); + extension.clearLoadExtensionArgs(); + }); + test('Must register command handler', async () => { + when(cmdManager.registerCommand(loadExtensionCommand, anything())).thenReturn( + commandRegistrationDisposable.object + ); + await extension.register(); + verify(cmdManager.registerCommand(loadExtensionCommand, anything())).once(); + extension.dispose(); + commandRegistrationDisposable.verify(d => d.dispose(), typemoq.Times.once()); + }); +}); diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index 51fe13d17612..6da677edb31d 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -5,17 +5,14 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; +import { instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; import { LanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; import { LanguageServer } from '../../../client/activation/languageServer/languageServer'; +import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; import { LanguageServerManager } from '../../../client/activation/languageServer/manager'; -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../../client/activation/types'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; -import { IDisposable } from '../../../client/common/types'; +import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension } from '../../../client/activation/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; import { sleep } from '../../core'; @@ -24,53 +21,42 @@ use(chaiAsPromised); // tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression -const loadExtensionCommand = 'python._loadLanguageServerExtension'; - suite('Language Server - Manager', () => { - class LanguageServerManagerTest extends LanguageServerManager { - public static initializeExtensionArgs(args: {}) { - LanguageServerManager.loadExtensionArgs = args; - } - public clearLoadExtensionArgs() { - LanguageServerManager.loadExtensionArgs = undefined; - } - } - let manager: LanguageServerManagerTest; + let manager: LanguageServerManager; let serviceContainer: IServiceContainer; let analysisOptions: ILanguageServerAnalysisOptions; let languageServer: ILanguageServer; - let commandManager: ICommandManager; - let onChangeHandler: Function; + let lsExtension: ILanguageServerExtension; + let onChangeAnalysisHandler: Function; const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; - let commandRegistrationDisposable: typemoq.IMock; setup(() => { serviceContainer = mock(ServiceContainer); analysisOptions = mock(LanguageServerAnalysisOptions); languageServer = mock(LanguageServer); - commandManager = mock(CommandManager); - commandRegistrationDisposable = typemoq.Mock.ofType(); - manager = new LanguageServerManagerTest( + lsExtension = mock(LanguageServerExtension); + manager = new LanguageServerManager( instance(serviceContainer), - instance(commandManager), - instance(analysisOptions) + instance(analysisOptions), + instance(lsExtension) ); - manager.clearLoadExtensionArgs(); }); [undefined, Uri.file(__filename)].forEach(resource => { async function startLanguageServer() { - when(commandManager.registerCommand(loadExtensionCommand, anything())).thenReturn( - commandRegistrationDisposable.object - ); - - let handlerRegistered = false; - const changeFn = (handler: Function) => { - handlerRegistered = true; - onChangeHandler = handler; + let invoked = false; + const lsExtensionChangeFn = (handler: Function) => { + invoked = true; + }; + when(lsExtension.invoked).thenReturn(lsExtensionChangeFn as any); + + let analysisHandlerRegistered = false; + const analysisChangeFn = (handler: Function) => { + analysisHandlerRegistered = true; + onChangeAnalysisHandler = handler; }; when(analysisOptions.initialize(resource)).thenResolve(); when(analysisOptions.getAnalysisOptions()).thenResolve(languageClientOptions); - when(analysisOptions.onDidChange).thenReturn(changeFn as any); + when(analysisOptions.onDidChange).thenReturn(analysisChangeFn as any); when(serviceContainer.get(ILanguageServer)).thenReturn(instance(languageServer)); when(languageServer.start(resource, languageClientOptions)).thenResolve(); @@ -80,10 +66,9 @@ suite('Language Server - Manager', () => { verify(analysisOptions.getAnalysisOptions()).once(); verify(serviceContainer.get(ILanguageServer)).once(); verify(languageServer.start(resource, languageClientOptions)).once(); - expect(handlerRegistered).to.be.true; + expect(invoked).to.be.true; + expect(analysisHandlerRegistered).to.be.true; verify(languageServer.dispose()).never(); - verify(commandManager.registerCommand(loadExtensionCommand, anything())).once(); - commandRegistrationDisposable.verify(d => d.dispose(), typemoq.Times.never()); } test('Start must register handlers and initialize analysis options', async () => { await startLanguageServer(); @@ -100,7 +85,7 @@ suite('Language Server - Manager', () => { test('Changes in analysis options must restart LS', async () => { await startLanguageServer(); - await onChangeHandler.call(manager); + await onChangeAnalysisHandler.call(manager); await sleep(1); verify(languageServer.dispose()).once(); @@ -112,15 +97,15 @@ suite('Language Server - Manager', () => { test('Changes in analysis options must throttled when restarting LS', async () => { await startLanguageServer(); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); await Promise.all([ - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager) + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) ]); await sleep(1); @@ -133,15 +118,15 @@ suite('Language Server - Manager', () => { test('Multiple changes in analysis options must restart LS twice', async () => { await startLanguageServer(); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); await Promise.all([ - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager) + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) ]); await sleep(1); @@ -151,15 +136,15 @@ suite('Language Server - Manager', () => { verify(serviceContainer.get(ILanguageServer)).twice(); verify(languageServer.start(resource, languageClientOptions)).twice(); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); - await onChangeHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); await Promise.all([ - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager), - onChangeHandler.call(manager) + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) ]); await sleep(1); @@ -169,27 +154,9 @@ suite('Language Server - Manager', () => { verify(serviceContainer.get(ILanguageServer)).thrice(); verify(languageServer.start(resource, languageClientOptions)).thrice(); }); - test('Must register command handler', async () => { - await startLanguageServer(); - manager.dispose(); - - commandRegistrationDisposable.verify(d => d.dispose(), typemoq.Times.once()); - }); - test('Must load extension when command is sent', async () => { - const args = { x: 1 }; - await startLanguageServer(); - - verify(languageServer.loadExtension(args)).never(); - - const cb = capture(commandManager.registerCommand).first()[1] as Function; - cb.call(manager, args); - - verify(languageServer.loadExtension(args)).once(); - commandRegistrationDisposable.verify(d => d.dispose(), typemoq.Times.never()); - }); test('Must load extension when command was been sent before starting LS', async () => { const args = { x: 1 }; - LanguageServerManagerTest.initializeExtensionArgs(args); + when(lsExtension.loadExtensionArgs).thenReturn(args as any); await startLanguageServer();