diff --git a/.eslintignore b/.eslintignore index aa024ed0e068..20f49860c5a0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -174,7 +174,6 @@ src/client/activation/commands.ts src/client/activation/progress.ts src/client/activation/extensionSurvey.ts src/client/activation/common/analysisOptions.ts -src/client/activation/refCountedLanguageServer.ts src/client/activation/languageClientMiddleware.ts src/client/formatters/serviceRegistry.ts diff --git a/package.nls.json b/package.nls.json index cd93d1a26861..443e6723515c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -175,9 +175,9 @@ "LanguageService.extractionCompletedOutputMessage": "Language server download complete", "LanguageService.extractionDoneOutputMessage": "done", "LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly", - "LanguageService.startingPylance": "Starting Pylance language server for {0}.", + "LanguageService.startingPylance": "Starting Pylance language server.", "LanguageService.startingJedi": "Starting Jedi language server for {0}.", - "LanguageService.startingNone": "Editor support is inactive since language server is set to None for {0}.", + "LanguageService.startingNone": "Editor support is inactive since language server is set to None.", "LanguageService.reloadAfterLanguageServerChange": "Please reload the window switching between language servers.", "AttachProcess.unsupportedOS": "Operating system '{0}' not supported.", "AttachProcess.attachTitle": "Attach to process", diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts deleted file mode 100644 index 05280218f7b2..000000000000 --- a/src/client/activation/refCountedLanguageServer.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit, -} from 'vscode'; - -import { Resource } from '../common/types'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ILanguageServerActivator, LanguageServerType } from './types'; - -export class RefCountedLanguageServer implements ILanguageServerActivator { - private refCount = 1; - constructor( - private impl: ILanguageServerActivator, - private _type: LanguageServerType, - private disposeCallback: () => void, - ) {} - - public increment = () => { - this.refCount += 1; - }; - - public get type() { - return this._type; - } - - public dispose() { - this.refCount = Math.max(0, this.refCount - 1); - if (this.refCount === 0) { - this.disposeCallback(); - } - } - - public start(_resource: Resource, _interpreter: PythonEnvironment | undefined): Promise { - throw new Error('Server should have already been started. Do not start the wrapper.'); - } - - public activate() { - this.impl.activate(); - } - - public deactivate() { - this.impl.deactivate(); - } - - public get connection() { - return this.impl.connection; - } - - public get capabilities() { - return this.impl.capabilities; - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - ): ProviderResult { - return this.impl.provideRenameEdits(document, position, newName, token); - } - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - ): ProviderResult { - return this.impl.provideDefinition(document, position, token); - } - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - return this.impl.provideHover(document, position, token); - } - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken, - ): ProviderResult { - return this.impl.provideReferences(document, position, context, token); - } - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext, - ): ProviderResult { - return this.impl.provideCompletionItems(document, position, token, context); - } - public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { - if (this.impl.resolveCompletionItem) { - return this.impl.resolveCompletionItem(item, token); - } - } - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - return this.impl.provideCodeLenses(document, token); - } - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - ): ProviderResult { - return this.impl.provideDocumentSymbols(document, token); - } - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - context: SignatureHelpContext, - ): ProviderResult { - return this.impl.provideSignatureHelp(document, position, token, context); - } -} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 327db695ef12..c183099f99bf 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -213,14 +213,11 @@ export namespace LanguageService { text: localize('LanguageService.statusItem.text', 'Partial Mode'), detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), }; - export const startingPylance = localize( - 'LanguageService.startingPylance', - 'Starting Pylance language server for {0}.', - ); + export const startingPylance = localize('LanguageService.startingPylance', 'Starting Pylance language server.'); export const startingJedi = localize('LanguageService.startingJedi', 'Starting Jedi language server for {0}.'); export const startingNone = localize( 'LanguageService.startingNone', - 'Editor support is inactive since language server is set to None for {0}.', + 'Editor support is inactive since language server is set to None.', ); export const untrustedWorkspaceMessage = localize( 'LanguageService.untrustedWorkspaceMessage', diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 25f0c9ae7195..503cadab27b8 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Uri } from 'vscode'; +import { ConfigurationChangeEvent, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; import { IExtensionActivationService, @@ -50,7 +50,9 @@ export class LanguageServerWatcher private workspaceInterpreters: Map; - // In a multiroot workspace scenario we will have one language server per folder. + // In a multiroot workspace scenario we may have multiple language servers running: + // When using Jedi, there will be one language server per workspace folder. + // When using Pylance, there will only be one language server for the project. private workspaceLanguageServers: Map; private languageServerChangeHandler: LanguageServerChangeHandler; @@ -77,6 +79,10 @@ export class LanguageServerWatcher disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + disposables.push( + this.workspaceService.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders.bind(this)), + ); + if (this.workspaceService.isTrusted) { disposables.push(this.interpreterPathService.onDidChange(this.onDidChangeInterpreter.bind(this))); } @@ -113,7 +119,7 @@ export class LanguageServerWatcher languageServerType: LanguageServerType, resource?: Resource, ): Promise { - const lsResource = this.getWorkspaceKey(resource); + const lsResource = this.getWorkspaceUri(resource); const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); const interpreter = await this.interpreterService?.getActiveInterpreter(resource); @@ -142,6 +148,15 @@ export class LanguageServerWatcher serverType = LanguageServerType.None; } + // If the language server type is Pylance or None, + // We only need to instantiate the language server once, even in multiroot workspace scenarios, + // so we only need one language server extension manager. + const key = this.getWorkspaceKey(resource, serverType); + const languageServer = this.workspaceLanguageServers.get(key); + if ((serverType === LanguageServerType.Node || serverType === LanguageServerType.None) && languageServer) { + return languageServer; + } + // Instantiate the language server extension manager. const languageServerExtensionManager = this.createLanguageServer(serverType); @@ -156,7 +171,7 @@ export class LanguageServerWatcher await languageServerExtensionManager.languageServerNotAvailable(); } - this.workspaceLanguageServers.set(lsResource.fsPath, languageServerExtensionManager); + this.workspaceLanguageServers.set(key, languageServerExtensionManager); return languageServerExtensionManager; } @@ -164,8 +179,8 @@ export class LanguageServerWatcher // ILanguageServerCache public async get(resource?: Resource): Promise { - const lsResource = this.getWorkspaceKey(resource); - let languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); + const key = this.getWorkspaceKey(resource, this.languageServerType); + let languageServerExtensionManager = this.workspaceLanguageServers.get(key); if (!languageServerExtensionManager) { languageServerExtensionManager = await this.startAndGetLanguageServer(this.languageServerType, resource); @@ -177,13 +192,13 @@ export class LanguageServerWatcher // Private methods private stopLanguageServer(resource?: Resource): void { - const lsResource = this.getWorkspaceKey(resource); - const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); + const key = this.getWorkspaceKey(resource, this.languageServerType); + const languageServerExtensionManager = this.workspaceLanguageServers.get(key); if (languageServerExtensionManager) { languageServerExtensionManager.stopLanguageServer(); languageServerExtensionManager.dispose(); - this.workspaceLanguageServers.delete(lsResource.fsPath); + this.workspaceLanguageServers.delete(key); } } @@ -228,11 +243,11 @@ export class LanguageServerWatcher } private async refreshLanguageServer(resource?: Resource): Promise { - const lsResource = this.getWorkspaceKey(resource); + const lsResource = this.getWorkspaceUri(resource); const languageServerType = this.configurationService.getSettings(lsResource).languageServer; if (languageServerType !== this.languageServerType) { - this.stopLanguageServer(lsResource); + this.stopLanguageServer(resource); await this.startLanguageServer(languageServerType, lsResource); } } @@ -267,8 +282,19 @@ export class LanguageServerWatcher } } - // Get the workspace key for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. - private getWorkspaceKey(resource?: Resource): Uri { + // Watch for workspace folder changes. + private async onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise { + // Since Jedi is the only language server type where we instantiate multiple language servers, + // Make sure to dispose of them only in that scenario. + if (event.removed.length && this.languageServerType === LanguageServerType.Jedi) { + event.removed.forEach((workspace) => { + this.stopLanguageServer(workspace.uri); + }); + } + } + + // Get the workspace Uri for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. + private getWorkspaceUri(resource?: Resource): Uri { let uri; if (resource) { @@ -279,6 +305,19 @@ export class LanguageServerWatcher return uri ?? Uri.parse('default'); } + + // Get the key used to identify which language server extension manager is associated to which workspace. + // When using Pylance or having no LS enabled, we return a static key since there should only be one LS extension manager for these LS types. + private getWorkspaceKey(resource: Resource | undefined, languageServerType: LanguageServerType): string { + switch (languageServerType) { + case LanguageServerType.Node: + return 'Pylance'; + case LanguageServerType.None: + return 'None'; + default: + return this.getWorkspaceUri(resource).fsPath; + } + } } function logStartup(languageServerType: LanguageServerType, resource: Uri): void { @@ -290,10 +329,10 @@ function logStartup(languageServerType: LanguageServerType, resource: Uri): void outputLine = LanguageService.startingJedi().format(basename); break; case LanguageServerType.Node: - outputLine = LanguageService.startingPylance().format(basename); + outputLine = LanguageService.startingPylance(); break; case LanguageServerType.None: - outputLine = LanguageService.startingNone().format(basename); + outputLine = LanguageService.startingNone(); break; default: throw new Error(`Unknown language server type: ${languageServerType}`); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 74a6939ec97b..ca26633600a2 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -3,7 +3,9 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { ConfigurationChangeEvent, Disposable, Uri } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { JediLanguageServerManager } from '../../client/activation/jedi/manager'; +import { NodeLanguageServerManager } from '../../client/activation/node/manager'; import { ILanguageServerOutputChannel, LanguageServerType } from '../../client/activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; @@ -17,6 +19,7 @@ import { LanguageService } from '../../client/common/utils/localize'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; import { IInterpreterHelper, IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; +import { JediLSExtensionManager } from '../../client/languageServer/jediLSExtensionManager'; import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; import { LanguageServerWatcher } from '../../client/languageServer/watcher'; @@ -51,6 +54,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -97,6 +103,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, {} as ICommandManager, {} as IFileSystem, @@ -110,7 +119,7 @@ suite('Language server watcher', () => { disposables, ); - assert.strictEqual(disposables.length, 4); + assert.strictEqual(disposables.length, 5); }); test('The constructor should not add a listener to onDidChange to the list of disposables if it is not a trusted workspace', () => { @@ -137,6 +146,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, {} as ICommandManager, {} as IFileSystem, @@ -150,7 +162,7 @@ suite('Language server watcher', () => { disposables, ); - assert.strictEqual(disposables.length, 3); + assert.strictEqual(disposables.length, 4); }); test(`When starting the language server, the language server extension manager should not be undefined`, async () => { @@ -161,7 +173,7 @@ suite('Language server watcher', () => { assert.notStrictEqual(extensionManager, undefined); }); - test(`When starting the language server, if the interpreter changed, the existing language server should be stopped if there is one`, async () => { + test(`If the interpreter changed, the existing language server should be stopped if there is one`, async () => { const getActiveInterpreterStub = sandbox.stub(); getActiveInterpreterStub.onFirstCall().returns('python'); getActiveInterpreterStub.onSecondCall().returns('other/python'); @@ -201,6 +213,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -269,6 +284,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -320,6 +338,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { onDidChangeConfigListener = listener; }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService; watcher = new LanguageServerWatcher( @@ -376,6 +397,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { onDidChangeConfigListener = listener; }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, workspaceFolders: [{ uri: Uri.parse('workspace') }], } as unknown) as IWorkspaceService; @@ -460,6 +484,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -515,6 +542,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -568,6 +598,9 @@ suite('Language server watcher', () => { onDidChangeConfiguration: () => { /* do nothing */ }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, } as unknown) as IWorkspaceService, ({ registerCommand: () => { @@ -590,57 +623,173 @@ suite('Language server watcher', () => { assert.ok(startLanguageServerStub.calledOnce); }); - test('When starting language servers with different resources, multiple language servers should be instantiated', async () => { - const getActiveInterpreterStub = sandbox.stub(); - getActiveInterpreterStub.onFirstCall().returns({ path: 'folder1/python', version: { major: 2, minor: 7 } }); - getActiveInterpreterStub.onSecondCall().returns({ path: 'folder2/python', version: { major: 2, minor: 7 } }); - const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); - startLanguageServerStub.returns(Promise.resolve()); - const stopLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'stopLanguageServer'); + [ + { + languageServer: LanguageServerType.Jedi, + multiLS: true, + extensionLSCls: JediLSExtensionManager, + lsManagerCls: JediLanguageServerManager, + }, + { + languageServer: LanguageServerType.Node, + multiLS: false, + extensionLSCls: PylanceLSExtensionManager, + lsManagerCls: NodeLanguageServerManager, + }, + { + languageServer: LanguageServerType.None, + multiLS: false, + extensionLSCls: NoneLSExtensionManager, + lsManagerCls: undefined, + }, + ].forEach(({ languageServer, multiLS, extensionLSCls, lsManagerCls }) => { + test(`When starting language servers with different resources, ${ + multiLS ? 'multiple' : 'a single' + } language server${multiLS ? 's' : ''} should be instantiated when using ${languageServer}`, async () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns({ path: 'folder1/python', version: { major: 3, minor: 9 } }); + getActiveInterpreterStub + .onSecondCall() + .returns({ path: 'folder2/python', version: { major: 3, minor: 10 } }); + const startLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + sandbox.stub(extensionLSCls.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: getActiveInterpreterStub, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(languageServer, Uri.parse('folder1')); + await watcher.startLanguageServer(languageServer, Uri.parse('folder2')); + + // If multiLS set to true, then we expect to have called startLanguageServer twice. + // If multiLS set to false, then we expect to have called startLanguageServer once. + assert.ok(startLanguageServerStub.calledTwice === multiLS); + assert.ok(startLanguageServerStub.calledOnce === !multiLS); + assert.ok(getActiveInterpreterStub.calledTwice); + assert.ok(stopLanguageServerStub.notCalled); + }); - watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, - { - getSettings: () => ({ languageServer: LanguageServerType.Jedi }), - } as IConfigurationService, - {} as IExperimentService, - ({ - getActiveWorkspaceUri: () => undefined, - } as unknown) as IInterpreterHelper, - {} as IInterpreterPathService, - ({ - getActiveInterpreter: getActiveInterpreterStub, - } as unknown) as IInterpreterService, - {} as IEnvironmentVariablesProvider, - ({ - isTrusted: false, + test(`${languageServer} language server(s) should ${ + multiLS ? '' : 'not' + } be stopped if a workspace gets removed from the current project`, async () => { + sandbox.stub(extensionLSCls.prototype, 'startLanguageServer').returns(Promise.resolve()); + if (lsManagerCls) { + sandbox.stub(lsManagerCls.prototype, 'dispose').returns(); + } + + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + stopLanguageServerStub.returns(); + + let onDidChangeWorkspaceFoldersListener: (event: WorkspaceFoldersChangeEvent) => Promise = () => + Promise.resolve(); + + const workspaceService = ({ getWorkspaceFolder: (uri: Uri) => ({ uri }), onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown) as IWorkspaceService, - ({ - registerCommand: () => { - /* do nothing */ - }, - } as unknown) as ICommandManager, - {} as IFileSystem, - ({ - getExtension: () => undefined, - onDidChange: () => { - /* do nothing */ + onDidChangeWorkspaceFolders: (listener: (event: WorkspaceFoldersChangeEvent) => Promise) => { + onDidChangeWorkspaceFoldersListener = listener; }, - } as unknown) as IExtensions, - {} as IApplicationShell, - [] as Disposable[], - ); - - await watcher.startLanguageServer(LanguageServerType.None, Uri.parse('folder1')); - await watcher.startLanguageServer(LanguageServerType.None, Uri.parse('folder2')); - - assert.ok(startLanguageServerStub.calledTwice); - assert.ok(getActiveInterpreterStub.calledTwice); - assert.ok(stopLanguageServerStub.notCalled); + workspaceFolders: [{ uri: Uri.parse('workspace1') }, { uri: Uri.parse('workspace2') }], + isTrusted: true, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 3, minor: 7 } }), + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(languageServer, Uri.parse('workspace1')); + await watcher.startLanguageServer(languageServer, Uri.parse('workspace2')); + + await onDidChangeWorkspaceFoldersListener({ + added: [], + removed: [{ uri: Uri.parse('workspace2') } as WorkspaceFolder], + }); + + // If multiLS set to true, then we expect to have stopped a language server. + // If multiLS set to false, then we expect to not have stopped a language server. + assert.ok(stopLanguageServerStub.calledOnce === multiLS); + assert.ok(stopLanguageServerStub.notCalled === !multiLS); + }); }); });