diff --git a/.eslintignore b/.eslintignore index 35ae06dc99f7..aa024ed0e068 100644 --- a/.eslintignore +++ b/.eslintignore @@ -35,10 +35,7 @@ src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts -src/test/activation/activationService.unit.test.ts src/test/activation/activeResource.unit.test.ts -src/test/activation/node/languageServerChangeHandler.unit.test.ts -src/test/activation/node/activator.unit.test.ts src/test/activation/extensionSurvey.unit.test.ts src/test/utils/fs.ts @@ -176,18 +173,9 @@ src/client/terminals/codeExecution/djangoContext.ts src/client/activation/commands.ts src/client/activation/progress.ts src/client/activation/extensionSurvey.ts -src/client/activation/common/languageServerChangeHandler.ts -src/client/activation/common/activatorBase.ts src/client/activation/common/analysisOptions.ts src/client/activation/refCountedLanguageServer.ts src/client/activation/languageClientMiddleware.ts -src/client/activation/node/manager.ts -src/client/activation/node/languageServerProxy.ts -src/client/activation/node/languageClientFactory.ts -src/client/activation/node/languageServerFolderService.ts -src/client/activation/node/analysisOptions.ts -src/client/activation/node/activator.ts -src/client/activation/none/activator.ts src/client/formatters/serviceRegistry.ts src/client/formatters/helper.ts diff --git a/news/1 Enhancements/18509.md b/news/1 Enhancements/18509.md new file mode 100644 index 000000000000..82e7ce4b4389 --- /dev/null +++ b/news/1 Enhancements/18509.md @@ -0,0 +1 @@ +Do not require a reload when swapping between language servers. diff --git a/package.nls.json b/package.nls.json index 0feacbd3df62..f2d12974ba41 100644 --- a/package.nls.json +++ b/package.nls.json @@ -171,9 +171,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.", - "LanguageService.startingJedi": "Starting Jedi language server.", - "LanguageService.startingNone": "Editor support is inactive since language server is set to None.", + "LanguageService.startingPylance": "Starting Pylance language server for {0}.", + "LanguageService.startingJedi": "Starting Jedi language server for {0}.", + "LanguageService.startingNone": "Editor support is inactive since language server is set to None for {0}.", "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/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index eed61fc439d3..e30a8adc1736 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -4,7 +4,6 @@ "onDidChange": false, "defaultInterpreterPath": "placeholder", "defaultLS": true, - "downloadLanguageServer": true, "envFile": "placeholder", "venvPath": "placeholder", "venvFolders": "placeholder", @@ -14,7 +13,6 @@ "devOptions": false, "disableInstallationChecks": false, "globalModuleInstallation": false, - "autoUpdateLanguageServer": false, "languageServer": true, "languageServerIsDefault": false, "logging": true, diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts deleted file mode 100644 index 4600b1d5e60e..000000000000 --- a/src/client/activation/activationService.ts +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { - IConfigurationService, - IDisposableRegistry, - IExtensions, - IPersistentStateFactory, - IPythonSettings, - Resource, -} from '../common/types'; -import { swallowExceptions } from '../common/utils/decorators'; -import { LanguageService } from '../common/utils/localize'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { LanguageServerChangeHandler } from './common/languageServerChangeHandler'; -import { RefCountedLanguageServer } from './refCountedLanguageServer'; -import { - IExtensionActivationService, - ILanguageServerActivator, - ILanguageServerCache, - LanguageServerType, -} from './types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { traceError, traceLog } from '../logging'; - -const languageServerSetting: keyof IPythonSettings = 'languageServer'; -const workspacePathNameForGlobalWorkspaces = ''; - -interface IActivatedServer { - key: string; - server: ILanguageServerActivator; - jedi: boolean; -} - -function logStartup(serverType: LanguageServerType): void { - let outputLine; - switch (serverType) { - case LanguageServerType.Jedi: - outputLine = LanguageService.startingJedi(); - break; - case LanguageServerType.Node: - outputLine = LanguageService.startingPylance(); - break; - case LanguageServerType.None: - outputLine = LanguageService.startingNone(); - break; - default: - throw new Error('Unknown language server type in activator.'); - } - traceLog(outputLine); -} - -@injectable() -export class LanguageServerExtensionActivationService - implements IExtensionActivationService, ILanguageServerCache, Disposable { - private cache = new Map>(); - - private activatedServer?: IActivatedServer; - - public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; - - private readonly workspaceService: IWorkspaceService; - - private readonly configurationService: IConfigurationService; - - private readonly interpreterService: IInterpreterService; - - private readonly languageServerChangeHandler: LanguageServerChangeHandler; - - private resource!: Resource; - - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - ) { - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.configurationService = this.serviceContainer.get(IConfigurationService); - this.interpreterService = this.serviceContainer.get(IInterpreterService); - 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)); - if (this.workspaceService.isTrusted) { - this.interpreterService = this.serviceContainer.get(IInterpreterService); - disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); - } - - this.languageServerChangeHandler = new LanguageServerChangeHandler( - this.getCurrentLanguageServerType(), - this.serviceContainer.get(IExtensions), - this.serviceContainer.get(IApplicationShell), - this.serviceContainer.get(ICommandManager), - this.workspaceService, - this.configurationService, - ); - disposables.push(this.languageServerChangeHandler); - } - - public async activate(resource: Resource): Promise { - const stopWatch = new StopWatch(); - // Get a new server and dispose of the old one (might be the same one) - this.resource = resource; - const interpreter = await this.interpreterService?.getActiveInterpreter(resource); - const key = await this.getKey(resource, interpreter); - - // If we have an old server with a different key, then deactivate it as the - // creation of the new server may fail if this server is still connected - if (this.activatedServer && this.activatedServer.key !== key) { - this.activatedServer.server.deactivate(); - } - - // Get the new item - const result = await this.get(resource, interpreter); - - // Now we dispose. This ensures the object stays alive if it's the same object because - // we dispose after we increment the ref count. - if (this.activatedServer) { - this.activatedServer.server.dispose(); - } - - // Save our active server. - this.activatedServer = { key, server: result, jedi: result.type === LanguageServerType.Jedi }; - - // Force this server to reconnect (if disconnected) as it should be the active - // language server for all of VS code. - this.activatedServer.server.activate(); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_STARTUP_DURATION, stopWatch.elapsedTime, { - languageServerType: result.type, - }); - } - - public async get(resource: Resource, interpreter?: PythonEnvironment): Promise { - // See if we already have it or not - const key = await this.getKey(resource, interpreter); - let result: Promise | undefined = this.cache.get(key); - if (!result) { - // Create a special ref counted result so we don't dispose of the - // server too soon. - result = this.createRefCountedServer(resource, interpreter, key); - this.cache.set(key, result); - } else { - // Increment ref count if already exists. - result = result.then((r) => { - r.increment(); - return r; - }); - } - return result; - } - - public dispose(): void { - if (this.activatedServer) { - this.activatedServer.server.dispose(); - } - } - - @swallowExceptions('Send telemetry for language server current selection') - public async sendTelemetryForChosenLanguageServer(languageServer: LanguageServerType): Promise { - const state = this.stateFactory.createGlobalPersistentState( - 'SWITCH_LS', - undefined, - ); - if (typeof state.value !== 'string') { - await state.updateValue(languageServer); - } - if (state.value !== languageServer) { - await state.updateValue(languageServer); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - switchTo: languageServer, - }); - } else { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - lsStartup: languageServer, - }); - } - } - - /** - * Checks if user does not have any `languageServer` setting set. - * @param resource - * @returns `true` if user is using default configuration, `false` if user has `languageServer` setting added. - */ - public isJediUsingDefaultConfiguration(resource: Resource): boolean { - const settings = this.workspaceService - .getConfiguration('python', resource) - .inspect('languageServer'); - if (!settings) { - traceError('WorkspaceConfiguration.inspect returns `undefined` for setting `python.languageServer`'); - return false; - } - return ( - settings.globalValue === undefined && - settings.workspaceValue === undefined && - settings.workspaceFolderValue === undefined - ); - } - - protected async onWorkspaceFoldersChanged(): Promise { - // If an activated workspace folder was removed, dispose its activator - const workspaceKeys = await Promise.all( - this.workspaceService.workspaceFolders!.map((workspaceFolder) => this.getKey(workspaceFolder.uri)), - ); - const activatedWkspcKeys = Array.from(this.cache.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); - if (activatedWkspcFoldersRemoved.length > 0) { - for (const folder of activatedWkspcFoldersRemoved) { - const server = await this.cache.get(folder); - server?.dispose(); // This should remove it from the cache if this is the last instance. - } - } - } - - private async onDidChangeInterpreter() { - // Reactivate the resource. It should destroy the old one if it's different. - return this.activate(this.resource); - } - - private getCurrentLanguageServerType(): LanguageServerType { - const configurationService = this.serviceContainer.get(IConfigurationService); - return configurationService.getSettings(this.resource).languageServer; - } - - private getCurrentLanguageServerTypeIsDefault(): boolean { - const configurationService = this.serviceContainer.get(IConfigurationService); - return configurationService.getSettings(this.resource).languageServerIsDefault; - } - - private async createRefCountedServer( - resource: Resource, - interpreter: PythonEnvironment | undefined, - key: string, - ): Promise { - let serverType = this.getCurrentLanguageServerType(); - - // If the interpreter is Python 2 and the LS setting is explicitly set to Jedi, turn it off. - // If set to Default, use Pylance. - if (interpreter && (interpreter.version?.major ?? 0) < 3) { - if (serverType === LanguageServerType.Jedi) { - serverType = LanguageServerType.None; - } else if (this.getCurrentLanguageServerTypeIsDefault()) { - serverType = LanguageServerType.Node; - } - } - - if ( - !this.workspaceService.isTrusted && - serverType !== LanguageServerType.Node && - serverType !== LanguageServerType.None - ) { - traceLog(LanguageService.untrustedWorkspaceMessage()); - serverType = LanguageServerType.None; - } - this.sendTelemetryForChosenLanguageServer(serverType).ignoreErrors(); - - logStartup(serverType); - let server = this.serviceContainer.get(ILanguageServerActivator, serverType); - try { - await server.start(resource, interpreter); - } catch (ex) { - if (serverType === LanguageServerType.Jedi) { - throw ex; - } - traceError(ex); - traceLog(LanguageService.lsFailedToStart()); - serverType = LanguageServerType.Jedi; - server = this.serviceContainer.get(ILanguageServerActivator, serverType); - await server.start(resource, interpreter); - } - - // Wrap the returned server in something that ref counts it. - return new RefCountedLanguageServer(server, serverType, () => { - // When we finally remove the last ref count, remove from the cache - this.cache.delete(key); - - // Dispose of the actual server. - server.dispose(); - }); - } - - private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { - const workspacesUris: (Uri | undefined)[] = this.workspaceService.workspaceFolders?.map( - (workspace) => workspace.uri, - ) ?? [undefined]; - if ( - workspacesUris.findIndex((uri) => event.affectsConfiguration(`python.${languageServerSetting}`, uri)) === -1 - ) { - return; - } - const lsType = this.getCurrentLanguageServerType(); - if (this.activatedServer?.key !== lsType) { - await this.languageServerChangeHandler.handleLanguageServerChange(lsType); - } - } - - private async getKey(resource: Resource, interpreter?: PythonEnvironment): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - const serverType = configurationService.getSettings(this.resource).languageServer; - if (serverType === LanguageServerType.Node) { - return LanguageServerType.Node; - } - - const resourcePortion = this.workspaceService.getWorkspaceFolderIdentifier( - resource, - workspacePathNameForGlobalWorkspaces, - ); - interpreter = interpreter || (await this.interpreterService?.getActiveInterpreter(resource)); - const interperterPortion = interpreter ? `${interpreter.path}-${interpreter.envName}` : ''; - return `${resourcePortion}-${interperterPortion}`; - } -} diff --git a/src/client/activation/common/defaultlanguageServer.ts b/src/client/activation/common/defaultlanguageServer.ts index d901ed72155a..dc40a2c0ed5b 100644 --- a/src/client/activation/common/defaultlanguageServer.ts +++ b/src/client/activation/common/defaultlanguageServer.ts @@ -5,7 +5,6 @@ import { injectable } from 'inversify'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; import { IDefaultLanguageServer, IExtensions, DefaultLSType } from '../../common/types'; import { IServiceManager } from '../../ioc/types'; -import { ILSExtensionApi } from '../node/languageServerFolderService'; import { LanguageServerType } from '../types'; @injectable() @@ -29,7 +28,7 @@ export async function setDefaultLanguageServer( } async function getDefaultLanguageServer(extensions: IExtensions): Promise { - if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { + if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { return LanguageServerType.Node; } diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts index 99b59e3eba32..daae78a88d2b 100644 --- a/src/client/activation/common/languageServerChangeHandler.ts +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -6,7 +6,7 @@ import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../com import { PYLANCE_EXTENSION_ID } from '../../common/constants'; import { IConfigurationService, IExtensions } from '../../common/types'; import { createDeferred } from '../../common/utils/async'; -import { Common, LanguageService, Pylance } from '../../common/utils/localize'; +import { Pylance } from '../../common/utils/localize'; import { LanguageServerType } from '../types'; export async function promptForPylanceInstall( @@ -36,7 +36,6 @@ export async function promptForPylanceInstall( if (target) { await configService.updateSetting('languageServer', LanguageServerType.Jedi, undefined, target); - commandManager.executeCommand('workbench.action.reloadWindow'); } } } @@ -45,7 +44,9 @@ export async function promptForPylanceInstall( export class LanguageServerChangeHandler implements Disposable { // For tests that need to track Pylance install completion. private readonly pylanceInstallCompletedDeferred = createDeferred(); + private readonly disposables: Disposable[] = []; + private pylanceInstalled = false; constructor( @@ -85,42 +86,23 @@ export class LanguageServerChangeHandler implements Disposable { // may get one reload prompt now and then another when Pylance is finally installed. // Instead, check the installation and suppress prompt if Pylance is not there. // Extensions change event handler will then show its own prompt. - let response: string | undefined; if (lsType === LanguageServerType.Node && !this.isPylanceInstalled()) { // If not installed, point user to Pylance at the store. await promptForPylanceInstall(this.appShell, this.commands, this.workspace, this.configService); // At this point Pylance is not yet installed. Skip reload prompt // since we are going to show it when Pylance becomes available. - } else { - response = await this.appShell.showInformationMessage( - LanguageService.reloadAfterLanguageServerChange(), - Common.reload(), - ); - if (response === Common.reload()) { - this.commands.executeCommand('workbench.action.reloadWindow'); - } } + this.currentLsType = lsType; } private async extensionsChangeHandler(): Promise { // Track Pylance extension installation state and prompt to reload when it becomes available. const oldInstallState = this.pylanceInstalled; + this.pylanceInstalled = this.isPylanceInstalled(); if (oldInstallState === this.pylanceInstalled) { this.pylanceInstallCompletedDeferred.resolve(); - return; - } - - const response = await this.appShell.showWarningMessage( - Pylance.pylanceInstalledReloadPromptMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo(), - ); - - this.pylanceInstallCompletedDeferred.resolve(); - if (response === Common.bannerLabelYes()) { - this.commands.executeCommand('workbench.action.reloadWindow'); } } diff --git a/src/client/activation/jedi/activator.ts b/src/client/activation/jedi/activator.ts deleted file mode 100644 index 7c9af8962340..000000000000 --- a/src/client/activation/jedi/activator.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; - -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; -import { LanguageServerActivatorBase } from '../common/activatorBase'; -import { ILanguageServerManager } from '../types'; - -/** - * Starts jedi language server manager. - * - * @export - * @class JediLanguageServerActivator - * @implements {ILanguageServerActivator} - * @extends {LanguageServerActivatorBase} - */ -@injectable() -export class JediLanguageServerActivator extends LanguageServerActivatorBase { - // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor( - @inject(ILanguageServerManager) manager: ILanguageServerManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IFileSystem) fs: IFileSystem, - @inject(IConfigurationService) configurationService: IConfigurationService, - ) { - super(manager, workspace, fs, configurationService); - } - - // eslint-disable-next-line class-methods-use-this - public async ensureLanguageServerIsAvailable(_resource: Resource): Promise { - // Nothing to do here. Jedi language server is shipped with the extension - } -} diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts index 924e8b79eb6d..b1a184c91118 100644 --- a/src/client/activation/jedi/analysisOptions.ts +++ b/src/client/activation/jedi/analysisOptions.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; + import * as path from 'path'; import { WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; @@ -13,15 +13,14 @@ import { ILanguageServerOutputChannel } from '../types'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types, class-methods-use-this */ -@injectable() export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsWithEnv { private resource: Resource | undefined; constructor( - @inject(IEnvironmentVariablesProvider) envVarsProvider: IEnvironmentVariablesProvider, - @inject(ILanguageServerOutputChannel) lsOutputChannel: ILanguageServerOutputChannel, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IWorkspaceService) workspace: IWorkspaceService, + envVarsProvider: IEnvironmentVariablesProvider, + lsOutputChannel: ILanguageServerOutputChannel, + private readonly configurationService: IConfigurationService, + workspace: IWorkspaceService, ) { super(envVarsProvider, lsOutputChannel, workspace); this.resource = undefined; diff --git a/src/client/activation/jedi/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts index 82616ca36f15..e0ddd9b1a0a7 100644 --- a/src/client/activation/jedi/languageClientFactory.ts +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; import * as path from 'path'; import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; @@ -13,9 +12,8 @@ import { ILanguageClientFactory } from '../types'; const languageClientName = 'Python Tools'; -@injectable() export class JediLanguageClientFactory implements ILanguageClientFactory { - constructor(@inject(IInterpreterService) private interpreterService: IInterpreterService) {} + constructor(private interpreterService: IInterpreterService) {} public async createLanguageClient( resource: Resource, @@ -30,13 +28,6 @@ export class JediLanguageClientFactory implements ILanguageClientFactory { args: [lsScriptPath], }; - // eslint-disable-next-line global-require - const vscodeLanguageClient = require('vscode-languageclient/node') as typeof import('vscode-languageclient/node'); // NOSONAR - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions, - ); + return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); } } diff --git a/src/client/activation/jedi/languageServerProxy.ts b/src/client/activation/jedi/languageServerProxy.ts index ca7136bf16f7..2f94664f52cb 100644 --- a/src/client/activation/jedi/languageServerProxy.ts +++ b/src/client/activation/jedi/languageServerProxy.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + import '../../common/extensions'; -import { inject, injectable } from 'inversify'; import { DidChangeConfigurationNotification, Disposable, @@ -22,7 +22,6 @@ import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; import { killPid } from '../../common/process/rawProcessApis'; import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; -@injectable() export class JediLanguageServerProxy implements ILanguageServerProxy { public languageClient: LanguageClient | undefined; @@ -35,8 +34,8 @@ export class JediLanguageServerProxy implements ILanguageServerProxy { private lsVersion: string | undefined; constructor( - @inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory, - @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + private readonly factory: ILanguageClientFactory, + private readonly interpreterPathService: IInterpreterPathService, ) {} private static versionTelemetryProps(instance: JediLanguageServerProxy) { diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index d74158df6138..c59ef70b9272 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + import * as fs from 'fs-extra'; import * as path from 'path'; import '../../common/extensions'; -import { inject, injectable, named } from 'inversify'; - import { ICommandManager } from '../../common/application/types'; import { IDisposable, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; @@ -24,10 +23,7 @@ import { } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging'; -@injectable() export class JediLanguageServerManager implements ILanguageServerManager { - private languageServerProxy?: ILanguageServerProxy; - private resource!: Resource; private interpreter: PythonEnvironment | undefined; @@ -43,11 +39,10 @@ export class JediLanguageServerManager implements ILanguageServerManager { private lsVersion: string | undefined; constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) - @named(LanguageServerType.Jedi) + private readonly serviceContainer: IServiceContainer, private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ICommandManager) commandManager: ICommandManager, + private readonly languageServerProxy: ILanguageServerProxy, + commandManager: ICommandManager, ) { if (JediLanguageServerManager.commandDispose) { JediLanguageServerManager.commandDispose.dispose(); @@ -77,9 +72,6 @@ export class JediLanguageServerManager implements ILanguageServerManager { @traceDecoratorError('Failed to start language server') public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { - if (this.languageProxy) { - throw new Error('Language server already started'); - } this.resource = resource; this.interpreter = interpreter; this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); @@ -107,13 +99,17 @@ export class JediLanguageServerManager implements ILanguageServerManager { } public connect(): void { - this.connected = true; - this.middleware?.connect(); + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } } public disconnect(): void { - this.connected = false; - this.middleware?.disconnect(); + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } } @debounceSync(1000) @@ -139,8 +135,6 @@ export class JediLanguageServerManager implements ILanguageServerManager { ) @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { - this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); - const options = await this.analysisOptions.getAnalysisOptions(); this.middleware = new LanguageClientMiddleware(this.serviceContainer, LanguageServerType.Jedi, this.lsVersion); options.middleware = this.middleware; diff --git a/src/client/activation/node/activator.ts b/src/client/activation/node/activator.ts deleted file mode 100644 index f0de5687c44c..000000000000 --- a/src/client/activation/node/activator.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { CancellationToken, CompletionItem, ProviderResult } from 'vscode'; - -import ProtocolCompletionItem from 'vscode-languageclient/lib/common/protocolCompletionItem'; -import { CompletionResolveRequest } from 'vscode-languageclient/node'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IExtensions, Resource } from '../../common/types'; -import { Pylance } from '../../common/utils/localize'; -import { LanguageServerActivatorBase } from '../common/activatorBase'; -import { promptForPylanceInstall } from '../common/languageServerChangeHandler'; -import { ILanguageServerManager } from '../types'; - -/** - * Starts Pylance language server manager. - * - * @export - * @class NodeLanguageServerActivator - * @implements {ILanguageServerActivator} - * @extends {LanguageServerActivatorBase} - */ -@injectable() -export class NodeLanguageServerActivator extends LanguageServerActivatorBase { - constructor( - @inject(ILanguageServerManager) manager: ILanguageServerManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IFileSystem) fs: IFileSystem, - @inject(IConfigurationService) configurationService: IConfigurationService, - @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(ICommandManager) readonly commandManager: ICommandManager, - ) { - super(manager, workspace, fs, configurationService); - } - - public async ensureLanguageServerIsAvailable(resource: Resource): Promise { - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer === false) { - // Development mode. - return; - } - if (!this.extensions.getExtension(PYLANCE_EXTENSION_ID)) { - // Pylance is not yet installed. Throw will cause activator to use Jedi - // temporarily. Language server installation tracker will prompt for window - // reload when Pylance becomes available. - await promptForPylanceInstall( - this.appShell, - this.commandManager, - this.workspace, - this.configurationService, - ); - throw new Error(Pylance.pylanceNotInstalledMessage()); - } - } - - public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { - return this.handleResolveCompletionItem(item, token); - } - - private async handleResolveCompletionItem( - item: CompletionItem, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - - if (languageClient) { - // Turn our item into a ProtocolCompletionItem before we convert it. This preserves the .data - // attribute that it has and is needed to match on the language server side. - const protoItem: ProtocolCompletionItem = new ProtocolCompletionItem( - typeof item.label === 'string' ? item.label : item.label.label, - ); - Object.assign(protoItem, item); - - const args = languageClient.code2ProtocolConverter.asCompletionItem(protoItem); - const result = await languageClient.sendRequest(CompletionResolveRequest.type, args, token); - - if (result) { - return languageClient.protocol2CodeConverter.asCompletionItem(result); - } - } - } -} diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 077665b7b2c0..7518666aa9d0 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,24 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; + +import { LanguageClientOptions } from 'vscode-languageclient'; import { IWorkspaceService } from '../../common/application/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; -@injectable() export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { - constructor( - @inject(ILanguageServerOutputChannel) lsOutputChannel: ILanguageServerOutputChannel, - @inject(IWorkspaceService) workspace: IWorkspaceService, - ) { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService) { super(lsOutputChannel, workspace); } - protected async getInitializationOptions() { - return { + // eslint-disable-next-line class-methods-use-this + protected async getInitializationOptions(): Promise { + return ({ experimentationSupport: true, trustedWorkspaceSupport: true, - }; + } as unknown) as LanguageClientOptions; } } diff --git a/src/client/activation/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index 0c1534dd5619..6516ff9cb23f 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -1,29 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; import * as path from 'path'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; +import { PYLANCE_EXTENSION_ID, PYTHON_LANGUAGE } from '../../common/constants'; import { IFileSystem } from '../../common/platform/types'; -import { Resource } from '../../common/types'; +import { IExtensions, Resource } from '../../common/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; -import { ILanguageClientFactory, ILanguageServerFolderService } from '../types'; +import { ILanguageClientFactory } from '../types'; const languageClientName = 'Python Tools'; -@injectable() export class NodeLanguageClientFactory implements ILanguageClientFactory { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(ILanguageServerFolderService) - private readonly languageServerFolderService: ILanguageServerFolderService, - ) {} + constructor(private readonly fs: IFileSystem, private readonly extensions: IExtensions) {} public async createLanguageClient( - resource: Resource, + _resource: Resource, _interpreter: PythonEnvironment | undefined, clientOptions: LanguageClientOptions, ): Promise { @@ -31,13 +25,10 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { const commandArgs = (clientOptions.connectionOptions ?.cancellationStrategy as FileBasedCancellationStrategy).getCommandLineArguments(); - const folderName = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const languageServerFolder = path.isAbsolute(folderName) - ? folderName - : path.join(EXTENSION_ROOT_DIR, folderName); - - const bundlePath = path.join(languageServerFolder, 'server.bundle.js'); - const nonBundlePath = path.join(languageServerFolder, 'server.js'); + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + const languageServerFolder = extension ? extension.extensionPath : ''; + const bundlePath = path.join(languageServerFolder, 'dist', 'server.bundle.js'); + const nonBundlePath = path.join(languageServerFolder, 'dist', 'server.js'); const modulePath = (await this.fs.fileExists(nonBundlePath)) ? nonBundlePath : bundlePath; const debugOptions = { execArgv: ['--nolazy', '--inspect=6600'] }; @@ -59,12 +50,6 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { }, }; - const vscodeLanguageClient = require('vscode-languageclient/node'); - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions, - ); + return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); } } diff --git a/src/client/activation/node/languageServerFolderService.ts b/src/client/activation/node/languageServerFolderService.ts deleted file mode 100644 index 846d35d50407..000000000000 --- a/src/client/activation/node/languageServerFolderService.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { IExtensions, Resource } from '../../common/types'; -import { FolderVersionPair, ILanguageServerFolderService } from '../types'; - -// Exported for testing. -export interface ILanguageServerFolder { - path: string; - version: string; // SemVer, in string form to avoid cross-extension type issues. -} - -// Exported for testing. -export interface ILSExtensionApi { - languageServerFolder?(): Promise; -} - -@injectable() -export class NodeLanguageServerFolderService implements ILanguageServerFolderService { - constructor(@inject(IExtensions) readonly extensions: IExtensions) {} - - public async skipDownload(): Promise { - return (await this.lsExtensionApi()) !== undefined; - } - - public async getLanguageServerFolderName(_resource: Resource): Promise { - const lsf = await this.languageServerFolder(); - if (lsf) { - assert.ok(path.isAbsolute(lsf.path)); - return lsf.path; - } - throw new Error(`${PYLANCE_EXTENSION_ID} not installed`); - } - - public async getCurrentLanguageServerDirectory(): Promise { - const lsf = await this.languageServerFolder(); - if (lsf) { - assert.ok(path.isAbsolute(lsf.path)); - return { - path: lsf.path, - version: new SemVer(lsf.version), - }; - } - throw new Error(`${PYLANCE_EXTENSION_ID} not installed`); - } - - protected async languageServerFolder(): Promise { - const extension = await this.lsExtensionApi(); - if (!extension?.languageServerFolder) { - return undefined; - } - return extension.languageServerFolder(); - } - - private async lsExtensionApi(): Promise { - const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); - if (!extension) { - return undefined; - } - - if (!extension.isActive) { - return extension.activate(); - } - - return extension.exports; - } -} diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index 8d58c39e688c..697dfce1f9ed 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable } from 'inversify'; import { DidChangeConfigurationNotification, Disposable, @@ -11,19 +10,21 @@ import { State, } from 'vscode-languageclient/node'; -import { IConfigurationService, IExperimentService, IInterpreterPathService, Resource } from '../../common/types'; +import { IExperimentService, IExtensions, IInterpreterPathService, Resource } from '../../common/types'; import { createDeferred, Deferred, sleep } from '../../common/utils/async'; import { noop } from '../../common/utils/misc'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; import { ProgressReporting } from '../progress'; -import { ILanguageClientFactory, ILanguageServerFolderService, ILanguageServerProxy } from '../types'; +import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; import { IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +// eslint-disable-next-line @typescript-eslint/no-namespace namespace InExperiment { export const Method = 'python/inExperiment'; @@ -36,6 +37,7 @@ namespace InExperiment { } } +// eslint-disable-next-line @typescript-eslint/no-namespace namespace GetExperimentValue { export const Method = 'python/getExperimentValue'; @@ -48,23 +50,26 @@ namespace GetExperimentValue { } } -@injectable() export class NodeLanguageServerProxy implements ILanguageServerProxy { public languageClient: LanguageClient | undefined; + private startupCompleted: Deferred; + private cancellationStrategy: FileBasedCancellationStrategy | undefined; + private readonly disposables: Disposable[] = []; - private disposed: boolean = false; + + private disposed = false; + private lsVersion: string | undefined; constructor( - @inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ILanguageServerFolderService) private readonly folderService: ILanguageServerFolderService, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, - @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + private readonly factory: ILanguageClientFactory, + private readonly experimentService: IExperimentService, + private readonly interpreterPathService: IInterpreterPathService, + private readonly environmentService: IEnvironmentVariablesProvider, + private readonly workspace: IWorkspaceService, + private readonly extensions: IExtensions, ) { this.startupCompleted = createDeferred(); } @@ -76,7 +81,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { } @traceDecoratorVerbose('Stopping language server') - public dispose() { + public dispose(): void { if (this.languageClient) { // Do not await on this. this.languageClient.stop().then(noop, (ex) => traceError('Stopping language client failed', ex)); @@ -111,8 +116,8 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { options: LanguageClientOptions, ): Promise { if (!this.languageClient) { - const directory = await this.folderService.getCurrentLanguageServerDirectory(); - this.lsVersion = directory?.version.format(); + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + this.lsVersion = extension?.packageJSON.version || '0'; this.cancellationStrategy = new FileBasedCancellationStrategy(); options.connectionOptions = { cancellationStrategy: this.cancellationStrategy }; @@ -141,14 +146,16 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { if (this.disposed) { // Check if it got disposed in the interim. - return; } } else { await this.startupCompleted.promise; } } - public loadExtension(_args?: {}) {} + // eslint-disable-next-line class-methods-use-this + public loadExtension(): void { + // No body. + } @captureTelemetry( EventName.LANGUAGE_SERVER_READY, @@ -167,7 +174,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { this.startupCompleted.resolve(); } - private registerHandlers(resource: Resource) { + private registerHandlers(_resource: Resource) { if (this.disposed) { // Check if it got disposed in the interim. return; @@ -195,24 +202,6 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { }), ); - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer) { - this.languageClient!.onTelemetry((telemetryEvent) => { - const eventName = telemetryEvent.EventName || EventName.LANGUAGE_SERVER_TELEMETRY; - const formattedProperties = { - ...telemetryEvent.Properties, - // Replace all slashes in the method name so it doesn't get scrubbed by vscode-extension-telemetry. - method: telemetryEvent.Properties.method?.replace(/\//g, '.'), - }; - sendTelemetryEvent( - eventName, - telemetryEvent.Measurements, - formattedProperties, - telemetryEvent.Exception, - ); - }); - } - this.languageClient!.onRequest( InExperiment.Method, async (params: InExperiment.IRequest): Promise => { @@ -232,11 +221,9 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { ); this.disposables.push( - this.languageClient!.onRequest('python/isTrustedWorkspace', async () => { - return { - isTrusted: this.workspace.isTrusted, - }; - }), + this.languageClient!.onRequest('python/isTrustedWorkspace', async () => ({ + isTrusted: this.workspace.isTrusted, + })), ); } } diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index ee0c52b859d7..961721a333af 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -2,10 +2,8 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable, named } from 'inversify'; - import { ICommandManager } from '../../common/application/types'; -import { IDisposable, Resource } from '../../common/types'; +import { IDisposable, IExtensions, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -15,31 +13,34 @@ import { Commands } from '../commands'; import { LanguageClientMiddleware } from '../languageClientMiddleware'; import { ILanguageServerAnalysisOptions, - ILanguageServerFolderService, ILanguageServerManager, ILanguageServerProxy, LanguageServerType, } from '../types'; import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -@injectable() export class NodeLanguageServerManager implements ILanguageServerManager { - private languageServerProxy?: ILanguageServerProxy; private resource!: Resource; + private interpreter: PythonEnvironment | undefined; + private middleware: LanguageClientMiddleware | undefined; + private disposables: IDisposable[] = []; - private connected: boolean = false; + + private connected = false; + private lsVersion: string | undefined; + private started = false; + constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) - @named(LanguageServerType.Node) + private readonly serviceContainer: IServiceContainer, private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerFolderService) - private readonly folderService: ILanguageServerFolderService, - @inject(ICommandManager) commandManager: ICommandManager, + private readonly languageServerProxy: ILanguageServerProxy, + commandManager: ICommandManager, + private readonly extensions: IExtensions, ) { this.disposables.push( commandManager.registerCommand(Commands.RestartLS, () => { @@ -54,41 +55,47 @@ export class NodeLanguageServerManager implements ILanguageServerManager { }; } - public dispose() { + public dispose(): void { if (this.languageProxy) { this.languageProxy.dispose(); } this.disposables.forEach((d) => d.dispose()); } - public get languageProxy() { + public get languageProxy(): ILanguageServerProxy { return this.languageServerProxy; } @traceDecoratorError('Failed to start language server') public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { - if (this.languageProxy) { + if (this.started) { throw new Error('Language server already started'); } this.resource = resource; this.interpreter = interpreter; this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); - const versionPair = await this.folderService.getCurrentLanguageServerDirectory(); - this.lsVersion = versionPair?.version.format(); + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + this.lsVersion = extension?.packageJSON.version || '0'; await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); + + this.started = true; } - public connect() { - this.connected = true; - this.middleware?.connect(); + public connect(): void { + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } } - public disconnect() { - this.connected = false; - this.middleware?.disconnect(); + public disconnect(): void { + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } } @debounceSync(1000) @@ -114,14 +121,9 @@ export class NodeLanguageServerManager implements ILanguageServerManager { ) @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { - this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); - const options = await this.analysisOptions.getAnalysisOptions(); - options.middleware = this.middleware = new LanguageClientMiddleware( - this.serviceContainer, - LanguageServerType.Node, - this.lsVersion, - ); + this.middleware = new LanguageClientMiddleware(this.serviceContainer, LanguageServerType.Node, this.lsVersion); + options.middleware = this.middleware; // Make sure the middleware is connected if we restart and we we're already connected. if (this.connected) { diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index a2c2ad561578..53042df2c4de 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -3,48 +3,23 @@ import { IServiceManager } from '../ioc/types'; import { ExtensionActivationManager } from './activationManager'; -import { LanguageServerExtensionActivationService } from './activationService'; import { ExtensionSurveyPrompt } from './extensionSurvey'; -import { JediLanguageServerAnalysisOptions } from './jedi/analysisOptions'; -import { JediLanguageClientFactory } from './jedi/languageClientFactory'; -import { JediLanguageServerProxy } from './jedi/languageServerProxy'; -import { JediLanguageServerManager } from './jedi/manager'; import { LanguageServerOutputChannel } from './common/outputChannel'; -import { NodeLanguageServerActivator } from './node/activator'; -import { NodeLanguageServerAnalysisOptions } from './node/analysisOptions'; -import { NodeLanguageClientFactory } from './node/languageClientFactory'; -import { NodeLanguageServerFolderService } from './node/languageServerFolderService'; -import { NodeLanguageServerProxy } from './node/languageServerProxy'; -import { NodeLanguageServerManager } from './node/manager'; -import { NoLanguageServerExtensionActivator } from './none/activator'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, - ILanguageClientFactory, - ILanguageServerActivator, - ILanguageServerAnalysisOptions, ILanguageServerCache, - ILanguageServerFolderService, - ILanguageServerManager, ILanguageServerOutputChannel, - ILanguageServerProxy, - LanguageServerType, } from './types'; -import { JediLanguageServerActivator } from './jedi/activator'; import { LoadLanguageServerExtension } from './common/loadLanguageServerExtension'; import { PartialModeStatusItem } from './partialModeStatus'; +import { ILanguageServerWatcher } from '../languageServer/types'; +import { LanguageServerWatcher } from '../languageServer/watcher'; -export function registerTypes(serviceManager: IServiceManager, languageServerType: LanguageServerType): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); - serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); - serviceManager.addBinding(ILanguageServerCache, IExtensionActivationService); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); - serviceManager.add( - ILanguageServerActivator, - NoLanguageServerExtensionActivator, - LanguageServerType.None, - ); serviceManager.addSingleton( ILanguageServerOutputChannel, LanguageServerOutputChannel, @@ -58,39 +33,7 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp LoadLanguageServerExtension, ); - if (languageServerType === LanguageServerType.Node) { - serviceManager.add( - ILanguageServerAnalysisOptions, - NodeLanguageServerAnalysisOptions, - LanguageServerType.Node, - ); - serviceManager.add( - ILanguageServerActivator, - NodeLanguageServerActivator, - LanguageServerType.Node, - ); - serviceManager.addSingleton(ILanguageClientFactory, NodeLanguageClientFactory); - serviceManager.add(ILanguageServerManager, NodeLanguageServerManager); - serviceManager.add(ILanguageServerProxy, NodeLanguageServerProxy); - serviceManager.addSingleton( - ILanguageServerFolderService, - NodeLanguageServerFolderService, - ); - } else if (languageServerType === LanguageServerType.Jedi) { - serviceManager.add( - ILanguageServerActivator, - JediLanguageServerActivator, - LanguageServerType.Jedi, - ); - - serviceManager.add( - ILanguageServerAnalysisOptions, - JediLanguageServerAnalysisOptions, - LanguageServerType.Jedi, - ); - - serviceManager.addSingleton(ILanguageClientFactory, JediLanguageClientFactory); - serviceManager.add(ILanguageServerManager, JediLanguageServerManager); - serviceManager.add(ILanguageServerProxy, JediLanguageServerProxy); - } + serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); + serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); + serviceManager.addBinding(ILanguageServerWatcher, ILanguageServerCache); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index e90b8b8e88ee..e032e52a5345 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -3,7 +3,6 @@ 'use strict'; -import { SemVer } from 'semver'; import { CodeLensProvider, CompletionItemProvider, @@ -79,7 +78,7 @@ export type ILanguageServerConnection = Pick< 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' >; -interface ILanguageServer +export interface ILanguageServer extends RenameProvider, DefinitionProvider, HoverProvider, @@ -105,15 +104,6 @@ export interface ILanguageServerCache { get(resource: Resource, interpreter?: PythonEnvironment): Promise; } -export type FolderVersionPair = { path: string; version: SemVer }; -export const ILanguageServerFolderService = Symbol('ILanguageServerFolderService'); - -export interface ILanguageServerFolderService { - getLanguageServerFolderName(resource: Resource): Promise; - getCurrentLanguageServerDirectory(): Promise; - skipDownload(): Promise; -} - export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); export interface ILanguageClientFactory { createLanguageClient( diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 0f0a11d8f641..88506e95df90 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -6,7 +6,6 @@ import TelemetryReporter from 'vscode-extension-telemetry'; import { LanguageClientOptions, State } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/browser'; import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; -import { ILSExtensionApi } from '../activation/node/languageServerFolderService'; import { LanguageServerType } from '../activation/types'; import { AppinsightsKey, PVSC_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; import { loadLocalizedStringsForBrowser } from '../common/utils/localizeHelpers'; @@ -20,14 +19,14 @@ interface BrowserConfig { export async function activate(context: vscode.ExtensionContext): Promise { // Run in a promise and return early so that VS Code can go activate Pylance. await loadLocalizedStringsForBrowser(); - const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { runPylance(context, pylanceExtension); return; } const changeDisposable = vscode.extensions.onDidChange(() => { - const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (newPylanceExtension) { changeDisposable.dispose(); runPylance(context, newPylanceExtension); @@ -37,14 +36,10 @@ export async function activate(context: vscode.ExtensionContext): Promise async function runPylance( context: vscode.ExtensionContext, - pylanceExtension: vscode.Extension, + pylanceExtension: vscode.Extension, ): Promise { - const pylanceApi = await pylanceExtension.activate(); - if (!pylanceApi.languageServerFolder) { - throw new Error('Could not find Pylance extension'); - } - - const { path: distUrl, version } = await pylanceApi.languageServerFolder(); + const { extensionPath, packageJSON } = pylanceExtension; + const distUrl = `${extensionPath}/dist`; try { const worker = new Worker(`${distUrl}/browser.server.bundle.js`); @@ -63,7 +58,7 @@ async function runPylance( undefined, LanguageServerType.Node, sendTelemetryEventBrowser, - version, + packageJSON.version, ); middleware.connect(); diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index bf25b78a5110..a80ce8f206eb 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -84,8 +84,6 @@ export class PythonSettings implements IPythonSettings { private static pythonSettings: Map = new Map(); - public downloadLanguageServer = true; - public envFile = ''; public venvPath = ''; @@ -118,8 +116,6 @@ export class PythonSettings implements IPythonSettings { public globalModuleInstallation = false; - public autoUpdateLanguageServer = true; - public experiments!: IExperiments; public languageServer: LanguageServerType = LanguageServerType.Node; @@ -250,13 +246,6 @@ export class PythonSettings implements IPythonSettings { const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; - this.downloadLanguageServer = systemVariables.resolveAny( - pythonSettings.get('downloadLanguageServer', true), - )!; - this.autoUpdateLanguageServer = systemVariables.resolveAny( - pythonSettings.get('autoUpdateLanguageServer', true), - )!; - // Get as a string and verify; don't just accept. let userLS = pythonSettings.get('languageServer'); userLS = systemVariables.resolveAny(userLS); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 89c47d263856..a4281a42fd3e 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -180,7 +180,6 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; - readonly downloadLanguageServer: boolean; readonly devOptions: string[]; readonly linting: ILintingSettings; readonly formatting: IFormattingSettings; @@ -191,7 +190,6 @@ export interface IPythonSettings { readonly envFile: string; readonly disableInstallationChecks: boolean; readonly globalModuleInstallation: boolean; - readonly autoUpdateLanguageServer: boolean; readonly onDidChange: Event; readonly experiments: IExperiments; readonly languageServer: LanguageServerType; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e3b4f760ff9f..310c07e233bb 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -212,11 +212,14 @@ 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.'); - export const startingJedi = localize('LanguageService.startingJedi', 'Starting Jedi language server.'); + export const startingPylance = localize( + 'LanguageService.startingPylance', + 'Starting Pylance language server for {0}.', + ); + 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.', + 'Editor support is inactive since language server is set to None for {0}.', ); export const untrustedWorkspaceMessage = localize( 'LanguageService.untrustedWorkspaceMessage', diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 74b6cc066c7b..13c552726661 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -125,12 +125,11 @@ async function activateLegacy(ext: ExtensionState): Promise { const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().initialize(); - const languageServerType = configuration.getSettings().languageServer; // Language feature registrations. appRegisterTypes(serviceManager); providersRegisterTypes(serviceManager); - activationRegisterTypes(serviceManager, languageServerType); + activationRegisterTypes(serviceManager); // "initialize" "services" diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts new file mode 100644 index 000000000000..416f64561519 --- /dev/null +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { JediLanguageServerAnalysisOptions } from '../activation/jedi/analysisOptions'; +import { JediLanguageClientFactory } from '../activation/jedi/languageClientFactory'; +import { JediLanguageServerProxy } from '../activation/jedi/languageServerProxy'; +import { JediLanguageServerManager } from '../activation/jedi/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IWorkspaceService, ICommandManager } from '../common/application/types'; +import { + IExperimentService, + IInterpreterPathService, + IConfigurationService, + Resource, + IDisposable, +} from '../common/types'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { LanguageServerCapabilities } from './languageServerCapabilities'; +import { ILanguageServerExtensionManager } from './types'; + +export class JediLSExtensionManager extends LanguageServerCapabilities + implements IDisposable, ILanguageServerExtensionManager { + serverManager: JediLanguageServerManager; + + serverProxy: JediLanguageServerProxy; + + clientFactory: JediLanguageClientFactory; + + analysisOptions: JediLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + _experimentService: IExperimentService, + workspaceService: IWorkspaceService, + configurationService: IConfigurationService, + interpreterPathService: IInterpreterPathService, + interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + commandManager: ICommandManager, + ) { + super(); + + this.analysisOptions = new JediLanguageServerAnalysisOptions( + environmentService, + outputChannel, + configurationService, + workspaceService, + ); + this.clientFactory = new JediLanguageClientFactory(interpreterService); + this.serverProxy = new JediLanguageServerProxy(this.clientFactory, interpreterPathService); + this.serverManager = new JediLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + stopLanguageServer(): void { + this.serverManager.disconnect(); + this.serverProxy.dispose(); + } + + // eslint-disable-next-line class-methods-use-this + canStartLanguageServer(): boolean { + // Return true for now since it's shipped with the extension. + // Update this when JediLSP is pulled in a separate extension. + + return true; + } + + // eslint-disable-next-line class-methods-use-this + languageServerNotAvailable(): Promise { + // Nothing to do here. + // Update this when JediLSP is pulled in a separate extension. + return Promise.resolve(); + } +} diff --git a/src/client/activation/common/activatorBase.ts b/src/client/languageServer/languageServerCapabilities.ts similarity index 81% rename from src/client/activation/common/activatorBase.ts rename to src/client/languageServer/languageServerCapabilities.ts index 56043c289185..e5fc1e3f276e 100644 --- a/src/client/activation/common/activatorBase.ts +++ b/src/client/languageServer/languageServerCapabilities.ts @@ -21,60 +21,24 @@ import { WorkspaceEdit, } from 'vscode'; import * as vscodeLanguageClient from 'vscode-languageclient/node'; +import { ILanguageServer, ILanguageServerConnection, ILanguageServerProxy } from '../activation/types'; +import { ILanguageServerCapabilities } from './types'; -import { injectable } from 'inversify'; -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; -import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { ILanguageServerActivator, ILanguageServerManager } from '../types'; -import { traceDecoratorError } from '../../logging'; - -/** - * Starts the language server managers per workspaces (currently one for first workspace). - * - * @export - * @class LanguageServerActivatorBase - * @implements {ILanguageServerActivator} +/* + * The Language Server Capabilities class implements the ILanguageServer interface to provide support for the existing Jupyter integration. */ -@injectable() -export abstract class LanguageServerActivatorBase implements ILanguageServerActivator { - protected resource?: Resource; - constructor( - protected readonly manager: ILanguageServerManager, - protected readonly workspace: IWorkspaceService, - protected readonly fs: IFileSystem, - protected readonly configurationService: IConfigurationService, - ) {} - - @traceDecoratorError('Failed to activate language server') - public async start(resource: Resource, interpreter?: PythonEnvironment): Promise { - if (!resource) { - resource = - this.workspace.workspaceFolders && this.workspace.workspaceFolders.length > 0 - ? this.workspace.workspaceFolders[0].uri - : undefined; - } - this.resource = resource; - await this.ensureLanguageServerIsAvailable(resource); - await this.manager.start(resource, interpreter); - } +export class LanguageServerCapabilities implements ILanguageServerCapabilities { + serverProxy: ILanguageServerProxy | undefined; public dispose(): void { - this.manager.dispose(); - } - - public abstract ensureLanguageServerIsAvailable(resource: Resource): Promise; - - public activate(): void { - this.manager.connect(); + // Nothing to do here. } - public deactivate(): void { - this.manager.disconnect(); + get(): Promise { + return Promise.resolve(this); } - public get connection() { + public get connection(): ILanguageServerConnection | undefined { const languageClient = this.getLanguageClient(); if (languageClient) { // Return an object that looks like a connection @@ -87,13 +51,17 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi onProgress: languageClient.onProgress.bind(languageClient), }; } + + return undefined; } - public get capabilities() { + public get capabilities(): vscodeLanguageClient.ServerCapabilities | undefined { const languageClient = this.getLanguageClient(); if (languageClient) { return languageClient.initializeResult?.capabilities; } + + return undefined; } public provideRenameEdits( @@ -156,10 +124,7 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi } protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { - const proxy = this.manager.languageProxy; - if (proxy) { - return proxy.languageClient; - } + return this.serverProxy?.languageClient; } private async handleProvideRenameEdits( @@ -180,6 +145,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); } } + + return undefined; } private async handleProvideDefinition( @@ -198,6 +165,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asDefinitionResult(result); } } + + return undefined; } private async handleProvideHover( @@ -216,6 +185,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asHover(result); } } + + return undefined; } private async handleProvideReferences( @@ -240,6 +211,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi }); } } + + return undefined; } private async handleProvideCodeLenses( @@ -256,6 +229,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asCodeLenses(result); } } + + return undefined; } private async handleProvideCompletionItems( @@ -272,6 +247,8 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asCompletionResult(result); } } + + return undefined; } private async handleProvideDocumentSymbols( @@ -289,17 +266,18 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi token, ); if (result && result.length) { - if ((result[0] as any).range) { + if ((result[0] as DocumentSymbol).range) { // Document symbols const docSymbols = result as vscodeLanguageClient.DocumentSymbol[]; return languageClient.protocol2CodeConverter.asDocumentSymbols(docSymbols); - } else { - // Document symbols - const symbols = result as vscodeLanguageClient.SymbolInformation[]; - return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); } + // Document symbols + const symbols = result as vscodeLanguageClient.SymbolInformation[]; + return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); } } + + return undefined; } private async handleProvideSignatureHelp( @@ -323,5 +301,7 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return languageClient.protocol2CodeConverter.asSignatureHelp(result); } } + + return undefined; } } diff --git a/src/client/activation/none/activator.ts b/src/client/languageServer/noneLSExtensionManager.ts similarity index 67% rename from src/client/activation/none/activator.ts rename to src/client/languageServer/noneLSExtensionManager.ts index c747e82f7779..397fc728cdba 100644 --- a/src/client/activation/none/activator.ts +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; + +/* eslint-disable class-methods-use-this */ + import { CancellationToken, CodeLens, @@ -20,26 +22,42 @@ import { TextDocument, WorkspaceEdit, } from 'vscode'; -import { Resource } from '../../common/types'; -import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { ILanguageServerActivator } from '../types'; +import { ILanguageServer, ILanguageServerProxy } from '../activation/types'; +import { ILanguageServerExtensionManager } from './types'; + +// This LS manager implements ILanguageServer directly +// instead of extending LanguageServerCapabilities because it doesn't need to do anything. +export class NoneLSExtensionManager implements ILanguageServer, ILanguageServerExtensionManager { + serverProxy: ILanguageServerProxy | undefined; + + constructor() { + this.serverProxy = undefined; + } -/** - * Provides 'no language server' pseudo-activator. - * - * @export - * @class NoLanguageServerExtensionActivator - * @implements {ILanguageServerActivator} - */ -@injectable() -export class NoLanguageServerExtensionActivator implements ILanguageServerActivator { - public async start(_resource: Resource, _interpreter?: PythonEnvironment): Promise {} + dispose(): void { + // Nothing to do here. + } + + get(): Promise { + return Promise.resolve(this); + } + + startLanguageServer(): Promise { + return Promise.resolve(); + } - public dispose(): void {} + stopLanguageServer(): void { + // Nothing to do here. + } - public activate(): void {} + canStartLanguageServer(): boolean { + return true; + } - public deactivate(): void {} + languageServerNotAvailable(): Promise { + // Nothing to do here. + return Promise.resolve(); + } public provideRenameEdits( _document: TextDocument, @@ -49,6 +67,7 @@ export class NoLanguageServerExtensionActivator implements ILanguageServerActiva ): ProviderResult { return null; } + public provideDefinition( _document: TextDocument, _position: Position, @@ -56,6 +75,7 @@ export class NoLanguageServerExtensionActivator implements ILanguageServerActiva ): ProviderResult { return null; } + public provideHover( _document: TextDocument, _position: Position, @@ -63,6 +83,7 @@ export class NoLanguageServerExtensionActivator implements ILanguageServerActiva ): ProviderResult { return null; } + public provideReferences( _document: TextDocument, _position: Position, @@ -71,6 +92,7 @@ export class NoLanguageServerExtensionActivator implements ILanguageServerActiva ): ProviderResult { return null; } + public provideCompletionItems( _document: TextDocument, _position: Position, @@ -79,15 +101,18 @@ export class NoLanguageServerExtensionActivator implements ILanguageServerActiva ): ProviderResult { return null; } + public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { return null; } + public provideDocumentSymbols( _document: TextDocument, _token: CancellationToken, ): ProviderResult { return null; } + public provideSignatureHelp( _document: TextDocument, _position: Position, diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts new file mode 100644 index 000000000000..34c94af1eb66 --- /dev/null +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { promptForPylanceInstall } from '../activation/common/languageServerChangeHandler'; +import { NodeLanguageServerAnalysisOptions } from '../activation/node/analysisOptions'; +import { NodeLanguageClientFactory } from '../activation/node/languageClientFactory'; +import { NodeLanguageServerProxy } from '../activation/node/languageServerProxy'; +import { NodeLanguageServerManager } from '../activation/node/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposable, + IExperimentService, + IExtensions, + IInterpreterPathService, + Resource, +} from '../common/types'; +import { Pylance } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { LanguageServerCapabilities } from './languageServerCapabilities'; +import { ILanguageServerExtensionManager } from './types'; + +export class PylanceLSExtensionManager extends LanguageServerCapabilities + implements IDisposable, ILanguageServerExtensionManager { + serverManager: NodeLanguageServerManager; + + serverProxy: NodeLanguageServerProxy; + + clientFactory: NodeLanguageClientFactory; + + analysisOptions: NodeLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + experimentService: IExperimentService, + readonly workspaceService: IWorkspaceService, + readonly configurationService: IConfigurationService, + interpreterPathService: IInterpreterPathService, + _interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + readonly commandManager: ICommandManager, + fileSystem: IFileSystem, + private readonly extensions: IExtensions, + readonly applicationShell: IApplicationShell, + ) { + super(); + + this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); + this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); + this.serverProxy = new NodeLanguageServerProxy( + this.clientFactory, + experimentService, + interpreterPathService, + environmentService, + workspaceService, + extensions, + ); + this.serverManager = new NodeLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + extensions, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + stopLanguageServer(): void { + this.serverManager.disconnect(); + this.serverProxy.dispose(); + } + + canStartLanguageServer(): boolean { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + return !!extension; + } + + async languageServerNotAvailable(): Promise { + await promptForPylanceInstall( + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ); + + traceLog(Pylance.pylanceNotInstalledMessage()); + } +} diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts new file mode 100644 index 000000000000..acaf54db3c18 --- /dev/null +++ b/src/client/languageServer/types.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ILanguageServer, ILanguageServerProxy, LanguageServerType } from '../activation/types'; +import { Resource } from '../common/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; + +export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); +/** + * The language server watcher serves as a singleton that watches for changes to the language server setting, + * and instantiates the relevant language server extension manager. + */ +export interface ILanguageServerWatcher { + readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + readonly languageServerType: LanguageServerType; + startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + ): Promise; +} + +export interface ILanguageServerCapabilities extends ILanguageServer { + serverProxy: ILanguageServerProxy | undefined; + + get(): Promise; +} + +/** + * `ILanguageServerExtensionManager` implementations act as wrappers for anything related to their specific language server extension. + * They are responsible for starting and stopping the language server provided by their LS extension. + * They also extend the `ILanguageServer` interface via `ILanguageServerCapabilities` to continue supporting the Jupyter integration. + * + * Note that the methods exposed in this interface shouldn't be used outside of the language server watcher (and tests). + */ +export interface ILanguageServerExtensionManager extends ILanguageServerCapabilities { + startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; + stopLanguageServer(): void; + canStartLanguageServer(): boolean; + languageServerNotAvailable(): Promise; + dispose(): void; +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts new file mode 100644 index 000000000000..9fc138f80a0e --- /dev/null +++ b/src/client/languageServer/watcher.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Uri } from 'vscode'; +import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; +import { + IExtensionActivationService, + ILanguageServer, + ILanguageServerCache, + ILanguageServerOutputChannel, + LanguageServerType, +} from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExtensions, + IInterpreterPathService, + InterpreterConfigurationScope, + Resource, +} from '../common/types'; +import { LanguageService } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterHelper, IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { JediLSExtensionManager } from './jediLSExtensionManager'; +import { NoneLSExtensionManager } from './noneLSExtensionManager'; +import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; +import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; + +@injectable() +/** + * The Language Server Watcher class implements the ILanguageServerWatcher interface, which is the one-stop shop for language server activation. + * + * It also implements the ILanguageServerCache interface needed by our Jupyter support. + */ +export class LanguageServerWatcher + implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + + languageServerType: LanguageServerType; + + private workspaceInterpreters: Map; + + // In a multiroot workspace scenario we will have one language server per folder. + private workspaceLanguageServers: Map; + + private languageServerChangeHandler: LanguageServerChangeHandler; + + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IFileSystem) private readonly fileSystem: IFileSystem, + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IApplicationShell) readonly applicationShell: IApplicationShell, + @inject(IDisposableRegistry) readonly disposables: IDisposableRegistry, + ) { + this.workspaceInterpreters = new Map(); + this.workspaceLanguageServers = new Map(); + this.languageServerType = this.configurationService.getSettings().languageServer; + + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + + if (this.workspaceService.isTrusted) { + disposables.push(this.interpreterPathService.onDidChange(this.onDidChangeInterpreter.bind(this))); + } + + this.languageServerChangeHandler = new LanguageServerChangeHandler( + this.languageServerType, + this.extensions, + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ); + disposables.push(this.languageServerChangeHandler); + + disposables.push( + extensions.onDidChange(async () => { + await this.extensionsChangeHandler(); + }), + ); + } + + // IExtensionActivationService + + public async activate(resource?: Resource): Promise { + await this.startLanguageServer(this.languageServerType, resource); + } + + // ILanguageServerWatcher + + public async startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + ): Promise { + const lsResource = this.getWorkspaceKey(resource); + const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); + const interpreter = await this.interpreterService?.getActiveInterpreter(resource); + + // Destroy the old language server if it's different. + if (currentInterpreter && interpreter !== currentInterpreter) { + this.stopLanguageServer(lsResource); + } + + // If the interpreter is Python 2 and the LS setting is explicitly set to Jedi, turn it off. + // If set to Default, use Pylance. + let serverType = languageServerType; + if (interpreter && (interpreter.version?.major ?? 0) < 3) { + if (serverType === LanguageServerType.Jedi) { + serverType = LanguageServerType.None; + } else if (this.getCurrentLanguageServerTypeIsDefault()) { + serverType = LanguageServerType.Node; + } + } + + if ( + !this.workspaceService.isTrusted && + serverType !== LanguageServerType.Node && + serverType !== LanguageServerType.None + ) { + traceLog(LanguageService.untrustedWorkspaceMessage()); + serverType = LanguageServerType.None; + } + + // Instantiate the language server extension manager. + const languageServerExtensionManager = this.createLanguageServer(serverType); + + if (languageServerExtensionManager.canStartLanguageServer()) { + // Start the language server. + await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); + + logStartup(languageServerType, lsResource); + this.languageServerType = languageServerType; + this.workspaceInterpreters.set(lsResource.fsPath, interpreter); + } else { + await languageServerExtensionManager.languageServerNotAvailable(); + } + + this.workspaceLanguageServers.set(lsResource.fsPath, languageServerExtensionManager); + + return languageServerExtensionManager; + } + + // ILanguageServerCache + + public async get(resource?: Resource): Promise { + const lsResource = this.getWorkspaceKey(resource); + let languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); + + if (!languageServerExtensionManager) { + languageServerExtensionManager = await this.startLanguageServer(this.languageServerType, resource); + } + + return Promise.resolve(languageServerExtensionManager.get()); + } + + // Private methods + + private stopLanguageServer(resource?: Resource): void { + const lsResource = this.getWorkspaceKey(resource); + const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); + + if (languageServerExtensionManager) { + languageServerExtensionManager.stopLanguageServer(); + languageServerExtensionManager.dispose(); + this.workspaceLanguageServers.delete(lsResource.fsPath); + } + } + + private createLanguageServer(languageServerType: LanguageServerType): ILanguageServerExtensionManager { + switch (languageServerType) { + case LanguageServerType.Jedi: + this.languageServerExtensionManager = new JediLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + ); + break; + case LanguageServerType.Node: + this.languageServerExtensionManager = new PylanceLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + this.fileSystem, + this.extensions, + this.applicationShell, + ); + break; + case LanguageServerType.None: + default: + this.languageServerExtensionManager = new NoneLSExtensionManager(); + break; + } + + return this.languageServerExtensionManager; + } + + private async refreshLanguageServer(resource?: Resource): Promise { + const lsResource = this.getWorkspaceKey(resource); + const languageServerType = this.configurationService.getSettings(lsResource).languageServer; + + if (languageServerType !== this.languageServerType) { + this.stopLanguageServer(lsResource); + await this.startLanguageServer(languageServerType, lsResource); + } + } + + private getCurrentLanguageServerTypeIsDefault(): boolean { + return this.configurationService.getSettings().languageServerIsDefault; + } + + // Watch for settings changes. + private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { + const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? []; + + workspacesUris.forEach(async (resource) => { + if (event.affectsConfiguration(`python.languageServer`, resource)) { + await this.refreshLanguageServer(resource); + } + }); + } + + // Watch for interpreter changes. + private async onDidChangeInterpreter(event: InterpreterConfigurationScope): Promise { + // Reactivate the language server (if in a multiroot workspace scenario, pick the correct one). + return this.activate(event.uri); + } + + // Watch for extension changes. + private async extensionsChangeHandler(): Promise { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + await this.refreshLanguageServer(); + } + } + + // Get the workspace key for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. + private getWorkspaceKey(resource?: Resource): Uri { + let uri; + + if (resource) { + uri = this.workspaceService.getWorkspaceFolder(resource)?.uri; + } else { + uri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + } + + return uri ?? Uri.parse('default'); + } +} + +function logStartup(languageServerType: LanguageServerType, resource: Uri): void { + let outputLine; + const basename = path.basename(resource.fsPath); + + switch (languageServerType) { + case LanguageServerType.Jedi: + outputLine = LanguageService.startingJedi().format(basename); + break; + case LanguageServerType.Node: + outputLine = LanguageService.startingPylance().format(basename); + break; + case LanguageServerType.None: + outputLine = LanguageService.startingNone().format(basename); + break; + default: + throw new Error(`Unknown language server type: ${languageServerType}`); + } + traceLog(outputLine); +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 178e20fe27d1..81305350b958 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -65,9 +65,6 @@ export enum EventName { EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', - PYTHON_LANGUAGE_SERVER_STARTUP_DURATION = 'PYTHON_LANGUAGE_SERVER_STARTUP_DURATION', - PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION = 'PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION', - LANGUAGE_SERVER_ENABLED = 'LANGUAGE_SERVER.ENABLED', LANGUAGE_SERVER_STARTUP = 'LANGUAGE_SERVER.STARTUP', LANGUAGE_SERVER_READY = 'LANGUAGE_SERVER.READY', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 3a3c04c3d08d..9d2c482bff7b 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -4,7 +4,6 @@ import TelemetryReporter from 'vscode-extension-telemetry/lib/telemetryReporter'; -import { LanguageServerType } from '../activation/types'; import { DiagnosticCodes } from '../application/diagnostics/constants'; import { IWorkspaceService } from '../common/application/types'; import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; @@ -1381,44 +1380,6 @@ export interface IEventNamePropertyMapping { */ selection: 'Reload' | undefined; }; - /** - * Telemetry sent with details about the current selection of language server - */ - /* __GDPR__ - "python_language_server.current_selection" : { - "lsstartup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "switchto" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } - } - */ - - [EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION]: { - /** - * The startup value of the language server setting - */ - lsStartup?: LanguageServerType; - /** - * Used to track switch between language servers. Carries the final state after the switch. - */ - switchTo?: LanguageServerType; - }; - /** - * Telemetry event sent with details after selected Language server has finished activating. This event - * is sent with `duration` specifying the total duration of time that the given language server took - * to activate. - */ - /* __GDPR__ - "python_language_server.startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" }, - "languageservertype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "kimadeline" } - } - */ - [EventName.PYTHON_LANGUAGE_SERVER_STARTUP_DURATION]: { - /** - * Type of Language server activated. Note it can be different from one that is chosen, if the - * chosen one fails to start. - */ - languageServerType?: LanguageServerType; - }; /** * Telemetry event sent when the experiments service is initialized for the first time. */ diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 3aae2f30cdf6..8cc15400b6e5 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -9,8 +9,6 @@ import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { TextDocument, Uri, WorkspaceFolder } from 'vscode'; import { ExtensionActivationManager } from '../../client/activation/activationManager'; -import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; -import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IApplicationDiagnostics } from '../../client/application/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; @@ -45,8 +43,6 @@ suite('Activation Manager', () => { let activeResourceService: IActiveResourceService; let documentManager: typemoq.IMock; let interpreterPathService: typemoq.IMock; - let activationService1: IExtensionActivationService; - let activationService2: IExtensionActivationService; let fileSystem: IFileSystem; setup(() => { interpreterPathService = typemoq.Mock.ofType(); @@ -55,16 +51,6 @@ suite('Activation Manager', () => { appDiagnostics = typemoq.Mock.ofType(); autoSelection = typemoq.Mock.ofType(); documentManager = typemoq.Mock.ofType(); - activationService1 = mock(LanguageServerExtensionActivationService); - when(activationService1.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); - activationService2 = mock(LanguageServerExtensionActivationService); - when(activationService2.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); fileSystem = mock(FileSystem); interpreterPathService .setup((i) => i.onDidChange(typemoq.It.isAny())) @@ -72,7 +58,7 @@ suite('Activation Manager', () => { when(workspaceService.isTrusted).thenReturn(true); when(workspaceService.isVirtualWorkspace).thenReturn(false); managerTest = new ExtensionActivationManagerTest( - [instance(activationService1), instance(activationService2)], + [], [], documentManager.object, autoSelection.object, @@ -91,17 +77,7 @@ suite('Activation Manager', () => { test('If running in a virtual workspace, do not activate services that do not support it', async () => { when(workspaceService.isVirtualWorkspace).thenReturn(true); - when(activationService1.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: false, - untrustedWorkspace: true, - }); - when(activationService2.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -113,7 +89,7 @@ suite('Activation Manager', () => { .verifiable(typemoq.Times.once()); managerTest = new ExtensionActivationManagerTest( - [instance(activationService1), instance(activationService2)], + [], [], documentManager.object, autoSelection.object, @@ -124,25 +100,13 @@ suite('Activation Manager', () => { ); await managerTest.activateWorkspace(resource); - verify(activationService1.activate(resource)).never(); - verify(activationService2.activate(resource)).once(); autoSelection.verifyAll(); appDiagnostics.verifyAll(); }); test('If running in a untrusted workspace, do not activate services that do not support it', async () => { when(workspaceService.isTrusted).thenReturn(false); - when(activationService1.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: false, - }); - when(activationService2.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -154,7 +118,7 @@ suite('Activation Manager', () => { .verifiable(typemoq.Times.once()); managerTest = new ExtensionActivationManagerTest( - [instance(activationService1), instance(activationService2)], + [], [], documentManager.object, autoSelection.object, @@ -165,16 +129,11 @@ suite('Activation Manager', () => { ); await managerTest.activateWorkspace(resource); - verify(activationService1.activate(resource)).never(); - verify(activationService2.activate(resource)).once(); - autoSelection.verifyAll(); appDiagnostics.verifyAll(); }); test('Otherwise activate all services filtering to the current resource', async () => { const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -187,8 +146,6 @@ suite('Activation Manager', () => { await managerTest.activateWorkspace(resource); - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); autoSelection.verifyAll(); appDiagnostics.verifyAll(); }); @@ -289,8 +246,6 @@ suite('Activation Manager', () => { when(workspaceService.getWorkspaceFolder(document.object.uri)).thenReturn(folder2); when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder2); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) @@ -315,14 +270,10 @@ suite('Activation Manager', () => { verify(workspaceService.onDidChangeWorkspaceFolders).once(); verify(workspaceService.workspaceFolders).atLeast(1); verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); }); test("The same workspace isn't activated more than once", async () => { const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); autoSelection .setup((a) => a.autoSelectInterpreter(resource)) @@ -336,8 +287,6 @@ suite('Activation Manager', () => { await managerTest.activateWorkspace(resource); await managerTest.activateWorkspace(resource); - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); autoSelection.verifyAll(); appDiagnostics.verifyAll(); }); @@ -436,87 +385,4 @@ suite('Activation Manager', () => { assert.deepEqual(Array.from(managerTest.activatedWorkspaces.keys()), ['one']); }); }); - - suite('Language Server Activation - activate()', () => { - let workspaceService: IWorkspaceService; - let appDiagnostics: typemoq.IMock; - let autoSelection: typemoq.IMock; - let activeResourceService: IActiveResourceService; - let documentManager: typemoq.IMock; - let activationService1: IExtensionActivationService; - let activationService2: IExtensionActivationService; - let fileSystem: IFileSystem; - let singleActivationService: typemoq.IMock; - let initialize: sinon.SinonStub; - let activateWorkspace: sinon.SinonStub; - let managerTest: ExtensionActivationManager; - const resource = Uri.parse('a'); - let interpreterPathService: typemoq.IMock; - - setup(() => { - workspaceService = mock(WorkspaceService); - activeResourceService = mock(ActiveResourceService); - appDiagnostics = typemoq.Mock.ofType(); - autoSelection = typemoq.Mock.ofType(); - interpreterPathService = typemoq.Mock.ofType(); - documentManager = typemoq.Mock.ofType(); - activationService1 = mock(LanguageServerExtensionActivationService); - when(activationService1.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); - activationService2 = mock(LanguageServerExtensionActivationService); - when(activationService2.supportedWorkspaceTypes).thenReturn({ - virtualWorkspace: true, - untrustedWorkspace: true, - }); - when(workspaceService.isTrusted).thenReturn(true); - when(workspaceService.isVirtualWorkspace).thenReturn(false); - fileSystem = mock(FileSystem); - singleActivationService = typemoq.Mock.ofType(); - initialize = sinon.stub(ExtensionActivationManager.prototype, 'initialize'); - initialize.resolves(); - activateWorkspace = sinon.stub(ExtensionActivationManager.prototype, 'activateWorkspace'); - activateWorkspace.resolves(); - interpreterPathService - .setup((i) => i.onDidChange(typemoq.It.isAny())) - .returns(() => typemoq.Mock.ofType().object); - managerTest = new ExtensionActivationManager( - [instance(activationService1), instance(activationService2)], - [singleActivationService.object], - documentManager.object, - autoSelection.object, - appDiagnostics.object, - instance(workspaceService), - instance(fileSystem), - instance(activeResourceService), - ); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Execution goes as expected if there are no errors', async () => { - singleActivationService - .setup((s) => s.activate()) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - when(activeResourceService.getActiveResource()).thenReturn(resource); - await managerTest.activate(); - assert.ok(initialize.calledOnce); - assert.ok(activateWorkspace.calledOnce); - singleActivationService.verifyAll(); - }); - - test('Throws error if execution fails', async () => { - singleActivationService - .setup((s) => s.activate()) - .returns(() => Promise.reject(new Error('Kaboom'))) - .verifiable(typemoq.Times.once()); - when(activeResourceService.getActiveResource()).thenReturn(resource); - const promise = managerTest.activate(); - await expect(promise).to.eventually.be.rejectedWith('Kaboom'); - }); - }); }); diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts deleted file mode 100644 index c0f9d20166f6..000000000000 --- a/src/test/activation/activationService.unit.test.ts +++ /dev/null @@ -1,910 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { expect } from 'chai'; -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import * as sinon from 'sinon'; -import { ConfigurationChangeEvent, Disposable, EventEmitter, Uri, WorkspaceConfiguration } from 'vscode'; - -import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; -import { - FolderVersionPair, - IExtensionActivationService, - ILanguageServerActivator, - ILanguageServerFolderService, - LanguageServerType, -} from '../../client/activation/types'; -import { IDiagnostic, IDiagnosticsService } from '../../client/application/diagnostics/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { IPlatformService } from '../../client/common/platform/types'; -import { - IConfigurationService, - IDisposable, - IDisposableRegistry, - IExtensions, - IPersistentState, - IPersistentStateFactory, - IPythonSettings, - Resource, -} from '../../client/common/types'; -import { LanguageService } from '../../client/common/utils/localize'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { PythonEnvironment } from '../../client/pythonEnvironments/info'; -import * as logging from '../../client/logging'; - -suite('Language Server Activation - ActivationService', () => { - [LanguageServerType.Jedi].forEach((languageServerType) => { - suite( - `Test activation - ${ - languageServerType === LanguageServerType.Jedi ? 'Jedi is enabled' : 'Jedi is disabled' - }`, - () => { - let serviceContainer: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let cmdManager: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let lsNotSupportedDiagnosticService: TypeMoq.IMock; - let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; - let workspaceConfig: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - cmdManager = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); - const configService = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - const langFolderServiceMock = TypeMoq.Mock.ofType(); - const extensionsMock = TypeMoq.Mock.ofType(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3'), - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType(); - - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - interpreterService = TypeMoq.Mock.ofType(); - const disposable = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.onDidChangeInterpreter(TypeMoq.It.isAny())) - .returns(() => disposable.object); - langFolderServiceMock - .setup((l) => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory - .setup((f) => - f.createGlobalPersistentState( - TypeMoq.It.isValue('SWITCH_LS'), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => state.object); - state.setup((s) => s.value).returns(() => undefined); - state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.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); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IExtensions))) - .returns(() => extensionsMock.object); - }); - - async function testActivation( - activationService: IExtensionActivationService, - activator: TypeMoq.IMock, - lsSupported: boolean = true, - activatorName: LanguageServerType = LanguageServerType.Jedi, - ) { - activator - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); - - if ( - activatorName !== LanguageServerType.None && - lsSupported && - activatorName !== LanguageServerType.Jedi - ) { - activatorName = LanguageServerType.Node; - } - - let diagnostics: IDiagnostic[]; - if (!lsSupported && activatorName !== LanguageServerType.Jedi) { - diagnostics = [TypeMoq.It.isAny()]; - } else { - diagnostics = []; - } - - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup((c) => - c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(activatorName)), - ) - .returns(() => activator.object) - .verifiable(TypeMoq.Times.once()); - - await activationService.activate(undefined); - - activator.verifyAll(); - serviceContainer.verifyAll(); - } - - async function testReloadMessage(settingName: string): Promise { - 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.languageServer).returns(() => languageServerType); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => - e.affectsConfiguration(TypeMoq.It.isValue(`python.${settingName}`), TypeMoq.It.isAny()), - ) - .returns(() => true) - .verifiable(TypeMoq.Times.never()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve('Reload')) - .verifiable(TypeMoq.Times.never()); - 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. - languageServerType = - languageServerType === LanguageServerType.Jedi - ? LanguageServerType.None - : LanguageServerType.Jedi; - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - } - - test('LS is supported', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await testActivation(activationService, activator, true); - }); - test('LS is not supported', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await testActivation(activationService, activator, false); - }); - - test('Activator must be activated', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await testActivation(activationService, activator); - }); - test('Activator must be deactivated', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await testActivation(activationService, activator); - - activator.setup((a) => a.dispose()).verifiable(TypeMoq.Times.once()); - - activationService.dispose(); - activator.verifyAll(); - }); - test('No language service', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.None); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - await testActivation(activationService, activator, false, LanguageServerType.None); - }); - test('Prompt user to reload VS Code and reload, when languageServer setting is toggled', async () => { - await testReloadMessage('languageServer'); - }); - 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.languageServer).returns(() => LanguageServerType.Node); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => - e.affectsConfiguration(TypeMoq.It.isValue('python.languageServer'), TypeMoq.It.isAny()), - ) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - 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(); - }); - if (languageServerType !== LanguageServerType.Jedi) { - test('Revert to jedi when LS activation fails', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - const activatorLS = TypeMoq.Mock.ofType(); - const activatorJedi = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - const diagnostics: IDiagnostic[] = []; - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Node), - ), - ) - .returns(() => activatorLS.object) - .verifiable(TypeMoq.Times.once()); - activatorLS - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.reject(new Error(''))) - .verifiable(TypeMoq.Times.once()); - serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Jedi), - ), - ) - .returns(() => activatorJedi.object) - .verifiable(TypeMoq.Times.once()); - activatorJedi - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activatorJedi - .setup((a) => a.activate()) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await activationService.activate(undefined); - - activatorLS.verifyAll(); - activatorJedi.verifyAll(); - serviceContainer.verifyAll(); - }); - async function testActivationOfResource( - activationService: IExtensionActivationService, - activator: TypeMoq.IMock, - resource: Resource, - ) { - activator - .setup((a) => a.start(TypeMoq.It.isValue(resource), undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator.setup((a) => a.activate()).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(LanguageServerType.Node), - ), - ) - .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 and LS is "Pylance"', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Node); - 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, - stateFactory.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()); - await workspaceFoldersChangedHandler.call(activationService); - workspaceService.verifyAll(); - activator3.verifyAll(); - }); - } else { - test('Jedi is only started once', async () => { - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - const activator1 = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.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(LanguageServerType.Jedi), - ), - ) - .returns(() => activator1.object) - .verifiable(TypeMoq.Times.once()); - activator1 - .setup((a) => a.start(folder1.uri, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await activationService.activate(folder1.uri); - activator1.verifyAll(); - activator1.verify((a) => a.activate(), TypeMoq.Times.once()); - serviceContainer.verifyAll(); - - const activator2 = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Jedi), - ), - ) - .returns(() => activator2.object) - .verifiable(TypeMoq.Times.once()); - activator2 - .setup((a) => a.start(folder2.uri, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - activator2.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); - await activationService.activate(folder2.uri); - serviceContainer.verifyAll(); - activator1.verifyAll(); - activator1.verify((a) => a.activate(), TypeMoq.Times.exactly(2)); - activator2.verifyAll(); - }); - } - }, - ); - }); - - suite('Test language server swap when using Python 2.7', () => { - let serviceContainer: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let cmdManager: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; - let workspaceConfig: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let configurationService: TypeMoq.IMock; - let traceLogStub: sinon.SinonStub; - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - cmdManager = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); - configurationService = TypeMoq.Mock.ofType(); - const extensionsMock = TypeMoq.Mock.ofType(); - - traceLogStub = sinon.stub(logging, 'traceLog'); - - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - interpreterService = TypeMoq.Mock.ofType(); - state.setup((s) => s.value).returns(() => undefined); - state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.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(() => configurationService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); - }); - - teardown(() => { - sinon.restore(); - }); - - const values: { ls: LanguageServerType; expected: LanguageServerType; outputString: string }[] = [ - { - ls: LanguageServerType.Jedi, - expected: LanguageServerType.None, - outputString: LanguageService.startingNone(), - }, - { - ls: LanguageServerType.Node, - expected: LanguageServerType.Node, - outputString: LanguageService.startingPylance(), - }, - { - ls: LanguageServerType.None, - expected: LanguageServerType.None, - outputString: LanguageService.startingNone(), - }, - ]; - - const interpreter = { - version: { major: 2, minor: 7, patch: 10 }, - } as PythonEnvironment; - - values.forEach(({ ls, expected, outputString }) => { - test(`When language server setting explicitly set to ${ls} and using Python 2.7, use a language server of type ${expected}`, async () => { - const resource = Uri.parse('one.py'); - const activator = TypeMoq.Mock.ofType(); - - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), expected)) - .returns(() => activator.object); - configurationService - .setup((c) => c.getSettings(TypeMoq.It.isAny())) - .returns(() => ({ languageServer: ls, languageServerIsDefault: false } as PythonSettings)); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await activationService.get(resource, interpreter); - - sinon.assert.calledOnceWithExactly(traceLogStub, outputString); - activator.verify((a) => a.start(resource, interpreter), TypeMoq.Times.once()); - }); - }); - - test('When default language server setting set to true and using Python 2.7, use Pylance', async () => { - const resource = Uri.parse('one.py'); - const activator = TypeMoq.Mock.ofType(); - - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), LanguageServerType.Node)) - .returns(() => activator.object); - configurationService - .setup((c) => c.getSettings(TypeMoq.It.isAny())) - .returns(() => ({ languageServerIsDefault: true } as PythonSettings)); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - - await activationService.get(resource, interpreter); - - sinon.assert.calledOnceWithExactly(traceLogStub, LanguageService.startingPylance()); - activator.verify((a) => a.start(resource, interpreter), TypeMoq.Times.once()); - }); - }); - - suite('Test sendTelemetryForChosenLanguageServer()', () => { - let serviceContainer: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let cmdManager: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; - let workspaceConfig: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - cmdManager = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); - const configService = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - interpreterService = TypeMoq.Mock.ofType(); - const e = new EventEmitter(); - interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event); - const langFolderServiceMock = TypeMoq.Mock.ofType(); - const extensionsMock = TypeMoq.Mock.ofType(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3'), - }; - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup((l) => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory - .setup((f) => - f.createGlobalPersistentState( - TypeMoq.It.isValue('SWITCH_LS'), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => state.object); - state.setup((s) => s.value).returns(() => undefined); - state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.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); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); - }); - - test('Track current LS usage for first usage', async () => { - state.reset(); - state - .setup((s) => s.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.exactly(2)); - state - .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) - .returns(() => { - state.setup((s) => s.value).returns(() => LanguageServerType.Jedi); - return Promise.resolve(); - }) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); - - state.verifyAll(); - }); - test('Track switch to LS', async () => { - state.reset(); - state - .setup((s) => s.value) - .returns(() => LanguageServerType.Jedi) - .verifiable(TypeMoq.Times.exactly(2)); - state - .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Node))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Node); - - state.verifyAll(); - }); - test('Track switch to Jedi', async () => { - state.reset(); - state - .setup((s) => s.value) - .returns(() => LanguageServerType.Node) - .verifiable(TypeMoq.Times.exactly(2)); - state - .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); - - state.verifyAll(); - }); - test('Track startup value', async () => { - state.reset(); - state - .setup((s) => s.value) - .returns(() => LanguageServerType.Jedi) - .verifiable(TypeMoq.Times.exactly(2)); - state - .setup((s) => s.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); - - state.verifyAll(); - }); - }); - - suite('Function isJediUsingDefaultConfiguration()', () => { - let serviceContainer: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let cmdManager: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; - let workspaceConfig: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - cmdManager = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); - const configService = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - interpreterService = TypeMoq.Mock.ofType(); - const e = new EventEmitter(); - interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event); - const langFolderServiceMock = TypeMoq.Mock.ofType(); - const extensionsMock = TypeMoq.Mock.ofType(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3'), - }; - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup((l) => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory - .setup((f) => - f.createGlobalPersistentState( - TypeMoq.It.isValue('SWITCH_LS'), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => state.object); - state.setup((s) => s.value).returns(() => undefined); - state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.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); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); - }); - const value = [undefined, true, false]; // Possible values of settings - const index = [0, 1, 2]; // Index associated with each value - const expectedResults: boolean[][][] = Array(3) // Initializing a 3D array with default value `false` - .fill(false) - .map(() => - Array(3) - .fill(false) - .map(() => Array(3).fill(false)), - ); - expectedResults[0][0][0] = true; - for (const globalIndex of index) { - for (const workspaceIndex of index) { - for (const workspaceFolderIndex of index) { - const expectedResult = expectedResults[globalIndex][workspaceIndex][workspaceFolderIndex]; - const settings = { - globalValue: value[globalIndex], - workspaceValue: value[workspaceIndex], - workspaceFolderValue: value[workspaceFolderIndex], - }; - const testName = `Returns ${expectedResult} for setting = ${JSON.stringify(settings)}`; - test(testName, async () => { - workspaceConfig.reset(); - workspaceConfig - .setup((c) => c.inspect('languageServer')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); - expect(result).to.equal(expectedResult); - - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - } - } - } - test('Returns false for settings = undefined', async () => { - workspaceConfig.reset(); - workspaceConfig - .setup((c) => c.inspect('languageServer')) - .returns(() => undefined as any) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - ); - const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); - expect(result).to.equal(false, 'Return value should be false'); - - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - }); -}); diff --git a/src/test/activation/node/activator.unit.test.ts b/src/test/activation/node/activator.unit.test.ts deleted file mode 100644 index 2b91ed0f7d20..000000000000 --- a/src/test/activation/node/activator.unit.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { EventEmitter, Extension, Uri } from 'vscode'; -import { NodeLanguageServerActivator } from '../../../client/activation/node/activator'; -import { NodeLanguageServerManager } from '../../../client/activation/node/manager'; -import { ILanguageServerManager } from '../../../client/activation/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { PythonSettings } from '../../../client/common/configSettings'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IConfigurationService, IExtensions, IPythonSettings } from '../../../client/common/types'; -import { Pylance } from '../../../client/common/utils/localize'; - -suite('Pylance Language Server - Activator', () => { - let activator: NodeLanguageServerActivator; - let workspaceService: IWorkspaceService; - let manager: ILanguageServerManager; - let fs: IFileSystem; - let configuration: IConfigurationService; - let settings: IPythonSettings; - let extensions: IExtensions; - let appShell: IApplicationShell; - let commandManager: ICommandManager; - let extensionsChangedEvent: EventEmitter; - - let pylanceExtension: Extension; - setup(() => { - manager = mock(NodeLanguageServerManager); - workspaceService = mock(WorkspaceService); - fs = mock(FileSystem); - configuration = mock(ConfigurationService); - settings = mock(PythonSettings); - extensions = mock(); - appShell = mock(); - commandManager = mock(); - - pylanceExtension = mock>(); - when(configuration.getSettings(anything())).thenReturn(instance(settings)); - - extensionsChangedEvent = new EventEmitter(); - when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); - - activator = new NodeLanguageServerActivator( - instance(manager), - instance(workspaceService), - instance(fs), - instance(configuration), - instance(extensions), - instance(appShell), - instance(commandManager), - ); - }); - teardown(() => { - extensionsChangedEvent.dispose(); - }); - - test('Manager must be started without any workspace', async () => { - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); - when(manager.start(undefined, undefined)).thenResolve(); - - await activator.start(undefined); - verify(manager.start(undefined, undefined)).once(); - verify(workspaceService.workspaceFolders).once(); - }); - - test('Manager must be disposed', async () => { - activator.dispose(); - verify(manager.dispose()).once(); - }); - - test('Activator should check if Pylance is installed', async () => { - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); - await activator.start(undefined); - verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); - }); - - test('Activator should not check if Pylance is installed in development mode', async () => { - when(settings.downloadLanguageServer).thenReturn(false); - await activator.start(undefined); - verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).never(); - }); - - test('When Pylance is not installed activator should show install prompt ', async () => { - when( - appShell.showWarningMessage( - Pylance.pylanceRevertToJediPrompt(), - Pylance.pylanceInstallPylance(), - Pylance.pylanceRevertToJedi(), - Pylance.remindMeLater(), - ), - ).thenReturn(Promise.resolve(Pylance.remindMeLater())); - - try { - await activator.start(undefined); - } catch {} - verify( - appShell.showWarningMessage( - Pylance.pylanceRevertToJediPrompt(), - Pylance.pylanceInstallPylance(), - Pylance.pylanceRevertToJedi(), - Pylance.remindMeLater(), - ), - ).once(); - verify(commandManager.executeCommand('extension.open', PYLANCE_EXTENSION_ID)).never(); - }); - - test('When Pylance is not installed activator should open Pylance install page if users clicks Yes', async () => { - when( - appShell.showWarningMessage( - Pylance.pylanceRevertToJediPrompt(), - Pylance.pylanceInstallPylance(), - Pylance.pylanceRevertToJedi(), - Pylance.remindMeLater(), - ), - ).thenReturn(Promise.resolve(Pylance.pylanceInstallPylance())); - - try { - await activator.start(undefined); - } catch {} - verify(commandManager.executeCommand('extension.open', PYLANCE_EXTENSION_ID)).once(); - }); - - test('Activator should throw if Pylance is not installed', async () => { - expect(activator.start(undefined)) - .to.eventually.be.rejectedWith(Pylance.pylanceNotInstalledMessage()) - .and.be.an.instanceOf(Error); - }); - - test('Manager must be started with resource for first available workspace', async () => { - const uri = Uri.file(__filename); - when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri }]); - when(manager.start(uri, undefined)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(false); - - await activator.start(undefined); - - verify(manager.start(uri, undefined)).once(); - verify(workspaceService.workspaceFolders).atLeast(1); - }); -}); diff --git a/src/test/activation/node/languageServerChangeHandler.unit.test.ts b/src/test/activation/node/languageServerChangeHandler.unit.test.ts index 09c3994f55aa..0b06cccae812 100644 --- a/src/test/activation/node/languageServerChangeHandler.unit.test.ts +++ b/src/test/activation/node/languageServerChangeHandler.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { anyString, instance, mock, verify, when, anything } from 'ts-mockito'; -import { ConfigurationTarget, EventEmitter, Extension, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationTarget, EventEmitter, WorkspaceConfiguration } from 'vscode'; import { LanguageServerChangeHandler } from '../../../client/activation/common/languageServerChangeHandler'; import { LanguageServerType } from '../../../client/activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -22,7 +22,6 @@ suite('Language Server - Change Handler', () => { let workspace: IWorkspaceService; let configService: IConfigurationService; - let pylanceExtension: Extension; setup(() => { extensions = mock(); appShell = mock(); @@ -30,8 +29,6 @@ suite('Language Server - Change Handler', () => { workspace = mock(); configService = mock(); - pylanceExtension = mock>(); - extensionsChangedEvent = new EventEmitter(); when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); }); @@ -52,40 +49,6 @@ suite('Language Server - Change Handler', () => { }); }); - [LanguageServerType.None, LanguageServerType.Jedi, LanguageServerType.Node].forEach(async (t) => { - test(`Handler should prompt for reload when language server type changes to ${t}, Pylance is installed ans user clicks Reload`, async () => { - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); - when( - appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()), - ).thenReturn(Promise.resolve(Common.reload())); - - handler = makeHandler(undefined); - await handler.handleLanguageServerChange(t); - - verify( - appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()), - ).once(); - verify(commands.executeCommand('workbench.action.reloadWindow')).once(); - }); - }); - - [LanguageServerType.None, LanguageServerType.Jedi, LanguageServerType.Node].forEach(async (t) => { - test(`Handler should not prompt for reload when language server type changes to ${t}, Pylance is installed ans user does not clicks Reload`, async () => { - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); - when( - appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()), - ).thenReturn(Promise.resolve(undefined)); - - handler = makeHandler(undefined); - await handler.handleLanguageServerChange(t); - - verify( - appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()), - ).once(); - verify(commands.executeCommand('workbench.action.reloadWindow')).never(); - }); - }); - test('Handler should prompt for install when language server changes to Pylance and Pylance is not installed', async () => { when( appShell.showWarningMessage( @@ -146,40 +109,6 @@ suite('Language Server - Change Handler', () => { verify(commands.executeCommand('workbench.action.reloadWindow')).never(); }); - test('If Pylance was not installed and now it is, reload should be called if user agreed to it', async () => { - when( - appShell.showWarningMessage( - Pylance.pylanceInstalledReloadPromptMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo(), - ), - ).thenReturn(Promise.resolve(Common.bannerLabelYes())); - handler = makeHandler(LanguageServerType.Node); - - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(pylanceExtension); - extensionsChangedEvent.fire(); - - await handler.pylanceInstallCompleted; - verify(commands.executeCommand('workbench.action.reloadWindow')).once(); - }); - - test('If Pylance was not installed and now it is, reload should not be called if user refused it', async () => { - when( - appShell.showWarningMessage( - Pylance.pylanceInstalledReloadPromptMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo(), - ), - ).thenReturn(Promise.resolve(Common.bannerLabelNo())); - handler = makeHandler(LanguageServerType.Node); - - when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(pylanceExtension); - extensionsChangedEvent.fire(); - - await handler.pylanceInstallCompleted; - verify(commands.executeCommand('workbench.action.reloadWindow')).never(); - }); - [ConfigurationTarget.Global, ConfigurationTarget.Workspace].forEach((target) => { const targetName = target === ConfigurationTarget.Global ? 'global' : 'workspace'; test(`Revert to Jedi with setting in ${targetName} config`, async () => { diff --git a/src/test/activation/node/languageServerFolderService.unit.test.ts b/src/test/activation/node/languageServerFolderService.unit.test.ts deleted file mode 100644 index c1fcc95ce696..000000000000 --- a/src/test/activation/node/languageServerFolderService.unit.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert, expect, use } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Extension, Uri } from 'vscode'; -import * as chaiAsPromised from 'chai-as-promised'; -import { - ILanguageServerFolder, - ILSExtensionApi, - NodeLanguageServerFolderService, -} from '../../../client/activation/node/languageServerFolderService'; -import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; -import { IExtensions } from '../../../client/common/types'; - -use(chaiAsPromised); - -suite('Node Language Server Folder Service', () => { - const resource = Uri.parse('a'); - - let extensions: TypeMoq.IMock; - - class TestService extends NodeLanguageServerFolderService { - public languageServerFolder(): Promise { - return super.languageServerFolder(); - } - } - - setup(() => { - extensions = TypeMoq.Mock.ofType(); - }); - - test('Not installed', async () => { - extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => undefined); - - const folderService = new TestService(extensions.object); - - const lsf = await folderService.languageServerFolder(); - expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); - expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); - - await expect(folderService.getCurrentLanguageServerDirectory()).to.eventually.rejected; - await expect(folderService.getLanguageServerFolderName(resource)).to.eventually.rejected; - }); - - suite('Valid configuration', () => { - const lsPath = '/some/absolute/path'; - const lsVersion = '0.0.1-test'; - const extensionApi: ILSExtensionApi = { - languageServerFolder: async () => ({ - path: lsPath, - version: lsVersion, - }), - }; - - let folderService: TestService; - let extension: TypeMoq.IMock>; - - setup(() => { - extension = TypeMoq.Mock.ofType>(); - extension.setup((e) => e.activate()).returns(() => Promise.resolve(extensionApi)); - extension.setup((e) => e.exports).returns(() => extensionApi); - extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => extension.object); - folderService = new TestService(extensions.object); - }); - - test('skipDownload is true', async () => { - const skipDownload = await folderService.skipDownload(); - expect(skipDownload).to.be.equal(true, 'skipDownload should be true'); - }); - - test('Parsed version is correct', async () => { - const lsf = await folderService.languageServerFolder(); - assert(lsf); - expect(lsf!.version.format()).to.be.equal(lsVersion); - expect(lsf!.path).to.be.equal(lsPath); - }); - - test('getLanguageServerFolderName', async () => { - const folderName = await folderService.getLanguageServerFolderName(resource); - expect(folderName).to.be.equal(lsPath); - }); - - test('Method getCurrentLanguageServerDirectory()', async () => { - const dir = await folderService.getCurrentLanguageServerDirectory(); - assert(dir); - expect(dir!.path).to.equal(lsPath); - expect(dir!.version.format()).to.be.equal(lsVersion); - }); - }); -}); diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index fd698fcaf1af..1d3f4f383082 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -3,38 +3,20 @@ import { instance, mock, verify } from 'ts-mockito'; import { ExtensionActivationManager } from '../../client/activation/activationManager'; -import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; import { ExtensionSurveyPrompt } from '../../client/activation/extensionSurvey'; import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; -import { NoLanguageServerExtensionActivator } from '../../client/activation/none/activator'; import { registerTypes } from '../../client/activation/serviceRegistry'; import { IExtensionActivationManager, IExtensionSingleActivationService, - ILanguageClientFactory, - ILanguageServerActivator, - ILanguageServerAnalysisOptions, ILanguageServerCache, - ILanguageServerFolderService, - ILanguageServerManager, ILanguageServerOutputChannel, - ILanguageServerProxy, - LanguageServerType, } from '../../client/activation/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; -import { NodeLanguageServerActivator } from '../../client/activation/node/activator'; -import { NodeLanguageServerAnalysisOptions } from '../../client/activation/node/analysisOptions'; -import { NodeLanguageClientFactory } from '../../client/activation/node/languageClientFactory'; -import { NodeLanguageServerFolderService } from '../../client/activation/node/languageServerFolderService'; -import { NodeLanguageServerProxy } from '../../client/activation/node/languageServerProxy'; -import { NodeLanguageServerManager } from '../../client/activation/node/manager'; -import { JediLanguageServerActivator } from '../../client/activation/jedi/activator'; -import { JediLanguageServerAnalysisOptions } from '../../client/activation/jedi/analysisOptions'; -import { JediLanguageClientFactory } from '../../client/activation/jedi/languageClientFactory'; -import { JediLanguageServerProxy } from '../../client/activation/jedi/languageServerProxy'; -import { JediLanguageServerManager } from '../../client/activation/jedi/manager'; import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; +import { ILanguageServerWatcher } from '../../client/languageServer/types'; +import { LanguageServerWatcher } from '../../client/languageServer/watcher'; suite('Unit Tests - Language Server Activation Service Registry', () => { let serviceManager: IServiceManager; @@ -43,13 +25,10 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { serviceManager = mock(ServiceManager); }); - function verifyCommon() { - verify( - serviceManager.addSingleton( - ILanguageServerCache, - LanguageServerExtensionActivationService, - ), - ).once(); + test('Ensure common services are registered', async () => { + registerTypes(instance(serviceManager)); + + verify(serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher)).once(); verify( serviceManager.add(IExtensionActivationManager, ExtensionActivationManager), ).once(); @@ -71,71 +50,5 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { LoadLanguageServerExtension, ), ).once(); - verify( - serviceManager.add( - ILanguageServerActivator, - NoLanguageServerExtensionActivator, - LanguageServerType.None, - ), - ).once(); - } - - test('Ensure services are registered: Node', async () => { - registerTypes(instance(serviceManager), LanguageServerType.Node); - - verifyCommon(); - - verify( - serviceManager.add( - ILanguageServerAnalysisOptions, - NodeLanguageServerAnalysisOptions, - LanguageServerType.Node, - ), - ).once(); - verify( - serviceManager.add( - ILanguageServerActivator, - NodeLanguageServerActivator, - LanguageServerType.Node, - ), - ).once(); - verify( - serviceManager.addSingleton(ILanguageClientFactory, NodeLanguageClientFactory), - ).once(); - verify(serviceManager.add(ILanguageServerManager, NodeLanguageServerManager)).once(); - verify(serviceManager.add(ILanguageServerProxy, NodeLanguageServerProxy)).once(); - verify( - serviceManager.addSingleton( - ILanguageServerFolderService, - NodeLanguageServerFolderService, - ), - ).once(); - }); - test('Ensure services are registered: Jedi', async () => { - registerTypes(instance(serviceManager), LanguageServerType.Jedi); - - verifyCommon(); - - verify( - serviceManager.add( - ILanguageServerActivator, - JediLanguageServerActivator, - LanguageServerType.Jedi, - ), - ).once(); - - verify( - serviceManager.add( - ILanguageServerAnalysisOptions, - JediLanguageServerAnalysisOptions, - LanguageServerType.Jedi, - ), - ).once(); - - verify( - serviceManager.addSingleton(ILanguageClientFactory, JediLanguageClientFactory), - ).once(); - verify(serviceManager.add(ILanguageServerManager, JediLanguageServerManager)).once(); - verify(serviceManager.add(ILanguageServerProxy, JediLanguageServerProxy)).once(); }); }); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index f70014f955f8..6fab38bdffc2 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -94,12 +94,6 @@ suite('Python Settings', async () => { } // boolean settings - for (const name of ['downloadLanguageServer', 'autoUpdateLanguageServer']) { - config - .setup((c) => c.get(name, true)) - - .returns(() => (sourceSettings as any)[name]); - } for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { config .setup((c) => c.get(name)) @@ -146,11 +140,9 @@ suite('Python Settings', async () => { }); suite('Boolean settings', async () => { - ['downloadLanguageServer', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( - async (settingName) => { - testIfValueIsUpdated(settingName, true); - }, - ); + ['globalModuleInstallation'].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, true); + }); }); test('condaPath updated', () => { diff --git a/src/test/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..82fe5a8262b8 --- /dev/null +++ b/src/test/languageServer/jediLSExtensionManager.unit.test.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { ILanguageServerOutputChannel } from '../../client/activation/types'; +import { IWorkspaceService, ICommandManager } from '../../client/common/application/types'; +import { IExperimentService, IConfigurationService, IInterpreterPathService } from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediLSExtensionManager } from '../../client/languageServer/jediLSExtensionManager'; + +suite('Language Server - Jedi LS extension manager', () => { + let manager: JediLSExtensionManager; + + setup(() => { + manager = new JediLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + ); + }); + + test('Constructor should create a client proxy, a server manager and a server proxy', () => { + assert.notStrictEqual(manager.clientFactory, undefined); + assert.notStrictEqual(manager.serverManager, undefined); + assert.notStrictEqual(manager.serverProxy, undefined); + }); + + test('canStartLanguageServer should return true', () => { + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, true); + }); +}); diff --git a/src/test/languageServer/languageServerCapabilities.unit.test.ts b/src/test/languageServer/languageServerCapabilities.unit.test.ts new file mode 100644 index 000000000000..4392e2901af8 --- /dev/null +++ b/src/test/languageServer/languageServerCapabilities.unit.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { ILanguageServerProxy } from '../../client/activation/types'; +import { LanguageServerCapabilities } from '../../client/languageServer/languageServerCapabilities'; + +suite('Language server - capabilities', () => { + test('get() should not return undefined', async () => { + const capabilities = new LanguageServerCapabilities(); + + const result = await capabilities.get(); + + assert.notDeepStrictEqual(result, undefined); + }); + + test('The connection property should return an object if there is a language client', () => { + const serverProxy = ({ + languageClient: { + sendNotification: () => { + /* nothing */ + }, + sendRequest: () => { + /* nothing */ + }, + sendProgress: () => { + /* nothing */ + }, + onRequest: () => { + /* nothing */ + }, + onNotification: () => { + /* nothing */ + }, + onProgress: () => { + /* nothing */ + }, + }, + } as unknown) as ILanguageServerProxy; + + const capabilities = new LanguageServerCapabilities(); + capabilities.serverProxy = serverProxy; + + const result = capabilities.connection; + + assert.notDeepStrictEqual(result, undefined); + assert.strictEqual(typeof result, 'object'); + }); + + test('The connection property should return undefined if there is no language client', () => { + const serverProxy = ({} as unknown) as ILanguageServerProxy; + + const capabilities = new LanguageServerCapabilities(); + capabilities.serverProxy = serverProxy; + + const result = capabilities.connection; + + assert.deepStrictEqual(result, undefined); + }); + + test('capabilities() should return an object if there is an initialized language client', () => { + const serverProxy = ({ + languageClient: { + initializeResult: { + capabilities: {}, + }, + }, + } as unknown) as ILanguageServerProxy; + + const capabilities = new LanguageServerCapabilities(); + capabilities.serverProxy = serverProxy; + + const result = capabilities.capabilities; + + assert.notDeepStrictEqual(result, undefined); + assert.strictEqual(typeof result, 'object'); + }); + + test('capabilities() should return undefined if there is no language client', () => { + const serverProxy = ({} as unknown) as ILanguageServerProxy; + + const capabilities = new LanguageServerCapabilities(); + capabilities.serverProxy = serverProxy; + + const result = capabilities.capabilities; + + assert.deepStrictEqual(result, undefined); + }); + + test('capabilities() should return undefined if the language client is not initialized', () => { + const serverProxy = ({ + languageClient: { + initializeResult: undefined, + }, + } as unknown) as ILanguageServerProxy; + + const capabilities = new LanguageServerCapabilities(); + capabilities.serverProxy = serverProxy; + + const result = capabilities.capabilities; + + assert.deepStrictEqual(result, undefined); + }); +}); diff --git a/src/test/languageServer/noneLSExtensionManager.unit.test.ts b/src/test/languageServer/noneLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..f662dc152e69 --- /dev/null +++ b/src/test/languageServer/noneLSExtensionManager.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; + +suite('Language Server - No LS extension manager', () => { + let manager: NoneLSExtensionManager; + + setup(() => { + manager = new NoneLSExtensionManager(); + }); + + test('Constructor should not create a server proxy', () => { + assert.strictEqual(manager.serverProxy, undefined); + }); + + test('canStartLanguageServer should return true', () => { + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, true); + }); +}); diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..418aa7812795 --- /dev/null +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { ILanguageServerOutputChannel } from '../../client/activation/types'; +import { IWorkspaceService, ICommandManager, IApplicationShell } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IExperimentService, + IConfigurationService, + IInterpreterPathService, + IExtensions, +} from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; + +suite('Language Server - Pylance LS extension manager', () => { + let manager: PylanceLSExtensionManager; + + setup(() => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + {} as IExtensions, + {} as IApplicationShell, + ); + }); + + test('Constructor should create a client proxy, a server manager and a server proxy', () => { + assert.notStrictEqual(manager.clientFactory, undefined); + assert.notStrictEqual(manager.serverManager, undefined); + assert.notStrictEqual(manager.serverProxy, undefined); + }); + + test('canStartLanguageServer should return true if Pylance is installed', () => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => ({}), + } as unknown) as IExtensions, + {} as IApplicationShell, + ); + + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, true); + }); + + test('canStartLanguageServer should return false if Pylance is not installed', () => { + manager = new PylanceLSExtensionManager( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + {} as IExperimentService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + } as unknown) as IExtensions, + {} as IApplicationShell, + ); + + const result = manager.canStartLanguageServer(); + + assert.strictEqual(result, false); + }); +}); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts new file mode 100644 index 000000000000..74a6939ec97b --- /dev/null +++ b/src/test/languageServer/watcher.unit.test.ts @@ -0,0 +1,646 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ConfigurationChangeEvent, Disposable, Uri } from 'vscode'; +import { ILanguageServerOutputChannel, LanguageServerType } from '../../client/activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IConfigurationService, + IExperimentService, + IExtensions, + IInterpreterPathService, +} from '../../client/common/types'; +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 { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; +import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; +import { LanguageServerWatcher } from '../../client/languageServer/watcher'; +import * as Logging from '../../client/logging'; + +suite('Language server watcher', () => { + let watcher: LanguageServerWatcher; + const sandbox = sinon.createSandbox(); + + setup(() => { + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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 */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + [] as Disposable[], + ); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('The constructor should add a listener to onDidChange to the list of disposables if it is a trusted workspace', () => { + const disposables: Disposable[] = []; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + {} as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + {} as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + + assert.strictEqual(disposables.length, 4); + }); + + test('The constructor should not add a listener to onDidChange to the list of disposables if it is not a trusted workspace', () => { + const disposables: Disposable[] = []; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + {} as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + {} as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + {} as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + + assert.strictEqual(disposables.length, 3); + }); + + test(`When starting the language server, the language server extension manager should not be undefined`, async () => { + // First start + await watcher.startLanguageServer(LanguageServerType.None); + const extensionManager = watcher.languageServerExtensionManager!; + + 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 () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns('python'); + getActiveInterpreterStub.onSecondCall().returns('other/python'); + + const interpreterService = ({ + getActiveInterpreter: getActiveInterpreterStub, + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + 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 */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + [] as Disposable[], + ); + + // First start, get the reference to the extension manager. + await watcher.startLanguageServer(LanguageServerType.None); + + const extensionManager = watcher.languageServerExtensionManager!; + const stopLanguageServerSpy = sandbox.spy(extensionManager, 'stopLanguageServer'); + + // Second start, check if the first server manager was stopped and disposed of. + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(stopLanguageServerSpy.calledOnce); + }); + + test(`When starting the language server, if the language server can be started, it should call startLanguageServer on the language server extension manager`, async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test(`When starting the language server, if the language server can be started, there should be logs written in the output channel`, async () => { + let output = ''; + sandbox.stub(Logging, 'traceLog').callsFake((...args: unknown[]) => { + output = output.concat(...(args as string[])); + }); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => ({ folderUri: Uri.parse('workspace') }), + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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 */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.strictEqual(output, LanguageService.startingNone().format('workspace')); + }); + + test(`When starting the language server, if the language server can be started, this.languageServerType should reflect the new language server type`, async () => { + await watcher.startLanguageServer(LanguageServerType.None); + + assert.deepStrictEqual(watcher.languageServerType, LanguageServerType.None); + }); + + test(`When starting the language server, if the language server cannot be started, it should call languageServerNotAvailable`, async () => { + const canStartLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'canStartLanguageServer'); + canStartLanguageServerStub.returns(false); + const languageServerNotAvailableStub = sandbox.stub( + NoneLSExtensionManager.prototype, + 'languageServerNotAvailable', + ); + languageServerNotAvailableStub.returns(Promise.resolve()); + + await watcher.startLanguageServer(LanguageServerType.None); + + assert.ok(canStartLanguageServerStub.calledOnce); + assert.ok(languageServerNotAvailableStub.calledOnce); + }); + + test('When the config settings change, but the python.languageServer setting is not affected, the watcher should not restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { + onDidChangeConfigListener = listener; + }, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + } 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, + {} as IApplicationShell, + [] as Disposable[], + ); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => false }); + + // Check that startLanguageServer was only called once: When we called it above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('When the config settings change, and the python.languageServer setting is affected, the watcher should restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { + onDidChangeConfigListener = listener; + }, + workspaceFolders: [{ uri: Uri.parse('workspace') }], + } as unknown) as IWorkspaceService; + + const getSettingsStub = sandbox.stub(); + getSettingsStub.onFirstCall().returns({ languageServer: LanguageServerType.None }); + getSettingsStub.onSecondCall().returns({ languageServer: LanguageServerType.Node }); + + const configService = ({ + getSettings: getSettingsStub, + } as unknown) as IConfigurationService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + configService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + } 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, + {} as IApplicationShell, + [] as Disposable[], + ); + + // Use a fake here so we don't actually start up language servers. + const startLanguageServerFake = sandbox.fake.resolves(undefined); + sandbox.replace(watcher, 'startLanguageServer', startLanguageServerFake); + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => true }); + + // Check that startLanguageServer was called twice: When we called it above, and implicitly because of the event. + assert.ok(startLanguageServerFake.calledTwice); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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 */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(LanguageServerType.Jedi); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is default, use Pylance', async () => { + const startLanguageServerStub = sandbox.stub(PylanceLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + sandbox.stub(PylanceLSExtensionManager.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ + languageServer: LanguageServerType.Jedi, + languageServerIsDefault: true, + }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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 */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(LanguageServerType.Node); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server in an untrusted workspace with Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + 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 */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + [] as Disposable[], + ); + + await watcher.startLanguageServer(LanguageServerType.Jedi); + + 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'); + + 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, + 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 */ + }, + } 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); + }); +});