diff --git a/src/client/activation/common/outputChannel.ts b/src/client/activation/common/outputChannel.ts index 60a99687793e..534bcd6e8f39 100644 --- a/src/client/activation/common/outputChannel.ts +++ b/src/client/activation/common/outputChannel.ts @@ -6,26 +6,30 @@ import { inject, injectable } from 'inversify'; import { IApplicationShell, ICommandManager } from '../../common/application/types'; import '../../common/extensions'; -import { IDisposableRegistry, ILogOutputChannel } from '../../common/types'; +import { IDisposable, ILogOutputChannel } from '../../common/types'; import { OutputChannelNames } from '../../common/utils/localize'; import { ILanguageServerOutputChannel } from '../types'; @injectable() export class LanguageServerOutputChannel implements ILanguageServerOutputChannel { public output: ILogOutputChannel | undefined; - + private disposables: IDisposable[] = []; private registered = false; constructor( @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposable: IDisposableRegistry, ) {} + public dispose(): void { + this.disposables.forEach((d) => d && d.dispose()); + this.disposables = []; + } + public get channel(): ILogOutputChannel { if (!this.output) { - this.output = this.appShell.createOutputChannel(OutputChannelNames.languageServer); - this.disposable.push(this.output); + this.output = this.appShell.createOutputChannel(OutputChannelNames.JediLanguageServer); + this.disposables.push(this.output); this.registerCommand().ignoreErrors(); } return this.output; @@ -39,10 +43,10 @@ export class LanguageServerOutputChannel implements ILanguageServerOutputChannel // This controls the visibility of the command used to display the LS Output panel. // We don't want to display it when Jedi is used instead of LS. await this.commandManager.executeCommand('setContext', 'python.hasLanguageServerOutputChannel', true); - this.disposable.push( + this.disposables.push( this.commandManager.registerCommand('python.viewLanguageServerOutput', () => this.output?.show(true)), ); - this.disposable.push({ + this.disposables.push({ dispose: () => { this.registered = false; }, diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts deleted file mode 100644 index 45d1d1a17fee..000000000000 --- a/src/client/activation/node/languageServerProxy.ts +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../../common/extensions'; - -import { - DidChangeConfigurationNotification, - Disposable, - LanguageClient, - LanguageClientOptions, -} from 'vscode-languageclient/node'; - -import { Extension } from 'vscode'; -import { IExperimentService, IExtensions, IInterpreterPathService, Resource } from '../../common/types'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; -import { ProgressReporting } from '../progress'; -import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; -import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; -import { IWorkspaceService } from '../../common/application/types'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { PylanceApi } from './pylanceApi'; - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace InExperiment { - export const Method = 'python/inExperiment'; - - export interface IRequest { - experimentName: string; - } - - export interface IResponse { - inExperiment: boolean; - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace GetExperimentValue { - export const Method = 'python/getExperimentValue'; - - export interface IRequest { - experimentName: string; - } - - export interface IResponse { - value: T | undefined; - } -} - -export class NodeLanguageServerProxy implements ILanguageServerProxy { - public languageClient: LanguageClient | undefined; - - private cancellationStrategy: FileBasedCancellationStrategy | undefined; - - private readonly disposables: Disposable[] = []; - - private lsVersion: string | undefined; - - private pylanceApi: PylanceApi | undefined; - - constructor( - private readonly factory: ILanguageClientFactory, - private readonly experimentService: IExperimentService, - private readonly interpreterPathService: IInterpreterPathService, - private readonly environmentService: IEnvironmentVariablesProvider, - private readonly workspace: IWorkspaceService, - private readonly extensions: IExtensions, - ) {} - - private static versionTelemetryProps(instance: NodeLanguageServerProxy) { - return { - lsVersion: instance.lsVersion, - }; - } - - @traceDecoratorVerbose('Disposing language server') - public dispose(): void { - this.stop().ignoreErrors(); - } - - @traceDecoratorError('Failed to start language server') - @captureTelemetry( - EventName.LANGUAGE_SERVER_ENABLED, - undefined, - true, - undefined, - NodeLanguageServerProxy.versionTelemetryProps, - ) - public async start( - resource: Resource, - interpreter: PythonEnvironment | undefined, - options: LanguageClientOptions, - ): Promise { - const extension = await this.getPylanceExtension(); - this.lsVersion = extension?.packageJSON.version || '0'; - - const api = extension?.exports; - if (api && api.client && api.client.isEnabled()) { - this.pylanceApi = api; - await api.client.start(); - return; - } - - this.cancellationStrategy = new FileBasedCancellationStrategy(); - options.connectionOptions = { cancellationStrategy: this.cancellationStrategy }; - - const client = await this.factory.createLanguageClient(resource, interpreter, options); - this.registerHandlers(client, resource); - - this.disposables.push( - this.workspace.onDidGrantWorkspaceTrust(() => { - client.sendNotification('python/workspaceTrusted', { isTrusted: true }); - }), - ); - - await client.start(); - - this.languageClient = client; - } - - @traceDecoratorVerbose('Disposing language server') - public async stop(): Promise { - if (this.pylanceApi) { - const api = this.pylanceApi; - this.pylanceApi = undefined; - await api.client!.stop(); - } - - while (this.disposables.length > 0) { - const d = this.disposables.shift()!; - d.dispose(); - } - - if (this.languageClient) { - const client = this.languageClient; - this.languageClient = undefined; - - try { - await client.stop(); - await client.dispose(); - } catch (ex) { - traceError('Stopping language client failed', ex); - } - } - - if (this.cancellationStrategy) { - this.cancellationStrategy.dispose(); - this.cancellationStrategy = undefined; - } - } - - // eslint-disable-next-line class-methods-use-this - public loadExtension(): void { - // No body. - } - - @captureTelemetry( - EventName.LANGUAGE_SERVER_READY, - undefined, - true, - undefined, - NodeLanguageServerProxy.versionTelemetryProps, - ) - private registerHandlers(client: LanguageClient, _resource: Resource) { - const progressReporting = new ProgressReporting(client); - this.disposables.push(progressReporting); - - this.disposables.push( - this.interpreterPathService.onDidChange(() => { - // Manually send didChangeConfiguration in order to get the server to requery - // the workspace configurations (to then pick up pythonPath set in the middleware). - // This is needed as interpreter changes via the interpreter path service happen - // outside of VS Code's settings (which would mean VS Code sends the config updates itself). - client.sendNotification(DidChangeConfigurationNotification.type, { - settings: null, - }); - }), - ); - this.disposables.push( - this.environmentService.onDidEnvironmentVariablesChange(() => { - client.sendNotification(DidChangeConfigurationNotification.type, { - settings: null, - }); - }), - ); - - client.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); - }); - - client.onRequest( - InExperiment.Method, - async (params: InExperiment.IRequest): Promise => { - const inExperiment = await this.experimentService.inExperiment(params.experimentName); - return { inExperiment }; - }, - ); - - client.onRequest( - GetExperimentValue.Method, - async ( - params: GetExperimentValue.IRequest, - ): Promise> => { - const value = await this.experimentService.getExperimentValue(params.experimentName); - return { value }; - }, - ); - - this.disposables.push( - client.onRequest('python/isTrustedWorkspace', async () => ({ - isTrusted: this.workspace.isTrusted, - })), - ); - } - - private async getPylanceExtension(): Promise | undefined> { - const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); - if (!extension) { - return undefined; - } - - if (!extension.isActive) { - await extension.activate(); - } - - return extension; - } -} diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts deleted file mode 100644 index 5a66e4abecd0..000000000000 --- a/src/client/activation/node/manager.ts +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import '../../common/extensions'; - -import { ICommandManager } from '../../common/application/types'; -import { IDisposable, IExtensions, Resource } from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IServiceContainer } from '../../ioc/types'; -import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { Commands } from '../commands'; -import { NodeLanguageClientMiddleware } from './languageClientMiddleware'; -import { ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types'; -import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { NodeLanguageServerProxy } from './languageServerProxy'; - -export class NodeLanguageServerManager implements ILanguageServerManager { - private resource!: Resource; - - private interpreter: PythonEnvironment | undefined; - - private middleware: NodeLanguageClientMiddleware | undefined; - - private disposables: IDisposable[] = []; - - private connected = false; - - private lsVersion: string | undefined; - - private started = false; - - private static commandDispose: IDisposable; - - constructor( - private readonly serviceContainer: IServiceContainer, - private readonly analysisOptions: ILanguageServerAnalysisOptions, - private readonly languageServerProxy: NodeLanguageServerProxy, - commandManager: ICommandManager, - private readonly extensions: IExtensions, - ) { - if (NodeLanguageServerManager.commandDispose) { - NodeLanguageServerManager.commandDispose.dispose(); - } - NodeLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => { - sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'command' }); - this.restartLanguageServer().ignoreErrors(); - }); - } - - private static versionTelemetryProps(instance: NodeLanguageServerManager) { - return { - lsVersion: instance.lsVersion, - }; - } - - public dispose(): void { - this.stopLanguageServer().ignoreErrors(); - NodeLanguageServerManager.commandDispose.dispose(); - this.disposables.forEach((d) => d.dispose()); - } - - @traceDecoratorError('Failed to start language server') - public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { - 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 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(): void { - if (!this.connected) { - this.connected = true; - this.middleware?.connect(); - } - } - - public disconnect(): void { - if (this.connected) { - this.connected = false; - this.middleware?.disconnect(); - } - } - - @debounceSync(1000) - protected restartLanguageServerDebounced(): void { - sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'settings' }); - this.restartLanguageServer().ignoreErrors(); - } - - @traceDecoratorError('Failed to restart language server') - @traceDecoratorVerbose('Restarting language server') - protected async restartLanguageServer(): Promise { - await this.stopLanguageServer(); - await this.startLanguageServer(); - } - - @captureTelemetry( - EventName.LANGUAGE_SERVER_STARTUP, - undefined, - true, - undefined, - NodeLanguageServerManager.versionTelemetryProps, - ) - @traceDecoratorVerbose('Starting language server') - protected async startLanguageServer(): Promise { - const options = await this.analysisOptions.getAnalysisOptions(); - this.middleware = new NodeLanguageClientMiddleware(this.serviceContainer, this.lsVersion); - options.middleware = this.middleware; - - // Make sure the middleware is connected if we restart and we we're already connected. - if (this.connected) { - this.middleware.connect(); - } - - // Then use this middleware to start a new language client. - await this.languageServerProxy.start(this.resource, this.interpreter, options); - } - - @traceDecoratorVerbose('Stopping language server') - protected async stopLanguageServer(): Promise { - if (this.languageServerProxy) { - await this.languageServerProxy.stop(); - } - } -} diff --git a/src/client/activation/node/pylanceApi.ts b/src/client/activation/node/pylanceApi.ts index 4b3d21d7527e..051ab7bdfd95 100644 --- a/src/client/activation/node/pylanceApi.ts +++ b/src/client/activation/node/pylanceApi.ts @@ -13,8 +13,6 @@ import { export interface PylanceApi { client?: { isEnabled(): boolean; - start(): Promise; - stop(): Promise; }; notebook?: { registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 875afa12f0b4..c85ebb24757d 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -4,13 +4,7 @@ import { IServiceManager } from '../ioc/types'; import { ExtensionActivationManager } from './activationManager'; import { ExtensionSurveyPrompt } from './extensionSurvey'; -import { LanguageServerOutputChannel } from './common/outputChannel'; -import { - IExtensionActivationManager, - IExtensionActivationService, - IExtensionSingleActivationService, - ILanguageServerOutputChannel, -} from './types'; +import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService } from './types'; import { LoadLanguageServerExtension } from './common/loadLanguageServerExtension'; import { PartialModeStatusItem } from './partialModeStatus'; import { ILanguageServerWatcher } from '../languageServer/types'; @@ -20,10 +14,6 @@ import { RequirementsTxtLinkActivator } from './requirementsTxtLinkActivator'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); - serviceManager.addSingleton( - ILanguageServerOutputChannel, - LanguageServerOutputChannel, - ); serviceManager.addSingleton( IExtensionSingleActivationService, ExtensionSurveyPrompt, diff --git a/src/client/api.ts b/src/client/api.ts index 908da4be7103..2ea09a0f1184 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -1,169 +1,154 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri, Event } from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { PYLANCE_NAME } from './activation/node/languageClientFactory'; -import { ILanguageServerOutputChannel } from './activation/types'; -import { PythonExtension } from './api/types'; -import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; -import { IConfigurationService, Resource } from './common/types'; -import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers'; -import { IInterpreterService } from './interpreter/contracts'; -import { IServiceContainer, IServiceManager } from './ioc/types'; -import { - JupyterExtensionIntegration, - JupyterExtensionPythonEnvironments, - JupyterPythonEnvironmentApi, -} from './jupyter/jupyterIntegration'; -import { traceError } from './logging'; -import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; -import { buildEnvironmentApi } from './environmentApi'; -import { ApiForPylance } from './pylanceApi'; -import { getTelemetryReporter } from './telemetry'; -import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; -import { getDebugpyPath } from './debugger/pythonDebugger'; - -export function buildApi( - ready: Promise, - serviceManager: IServiceManager, - serviceContainer: IServiceContainer, - discoveryApi: IDiscoveryAPI, -): PythonExtension { - const configurationService = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); - serviceManager.addSingleton( - JupyterExtensionPythonEnvironments, - JupyterExtensionPythonEnvironments, - ); - serviceManager.addSingleton( - TensorboardExtensionIntegration, - TensorboardExtensionIntegration, - ); - const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); - const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); - const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); - jupyterIntegration.registerEnvApi(environments); - const tensorboardIntegration = serviceContainer.get( - TensorboardExtensionIntegration, - ); - const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); - - const api: PythonExtension & { - /** - * Internal API just for Jupyter, hence don't include in the official types. - */ - jupyter: { - registerHooks(): void; - }; - /** - * Internal API just for Tensorboard, hence don't include in the official types. - */ - tensorboard: { - registerHooks(): void; - }; - } & { - /** - * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an - * iteration or two. - */ - pylance: ApiForPylance; - } & { - /** - * @deprecated Use PythonExtension.environments API instead. - * - * Return internal settings within the extension which are stored in VSCode storage - */ - settings: { - /** - * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. - */ - readonly onDidChangeExecutionDetails: Event; - /** - * Returns all the details the consumer needs to execute code within the selected environment, - * corresponding to the specified resource taking into account any workspace-specific settings - * for the workspace to which this resource belongs. - * @param {Resource} [resource] A resource for which the setting is asked for. - * * When no resource is provided, the setting scoped to the first workspace folder is returned. - * * If no folder is present, it returns the global setting. - */ - getExecutionDetails( - resource?: Resource, - ): { - /** - * E.g of execution commands returned could be, - * * `['']` - * * `['']` - * * `['conda', 'run', 'python']` which is used to run from within Conda environments. - * or something similar for some other Python environments. - * - * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. - * Otherwise, join the items returned using space to construct the full execution command. - */ - execCommand: string[] | undefined; - }; - }; - } = { - // 'ready' will propagate the exception, but we must log it here first. - ready: ready.catch((ex) => { - traceError('Failure during activation.', ex); - return Promise.reject(ex); - }), - jupyter: { - registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), - }, - tensorboard: { - registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), - }, - debug: { - async getRemoteLauncherCommand( - host: string, - port: number, - waitUntilDebuggerAttaches = true, - ): Promise { - return getDebugpyLauncherArgs({ - host, - port, - waitUntilDebuggerAttaches, - }); - }, - async getDebuggerPackagePath(): Promise { - return getDebugpyPath(); - }, - }, - settings: { - onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, - getExecutionDetails(resource?: Resource) { - const { pythonPath } = configurationService.getSettings(resource); - // If pythonPath equals an empty string, no interpreter is set. - return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; - }, - }, - pylance: { - createClient: (...args: any[]): BaseLanguageClient => { - // Make sure we share output channel so that we can share one with - // Jedi as well. - const clientOptions = args[1] as LanguageClientOptions; - clientOptions.outputChannel = clientOptions.outputChannel ?? outputChannel.channel; - - return new LanguageClient(PYTHON_LANGUAGE, PYLANCE_NAME, args[0], clientOptions); - }, - start: (client: BaseLanguageClient): Promise => client.start(), - stop: (client: BaseLanguageClient): Promise => client.stop(), - getTelemetryReporter: () => getTelemetryReporter(), - }, - environments, - }; - - // In test environment return the DI Container. - if (isTestExecution()) { - (api as any).serviceContainer = serviceContainer; - (api as any).serviceManager = serviceManager; - } - return api; -} +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri, Event } from 'vscode'; +import { PythonExtension } from './api/types'; +import { isTestExecution } from './common/constants'; +import { IConfigurationService, Resource } from './common/types'; +import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers'; +import { IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer, IServiceManager } from './ioc/types'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from './jupyter/jupyterIntegration'; +import { traceError } from './logging'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { buildEnvironmentApi } from './environmentApi'; +import { ApiForPylance } from './pylanceApi'; +import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; +import { getDebugpyPath } from './debugger/pythonDebugger'; + +export function buildApi( + ready: Promise, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer, + discoveryApi: IDiscoveryAPI, +): PythonExtension { + const configurationService = serviceContainer.get(IConfigurationService); + const interpreterService = serviceContainer.get(IInterpreterService); + serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); + const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); + const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); + + const api: PythonExtension & { + /** + * Internal API just for Jupyter, hence don't include in the official types. + */ + jupyter: { + registerHooks(): void; + }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; + } & { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: ApiForPylance; + } & { + /** + * @deprecated Use PythonExtension.environments API instead. + * + * Return internal settings within the extension which are stored in VSCode storage + */ + settings: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event; + /** + * Returns all the details the consumer needs to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param {Resource} [resource] A resource for which the setting is asked for. + * * When no resource is provided, the setting scoped to the first workspace folder is returned. + * * If no folder is present, it returns the global setting. + */ + getExecutionDetails( + resource?: Resource, + ): { + /** + * E.g of execution commands returned could be, + * * `['']` + * * `['']` + * * `['conda', 'run', 'python']` which is used to run from within Conda environments. + * or something similar for some other Python environments. + * + * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. + * Otherwise, join the items returned using space to construct the full execution command. + */ + execCommand: string[] | undefined; + }; + }; + } = { + // 'ready' will propagate the exception, but we must log it here first. + ready: ready.catch((ex) => { + traceError('Failure during activation.', ex); + return Promise.reject(ex); + }), + jupyter: { + registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), + }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, + debug: { + async getRemoteLauncherCommand( + host: string, + port: number, + waitUntilDebuggerAttaches = true, + ): Promise { + return getDebugpyLauncherArgs({ + host, + port, + waitUntilDebuggerAttaches, + }); + }, + async getDebuggerPackagePath(): Promise { + return getDebugpyPath(); + }, + }, + settings: { + onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, + getExecutionDetails(resource?: Resource) { + const { pythonPath } = configurationService.getSettings(resource); + // If pythonPath equals an empty string, no interpreter is set. + return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; + }, + }, + pylance: { + getTelemetryReporter: () => getTelemetryReporter(), + }, + environments, + }; + + // In test environment return the DI Container. + if (isTestExecution()) { + (api as any).serviceContainer = serviceContainer; + (api as any).serviceManager = serviceManager; + } + return api; +} diff --git a/src/client/browser/api.ts b/src/client/browser/api.ts index ac2df8d0ffed..850f5e2a87d0 100644 --- a/src/client/browser/api.ts +++ b/src/client/browser/api.ts @@ -1,32 +1,24 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { BaseLanguageClient } from 'vscode-languageclient'; -import { LanguageClient } from 'vscode-languageclient/browser'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { ApiForPylance, TelemetryReporter } from '../pylanceApi'; - -export interface IBrowserExtensionApi { - /** - * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an - * iteration or two. - */ - pylance: ApiForPylance; -} - -export function buildApi(reporter: TelemetryReporter): IBrowserExtensionApi { - const api: IBrowserExtensionApi = { - pylance: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createClient: (...args: any[]): BaseLanguageClient => - new LanguageClient(PYTHON_LANGUAGE, 'Python Language Server', args[0], args[1]), - start: (client: BaseLanguageClient): Promise => client.start(), - stop: (client: BaseLanguageClient): Promise => client.stop(), - getTelemetryReporter: () => reporter, - }, - }; - - return api; -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ApiForPylance, TelemetryReporter } from '../pylanceApi'; + +export interface IBrowserExtensionApi { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: ApiForPylance; +} + +export function buildApi(reporter: TelemetryReporter): IBrowserExtensionApi { + const api: IBrowserExtensionApi = { + pylance: { + getTelemetryReporter: () => reporter, + }, + }; + + return api; +} diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 132618430551..4e2cc2dd8f47 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -1,231 +1,222 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { LanguageClientOptions } from 'vscode-languageclient'; -import { LanguageClient } from 'vscode-languageclient/browser'; -import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; -import { LanguageServerType } from '../activation/types'; -import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; -import { EventName } from '../telemetry/constants'; -import { createStatusItem } from './intellisenseStatus'; -import { PylanceApi } from '../activation/node/pylanceApi'; -import { buildApi, IBrowserExtensionApi } from './api'; - -interface BrowserConfig { - distUrl: string; // URL to Pylance's dist folder. -} - -let languageClient: LanguageClient | undefined; -let pylanceApi: PylanceApi | undefined; - -export function activate(context: vscode.ExtensionContext): Promise { - const reporter = getTelemetryReporter(); - - const activationPromise = Promise.resolve(buildApi(reporter)); - const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); - if (pylanceExtension) { - // Make sure we run pylance once we activated core extension. - activationPromise.then(() => runPylance(context, pylanceExtension)); - return activationPromise; - } - - const changeDisposable = vscode.extensions.onDidChange(async () => { - const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); - if (newPylanceExtension) { - changeDisposable.dispose(); - await runPylance(context, newPylanceExtension); - } - }); - - return activationPromise; -} - -export async function deactivate(): Promise { - if (pylanceApi) { - const api = pylanceApi; - pylanceApi = undefined; - await api.client!.stop(); - } - - if (languageClient) { - const client = languageClient; - languageClient = undefined; - - await client.stop(); - await client.dispose(); - } -} - -async function runPylance( - context: vscode.ExtensionContext, - pylanceExtension: vscode.Extension, -): Promise { - context.subscriptions.push(createStatusItem()); - - pylanceExtension = await getActivatedExtension(pylanceExtension); - const api = pylanceExtension.exports; - if (api.client && api.client.isEnabled()) { - pylanceApi = api; - await api.client.start(); - return; - } - - const { extensionUri, packageJSON } = pylanceExtension; - const distUrl = vscode.Uri.joinPath(extensionUri, 'dist'); - - try { - const worker = new Worker(vscode.Uri.joinPath(distUrl, 'browser.server.bundle.js').toString()); - - // Pass the configuration as the first message to the worker so it can - // have info like the URL of the dist folder early enough. - // - // This is the same method used by the TS worker: - // https://github.com/microsoft/vscode/blob/90aa979bb75a795fd8c33d38aee263ea655270d0/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts#L55 - const config: BrowserConfig = { distUrl: distUrl.toString() }; - worker.postMessage(config); - - const middleware = new LanguageClientMiddlewareBase( - undefined, - LanguageServerType.Node, - sendTelemetryEventBrowser, - packageJSON.version, - ); - middleware.connect(); - - const clientOptions: LanguageClientOptions = { - // Register the server for python source files. - documentSelector: [ - { - language: 'python', - }, - ], - synchronize: { - // Synchronize the setting section to the server. - configurationSection: ['python', 'jupyter.runStartupCommands'], - }, - middleware, - }; - - const client = new LanguageClient('python', 'Python Language Server', worker, clientOptions); - languageClient = client; - - context.subscriptions.push( - vscode.commands.registerCommand('python.viewLanguageServerOutput', () => client.outputChannel.show()), - ); - - client.onTelemetry( - (telemetryEvent: { - EventName: EventName; - Properties: { method: string }; - Measurements: number | Record | undefined; - Exception: Error | undefined; - }) => { - 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, '.'), - }; - sendTelemetryEventBrowser( - eventName, - telemetryEvent.Measurements, - formattedProperties, - telemetryEvent.Exception, - ); - }, - ); - - await client.start(); - } catch (e) { - console.log(e); // necessary to use console.log for browser - } -} - -// Duplicate code from telemetry/index.ts to avoid pulling in winston, -// which doesn't support the browser. - -let telemetryReporter: TelemetryReporter | undefined; -function getTelemetryReporter() { - if (telemetryReporter) { - return telemetryReporter; - } - - // eslint-disable-next-line global-require - const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; - telemetryReporter = new Reporter(AppinsightsKey, [ - { - lookup: /(errorName|errorMessage|errorStack)/g, - }, - ]); - - return telemetryReporter; -} - -function sendTelemetryEventBrowser( - eventName: EventName, - measuresOrDurationMs?: Record | number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - properties?: any, - ex?: Error, -): void { - const reporter = getTelemetryReporter(); - const measures = - typeof measuresOrDurationMs === 'number' - ? { duration: measuresOrDurationMs } - : measuresOrDurationMs || undefined; - const customProperties: Record = {}; - const eventNameSent = eventName as string; - - if (properties) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = properties as any; - Object.getOwnPropertyNames(data).forEach((prop) => { - if (data[prop] === undefined || data[prop] === null) { - return; - } - try { - // If there are any errors in serializing one property, ignore that and move on. - // Else nothing will be sent. - switch (typeof data[prop]) { - case 'string': - customProperties[prop] = data[prop]; - break; - case 'object': - customProperties[prop] = 'object'; - break; - default: - customProperties[prop] = data[prop].toString(); - break; - } - } catch (exception) { - console.error(`Failed to serialize ${prop} for ${eventName}`, exception); // necessary to use console.log for browser - } - }); - } - - // Add shared properties to telemetry props (we may overwrite existing ones). - // Removed in the browser; there's no setSharedProperty. - // Object.assign(customProperties, sharedProperties); - - if (ex) { - const errorProps = { - errorName: ex.name, - errorStack: ex.stack ?? '', - }; - Object.assign(customProperties, errorProps); - - reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); - } else { - reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); - } -} - -async function getActivatedExtension(extension: vscode.Extension): Promise> { - if (!extension.isActive) { - await extension.activate(); - } - - return extension; -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/browser'; +import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; +import { LanguageServerType } from '../activation/types'; +import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { EventName } from '../telemetry/constants'; +import { createStatusItem } from './intellisenseStatus'; +import { PylanceApi } from '../activation/node/pylanceApi'; +import { buildApi, IBrowserExtensionApi } from './api'; + +interface BrowserConfig { + distUrl: string; // URL to Pylance's dist folder. +} + +let languageClient: LanguageClient | undefined; + +export function activate(context: vscode.ExtensionContext): Promise { + const reporter = getTelemetryReporter(); + + const activationPromise = Promise.resolve(buildApi(reporter)); + const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (pylanceExtension) { + // Make sure we run pylance once we activated core extension. + activationPromise.then(() => runPylance(context, pylanceExtension)); + return activationPromise; + } + + const changeDisposable = vscode.extensions.onDidChange(async () => { + const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (newPylanceExtension) { + changeDisposable.dispose(); + await runPylance(context, newPylanceExtension); + } + }); + + return activationPromise; +} + +export async function deactivate(): Promise { + if (languageClient) { + const client = languageClient; + languageClient = undefined; + + await client.stop(); + await client.dispose(); + } +} + +async function runPylance( + context: vscode.ExtensionContext, + pylanceExtension: vscode.Extension, +): Promise { + context.subscriptions.push(createStatusItem()); + + pylanceExtension = await getActivatedExtension(pylanceExtension); + const api = pylanceExtension.exports; + if (api.client && api.client.isEnabled()) { + return; + } + + const { extensionUri, packageJSON } = pylanceExtension; + const distUrl = vscode.Uri.joinPath(extensionUri, 'dist'); + + try { + const worker = new Worker(vscode.Uri.joinPath(distUrl, 'browser.server.bundle.js').toString()); + + // Pass the configuration as the first message to the worker so it can + // have info like the URL of the dist folder early enough. + // + // This is the same method used by the TS worker: + // https://github.com/microsoft/vscode/blob/90aa979bb75a795fd8c33d38aee263ea655270d0/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts#L55 + const config: BrowserConfig = { distUrl: distUrl.toString() }; + worker.postMessage(config); + + const middleware = new LanguageClientMiddlewareBase( + undefined, + LanguageServerType.Node, + sendTelemetryEventBrowser, + packageJSON.version, + ); + middleware.connect(); + + const clientOptions: LanguageClientOptions = { + // Register the server for python source files. + documentSelector: [ + { + language: 'python', + }, + ], + synchronize: { + // Synchronize the setting section to the server. + configurationSection: ['python', 'jupyter.runStartupCommands'], + }, + middleware, + }; + + const client = new LanguageClient('python', 'Python Language Server', worker, clientOptions); + languageClient = client; + + context.subscriptions.push( + vscode.commands.registerCommand('python.viewLanguageServerOutput', () => client.outputChannel.show()), + ); + + client.onTelemetry( + (telemetryEvent: { + EventName: EventName; + Properties: { method: string }; + Measurements: number | Record | undefined; + Exception: Error | undefined; + }) => { + 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, '.'), + }; + sendTelemetryEventBrowser( + eventName, + telemetryEvent.Measurements, + formattedProperties, + telemetryEvent.Exception, + ); + }, + ); + + await client.start(); + } catch (e) { + console.log(e); // necessary to use console.log for browser + } +} + +// Duplicate code from telemetry/index.ts to avoid pulling in winston, +// which doesn't support the browser. + +let telemetryReporter: TelemetryReporter | undefined; +function getTelemetryReporter() { + if (telemetryReporter) { + return telemetryReporter; + } + + // eslint-disable-next-line global-require + const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; + telemetryReporter = new Reporter(AppinsightsKey, [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ]); + + return telemetryReporter; +} + +function sendTelemetryEventBrowser( + eventName: EventName, + measuresOrDurationMs?: Record | number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties?: any, + ex?: Error, +): void { + const reporter = getTelemetryReporter(); + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; + const customProperties: Record = {}; + const eventNameSent = eventName as string; + + if (properties) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = properties as any; + Object.getOwnPropertyNames(data).forEach((prop) => { + if (data[prop] === undefined || data[prop] === null) { + return; + } + try { + // If there are any errors in serializing one property, ignore that and move on. + // Else nothing will be sent. + switch (typeof data[prop]) { + case 'string': + customProperties[prop] = data[prop]; + break; + case 'object': + customProperties[prop] = 'object'; + break; + default: + customProperties[prop] = data[prop].toString(); + break; + } + } catch (exception) { + console.error(`Failed to serialize ${prop} for ${eventName}`, exception); // necessary to use console.log for browser + } + }); + } + + // Add shared properties to telemetry props (we may overwrite existing ones). + // Removed in the browser; there's no setSharedProperty. + // Object.assign(customProperties, sharedProperties); + + if (ex) { + const errorProps = { + errorName: ex.name, + errorStack: ex.stack ?? '', + }; + Object.assign(customProperties, errorProps); + + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); + } else { + reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); + } +} + +async function getActivatedExtension(extension: vscode.Extension): Promise> { + if (!extension.isActive) { + await extension.activate(); + } + + return extension; +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 067275ad732c..6a99e16fef48 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -255,7 +255,7 @@ export namespace InterpreterQuickPickList { } export namespace OutputChannelNames { - export const languageServer = l10n.t('Python Language Server'); + export const JediLanguageServer = l10n.t('Jedi'); export const python = l10n.t('Python'); } diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts index 4cbfb6f33466..1a0ff0a98c30 100644 --- a/src/client/languageServer/jediLSExtensionManager.ts +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { LanguageServerOutputChannel } from '../activation/common/outputChannel'; 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 { IWorkspaceService, ICommandManager, IApplicationShell } from '../common/application/types'; import { IExperimentService, IInterpreterPathService, @@ -23,6 +23,7 @@ import { ILanguageServerExtensionManager } from './types'; export class JediLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { private serverProxy: JediLanguageServerProxy; + private outputChannel: LanguageServerOutputChannel; serverManager: JediLanguageServerManager; @@ -32,7 +33,6 @@ export class JediLSExtensionManager implements IDisposable, ILanguageServerExten constructor( serviceContainer: IServiceContainer, - outputChannel: ILanguageServerOutputChannel, _experimentService: IExperimentService, workspaceService: IWorkspaceService, configurationService: IConfigurationService, @@ -41,9 +41,11 @@ export class JediLSExtensionManager implements IDisposable, ILanguageServerExten environmentService: IEnvironmentVariablesProvider, commandManager: ICommandManager, ) { + const appShell = serviceContainer.get(IApplicationShell); + this.outputChannel = new LanguageServerOutputChannel(appShell, commandManager); this.analysisOptions = new JediLanguageServerAnalysisOptions( environmentService, - outputChannel, + this.outputChannel, configurationService, workspaceService, ); @@ -62,6 +64,7 @@ export class JediLSExtensionManager implements IDisposable, ILanguageServerExten this.serverManager.dispose(); this.serverProxy.dispose(); this.analysisOptions.dispose(); + this.outputChannel.dispose(); } async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts deleted file mode 100644 index 7b03d909a512..000000000000 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ /dev/null @@ -1,103 +0,0 @@ -// 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 { ILanguageServerExtensionManager } from './types'; - -export class PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { - private serverProxy: NodeLanguageServerProxy; - - serverManager: NodeLanguageServerManager; - - 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, - ) { - 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(); - } - - async stopLanguageServer(): Promise { - this.serverManager.disconnect(); - await this.serverProxy.stop(); - } - - 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/watcher.ts b/src/client/languageServer/watcher.ts index 39e6e0bb1ece..17425f13c001 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -5,9 +5,9 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, l10n, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; -import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; +import { IExtensionActivationService, LanguageServerType } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { IFileSystem } from '../common/platform/types'; + import { IConfigurationService, IDisposableRegistry, @@ -25,7 +25,6 @@ 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'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; @@ -53,7 +52,6 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang 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, @@ -62,7 +60,6 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang @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, @@ -233,7 +230,6 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang case LanguageServerType.Jedi: lsManager = new JediLSExtensionManager( this.serviceContainer, - this.lsOutputChannel, this.experimentService, this.workspaceService, this.configurationService, @@ -244,21 +240,6 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang ); break; case LanguageServerType.Node: - lsManager = 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: lsManager = new NoneLSExtensionManager(); diff --git a/src/client/pylanceApi.ts b/src/client/pylanceApi.ts index b839d0d9c2b7..657978192f09 100644 --- a/src/client/pylanceApi.ts +++ b/src/client/pylanceApi.ts @@ -1,26 +1,21 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry'; -import { BaseLanguageClient } from 'vscode-languageclient'; - -export interface TelemetryReporter { - sendTelemetryEvent( - eventName: string, - properties?: TelemetryEventProperties, - measurements?: TelemetryEventMeasurements, - ): void; - sendTelemetryErrorEvent( - eventName: string, - properties?: TelemetryEventProperties, - measurements?: TelemetryEventMeasurements, - ): void; -} - -export interface ApiForPylance { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createClient(...args: any[]): BaseLanguageClient; - start(client: BaseLanguageClient): Promise; - stop(client: BaseLanguageClient): Promise; - getTelemetryReporter(): TelemetryReporter; -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry'; + +export interface TelemetryReporter { + sendTelemetryEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; + sendTelemetryErrorEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; +} + +export interface ApiForPylance { + getTelemetryReporter(): TelemetryReporter; +} diff --git a/src/test/activation/outputChannel.unit.test.ts b/src/test/activation/outputChannel.unit.test.ts index f8f38783bb0e..a7bed20bf0c7 100644 --- a/src/test/activation/outputChannel.unit.test.ts +++ b/src/test/activation/outputChannel.unit.test.ts @@ -20,12 +20,12 @@ suite('Language Server Output Channel', () => { appShell = TypeMoq.Mock.ofType(); output = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); - languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object, []); + languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object); }); test('Create output channel if one does not exist before and return it', async () => { appShell - .setup((a) => a.createOutputChannel(OutputChannelNames.languageServer)) + .setup((a) => a.createOutputChannel(OutputChannelNames.JediLanguageServer)) .returns(() => output.object) .verifiable(TypeMoq.Times.once()); const { channel } = languageServerOutputChannel; diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 177eae810810..9e73bbc89302 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -4,13 +4,8 @@ import { instance, mock, verify } from 'ts-mockito'; import { ExtensionActivationManager } from '../../client/activation/activationManager'; import { ExtensionSurveyPrompt } from '../../client/activation/extensionSurvey'; -import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; import { registerTypes } from '../../client/activation/serviceRegistry'; -import { - IExtensionActivationManager, - IExtensionSingleActivationService, - ILanguageServerOutputChannel, -} from '../../client/activation/types'; +import { IExtensionActivationManager, IExtensionSingleActivationService } from '../../client/activation/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; @@ -29,12 +24,6 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { verify( serviceManager.add(IExtensionActivationManager, ExtensionActivationManager), ).once(); - verify( - serviceManager.addSingleton( - ILanguageServerOutputChannel, - LanguageServerOutputChannel, - ), - ).once(); verify( serviceManager.addSingleton( IExtensionSingleActivationService, diff --git a/src/test/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts index b57a0bbd096d..806ea4e37cc2 100644 --- a/src/test/languageServer/jediLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/jediLSExtensionManager.unit.test.ts @@ -2,9 +2,14 @@ // 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 { IWorkspaceService, ICommandManager, IApplicationShell } from '../../client/common/application/types'; +import { + IExperimentService, + IConfigurationService, + IInterpreterPathService, + ILogOutputChannel, +} from '../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; @@ -15,9 +20,26 @@ suite('Language Server - Jedi LS extension manager', () => { let manager: JediLSExtensionManager; setup(() => { + // Create a mock ILogOutputChannel + const mockOutputChannel = {} as ILogOutputChannel; + + // Create a mock IApplicationShell with createOutputChannel method + const mockApplicationShell = ({ + createOutputChannel: () => mockOutputChannel, + } as unknown) as IApplicationShell; + + // Create a mock service container with the required get method + const mockServiceContainer = { + get: (serviceIdentifier: any) => { + if (serviceIdentifier === IApplicationShell) { + return mockApplicationShell; + } + return undefined; + }, + } as IServiceContainer; + manager = new JediLSExtensionManager( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + mockServiceContainer, {} as IExperimentService, {} as IWorkspaceService, {} as IConfigurationService, diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts deleted file mode 100644 index 751b26d37d3c..000000000000 --- a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -// 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); - }); - - 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 index e86e19cf2055..4eae144f4799 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -5,16 +5,16 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { ConfigurationChangeEvent, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { JediLanguageServerManager } from '../../client/activation/jedi/manager'; -import { NodeLanguageServerManager } from '../../client/activation/node/manager'; -import { ILanguageServerOutputChannel, LanguageServerType } from '../../client/activation/types'; + +import { LanguageServerType } from '../../client/activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposable, IExperimentService, IExtensions, IInterpreterPathService, + ILogOutputChannel, } from '../../client/common/types'; import { LanguageService } from '../../client/common/utils/localize'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; @@ -22,12 +22,38 @@ import { IInterpreterHelper, IInterpreterService } from '../../client/interprete import { IServiceContainer } from '../../client/ioc/types'; import { JediLSExtensionManager } from '../../client/languageServer/jediLSExtensionManager'; import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; -import { PylanceLSExtensionManager } from '../../client/languageServer/pylanceLSExtensionManager'; + import { ILanguageServerExtensionManager } from '../../client/languageServer/types'; import { LanguageServerWatcher } from '../../client/languageServer/watcher'; import * as Logging from '../../client/logging'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +function makeMockServiceContainer(outputChannel?: ILogOutputChannel): IServiceContainer { + const mockOutputChannel = + outputChannel || + ({ + dispose: () => { + /* do nothing */ + }, + } as ILogOutputChannel); + + const mockApplicationShell = ({ + createOutputChannel: () => mockOutputChannel, + } as unknown) as IApplicationShell; + + // Only handles IApplicationShell for these tests; extend if needed. + const mockServiceContainer = ({ + get: (serviceIdentifier: unknown) => { + if (serviceIdentifier === IApplicationShell) { + return mockApplicationShell; + } + return undefined; + }, + } as unknown) as IServiceContainer; + + return mockServiceContainer; +} + suite('Language server watcher', () => { let watcher: LanguageServerWatcher; let disposables: IDisposable[]; @@ -35,9 +61,9 @@ suite('Language server watcher', () => { setup(() => { disposables = []; + watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -71,7 +97,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -91,8 +117,8 @@ suite('Language server watcher', () => { test('The constructor should add a listener to onDidChange to the list of disposables if it is a trusted workspace', () => { watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), + { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -120,7 +146,7 @@ suite('Language server watcher', () => { }, } as unknown) as IWorkspaceService, {} as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -136,8 +162,7 @@ suite('Language server watcher', () => { test('The constructor should not add a listener to onDidChange to the list of disposables if it is not a trusted workspace', () => { watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -165,7 +190,7 @@ suite('Language server watcher', () => { }, } as unknown) as IWorkspaceService, {} as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -202,12 +227,7 @@ suite('Language server watcher', () => { } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - ({ - get: () => { - /* do nothing */ - }, - } as unknown) as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -241,7 +261,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -282,8 +302,8 @@ suite('Language server watcher', () => { }); watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), + { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -317,7 +337,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -369,8 +389,8 @@ suite('Language server watcher', () => { } as unknown) as IWorkspaceService; watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), + { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -396,7 +416,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -440,8 +460,8 @@ suite('Language server watcher', () => { } as unknown) as IConfigurationService; watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), + configService, {} as IExperimentService, ({ @@ -465,7 +485,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -493,8 +513,8 @@ suite('Language server watcher', () => { startLanguageServerStub.returns(Promise.resolve()); watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), + { getSettings: () => ({ languageServer: LanguageServerType.Jedi }), } as IConfigurationService, @@ -528,7 +548,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -544,77 +564,13 @@ suite('Language server watcher', () => { 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 } }), - onDidChangeInterpreterInformation: () => { - /* do nothing */ - }, - } as unknown) as IInterpreterService, - {} as IEnvironmentVariablesProvider, - ({ - getWorkspaceFolder: (uri: Uri) => ({ uri }), - onDidChangeConfiguration: () => { - /* do nothing */ - }, - onDidChangeWorkspaceFolders: () => { - /* do nothing */ - }, - } as unknown) as IWorkspaceService, - ({ - registerCommand: () => { - /* do nothing */ - }, - } as unknown) as ICommandManager, - {} as IFileSystem, - ({ - getExtension: () => undefined, - onDidChange: () => { - /* do nothing */ - }, - } as unknown) as IExtensions, - ({ - showWarningMessage: () => Promise.resolve(undefined), - } as unknown) as IApplicationShell, - disposables, - ); - watcher.register(); - - 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, + makeMockServiceContainer(), + { getSettings: () => ({ languageServer: LanguageServerType.Jedi }), } as IConfigurationService, @@ -649,7 +605,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -676,8 +632,8 @@ suite('Language server watcher', () => { { languageServer: LanguageServerType.Node, multiLS: false, - extensionLSCls: PylanceLSExtensionManager, - lsManagerCls: NodeLanguageServerManager, + extensionLSCls: NoneLSExtensionManager, + lsManagerCls: undefined, }, { languageServer: LanguageServerType.None, @@ -700,8 +656,7 @@ suite('Language server watcher', () => { sandbox.stub(extensionLSCls.prototype, 'canStartLanguageServer').returns(true); watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer }), } as IConfigurationService, @@ -736,7 +691,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -788,8 +743,7 @@ suite('Language server watcher', () => { } as unknown) as IWorkspaceService; watcher = new LanguageServerWatcher( - {} as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer }), } as IConfigurationService, @@ -815,7 +769,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -866,12 +820,7 @@ suite('Language server watcher', () => { } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - ({ - get: () => { - /* do nothing */ - }, - } as unknown) as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -905,7 +854,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -946,12 +895,7 @@ suite('Language server watcher', () => { } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - ({ - get: () => { - /* do nothing */ - }, - } as unknown) as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -985,7 +929,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -1029,12 +973,7 @@ suite('Language server watcher', () => { } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - ({ - get: () => { - /* do nothing */ - }, - } as unknown) as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -1068,7 +1007,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => { @@ -1112,12 +1051,7 @@ suite('Language server watcher', () => { } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - ({ - get: () => { - /* do nothing */ - }, - } as unknown) as IServiceContainer, - {} as ILanguageServerOutputChannel, + makeMockServiceContainer(), { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, @@ -1151,7 +1085,7 @@ suite('Language server watcher', () => { /* do nothing */ }, } as unknown) as ICommandManager, - {} as IFileSystem, + ({ getExtension: () => undefined, onDidChange: () => {