From b43f2e46dd937e8d784668176cc95d4272290976 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Fri, 11 Mar 2022 08:56:39 -0800 Subject: [PATCH 01/30] Remove LSFolderService dependency --- .eslintignore | 1 - .../common/defaultlanguageServer.ts | 3 +- .../activation/node/languageClientFactory.ts | 22 ++--- .../node/languageServerFolderService.ts | 74 --------------- .../activation/node/languageServerProxy.ts | 34 ++----- src/client/activation/node/manager.ts | 21 +++-- src/client/activation/serviceRegistry.ts | 6 -- src/client/activation/types.ts | 10 -- src/client/browser/extension.ts | 17 ++-- .../activation/activationService.unit.test.ts | 36 ------- .../languageServerFolderService.unit.test.ts | 93 ------------------- .../activation/serviceRegistry.unit.test.ts | 8 -- 12 files changed, 35 insertions(+), 290 deletions(-) delete mode 100644 src/client/activation/node/languageServerFolderService.ts delete mode 100644 src/test/activation/node/languageServerFolderService.unit.test.ts diff --git a/.eslintignore b/.eslintignore index bf4bd2b5097c..a4c0e98ca444 100644 --- a/.eslintignore +++ b/.eslintignore @@ -189,7 +189,6 @@ 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 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/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index 0c1534dd5619..da131ceccec6 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -5,12 +5,12 @@ 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'; @@ -18,12 +18,11 @@ const languageClientName = 'Python Tools'; export class NodeLanguageClientFactory implements ILanguageClientFactory { constructor( @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(ILanguageServerFolderService) - private readonly languageServerFolderService: ILanguageServerFolderService, + @inject(IExtensions) private readonly extensions: IExtensions, ) {} public async createLanguageClient( - resource: Resource, + _resource: Resource, _interpreter: PythonEnvironment | undefined, clientOptions: LanguageClientOptions, ): Promise { @@ -31,13 +30,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'] }; 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..ea061b19e0b2 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -11,18 +11,19 @@ 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'; namespace InExperiment { export const Method = 'python/inExperiment'; @@ -59,12 +60,11 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { 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, + @inject(IExtensions) private readonly extensions: IExtensions, ) { this.startupCompleted = createDeferred(); } @@ -111,8 +111,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 }; @@ -167,7 +167,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 +195,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 => { diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index ee0c52b859d7..0f4d5ad9b862 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -5,7 +5,7 @@ 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 +15,32 @@ 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 lsVersion: string | undefined; + private started: boolean = false; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(ILanguageServerAnalysisOptions) @named(LanguageServerType.Node) private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerFolderService) - private readonly folderService: ILanguageServerFolderService, + @inject(ILanguageServerProxy) + private readonly languageServerProxy: ILanguageServerProxy, @inject(ICommandManager) commandManager: ICommandManager, + @inject(IExtensions) private readonly extensions: IExtensions, ) { this.disposables.push( commandManager.registerCommand(Commands.RestartLS, () => { @@ -67,18 +68,20 @@ export class NodeLanguageServerManager implements ILanguageServerManager { @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() { @@ -114,8 +117,6 @@ 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, diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index a2c2ad561578..0fb9f70b6126 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -13,7 +13,6 @@ 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'; @@ -25,7 +24,6 @@ import { ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCache, - ILanguageServerFolderService, ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerProxy, @@ -72,10 +70,6 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp serviceManager.addSingleton(ILanguageClientFactory, NodeLanguageClientFactory); serviceManager.add(ILanguageServerManager, NodeLanguageServerManager); serviceManager.add(ILanguageServerProxy, NodeLanguageServerProxy); - serviceManager.addSingleton( - ILanguageServerFolderService, - NodeLanguageServerFolderService, - ); } else if (languageServerType === LanguageServerType.Jedi) { serviceManager.add( ILanguageServerActivator, diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index e90b8b8e88ee..85995d4da558 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, @@ -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/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 7017506ca0f8..1e563a2e56b9 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -1,17 +1,14 @@ // 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'; @@ -63,12 +60,7 @@ suite('Language Server Activation - ActivationService', () => { 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.hasWorkspaceFolders).returns(() => false); @@ -79,9 +71,6 @@ suite('Language Server Activation - ActivationService', () => { 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( @@ -116,9 +105,6 @@ suite('Language Server Activation - ActivationService', () => { 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); @@ -653,18 +639,10 @@ suite('Language Server Activation - ActivationService', () => { 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.hasWorkspaceFolders).returns(() => false); 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( @@ -695,9 +673,6 @@ suite('Language Server Activation - ActivationService', () => { 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); }); @@ -806,18 +781,10 @@ suite('Language Server Activation - ActivationService', () => { 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.hasWorkspaceFolders).returns(() => false); 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( @@ -848,9 +815,6 @@ suite('Language Server Activation - ActivationService', () => { 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 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..34bcb3b51cce 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -15,7 +15,6 @@ import { ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCache, - ILanguageServerFolderService, ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerProxy, @@ -26,7 +25,6 @@ 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'; @@ -104,12 +102,6 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { ).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); From f6b36cf449ae57ae69a0a25cacc7e281346e835c Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 17 Mar 2022 13:45:39 -0700 Subject: [PATCH 02/30] No DI proof of concept --- src/client/activation/activationService.ts | 74 +++++---- src/client/activation/common/activatorBase.ts | 10 +- src/client/activation/node/activator.ts | 7 +- src/client/activation/serviceRegistry.ts | 93 ++++++----- src/client/extensionActivation.ts | 4 +- .../languageServer/jediLSExtensionManager.ts | 75 +++++++++ .../languageServer/noneLSExtensionManager.ts | 24 +++ .../pylanceLSExtensionManager.ts | 90 ++++++++++ src/client/languageServer/types.ts | 26 +++ src/client/languageServer/watcher.ts | 154 ++++++++++++++++++ .../activation/serviceRegistry.unit.test.ts | 4 +- 11 files changed, 466 insertions(+), 95 deletions(-) create mode 100644 src/client/languageServer/jediLSExtensionManager.ts create mode 100644 src/client/languageServer/noneLSExtensionManager.ts create mode 100644 src/client/languageServer/pylanceLSExtensionManager.ts create mode 100644 src/client/languageServer/types.ts create mode 100644 src/client/languageServer/watcher.ts diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 02ed57689cde..eaae3a084629 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -4,11 +4,12 @@ import '../common/extensions'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, Disposable, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IWorkspaceService } from '../common/application/types'; +// import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { IConfigurationService, IDisposableRegistry, - IExtensions, + // IExtensions, IPersistentStateFactory, IPythonSettings, Resource, @@ -20,7 +21,7 @@ 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 { LanguageServerChangeHandler } from './common/languageServerChangeHandler'; import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { IExtensionActivationService, @@ -28,7 +29,7 @@ import { ILanguageServerCache, LanguageServerType, } from './types'; -import { StopWatch } from '../common/utils/stopWatch'; +// import { StopWatch } from '../common/utils/stopWatch'; import { traceError, traceLog } from '../logging'; const languageServerSetting: keyof IPythonSettings = 'languageServer'; @@ -69,11 +70,11 @@ export class LanguageServerExtensionActivationService private readonly workspaceService: IWorkspaceService; - private readonly configurationService: IConfigurationService; + // private readonly configurationService: IConfigurationService; private readonly interpreterService: IInterpreterService; - private readonly languageServerChangeHandler: LanguageServerChangeHandler; + // private readonly languageServerChangeHandler: LanguageServerChangeHandler; private resource!: Resource; @@ -82,7 +83,7 @@ export class LanguageServerExtensionActivationService @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, ) { this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.configurationService = this.serviceContainer.get(IConfigurationService); + // this.configurationService = this.serviceContainer.get(IConfigurationService); this.interpreterService = this.serviceContainer.get(IInterpreterService); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); @@ -93,48 +94,48 @@ export class LanguageServerExtensionActivationService 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); + // 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(); + // 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); + // 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(); - } + // if (this.activatedServer && this.activatedServer.key !== key) { + // this.activatedServer.server.deactivate(); + // } // Get the new item - const result = await this.get(resource, interpreter); + // 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(); - } + // if (this.activatedServer) { + // this.activatedServer.server.dispose(); + // } // Save our active server. - this.activatedServer = { key, server: result, jedi: result.type === LanguageServerType.Jedi }; + // 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, - }); + // this.activatedServer.server.activate(); + // sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_STARTUP_DURATION, stopWatch.elapsedTime, { + // languageServerType: result.type, + // }); } public async get(resource: Resource, interpreter?: PythonEnvironment): Promise { @@ -292,12 +293,13 @@ export class LanguageServerExtensionActivationService 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); + // return; + // nothing to see here. } + // const lsType = this.getCurrentLanguageServerType(); + // if (this.activatedServer?.key !== lsType) { + // await this.languageServerChangeHandler.handleLanguageServerChange(lsType); + // } } private async getKey(resource: Resource, interpreter?: PythonEnvironment): Promise { diff --git a/src/client/activation/common/activatorBase.ts b/src/client/activation/common/activatorBase.ts index 958f9dcddc33..1605f4baecdb 100644 --- a/src/client/activation/common/activatorBase.ts +++ b/src/client/activation/common/activatorBase.ts @@ -48,27 +48,27 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi ) {} @traceDecoratorError('Failed to activate language server') - public async start(resource: Resource, interpreter?: PythonEnvironment): Promise { + public async start(resource: Resource, _interpreter?: PythonEnvironment): Promise { if (!resource) { resource = this.workspace.hasWorkspaceFolders ? this.workspace.workspaceFolders![0].uri : undefined; } this.resource = resource; await this.ensureLanguageServerIsAvailable(resource); - await this.manager.start(resource, interpreter); + // await this.manager.start(resource, interpreter); } public dispose(): void { - this.manager.dispose(); + // this.manager.dispose(); } public abstract ensureLanguageServerIsAvailable(resource: Resource): Promise; public activate(): void { - this.manager.connect(); + // this.manager.connect(); } public deactivate(): void { - this.manager.disconnect(); + // this.manager.disconnect(); } public get connection() { diff --git a/src/client/activation/node/activator.ts b/src/client/activation/node/activator.ts index f0de5687c44c..a879e7ca0be2 100644 --- a/src/client/activation/node/activator.ts +++ b/src/client/activation/node/activator.ts @@ -37,12 +37,7 @@ export class NodeLanguageServerActivator extends LanguageServerActivatorBase { super(manager, workspace, fs, configurationService); } - public async ensureLanguageServerIsAvailable(resource: Resource): Promise { - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer === false) { - // Development mode. - return; - } + public async ensureLanguageServerIsAvailable(_resource: Resource): Promise { 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 diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 0fb9f70b6126..d51879577a8e 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -5,35 +5,37 @@ 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 { 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 { NodeLanguageServerProxy } from './node/languageServerProxy'; -import { NodeLanguageServerManager } from './node/manager'; +// import { NodeLanguageServerActivator } from './node/activator'; +// import { NodeLanguageServerAnalysisOptions } from './node/analysisOptions'; +// import { NodeLanguageClientFactory } from './node/languageClientFactory'; +// import { NodeLanguageServerProxy } from './node/languageServerProxy'; +// import { NodeLanguageServerManager } from './node/manager'; import { NoLanguageServerExtensionActivator } from './none/activator'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, - ILanguageClientFactory, + // ILanguageClientFactory, ILanguageServerActivator, - ILanguageServerAnalysisOptions, + // ILanguageServerAnalysisOptions, ILanguageServerCache, - ILanguageServerManager, + // ILanguageServerManager, ILanguageServerOutputChannel, - ILanguageServerProxy, + // ILanguageServerProxy, LanguageServerType, } from './types'; -import { JediLanguageServerActivator } from './jedi/activator'; +// 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); @@ -56,35 +58,38 @@ 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); - } else if (languageServerType === LanguageServerType.Jedi) { - serviceManager.add( - ILanguageServerActivator, - JediLanguageServerActivator, - LanguageServerType.Jedi, - ); + serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); + serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); - serviceManager.add( - ILanguageServerAnalysisOptions, - JediLanguageServerAnalysisOptions, - LanguageServerType.Jedi, - ); + // 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); + // } else if (languageServerType === LanguageServerType.Jedi) { + // serviceManager.add( + // ILanguageServerActivator, + // JediLanguageServerActivator, + // LanguageServerType.Jedi, + // ); - serviceManager.addSingleton(ILanguageClientFactory, JediLanguageClientFactory); - serviceManager.add(ILanguageServerManager, JediLanguageServerManager); - serviceManager.add(ILanguageServerProxy, JediLanguageServerProxy); - } + // serviceManager.add( + // ILanguageServerAnalysisOptions, + // JediLanguageServerAnalysisOptions, + // LanguageServerType.Jedi, + // ); + + // serviceManager.addSingleton(ILanguageClientFactory, JediLanguageClientFactory); + // serviceManager.add(ILanguageServerManager, JediLanguageServerManager); + // serviceManager.add(ILanguageServerProxy, JediLanguageServerProxy); + // } } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 74b6cc066c7b..9536f689b685 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -125,12 +125,12 @@ 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; + // 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..cc13b21582b1 --- /dev/null +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -0,0 +1,75 @@ +// 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 } 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 { ILanguageServerExtensionManager } from './types'; + +export class JediLSExtensionManager implements 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, + ) { + 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 { + // TODO + console.warn(this.serverManager); + } + + canStartLanguageServer(): boolean { + // TODO + console.warn(this.serverManager); + return true; + } +} diff --git a/src/client/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts new file mode 100644 index 000000000000..38d3a790ab75 --- /dev/null +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable class-methods-use-this */ + +import { ILanguageServerExtensionManager } from './types'; + +export class NoneLSExtensionManager implements ILanguageServerExtensionManager { + dispose(): void { + // Nothing to do here. + } + + startLanguageServer(): Promise { + return Promise.resolve(); + } + + stopLanguageServer(): void { + // Nothing to do here. + } + + canStartLanguageServer(): boolean { + return true; + } +} diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts new file mode 100644 index 000000000000..4ec71979db52 --- /dev/null +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 { ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposable, + IExperimentService, + IExtensions, + IInterpreterPathService, + Resource, +} 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 { ILanguageServerExtensionManager } from './types'; + +export class PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + serverManager: NodeLanguageServerManager; + + serverProxy: NodeLanguageServerProxy; + + clientFactory: NodeLanguageClientFactory; + + analysisOptions: NodeLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + experimentService: IExperimentService, + workspaceService: IWorkspaceService, + _configurationService: IConfigurationService, + interpreterPathService: IInterpreterPathService, + _interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + commandManager: ICommandManager, + fileSystem: IFileSystem, + extensions: IExtensions, + ) { + 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 { + // TODO + this.serverManager.disconnect(); + + console.warn(this.serverManager); + } + + canStartLanguageServer(): boolean { + // TODO + console.warn(this.serverManager); + return true; + } +} diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts new file mode 100644 index 000000000000..f035607835b3 --- /dev/null +++ b/src/client/languageServer/types.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { 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 { + startLanguageServer(languageServerType: LanguageServerType): Promise; +} + +/** + * Language server extension manager implementations act as a wrapper for anything related to their language server extension. + * They are responsible for starting and stopping the language server provided by their LS extension. + */ +export interface ILanguageServerExtensionManager { + startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; + stopLanguageServer(): void; + canStartLanguageServer(): boolean; + dispose(): void; +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts new file mode 100644 index 000000000000..1ab999b7a14b --- /dev/null +++ b/src/client/languageServer/watcher.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent } from 'vscode'; +import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; +import { ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExtensions, + IInterpreterPathService, + Resource, +} from '../common/types'; +import { LanguageService } 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 { JediLSExtensionManager } from './jediLSExtensionManager'; +import { NoneLSExtensionManager } from './noneLSExtensionManager'; +import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; +import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; + +@injectable() +export class LanguageServerWatcher implements IExtensionActivationService, ILanguageServerWatcher { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + + languageServerType: LanguageServerType; + + resource: Resource; + + 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(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(IDisposableRegistry) readonly disposables: IDisposableRegistry, + ) { + this.languageServerType = this.configurationService.getSettings().languageServer; + + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + + public async activate(resource: Resource): Promise { + this.resource = resource; + await this.startLanguageServer(this.languageServerType); + } + + async startLanguageServer(languageServerType: LanguageServerType): Promise { + const interpreter = await this.interpreterService?.getActiveInterpreter(this.resource); + + // Instantiate the language server extension manager. + this.languageServerExtensionManager = this.createLanguageServer(languageServerType); + + // Start the language server. + await this.languageServerExtensionManager.startLanguageServer(this.resource, interpreter); + + logStartup(languageServerType); + this.languageServerType = languageServerType; + } + + private stopLanguageServer(): void { + if (this.languageServerExtensionManager) { + this.languageServerExtensionManager.stopLanguageServer(); + this.languageServerExtensionManager.dispose(); + this.languageServerExtensionManager = undefined; + } + } + + 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, + ); + break; + case LanguageServerType.None: + default: + this.languageServerExtensionManager = new NoneLSExtensionManager(); + break; + } + + return this.languageServerExtensionManager; + } + + private async refreshLanguageServer(): Promise { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + this.stopLanguageServer(); + await this.startLanguageServer(languageServerType); + } + } + + // Watch for settings changes. + private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { + if (event.affectsConfiguration('python.languageServer')) { + await this.refreshLanguageServer(); + } + } +} + +function logStartup(languageServerType: LanguageServerType): void { + let outputLine; + switch (languageServerType) { + 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: ${languageServerType}`); + } + traceLog(outputLine); +} diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 34bcb3b51cce..8fb51272e3b3 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -79,7 +79,7 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { } test('Ensure services are registered: Node', async () => { - registerTypes(instance(serviceManager), LanguageServerType.Node); + registerTypes(instance(serviceManager)); verifyCommon(); @@ -104,7 +104,7 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { verify(serviceManager.add(ILanguageServerProxy, NodeLanguageServerProxy)).once(); }); test('Ensure services are registered: Jedi', async () => { - registerTypes(instance(serviceManager), LanguageServerType.Jedi); + registerTypes(instance(serviceManager)); verifyCommon(); From 41a9900e9f2accf2198ab40e5e0c91a17a8f7163 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 17 Mar 2022 13:45:57 -0700 Subject: [PATCH 03/30] Add safeguard when connecting/disconnecting --- src/client/activation/jedi/manager.ts | 18 ++++++++++-------- src/client/activation/node/manager.ts | 12 ++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index d74158df6138..81f129e9b41c 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -26,8 +26,6 @@ import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../ @injectable() export class JediLanguageServerManager implements ILanguageServerManager { - private languageServerProxy?: ILanguageServerProxy; - private resource!: Resource; private interpreter: PythonEnvironment | undefined; @@ -47,6 +45,8 @@ export class JediLanguageServerManager implements ILanguageServerManager { @inject(ILanguageServerAnalysisOptions) @named(LanguageServerType.Jedi) private readonly analysisOptions: ILanguageServerAnalysisOptions, + @inject(ILanguageServerProxy) + private readonly languageServerProxy: ILanguageServerProxy, @inject(ICommandManager) commandManager: ICommandManager, ) { if (JediLanguageServerManager.commandDispose) { @@ -107,13 +107,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 +143,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/manager.ts b/src/client/activation/node/manager.ts index 0f4d5ad9b862..1ab63581721c 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -85,13 +85,17 @@ export class NodeLanguageServerManager implements ILanguageServerManager { } public connect() { - this.connected = true; - this.middleware?.connect(); + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } } public disconnect() { - this.connected = false; - this.middleware?.disconnect(); + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } } @debounceSync(1000) From e78d55bd7a8c127b3ccf5c4f40bab0d1ff0f843c Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 17 Mar 2022 15:18:15 -0700 Subject: [PATCH 04/30] Proper Pylance LS disposal --- .../languageServer/pylanceLSExtensionManager.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index 4ec71979db52..826a0eec48f7 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -7,6 +7,7 @@ import { NodeLanguageServerProxy } from '../activation/node/languageServerProxy' import { NodeLanguageServerManager } from '../activation/node/manager'; import { ILanguageServerOutputChannel } from '../activation/types'; import { ICommandManager, IWorkspaceService } from '../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; import { IConfigurationService, @@ -42,7 +43,7 @@ export class PylanceLSExtensionManager implements IDisposable, ILanguageServerEx environmentService: IEnvironmentVariablesProvider, commandManager: ICommandManager, fileSystem: IFileSystem, - extensions: IExtensions, + private readonly extensions: IExtensions, ) { this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); @@ -76,15 +77,12 @@ export class PylanceLSExtensionManager implements IDisposable, ILanguageServerEx } stopLanguageServer(): void { - // TODO this.serverManager.disconnect(); - - console.warn(this.serverManager); + this.serverProxy.dispose(); } canStartLanguageServer(): boolean { - // TODO - console.warn(this.serverManager); - return true; + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + return !!extension; } } From b2a6d8b3b99b1a8093adc0bf3901dc300f8e9d59 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 17 Mar 2022 15:18:30 -0700 Subject: [PATCH 05/30] Fix Jedi LS startup/disposal --- src/client/activation/jedi/manager.ts | 3 --- src/client/languageServer/jediLSExtensionManager.ts | 10 ++++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index 81f129e9b41c..feee99749922 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -77,9 +77,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); diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts index cc13b21582b1..c99a176fb4e2 100644 --- a/src/client/languageServer/jediLSExtensionManager.ts +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -63,13 +63,15 @@ export class JediLSExtensionManager implements ILanguageServerExtensionManager { } stopLanguageServer(): void { - // TODO - console.warn(this.serverManager); + this.serverManager.disconnect(); + this.serverProxy.dispose(); } + // eslint-disable-next-line class-methods-use-this canStartLanguageServer(): boolean { - // TODO - console.warn(this.serverManager); + // Return true for now since it's shipped with the extension. + // Update this when JediLSP is pulled in a separate extension. + return true; } } From 44308c7a8166a448b86b632f646e6398c1e69f82 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Sat, 19 Mar 2022 14:43:33 -0700 Subject: [PATCH 06/30] Add cache support --- src/client/activation/serviceRegistry.ts | 23 +- src/client/activation/types.ts | 2 +- .../languageServer/jediLSExtensionManager.ts | 14 +- .../languageServerCapabilities.ts | 307 ++++++++++++++++++ .../languageServer/noneLSExtensionManager.ts | 97 +++++- .../pylanceLSExtensionManager.ts | 6 +- src/client/languageServer/types.ts | 12 +- src/client/languageServer/watcher.ts | 53 ++- 8 files changed, 493 insertions(+), 21 deletions(-) create mode 100644 src/client/languageServer/languageServerCapabilities.ts diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index d51879577a8e..7ff95882df07 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -3,7 +3,7 @@ import { IServiceManager } from '../ioc/types'; import { ExtensionActivationManager } from './activationManager'; -import { LanguageServerExtensionActivationService } from './activationService'; +// import { LanguageServerExtensionActivationService } from './activationService'; import { ExtensionSurveyPrompt } from './extensionSurvey'; // import { JediLanguageServerAnalysisOptions } from './jedi/analysisOptions'; // import { JediLanguageClientFactory } from './jedi/languageClientFactory'; @@ -15,19 +15,19 @@ import { LanguageServerOutputChannel } from './common/outputChannel'; // import { NodeLanguageClientFactory } from './node/languageClientFactory'; // import { NodeLanguageServerProxy } from './node/languageServerProxy'; // import { NodeLanguageServerManager } from './node/manager'; -import { NoLanguageServerExtensionActivator } from './none/activator'; +// import { NoLanguageServerExtensionActivator } from './none/activator'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, // ILanguageClientFactory, - ILanguageServerActivator, + // ILanguageServerActivator, // ILanguageServerAnalysisOptions, ILanguageServerCache, // ILanguageServerManager, ILanguageServerOutputChannel, // ILanguageServerProxy, - LanguageServerType, + // LanguageServerType, } from './types'; // import { JediLanguageServerActivator } from './jedi/activator'; import { LoadLanguageServerExtension } from './common/loadLanguageServerExtension'; @@ -37,14 +37,14 @@ import { LanguageServerWatcher } from '../languageServer/watcher'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); - serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); - serviceManager.addBinding(ILanguageServerCache, IExtensionActivationService); + // serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); + // serviceManager.addBinding(ILanguageServerCache, IExtensionActivationService); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); - serviceManager.add( - ILanguageServerActivator, - NoLanguageServerExtensionActivator, - LanguageServerType.None, - ); + // serviceManager.add( + // ILanguageServerActivator, + // NoLanguageServerExtensionActivator, + // LanguageServerType.None, + // ); serviceManager.addSingleton( ILanguageServerOutputChannel, LanguageServerOutputChannel, @@ -60,6 +60,7 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); + serviceManager.addBinding(ILanguageServerWatcher, ILanguageServerCache); // if (languageServerType === LanguageServerType.Node) { // serviceManager.add( diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 85995d4da558..e032e52a5345 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -78,7 +78,7 @@ export type ILanguageServerConnection = Pick< 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' >; -interface ILanguageServer +export interface ILanguageServer extends RenameProvider, DefinitionProvider, HoverProvider, diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts index c99a176fb4e2..dbc451873440 100644 --- a/src/client/languageServer/jediLSExtensionManager.ts +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -7,14 +7,22 @@ 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 } from '../common/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 implements ILanguageServerExtensionManager { +export class JediLSExtensionManager extends LanguageServerCapabilities + implements IDisposable, ILanguageServerExtensionManager { serverManager: JediLanguageServerManager; serverProxy: JediLanguageServerProxy; @@ -34,6 +42,8 @@ export class JediLSExtensionManager implements ILanguageServerExtensionManager { environmentService: IEnvironmentVariablesProvider, commandManager: ICommandManager, ) { + super(); + this.analysisOptions = new JediLanguageServerAnalysisOptions( environmentService, outputChannel, diff --git a/src/client/languageServer/languageServerCapabilities.ts b/src/client/languageServer/languageServerCapabilities.ts new file mode 100644 index 000000000000..e5fc1e3f276e --- /dev/null +++ b/src/client/languageServer/languageServerCapabilities.ts @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + WorkspaceEdit, +} from 'vscode'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; +import { ILanguageServer, ILanguageServerConnection, ILanguageServerProxy } from '../activation/types'; +import { ILanguageServerCapabilities } from './types'; + +/* + * The Language Server Capabilities class implements the ILanguageServer interface to provide support for the existing Jupyter integration. + */ +export class LanguageServerCapabilities implements ILanguageServerCapabilities { + serverProxy: ILanguageServerProxy | undefined; + + public dispose(): void { + // Nothing to do here. + } + + get(): Promise { + return Promise.resolve(this); + } + + public get connection(): ILanguageServerConnection | undefined { + const languageClient = this.getLanguageClient(); + if (languageClient) { + // Return an object that looks like a connection + return { + sendNotification: languageClient.sendNotification.bind(languageClient), + sendRequest: languageClient.sendRequest.bind(languageClient), + sendProgress: languageClient.sendProgress.bind(languageClient), + onRequest: languageClient.onRequest.bind(languageClient), + onNotification: languageClient.onNotification.bind(languageClient), + onProgress: languageClient.onProgress.bind(languageClient), + }; + } + + return undefined; + } + + public get capabilities(): vscodeLanguageClient.ServerCapabilities | undefined { + const languageClient = this.getLanguageClient(); + if (languageClient) { + return languageClient.initializeResult?.capabilities; + } + + return undefined; + } + + public provideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + token: CancellationToken, + ): ProviderResult { + return this.handleProvideRenameEdits(document, position, newName, token); + } + + public provideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken, + ): ProviderResult { + return this.handleProvideDefinition(document, position, token); + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + return this.handleProvideHover(document, position, token); + } + + public provideReferences( + document: TextDocument, + position: Position, + context: ReferenceContext, + token: CancellationToken, + ): ProviderResult { + return this.handleProvideReferences(document, position, context, token); + } + + public provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken, + context: CompletionContext, + ): ProviderResult { + return this.handleProvideCompletionItems(document, position, token, context); + } + + public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { + return this.handleProvideCodeLenses(document, token); + } + + public provideDocumentSymbols( + document: TextDocument, + token: CancellationToken, + ): ProviderResult { + return this.handleProvideDocumentSymbols(document, token); + } + + public provideSignatureHelp( + document: TextDocument, + position: Position, + token: CancellationToken, + context: SignatureHelpContext, + ): ProviderResult { + return this.handleProvideSignatureHelp(document, position, token, context); + } + + protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { + return this.serverProxy?.languageClient; + } + + private async handleProvideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.RenameParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + newName, + }; + const result = await languageClient.sendRequest(vscodeLanguageClient.RenameRequest.type, args, token); + if (result) { + return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); + } + } + + return undefined; + } + + private async handleProvideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + }; + const result = await languageClient.sendRequest(vscodeLanguageClient.DefinitionRequest.type, args, token); + if (result) { + return languageClient.protocol2CodeConverter.asDefinitionResult(result); + } + } + + return undefined; + } + + private async handleProvideHover( + document: TextDocument, + position: Position, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + }; + const result = await languageClient.sendRequest(vscodeLanguageClient.HoverRequest.type, args, token); + if (result) { + return languageClient.protocol2CodeConverter.asHover(result); + } + } + + return undefined; + } + + private async handleProvideReferences( + document: TextDocument, + position: Position, + context: ReferenceContext, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.ReferenceParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + context, + }; + const result = await languageClient.sendRequest(vscodeLanguageClient.ReferencesRequest.type, args, token); + if (result) { + // Remove undefined part. + return result.map((l) => { + const r = languageClient!.protocol2CodeConverter.asLocation(l); + return r!; + }); + } + } + + return undefined; + } + + private async handleProvideCodeLenses( + document: TextDocument, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.CodeLensParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + }; + const result = await languageClient.sendRequest(vscodeLanguageClient.CodeLensRequest.type, args, token); + if (result) { + return languageClient.protocol2CodeConverter.asCodeLenses(result); + } + } + + return undefined; + } + + private async handleProvideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken, + context: CompletionContext, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args = languageClient.code2ProtocolConverter.asCompletionParams(document, position, context); + const result = await languageClient.sendRequest(vscodeLanguageClient.CompletionRequest.type, args, token); + if (result) { + return languageClient.protocol2CodeConverter.asCompletionResult(result); + } + } + + return undefined; + } + + private async handleProvideDocumentSymbols( + document: TextDocument, + token: CancellationToken, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.DocumentSymbolParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.DocumentSymbolRequest.type, + args, + token, + ); + if (result && result.length) { + if ((result[0] as DocumentSymbol).range) { + // Document symbols + const docSymbols = result as vscodeLanguageClient.DocumentSymbol[]; + return languageClient.protocol2CodeConverter.asDocumentSymbols(docSymbols); + } + // Document symbols + const symbols = result as vscodeLanguageClient.SymbolInformation[]; + return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); + } + } + + return undefined; + } + + private async handleProvideSignatureHelp( + document: TextDocument, + position: Position, + token: CancellationToken, + _context: SignatureHelpContext, + ): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.SignatureHelpRequest.type, + args, + token, + ); + if (result) { + return languageClient.protocol2CodeConverter.asSignatureHelp(result); + } + } + + return undefined; + } +} diff --git a/src/client/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts index 38d3a790ab75..7c4411e89e59 100644 --- a/src/client/languageServer/noneLSExtensionManager.ts +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -3,13 +3,45 @@ /* eslint-disable class-methods-use-this */ +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + WorkspaceEdit, +} from 'vscode'; +import { ILanguageServer, ILanguageServerProxy } from '../activation/types'; import { ILanguageServerExtensionManager } from './types'; -export class NoneLSExtensionManager implements ILanguageServerExtensionManager { +// 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; + } + dispose(): void { // Nothing to do here. } + get(): Promise { + return Promise.resolve(this); + } + startLanguageServer(): Promise { return Promise.resolve(); } @@ -21,4 +53,67 @@ export class NoneLSExtensionManager implements ILanguageServerExtensionManager { canStartLanguageServer(): boolean { return true; } + + public provideRenameEdits( + _document: TextDocument, + _position: Position, + _newName: string, + _token: CancellationToken, + ): ProviderResult { + return null; + } + + public provideDefinition( + _document: TextDocument, + _position: Position, + _token: CancellationToken, + ): ProviderResult { + return null; + } + + public provideHover( + _document: TextDocument, + _position: Position, + _token: CancellationToken, + ): ProviderResult { + return null; + } + + public provideReferences( + _document: TextDocument, + _position: Position, + _context: ReferenceContext, + _token: CancellationToken, + ): ProviderResult { + return null; + } + + public provideCompletionItems( + _document: TextDocument, + _position: Position, + _token: CancellationToken, + _context: CompletionContext, + ): 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, + _token: CancellationToken, + _context: SignatureHelpContext, + ): ProviderResult { + return null; + } } diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index 826a0eec48f7..a2f5b98e2c09 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -21,9 +21,11 @@ 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 PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { +export class PylanceLSExtensionManager extends LanguageServerCapabilities + implements IDisposable, ILanguageServerExtensionManager { serverManager: NodeLanguageServerManager; serverProxy: NodeLanguageServerProxy; @@ -45,6 +47,8 @@ export class PylanceLSExtensionManager implements IDisposable, ILanguageServerEx fileSystem: IFileSystem, private readonly extensions: IExtensions, ) { + super(); + this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); this.serverProxy = new NodeLanguageServerProxy( diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index f035607835b3..175c1c8c01fb 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { LanguageServerType } from '../activation/types'; +import { ILanguageServer, ILanguageServerProxy, LanguageServerType } from '../activation/types'; import { Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; @@ -14,11 +14,19 @@ export interface ILanguageServerWatcher { startLanguageServer(languageServerType: LanguageServerType): Promise; } +export interface ILanguageServerCapabilities extends ILanguageServer { + serverProxy: ILanguageServerProxy | undefined; + + get(): Promise; +} + /** * Language server extension manager implementations act as a wrapper for anything related to their 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 keep providing support to the Jupyter integration. */ -export interface ILanguageServerExtensionManager { +export interface ILanguageServerExtensionManager extends ILanguageServerCapabilities { startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; stopLanguageServer(): void; canStartLanguageServer(): boolean; diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 1ab999b7a14b..4bce1eaa0342 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -3,7 +3,13 @@ import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent } from 'vscode'; -import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; +import { + IExtensionActivationService, + ILanguageServer, + ILanguageServerCache, + ILanguageServerOutputChannel, + LanguageServerType, +} from '../activation/types'; import { ICommandManager, IWorkspaceService } from '../common/application/types'; import { IFileSystem } from '../common/platform/types'; import { @@ -19,13 +25,20 @@ 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 { JediLSExtensionManager } from './jediLSExtensionManager'; import { NoneLSExtensionManager } from './noneLSExtensionManager'; import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; @injectable() -export class LanguageServerWatcher implements IExtensionActivationService, ILanguageServerWatcher { +/** + * 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; @@ -34,6 +47,8 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang resource: Resource; + interpreter: PythonEnvironment | undefined; + constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, @@ -51,16 +66,29 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang this.languageServerType = this.configurationService.getSettings().languageServer; disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + + if (this.workspaceService.isTrusted) { + disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); + } } + // IExtensionActivationService + public async activate(resource: Resource): Promise { this.resource = resource; await this.startLanguageServer(this.languageServerType); } - async startLanguageServer(languageServerType: LanguageServerType): Promise { + // ILanguageServerWatcher; + + public async startLanguageServer(languageServerType: LanguageServerType): Promise { const interpreter = await this.interpreterService?.getActiveInterpreter(this.resource); + // Destroy the old language server if it's different. + if (interpreter !== this.interpreter) { + this.stopLanguageServer(); + } + // Instantiate the language server extension manager. this.languageServerExtensionManager = this.createLanguageServer(languageServerType); @@ -69,8 +97,21 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang logStartup(languageServerType); this.languageServerType = languageServerType; + this.interpreter = interpreter; } + // ILanguageServerCache + + public async get(): Promise { + if (!this.languageServerExtensionManager) { + this.startLanguageServer(this.languageServerType); + } + + return Promise.resolve(this.languageServerExtensionManager!.get()); + } + + // Private methods + private stopLanguageServer(): void { if (this.languageServerExtensionManager) { this.languageServerExtensionManager.stopLanguageServer(); @@ -133,6 +174,12 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang await this.refreshLanguageServer(); } } + + // Watch for interpreter changes. + private async onDidChangeInterpreter() { + // Reactivate the resource. It should destroy the old one if it's different. + return this.activate(this.resource); + } } function logStartup(languageServerType: LanguageServerType): void { From 4811fa84256dcc4216dfc7a8b0f9bc68e1cb750b Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Sat, 19 Mar 2022 14:52:06 -0700 Subject: [PATCH 07/30] Remove DI decorators + registry activation --- src/client/activation/jedi/analysisOptions.ts | 11 ++-- .../activation/jedi/languageClientFactory.ts | 4 +- .../activation/jedi/languageServerProxy.ts | 7 +-- src/client/activation/jedi/manager.ts | 11 +--- src/client/activation/node/analysisOptions.ts | 8 +-- .../activation/node/languageClientFactory.ts | 7 +-- .../activation/node/languageServerProxy.ts | 14 ++--- src/client/activation/node/manager.ts | 12 +--- src/client/activation/serviceRegistry.ts | 57 ------------------- 9 files changed, 24 insertions(+), 107 deletions(-) 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..2c2ccc96aa45 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, 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 feee99749922..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,7 +23,6 @@ import { } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging'; -@injectable() export class JediLanguageServerManager implements ILanguageServerManager { private resource!: Resource; @@ -41,13 +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(ILanguageServerProxy) private readonly languageServerProxy: ILanguageServerProxy, - @inject(ICommandManager) commandManager: ICommandManager, + commandManager: ICommandManager, ) { if (JediLanguageServerManager.commandDispose) { JediLanguageServerManager.commandDispose.dispose(); diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 077665b7b2c0..fd031fd79037 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,17 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; + 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, - ) { + constructor(lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService) { super(lsOutputChannel, workspace); } diff --git a/src/client/activation/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index da131ceccec6..003e79505237 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/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, TransportKind } from 'vscode-languageclient/node'; @@ -14,12 +13,8 @@ import { ILanguageClientFactory } from '../types'; const languageClientName = 'Python Tools'; -@injectable() export class NodeLanguageClientFactory implements ILanguageClientFactory { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IExtensions) private readonly extensions: IExtensions, - ) {} + constructor(private readonly fs: IFileSystem, private readonly extensions: IExtensions) {} public async createLanguageClient( _resource: Resource, diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index ea061b19e0b2..ac261772e681 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, @@ -49,7 +48,6 @@ namespace GetExperimentValue { } } -@injectable() export class NodeLanguageServerProxy implements ILanguageServerProxy { public languageClient: LanguageClient | undefined; private startupCompleted: Deferred; @@ -59,12 +57,12 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { private lsVersion: string | undefined; constructor( - @inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, - @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IExtensions) private readonly extensions: IExtensions, + 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(); } diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index 1ab63581721c..c0e15f89a3e0 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable, named } from 'inversify'; - import { ICommandManager } from '../../common/application/types'; import { IDisposable, IExtensions, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; @@ -22,7 +20,6 @@ import { import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -@injectable() export class NodeLanguageServerManager implements ILanguageServerManager { private resource!: Resource; private interpreter: PythonEnvironment | undefined; @@ -33,14 +30,11 @@ export class NodeLanguageServerManager implements ILanguageServerManager { private started: boolean = false; constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) - @named(LanguageServerType.Node) + private readonly serviceContainer: IServiceContainer, private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerProxy) private readonly languageServerProxy: ILanguageServerProxy, - @inject(ICommandManager) commandManager: ICommandManager, - @inject(IExtensions) private readonly extensions: IExtensions, + commandManager: ICommandManager, + private readonly extensions: IExtensions, ) { this.disposables.push( commandManager.registerCommand(Commands.RestartLS, () => { diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 7ff95882df07..53042df2c4de 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -3,33 +3,15 @@ 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 { NodeLanguageServerProxy } from './node/languageServerProxy'; -// import { NodeLanguageServerManager } from './node/manager'; -// import { NoLanguageServerExtensionActivator } from './none/activator'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, - // ILanguageClientFactory, - // ILanguageServerActivator, - // ILanguageServerAnalysisOptions, ILanguageServerCache, - // 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'; @@ -37,14 +19,7 @@ import { LanguageServerWatcher } from '../languageServer/watcher'; 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, @@ -61,36 +36,4 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); serviceManager.addBinding(ILanguageServerWatcher, ILanguageServerCache); - - // 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); - // } 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); - // } } From df4682f66fae73ce47445ffb9736cde787959db1 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Fri, 25 Mar 2022 10:53:57 -0700 Subject: [PATCH 08/30] Do not reload window when Pylance not installed --- .../common/languageServerChangeHandler.ts | 42 ++++++++-------- .../languageServer/jediLSExtensionManager.ts | 7 +++ .../languageServer/noneLSExtensionManager.ts | 5 ++ .../pylanceLSExtensionManager.ts | 22 +++++++-- src/client/languageServer/types.ts | 1 + src/client/languageServer/watcher.ts | 48 ++++++++++++++++--- 6 files changed, 93 insertions(+), 32 deletions(-) diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts index 99b59e3eba32..567403a23e51 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,7 @@ export async function promptForPylanceInstall( if (target) { await configService.updateSetting('languageServer', LanguageServerType.Jedi, undefined, target); - commandManager.executeCommand('workbench.action.reloadWindow'); + // commandManager.executeCommand('workbench.action.reloadWindow'); } } } @@ -85,21 +85,22 @@ 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; + // 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'); - } } + // else { + // response = await this.appShell.showInformationMessage( + // LanguageService.reloadAfterLanguageServerChange(), + // Common.reload(), + // ); + // if (response === Common.reload()) { + // this.commands.executeCommand('workbench.action.reloadWindow'); + // } + // } this.currentLsType = lsType; } @@ -109,19 +110,18 @@ export class LanguageServerChangeHandler implements Disposable { this.pylanceInstalled = this.isPylanceInstalled(); if (oldInstallState === this.pylanceInstalled) { this.pylanceInstallCompletedDeferred.resolve(); - return; } - const response = await this.appShell.showWarningMessage( - Pylance.pylanceInstalledReloadPromptMessage(), - Common.bannerLabelYes(), - Common.bannerLabelNo(), - ); + // 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'); - } + // this.pylanceInstallCompletedDeferred.resolve(); + // if (response === Common.bannerLabelYes()) { + // this.commands.executeCommand('workbench.action.reloadWindow'); + // } } private isPylanceInstalled(): boolean { diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts index dbc451873440..416f64561519 100644 --- a/src/client/languageServer/jediLSExtensionManager.ts +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -84,4 +84,11 @@ export class JediLSExtensionManager extends LanguageServerCapabilities 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/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts index 7c4411e89e59..397fc728cdba 100644 --- a/src/client/languageServer/noneLSExtensionManager.ts +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -54,6 +54,11 @@ export class NoneLSExtensionManager implements ILanguageServer, ILanguageServerE return true; } + languageServerNotAvailable(): Promise { + // Nothing to do here. + return Promise.resolve(); + } + public provideRenameEdits( _document: TextDocument, _position: Position, diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index a2f5b98e2c09..4376e14d4247 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -1,12 +1,13 @@ // 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 { ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { PYLANCE_EXTENSION_ID } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; import { @@ -17,6 +18,7 @@ import { 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'; @@ -38,14 +40,15 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities serviceContainer: IServiceContainer, outputChannel: ILanguageServerOutputChannel, experimentService: IExperimentService, - workspaceService: IWorkspaceService, - _configurationService: IConfigurationService, + readonly workspaceService: IWorkspaceService, + readonly configurationService: IConfigurationService, interpreterPathService: IInterpreterPathService, _interpreterService: IInterpreterService, environmentService: IEnvironmentVariablesProvider, - commandManager: ICommandManager, + readonly commandManager: ICommandManager, fileSystem: IFileSystem, private readonly extensions: IExtensions, + readonly applicationShell: IApplicationShell, ) { super(); @@ -89,4 +92,15 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); return !!extension; } + + async languageServerNotAvailable(): Promise { + await promptForPylanceInstall( + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ); + + throw new Error(Pylance.pylanceNotInstalledMessage()); + } } diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index 175c1c8c01fb..c64f4386cac2 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -30,5 +30,6 @@ export interface ILanguageServerExtensionManager extends ILanguageServerCapabili 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 index 4bce1eaa0342..b7587f6562e1 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent } from 'vscode'; +import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; import { IExtensionActivationService, ILanguageServer, @@ -10,7 +11,7 @@ import { ILanguageServerOutputChannel, LanguageServerType, } from '../activation/types'; -import { ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { IFileSystem } from '../common/platform/types'; import { IConfigurationService, @@ -49,6 +50,8 @@ export class LanguageServerWatcher interpreter: PythonEnvironment | undefined; + languageServerChangeHandler: LanguageServerChangeHandler; + constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, @@ -61,6 +64,7 @@ export class LanguageServerWatcher @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.languageServerType = this.configurationService.getSettings().languageServer; @@ -70,6 +74,22 @@ export class LanguageServerWatcher if (this.workspaceService.isTrusted) { disposables.push(this.interpreterService.onDidChangeInterpreter(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 @@ -92,12 +112,16 @@ export class LanguageServerWatcher // Instantiate the language server extension manager. this.languageServerExtensionManager = this.createLanguageServer(languageServerType); - // Start the language server. - await this.languageServerExtensionManager.startLanguageServer(this.resource, interpreter); + if (this.languageServerExtensionManager.canStartLanguageServer()) { + // Start the language server. + await this.languageServerExtensionManager.startLanguageServer(this.resource, interpreter); - logStartup(languageServerType); - this.languageServerType = languageServerType; - this.interpreter = interpreter; + logStartup(languageServerType); + this.languageServerType = languageServerType; + this.interpreter = interpreter; + } else { + await this.languageServerExtensionManager.languageServerNotAvailable(); + } } // ILanguageServerCache @@ -148,6 +172,7 @@ export class LanguageServerWatcher this.commandManager, this.fileSystem, this.extensions, + this.applicationShell, ); break; case LanguageServerType.None: @@ -176,10 +201,19 @@ export class LanguageServerWatcher } // Watch for interpreter changes. - private async onDidChangeInterpreter() { + private async onDidChangeInterpreter(): Promise { // Reactivate the resource. It should destroy the old one if it's different. return this.activate(this.resource); } + + // Watch for extension changes. + private async extensionsChangeHandler(): Promise { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + await this.refreshLanguageServer(); + } + } } function logStartup(languageServerType: LanguageServerType): void { From dbff16ef660af45fc783a81266e04d556a672533 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Tue, 5 Apr 2022 16:18:00 -0700 Subject: [PATCH 09/30] Comment/remove unused telemetry --- src/client/activation/activationService.ts | 19 ++++++----- src/client/telemetry/constants.ts | 3 -- src/client/telemetry/index.ts | 39 ---------------------- 3 files changed, 10 insertions(+), 51 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index bd771ea06a00..b60d10007a7e 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -19,8 +19,8 @@ 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 { sendTelemetryEvent } from '../telemetry'; +// import { EventName } from '../telemetry/constants'; // import { LanguageServerChangeHandler } from './common/languageServerChangeHandler'; import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { @@ -61,7 +61,8 @@ function logStartup(serverType: LanguageServerType): void { @injectable() export class LanguageServerExtensionActivationService - implements IExtensionActivationService, ILanguageServerCache, Disposable { + implements IExtensionActivationService, ILanguageServerCache, Disposable +{ private cache = new Map>(); private activatedServer?: IActivatedServer; @@ -174,13 +175,13 @@ export class LanguageServerExtensionActivationService } if (state.value !== languageServer) { await state.updateValue(languageServer); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - switchTo: languageServer, - }); + // sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { + // switchTo: languageServer, + // }); } else { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - lsStartup: languageServer, - }); + // sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { + // lsStartup: languageServer, + // }); } } 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. */ From b8e10ca6a8334ca05b9d786b604f94ad7eb37d4e Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Wed, 6 Apr 2022 11:53:20 -0700 Subject: [PATCH 10/30] languageServerCapabilities.unit.test.ts --- .../languageServerCapabilities.unit.test.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/test/languageServer/languageServerCapabilities.unit.test.ts diff --git a/src/test/languageServer/languageServerCapabilities.unit.test.ts b/src/test/languageServer/languageServerCapabilities.unit.test.ts new file mode 100644 index 000000000000..76ff7009df64 --- /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); + }); +}); From 069ef45b22a82c1ebeda5af8a8cfcb8f1eefdff2 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Wed, 6 Apr 2022 19:56:25 -0700 Subject: [PATCH 11/30] jedi/pylance/none extension managers --- .../jediLSExtensionManager.unit.test.ts | 45 +++++++++ .../noneLSExtensionManager.unit.test.ts | 23 +++++ .../pylanceLSExtensionManager.unit.test.ts | 94 +++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/test/languageServer/jediLSExtensionManager.unit.test.ts create mode 100644 src/test/languageServer/noneLSExtensionManager.unit.test.ts create mode 100644 src/test/languageServer/pylanceLSExtensionManager.unit.test.ts diff --git a/src/test/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts new file mode 100644 index 000000000000..45403635b751 --- /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/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..b0174e1cf6a3 --- /dev/null +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -0,0 +1,94 @@ +// 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, + {} 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, + {} 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, true); + }); +}); From fcbb2c5edc4d34d2d0a055746f36ca06f71b03fe Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 12:05:46 -0700 Subject: [PATCH 12/30] languageServer/watcher.unit.test.ts --- src/client/languageServer/types.ts | 2 + src/client/languageServer/watcher.ts | 9 +- src/test/languageServer/watcher.unit.test.ts | 402 +++++++++++++++++++ 3 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 src/test/languageServer/watcher.unit.test.ts diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index c64f4386cac2..2a47bb0a1e3d 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -11,6 +11,8 @@ export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); * and instantiates the relevant language server extension manager. */ export interface ILanguageServerWatcher { + readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + readonly languageServerType: LanguageServerType; startLanguageServer(languageServerType: LanguageServerType): Promise; } diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index b7587f6562e1..d1d6e09b6b56 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -39,18 +39,19 @@ import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types * It also implements the ILanguageServerCache interface needed by our Jupyter support. */ export class LanguageServerWatcher - implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache { + implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache +{ public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; languageServerExtensionManager: ILanguageServerExtensionManager | undefined; languageServerType: LanguageServerType; - resource: Resource; + private resource: Resource; - interpreter: PythonEnvironment | undefined; + private interpreter: PythonEnvironment | undefined; - languageServerChangeHandler: LanguageServerChangeHandler; + private languageServerChangeHandler: LanguageServerChangeHandler; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts new file mode 100644 index 000000000000..b409c3e751e3 --- /dev/null +++ b/src/test/languageServer/watcher.unit.test.ts @@ -0,0 +1,402 @@ +// 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 } 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 { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { NoneLSExtensionManager } from '../../client/languageServer/noneLSExtensionManager'; +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, + {} as IInterpreterPathService, + { + getActiveInterpreter: () => 'python', + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown as IInterpreterService, + {} as IEnvironmentVariablesProvider, + { + 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 onDidChangeInterpreter 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 IInterpreterPathService, + { + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown as IInterpreterService, + {} as IEnvironmentVariablesProvider, + { + isTrusted: true, + 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 onDidChangeInterpreter 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 IInterpreterPathService, + { + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown as IInterpreterService, + {} as IEnvironmentVariablesProvider, + { + isTrusted: false, + 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 = { + onDidChangeInterpreter: () => { + /* do nothing */ + }, + 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, + {} as IInterpreterPathService, + interpreterService, + { + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown as IEnvironmentVariablesProvider, + { + isTrusted: true, + getWorkspaceFolder: () => undefined, + 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, + {} as IInterpreterPathService, + { + getActiveInterpreter: () => 'python', + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown as IInterpreterService, + {} as IEnvironmentVariablesProvider, + { + 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()); + }); + + 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 watched should not restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); + + const workspaceService = { + 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, + {} as IInterpreterPathService, + { + getActiveInterpreter: () => 'python', + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } 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 watched should restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); + + const workspaceService = { + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { + onDidChangeConfigListener = listener; + }, + } 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, + {} as IInterpreterPathService, + { + getActiveInterpreter: () => 'python', + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } 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); + }); +}); From 5bbfa63152a9dbab537621973e0350824352d799 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 18:53:53 -0700 Subject: [PATCH 13/30] Fix pylanceLS manager + watcher tests --- .../pylanceLSExtensionManager.unit.test.ts | 26 ++-- src/test/languageServer/watcher.unit.test.ts | 120 +++++++++--------- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts index b0174e1cf6a3..418aa7812795 100644 --- a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -29,7 +29,11 @@ suite('Language Server - Pylance LS extension manager', () => { {} as IInterpreterPathService, {} as IInterpreterService, {} as IEnvironmentVariablesProvider, - {} as ICommandManager, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, {} as IFileSystem, {} as IExtensions, {} as IApplicationShell, @@ -52,11 +56,15 @@ suite('Language Server - Pylance LS extension manager', () => { {} as IInterpreterPathService, {} as IInterpreterService, {} as IEnvironmentVariablesProvider, - {} as ICommandManager, + ({ + registerCommand: () => { + /** do nothing */ + }, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => ({}), - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, ); @@ -75,20 +83,20 @@ suite('Language Server - Pylance LS extension manager', () => { {} as IInterpreterPathService, {} as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, ); const result = manager.canStartLanguageServer(); - assert.strictEqual(result, true); + assert.strictEqual(result, false); }); }); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index b409c3e751e3..7faa91059ad3 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -34,36 +34,38 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ getActiveInterpreter: () => 'python', onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown as IWorkspaceService, - { + } as unknown) as IWorkspaceService, + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, [] as Disposable[], ); }); - teardown(() => sandbox.restore()); + teardown(() => { + sandbox.restore(); + }); test('The constructor should add a listener to onDidChangeInterpreter to the list of disposables if it is a trusted workspace', () => { const disposables: Disposable[] = []; @@ -76,26 +78,26 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ isTrusted: true, onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown as IWorkspaceService, + } as unknown) as IWorkspaceService, {} as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, disposables, ); @@ -114,26 +116,26 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ isTrusted: false, onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown as IWorkspaceService, + } as unknown) as IWorkspaceService, {} as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, disposables, ); @@ -154,19 +156,19 @@ suite('Language server watcher', () => { getActiveInterpreterStub.onFirstCall().returns('python'); getActiveInterpreterStub.onSecondCall().returns('other/python'); - const interpreterService = { + const interpreterService = ({ onDidChangeInterpreter: () => { /* do nothing */ }, getActiveInterpreter: getActiveInterpreterStub, - } as unknown as IInterpreterService; + } as unknown) as IInterpreterService; watcher = new LanguageServerWatcher( - { + ({ get: () => { /* do nothing */ }, - } as unknown as IServiceContainer, + } as unknown) as IServiceContainer, {} as ILanguageServerOutputChannel, { getSettings: () => ({ languageServer: LanguageServerType.None }), @@ -174,30 +176,30 @@ suite('Language server watcher', () => { {} as IExperimentService, {} as IInterpreterPathService, interpreterService, - { + ({ onDidEnvironmentVariablesChange: () => { /* do nothing */ }, - } as unknown as IEnvironmentVariablesProvider, - { + } as unknown) as IEnvironmentVariablesProvider, + ({ isTrusted: true, getWorkspaceFolder: () => undefined, onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown as IWorkspaceService, - { + } as unknown) as IWorkspaceService, + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, [] as Disposable[], ); @@ -237,30 +239,30 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ getActiveInterpreter: () => 'python', onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ onDidChangeConfiguration: () => { /* do nothing */ }, - } as unknown as IWorkspaceService, - { + } as unknown) as IWorkspaceService, + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, [] as Disposable[], ); @@ -294,11 +296,11 @@ suite('Language server watcher', () => { test('When the config settings change, but the python.languageServer setting is not affected, the watched should not restart the language server', async () => { let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); - const workspaceService = { + const workspaceService = ({ onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { onDidChangeConfigListener = listener; }, - } as unknown as IWorkspaceService; + } as unknown) as IWorkspaceService; watcher = new LanguageServerWatcher( {} as IServiceContainer, @@ -308,26 +310,26 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ getActiveInterpreter: () => 'python', onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, workspaceService, - { + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, [] as Disposable[], ); @@ -345,19 +347,19 @@ suite('Language server watcher', () => { test('When the config settings change, and the python.languageServer setting is affected, the watched should restart the language server', async () => { let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise = () => Promise.resolve(); - const workspaceService = { + const workspaceService = ({ onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { onDidChangeConfigListener = listener; }, - } as unknown as IWorkspaceService; + } as unknown) as IWorkspaceService; const getSettingsStub = sandbox.stub(); getSettingsStub.onFirstCall().returns({ languageServer: LanguageServerType.None }); getSettingsStub.onSecondCall().returns({ languageServer: LanguageServerType.Node }); - const configService = { + const configService = ({ getSettings: getSettingsStub, - } as unknown as IConfigurationService; + } as unknown) as IConfigurationService; watcher = new LanguageServerWatcher( {} as IServiceContainer, @@ -365,26 +367,26 @@ suite('Language server watcher', () => { configService, {} as IExperimentService, {} as IInterpreterPathService, - { + ({ getActiveInterpreter: () => 'python', onDidChangeInterpreter: () => { /* do nothing */ }, - } as unknown as IInterpreterService, + } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, workspaceService, - { + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, {} as IFileSystem, - { + ({ getExtension: () => undefined, onDidChange: () => { /* do nothing */ }, - } as unknown as IExtensions, + } as unknown) as IExtensions, {} as IApplicationShell, [] as Disposable[], ); From c021d7566c2c2dd35079ec43bfcbfe9b145c27c2 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:02:54 -0700 Subject: [PATCH 14/30] Fix existing tests --- src/client/activation/activationService.ts | 321 ------- src/client/activation/node/activator.ts | 81 -- .../activation/activationManager.unit.test.ts | 140 +-- .../activation/activationService.unit.test.ts | 874 ------------------ .../activation/node/activator.unit.test.ts | 148 --- .../languageServerChangeHandler.unit.test.ts | 73 +- .../activation/serviceRegistry.unit.test.ts | 91 +- 7 files changed, 10 insertions(+), 1718 deletions(-) delete mode 100644 src/client/activation/activationService.ts delete mode 100644 src/client/activation/node/activator.ts delete mode 100644 src/test/activation/activationService.unit.test.ts delete mode 100644 src/test/activation/node/activator.unit.test.ts diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts deleted file mode 100644 index b60d10007a7e..000000000000 --- a/src/client/activation/activationService.ts +++ /dev/null @@ -1,321 +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 { IWorkspaceService } from '../common/application/types'; -// 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; - // nothing to see here. - } - // 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/node/activator.ts b/src/client/activation/node/activator.ts deleted file mode 100644 index a879e7ca0be2..000000000000 --- a/src/client/activation/node/activator.ts +++ /dev/null @@ -1,81 +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 { - 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/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 37c93268d702..000000000000 --- a/src/test/activation/activationService.unit.test.ts +++ /dev/null @@ -1,874 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { expect } from 'chai'; -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 { - IExtensionActivationService, - ILanguageServerActivator, - 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 extensionsMock = TypeMoq.Mock.ofType(); - 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); - 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(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 extensionsMock = TypeMoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - 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(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 extensionsMock = TypeMoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - 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(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/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 8fb51272e3b3..1d3f4f383082 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -3,36 +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, - 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 { 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; @@ -41,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(); @@ -69,65 +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)); - - 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(); - }); - test('Ensure services are registered: Jedi', async () => { - registerTypes(instance(serviceManager)); - - 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(); }); }); From 3a0746029a3f93d08f950554b0eb2e24ebe8ab78 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:10:25 -0700 Subject: [PATCH 15/30] News entry --- news/1 Enhancements/18509.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1 Enhancements/18509.md 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. From ab634c0773b837196a29b51a571ad62a11dc18f3 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:29:51 -0700 Subject: [PATCH 16/30] Cleanup (comments + unused files) + linting --- .eslintignore | 11 - src/client/activation/common/activatorBase.ts | 327 ------------------ .../common/languageServerChangeHandler.ts | 25 +- src/client/activation/jedi/activator.ts | 36 -- .../activation/jedi/languageClientFactory.ts | 9 +- src/client/activation/node/analysisOptions.ts | 9 +- .../activation/node/languageClientFactory.ts | 8 +- .../activation/node/languageServerProxy.ts | 25 +- src/client/activation/node/manager.ts | 25 +- src/client/extensionActivation.ts | 1 - 10 files changed, 42 insertions(+), 434 deletions(-) delete mode 100644 src/client/activation/common/activatorBase.ts delete mode 100644 src/client/activation/jedi/activator.ts diff --git a/.eslintignore b/.eslintignore index 46cb396c7f7b..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,17 +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/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/src/client/activation/common/activatorBase.ts b/src/client/activation/common/activatorBase.ts deleted file mode 100644 index 68f7d0effbc8..000000000000 --- a/src/client/activation/common/activatorBase.ts +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit, -} from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient/node'; - -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} - */ -@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); - } - - public dispose(): void { - // this.manager.dispose(); - } - - public abstract ensureLanguageServerIsAvailable(resource: Resource): Promise; - - public activate(): void { - // this.manager.connect(); - } - - public deactivate(): void { - // this.manager.disconnect(); - } - - public get connection() { - const languageClient = this.getLanguageClient(); - if (languageClient) { - // Return an object that looks like a connection - return { - sendNotification: languageClient.sendNotification.bind(languageClient), - sendRequest: languageClient.sendRequest.bind(languageClient), - sendProgress: languageClient.sendProgress.bind(languageClient), - onRequest: languageClient.onRequest.bind(languageClient), - onNotification: languageClient.onNotification.bind(languageClient), - onProgress: languageClient.onProgress.bind(languageClient), - }; - } - } - - public get capabilities() { - const languageClient = this.getLanguageClient(); - if (languageClient) { - return languageClient.initializeResult?.capabilities; - } - } - - public provideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideRenameEdits(document, position, newName, token); - } - - public provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideDefinition(document, position, token); - } - - public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { - return this.handleProvideHover(document, position, token); - } - - public provideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideReferences(document, position, context, token); - } - - public provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext, - ): ProviderResult { - return this.handleProvideCompletionItems(document, position, token, context); - } - - public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { - return this.handleProvideCodeLenses(document, token); - } - - public provideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - ): ProviderResult { - return this.handleProvideDocumentSymbols(document, token); - } - - public provideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - context: SignatureHelpContext, - ): ProviderResult { - return this.handleProvideSignatureHelp(document, position, token, context); - } - - protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { - const proxy = this.manager.languageProxy; - if (proxy) { - return proxy.languageClient; - } - } - - private async handleProvideRenameEdits( - document: TextDocument, - position: Position, - newName: string, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.RenameParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - newName, - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.RenameRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); - } - } - } - - private async handleProvideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.DefinitionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asDefinitionResult(result); - } - } - } - - private async handleProvideHover( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.HoverRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asHover(result); - } - } - } - - private async handleProvideReferences( - document: TextDocument, - position: Position, - context: ReferenceContext, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.ReferenceParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - context, - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.ReferencesRequest.type, args, token); - if (result) { - // Remove undefined part. - return result.map((l) => { - const r = languageClient!.protocol2CodeConverter.asLocation(l); - return r!; - }); - } - } - } - - private async handleProvideCodeLenses( - document: TextDocument, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.CodeLensParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - }; - const result = await languageClient.sendRequest(vscodeLanguageClient.CodeLensRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCodeLenses(result); - } - } - } - - private async handleProvideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args = languageClient.code2ProtocolConverter.asCompletionParams(document, position, context); - const result = await languageClient.sendRequest(vscodeLanguageClient.CompletionRequest.type, args, token); - if (result) { - return languageClient.protocol2CodeConverter.asCompletionResult(result); - } - } - } - - private async handleProvideDocumentSymbols( - document: TextDocument, - token: CancellationToken, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.DocumentSymbolParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.DocumentSymbolRequest.type, - args, - token, - ); - if (result && result.length) { - if ((result[0] as any).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); - } - } - } - } - - private async handleProvideSignatureHelp( - document: TextDocument, - position: Position, - token: CancellationToken, - _context: SignatureHelpContext, - ): Promise { - const languageClient = this.getLanguageClient(); - if (languageClient) { - const args: vscodeLanguageClient.TextDocumentPositionParams = { - textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: languageClient.code2ProtocolConverter.asPosition(position), - }; - const result = await languageClient.sendRequest( - vscodeLanguageClient.SignatureHelpRequest.type, - args, - token, - ); - if (result) { - return languageClient.protocol2CodeConverter.asSignatureHelp(result); - } - } - } -} diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts index 567403a23e51..fabebd6bddfa 100644 --- a/src/client/activation/common/languageServerChangeHandler.ts +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -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( @@ -92,36 +93,18 @@ export class LanguageServerChangeHandler implements Disposable { // 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(); } - - // 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'); - // } } private isPylanceInstalled(): boolean { 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/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts index 2c2ccc96aa45..e0ddd9b1a0a7 100644 --- a/src/client/activation/jedi/languageClientFactory.ts +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -28,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/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index fd031fd79037..7518666aa9d0 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,20 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { LanguageClientOptions } from 'vscode-languageclient'; import { IWorkspaceService } from '../../common/application/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { + // 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 003e79505237..6516ff9cb23f 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -50,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/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index ac261772e681..697dfce1f9ed 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -24,6 +24,7 @@ import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../lo 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'; @@ -50,10 +52,15 @@ namespace GetExperimentValue { 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( @@ -74,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)); @@ -139,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, @@ -212,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 c0e15f89a3e0..961721a333af 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -22,12 +22,18 @@ import { PYLANCE_EXTENSION_ID } from '../../common/constants'; export class NodeLanguageServerManager implements ILanguageServerManager { 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: boolean = false; + + private started = false; constructor( private readonly serviceContainer: IServiceContainer, @@ -49,14 +55,14 @@ 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; } @@ -78,14 +84,14 @@ export class NodeLanguageServerManager implements ILanguageServerManager { this.started = true; } - public connect() { + public connect(): void { if (!this.connected) { this.connected = true; this.middleware?.connect(); } } - public disconnect() { + public disconnect(): void { if (this.connected) { this.connected = false; this.middleware?.disconnect(); @@ -116,11 +122,8 @@ export class NodeLanguageServerManager implements ILanguageServerManager { @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { 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/extensionActivation.ts b/src/client/extensionActivation.ts index 9536f689b685..13c552726661 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -125,7 +125,6 @@ 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); From 8577f04bd7c01120ebf183180f7e24370bf6619c Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:33:35 -0700 Subject: [PATCH 17/30] Forgot a file + linting --- src/client/activation/none/activator.ts | 99 ------------------------- src/client/languageServer/watcher.ts | 3 +- 2 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 src/client/activation/none/activator.ts diff --git a/src/client/activation/none/activator.ts b/src/client/activation/none/activator.ts deleted file mode 100644 index c747e82f7779..000000000000 --- a/src/client/activation/none/activator.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { injectable } from 'inversify'; -import { - CancellationToken, - CodeLens, - CompletionContext, - CompletionItem, - CompletionList, - DocumentSymbol, - Hover, - Location, - LocationLink, - Position, - ProviderResult, - ReferenceContext, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocument, - WorkspaceEdit, -} from 'vscode'; -import { Resource } from '../../common/types'; -import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { ILanguageServerActivator } from '../types'; - -/** - * 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 {} - - public dispose(): void {} - - public activate(): void {} - - public deactivate(): void {} - - public provideRenameEdits( - _document: TextDocument, - _position: Position, - _newName: string, - _token: CancellationToken, - ): ProviderResult { - return null; - } - public provideDefinition( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - ): ProviderResult { - return null; - } - public provideHover( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - ): ProviderResult { - return null; - } - public provideReferences( - _document: TextDocument, - _position: Position, - _context: ReferenceContext, - _token: CancellationToken, - ): ProviderResult { - return null; - } - public provideCompletionItems( - _document: TextDocument, - _position: Position, - _token: CancellationToken, - _context: CompletionContext, - ): 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, - _token: CancellationToken, - _context: SignatureHelpContext, - ): ProviderResult { - return null; - } -} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index d1d6e09b6b56..d6e20254ad77 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -39,8 +39,7 @@ import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types * It also implements the ILanguageServerCache interface needed by our Jupyter support. */ export class LanguageServerWatcher - implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache -{ + implements IExtensionActivationService, ILanguageServerWatcher, ILanguageServerCache { public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; languageServerExtensionManager: ILanguageServerExtensionManager | undefined; From 8ddbcef6912e73c658480fbf891af899a1a2a8b4 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:36:54 -0700 Subject: [PATCH 18/30] Undo language client factory change --- src/client/activation/jedi/languageClientFactory.ts | 10 +++++++++- src/client/activation/node/languageClientFactory.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/client/activation/jedi/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts index e0ddd9b1a0a7..80116adbadfd 100644 --- a/src/client/activation/jedi/languageClientFactory.ts +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -28,6 +28,14 @@ export class JediLanguageClientFactory implements ILanguageClientFactory { args: [lsScriptPath], }; - return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); + // eslint-disable-next-line global-require + const vscodeLanguageClient = require('vscode-languageclient/node'); + + return new vscodeLanguageClient.LanguageClient( + PYTHON_LANGUAGE, + languageClientName, + serverOptions, + clientOptions, + ); } } diff --git a/src/client/activation/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index 6516ff9cb23f..2fbf6b615ab0 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -50,6 +50,14 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { }, }; - return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); + // eslint-disable-next-line global-require + const vscodeLanguageClient = require('vscode-languageclient/node'); + + return new vscodeLanguageClient.LanguageClient( + PYTHON_LANGUAGE, + languageClientName, + serverOptions, + clientOptions, + ); } } From 3df9996ad1c137945fb5ca9483d346f0634a6f47 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:36:59 -0700 Subject: [PATCH 19/30] Stray comment --- src/client/activation/common/languageServerChangeHandler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts index fabebd6bddfa..daae78a88d2b 100644 --- a/src/client/activation/common/languageServerChangeHandler.ts +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -86,7 +86,6 @@ 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); From 97bf6991cd82a8de457e0439966183a23f7f4533 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 19:44:01 -0700 Subject: [PATCH 20/30] Revert "Undo language client factory change" This reverts commit 8ddbcef6912e73c658480fbf891af899a1a2a8b4. --- src/client/activation/jedi/languageClientFactory.ts | 10 +--------- src/client/activation/node/languageClientFactory.ts | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/client/activation/jedi/languageClientFactory.ts b/src/client/activation/jedi/languageClientFactory.ts index 80116adbadfd..e0ddd9b1a0a7 100644 --- a/src/client/activation/jedi/languageClientFactory.ts +++ b/src/client/activation/jedi/languageClientFactory.ts @@ -28,14 +28,6 @@ export class JediLanguageClientFactory implements ILanguageClientFactory { args: [lsScriptPath], }; - // eslint-disable-next-line global-require - 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/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index 2fbf6b615ab0..6516ff9cb23f 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -50,14 +50,6 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { }, }; - // eslint-disable-next-line global-require - const vscodeLanguageClient = require('vscode-languageclient/node'); - - return new vscodeLanguageClient.LanguageClient( - PYTHON_LANGUAGE, - languageClientName, - serverOptions, - clientOptions, - ); + return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); } } From a981526af27c0e999e636c1558ff7ab153e53f40 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 7 Apr 2022 20:21:25 -0700 Subject: [PATCH 21/30] Add 2.7 behaviour + fix linting --- .../pylanceLSExtensionManager.ts | 3 +- src/client/languageServer/watcher.ts | 26 +++- .../jediLSExtensionManager.unit.test.ts | 4 +- .../languageServerCapabilities.unit.test.ts | 16 +- src/test/languageServer/watcher.unit.test.ts | 144 ++++++++++++++++++ 5 files changed, 181 insertions(+), 12 deletions(-) diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index 4376e14d4247..34c94af1eb66 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -22,6 +22,7 @@ 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'; @@ -101,6 +102,6 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities this.configurationService, ); - throw new Error(Pylance.pylanceNotInstalledMessage()); + traceLog(Pylance.pylanceNotInstalledMessage()); } } diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index d6e20254ad77..93bcbaae6be7 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -109,8 +109,28 @@ export class LanguageServerWatcher this.stopLanguageServer(); } + // 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. - this.languageServerExtensionManager = this.createLanguageServer(languageServerType); + this.languageServerExtensionManager = this.createLanguageServer(serverType); if (this.languageServerExtensionManager.canStartLanguageServer()) { // Start the language server. @@ -193,6 +213,10 @@ export class LanguageServerWatcher } } + private getCurrentLanguageServerTypeIsDefault(): boolean { + return this.configurationService.getSettings(this.resource).languageServerIsDefault; + } + // Watch for settings changes. private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { if (event.affectsConfiguration('python.languageServer')) { diff --git a/src/test/languageServer/jediLSExtensionManager.unit.test.ts b/src/test/languageServer/jediLSExtensionManager.unit.test.ts index 45403635b751..82fe5a8262b8 100644 --- a/src/test/languageServer/jediLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/jediLSExtensionManager.unit.test.ts @@ -23,11 +23,11 @@ suite('Language Server - Jedi LS extension manager', () => { {} as IInterpreterPathService, {} as IInterpreterService, {} as IEnvironmentVariablesProvider, - { + ({ registerCommand: () => { /* do nothing */ }, - } as unknown as ICommandManager, + } as unknown) as ICommandManager, ); }); diff --git a/src/test/languageServer/languageServerCapabilities.unit.test.ts b/src/test/languageServer/languageServerCapabilities.unit.test.ts index 76ff7009df64..4392e2901af8 100644 --- a/src/test/languageServer/languageServerCapabilities.unit.test.ts +++ b/src/test/languageServer/languageServerCapabilities.unit.test.ts @@ -15,7 +15,7 @@ suite('Language server - capabilities', () => { }); test('The connection property should return an object if there is a language client', () => { - const serverProxy = { + const serverProxy = ({ languageClient: { sendNotification: () => { /* nothing */ @@ -36,7 +36,7 @@ suite('Language server - capabilities', () => { /* nothing */ }, }, - } as unknown as ILanguageServerProxy; + } as unknown) as ILanguageServerProxy; const capabilities = new LanguageServerCapabilities(); capabilities.serverProxy = serverProxy; @@ -48,7 +48,7 @@ suite('Language server - capabilities', () => { }); test('The connection property should return undefined if there is no language client', () => { - const serverProxy = {} as unknown as ILanguageServerProxy; + const serverProxy = ({} as unknown) as ILanguageServerProxy; const capabilities = new LanguageServerCapabilities(); capabilities.serverProxy = serverProxy; @@ -59,13 +59,13 @@ suite('Language server - capabilities', () => { }); test('capabilities() should return an object if there is an initialized language client', () => { - const serverProxy = { + const serverProxy = ({ languageClient: { initializeResult: { capabilities: {}, }, }, - } as unknown as ILanguageServerProxy; + } as unknown) as ILanguageServerProxy; const capabilities = new LanguageServerCapabilities(); capabilities.serverProxy = serverProxy; @@ -77,7 +77,7 @@ suite('Language server - capabilities', () => { }); test('capabilities() should return undefined if there is no language client', () => { - const serverProxy = {} as unknown as ILanguageServerProxy; + const serverProxy = ({} as unknown) as ILanguageServerProxy; const capabilities = new LanguageServerCapabilities(); capabilities.serverProxy = serverProxy; @@ -88,11 +88,11 @@ suite('Language server - capabilities', () => { }); test('capabilities() should return undefined if the language client is not initialized', () => { - const serverProxy = { + const serverProxy = ({ languageClient: { initializeResult: undefined, }, - } as unknown as ILanguageServerProxy; + } as unknown) as ILanguageServerProxy; const capabilities = new LanguageServerCapabilities(); capabilities.serverProxy = serverProxy; diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 7faa91059ad3..3428b4846c5f 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -18,6 +18,7 @@ import { IEnvironmentVariablesProvider } from '../../client/common/variables/typ import { 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'; @@ -401,4 +402,147 @@ suite('Language server watcher', () => { // 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, + {} as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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, + {} as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + 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, + {} as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreter: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + 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); + }); }); From e7045899b2b6986280fb8e92b3ab410c116cd1fc Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Wed, 13 Apr 2022 15:51:18 -0700 Subject: [PATCH 22/30] Remove deprecated LS settings --- resources/report_issue_user_settings.json | 2 -- src/client/common/configSettings.ts | 11 ----------- src/client/common/types.ts | 2 -- .../configSettings/configSettings.unit.test.ts | 14 +++----------- 4 files changed, 3 insertions(+), 26 deletions(-) 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/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/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', () => { From 1345fed9d619e6108a1753c6f45e6246b3c85d8e Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 14 Apr 2022 16:44:30 -0700 Subject: [PATCH 23/30] Add support for 1 LS per workspace folder --- src/client/languageServer/types.ts | 2 +- src/client/languageServer/watcher.ts | 104 +++++++++++++++++---------- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index 2a47bb0a1e3d..09f615140ce0 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -13,7 +13,7 @@ export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); export interface ILanguageServerWatcher { readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; readonly languageServerType: LanguageServerType; - startLanguageServer(languageServerType: LanguageServerType): Promise; + startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; } export interface ILanguageServerCapabilities extends ILanguageServer { diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 93bcbaae6be7..0e553c3478fa 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent } from 'vscode'; +import { ConfigurationChangeEvent, Uri } from 'vscode'; import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; import { IExtensionActivationService, @@ -19,11 +19,12 @@ import { IExperimentService, IExtensions, IInterpreterPathService, + InterpreterConfigurationScope, Resource, } from '../common/types'; import { LanguageService } from '../common/utils/localize'; import { IEnvironmentVariablesProvider } from '../common/variables/types'; -import { IInterpreterService } from '../interpreter/contracts'; +import { IInterpreterHelper, IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { traceLog } from '../logging'; import { PythonEnvironment } from '../pythonEnvironments/info'; @@ -46,9 +47,10 @@ export class LanguageServerWatcher languageServerType: LanguageServerType; - private resource: Resource; + private workspaceInterpreters: Map; - private interpreter: PythonEnvironment | undefined; + // In a multiroot workspace scenario we will have one language server per folder. + private workspaceLanguageServers: Map; private languageServerChangeHandler: LanguageServerChangeHandler; @@ -57,6 +59,7 @@ export class LanguageServerWatcher @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, @@ -67,12 +70,14 @@ export class LanguageServerWatcher @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.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); + disposables.push(this.interpreterPathService.onDidChange(this.onDidChangeInterpreter.bind(this))); } this.languageServerChangeHandler = new LanguageServerChangeHandler( @@ -94,19 +99,20 @@ export class LanguageServerWatcher // IExtensionActivationService - public async activate(resource: Resource): Promise { - this.resource = resource; - await this.startLanguageServer(this.languageServerType); + public async activate(resource?: Resource): Promise { + await this.startLanguageServer(this.languageServerType, resource); } - // ILanguageServerWatcher; + // ILanguageServerWatcher - public async startLanguageServer(languageServerType: LanguageServerType): Promise { - const interpreter = await this.interpreterService?.getActiveInterpreter(this.resource); + public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise { + const lsResource = this.getWorkspaceKey(resource); + const currentInterpreter = this.workspaceInterpreters.get(lsResource); + const interpreter = await this.interpreterService?.getActiveInterpreter(resource); // Destroy the old language server if it's different. - if (interpreter !== this.interpreter) { - this.stopLanguageServer(); + if (interpreter !== currentInterpreter) { + this.stopLanguageServer(lsResource); } // If the interpreter is Python 2 and the LS setting is explicitly set to Jedi, turn it off. @@ -130,37 +136,45 @@ export class LanguageServerWatcher } // Instantiate the language server extension manager. - this.languageServerExtensionManager = this.createLanguageServer(serverType); + const languageServerExtensionManager = this.createLanguageServer(serverType); - if (this.languageServerExtensionManager.canStartLanguageServer()) { + if (languageServerExtensionManager.canStartLanguageServer()) { // Start the language server. - await this.languageServerExtensionManager.startLanguageServer(this.resource, interpreter); + await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); logStartup(languageServerType); this.languageServerType = languageServerType; - this.interpreter = interpreter; + this.workspaceInterpreters.set(lsResource, interpreter); } else { - await this.languageServerExtensionManager.languageServerNotAvailable(); + await languageServerExtensionManager.languageServerNotAvailable(); } + + this.workspaceLanguageServers.set(lsResource, languageServerExtensionManager); } // ILanguageServerCache - public async get(): Promise { - if (!this.languageServerExtensionManager) { - this.startLanguageServer(this.languageServerType); + public async get(resource?: Resource): Promise { + const lsResource = this.getWorkspaceKey(resource); + const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource); + + if (!languageServerExtensionManager) { + this.startLanguageServer(this.languageServerType, resource); } - return Promise.resolve(this.languageServerExtensionManager!.get()); + return Promise.resolve(languageServerExtensionManager!.get()); } // Private methods - private stopLanguageServer(): void { - if (this.languageServerExtensionManager) { - this.languageServerExtensionManager.stopLanguageServer(); - this.languageServerExtensionManager.dispose(); - this.languageServerExtensionManager = undefined; + private stopLanguageServer(resource?: Resource): void { + const lsResource = this.getWorkspaceKey(resource); + const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource); + + if (languageServerExtensionManager) { + languageServerExtensionManager.stopLanguageServer(); + languageServerExtensionManager.dispose(); + this.workspaceLanguageServers.delete(lsResource); } } @@ -204,30 +218,33 @@ export class LanguageServerWatcher return this.languageServerExtensionManager; } - private async refreshLanguageServer(): Promise { - const languageServerType = this.configurationService.getSettings().languageServer; + private async refreshLanguageServer(resource?: Resource): Promise { + const lsResource = this.getWorkspaceKey(resource); + const languageServerType = this.configurationService.getSettings(lsResource).languageServer; if (languageServerType !== this.languageServerType) { - this.stopLanguageServer(); - await this.startLanguageServer(languageServerType); + this.stopLanguageServer(lsResource); + await this.startLanguageServer(languageServerType, lsResource); } } private getCurrentLanguageServerTypeIsDefault(): boolean { - return this.configurationService.getSettings(this.resource).languageServerIsDefault; + return this.configurationService.getSettings().languageServerIsDefault; } // Watch for settings changes. private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { - if (event.affectsConfiguration('python.languageServer')) { - await this.refreshLanguageServer(); + const resource = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri; + + if (event.affectsConfiguration('python.languageServer', resource)) { + await this.refreshLanguageServer(resource); } } // Watch for interpreter changes. - private async onDidChangeInterpreter(): Promise { - // Reactivate the resource. It should destroy the old one if it's different. - return this.activate(this.resource); + 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. @@ -238,6 +255,19 @@ export class LanguageServerWatcher await this.refreshLanguageServer(); } } + + // Get the workspace key for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. + private getWorkspaceKey(resource?: Resource): Resource { + 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): void { From a2cb694c244e748ec15d0d70ff1956bf709c62e1 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Thu, 14 Apr 2022 17:13:53 -0700 Subject: [PATCH 24/30] Add tests --- src/client/languageServer/watcher.ts | 20 +-- src/test/languageServer/watcher.unit.test.ts | 173 +++++++++++++++---- 2 files changed, 145 insertions(+), 48 deletions(-) diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 0e553c3478fa..c83b7e45df5c 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -47,10 +47,10 @@ export class LanguageServerWatcher languageServerType: LanguageServerType; - private workspaceInterpreters: Map; + private workspaceInterpreters: Map; // In a multiroot workspace scenario we will have one language server per folder. - private workspaceLanguageServers: Map; + private workspaceLanguageServers: Map; private languageServerChangeHandler: LanguageServerChangeHandler; @@ -107,11 +107,11 @@ export class LanguageServerWatcher public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise { const lsResource = this.getWorkspaceKey(resource); - const currentInterpreter = this.workspaceInterpreters.get(lsResource); + const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); const interpreter = await this.interpreterService?.getActiveInterpreter(resource); // Destroy the old language server if it's different. - if (interpreter !== currentInterpreter) { + if (currentInterpreter && interpreter !== currentInterpreter) { this.stopLanguageServer(lsResource); } @@ -144,19 +144,19 @@ export class LanguageServerWatcher logStartup(languageServerType); this.languageServerType = languageServerType; - this.workspaceInterpreters.set(lsResource, interpreter); + this.workspaceInterpreters.set(lsResource.fsPath, interpreter); } else { await languageServerExtensionManager.languageServerNotAvailable(); } - this.workspaceLanguageServers.set(lsResource, languageServerExtensionManager); + this.workspaceLanguageServers.set(lsResource.fsPath, languageServerExtensionManager); } // ILanguageServerCache public async get(resource?: Resource): Promise { const lsResource = this.getWorkspaceKey(resource); - const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource); + const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); if (!languageServerExtensionManager) { this.startLanguageServer(this.languageServerType, resource); @@ -169,12 +169,12 @@ export class LanguageServerWatcher private stopLanguageServer(resource?: Resource): void { const lsResource = this.getWorkspaceKey(resource); - const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource); + const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); if (languageServerExtensionManager) { languageServerExtensionManager.stopLanguageServer(); languageServerExtensionManager.dispose(); - this.workspaceLanguageServers.delete(lsResource); + this.workspaceLanguageServers.delete(lsResource.fsPath); } } @@ -257,7 +257,7 @@ export class LanguageServerWatcher } // Get the workspace key for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. - private getWorkspaceKey(resource?: Resource): Resource { + private getWorkspaceKey(resource?: Resource): Uri { let uri; if (resource) { diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 3428b4846c5f..780fdcc1d84a 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -3,7 +3,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { ConfigurationChangeEvent, Disposable } from 'vscode'; +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'; @@ -15,7 +15,7 @@ import { } from '../../client/common/types'; import { LanguageService } from '../../client/common/utils/localize'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; +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'; @@ -34,15 +34,20 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => 'python', - onDidChangeInterpreter: () => { + 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 */ }, @@ -68,7 +73,7 @@ suite('Language server watcher', () => { sandbox.restore(); }); - test('The constructor should add a listener to onDidChangeInterpreter to the list of disposables if it is a trusted workspace', () => { + 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( @@ -78,15 +83,17 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, + {} as IInterpreterHelper, ({ - onDidChangeInterpreter: () => { + onDidChange: () => { /* do nothing */ }, - } as unknown) as IInterpreterService, + } as unknown) as IInterpreterPathService, + {} as IInterpreterService, {} as IEnvironmentVariablesProvider, ({ isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), onDidChangeConfiguration: () => { /* do nothing */ }, @@ -106,7 +113,7 @@ suite('Language server watcher', () => { assert.strictEqual(disposables.length, 4); }); - test('The constructor should not add a listener to onDidChangeInterpreter to the list of disposables if it is not a trusted workspace', () => { + 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( @@ -116,15 +123,17 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, + {} as IInterpreterHelper, ({ - onDidChangeInterpreter: () => { + onDidChange: () => { /* do nothing */ }, - } as unknown) as IInterpreterService, + } as unknown) as IInterpreterPathService, + {} as IInterpreterService, {} as IEnvironmentVariablesProvider, ({ isTrusted: false, + getWorkspaceFolder: (uri: Uri) => ({ uri }), onDidChangeConfiguration: () => { /* do nothing */ }, @@ -158,9 +167,6 @@ suite('Language server watcher', () => { getActiveInterpreterStub.onSecondCall().returns('other/python'); const interpreterService = ({ - onDidChangeInterpreter: () => { - /* do nothing */ - }, getActiveInterpreter: getActiveInterpreterStub, } as unknown) as IInterpreterService; @@ -175,7 +181,14 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, interpreterService, ({ onDidEnvironmentVariablesChange: () => { @@ -184,7 +197,7 @@ suite('Language server watcher', () => { } as unknown) as IEnvironmentVariablesProvider, ({ isTrusted: true, - getWorkspaceFolder: () => undefined, + getWorkspaceFolder: (uri: Uri) => ({ uri }), onDidChangeConfiguration: () => { /* do nothing */ }, @@ -239,15 +252,20 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => 'python', - onDidChangeInterpreter: () => { + 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 */ }, @@ -294,10 +312,11 @@ suite('Language server watcher', () => { assert.ok(languageServerNotAvailableStub.calledOnce); }); - test('When the config settings change, but the python.languageServer setting is not affected, the watched should not restart the language server', async () => { + 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; }, @@ -310,12 +329,16 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.None }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => 'python', - onDidChangeInterpreter: () => { + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { /* do nothing */ }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, workspaceService, @@ -345,10 +368,11 @@ suite('Language server watcher', () => { assert.ok(startLanguageServerSpy.calledOnce); }); - test('When the config settings change, and the python.languageServer setting is affected, the watched should restart the language server', async () => { + 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; }, @@ -367,12 +391,16 @@ suite('Language server watcher', () => { {} as ILanguageServerOutputChannel, configService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => 'python', - onDidChangeInterpreter: () => { + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { /* do nothing */ }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', } as unknown) as IInterpreterService, {} as IEnvironmentVariablesProvider, workspaceService, @@ -414,15 +442,20 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.Jedi }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), - onDidChangeInterpreter: () => { + 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 */ }, @@ -464,15 +497,20 @@ suite('Language server watcher', () => { }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), - onDidChangeInterpreter: () => { + 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 */ }, @@ -511,16 +549,21 @@ suite('Language server watcher', () => { getSettings: () => ({ languageServer: LanguageServerType.Jedi }), } as IConfigurationService, {} as IExperimentService, - {} as IInterpreterPathService, ({ - getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), - onDidChangeInterpreter: () => { + 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 */ }, @@ -545,4 +588,58 @@ suite('Language server watcher', () => { assert.ok(startLanguageServerStub.calledOnce); }); + + test('When starting language servers with different resources, multiple language servers should be instantiated', async () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns({ path: 'folder1/python', version: { major: 2, minor: 7 } }); + getActiveInterpreterStub.onSecondCall().returns({ path: 'folder2/python', version: { major: 2, minor: 7 } }); + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + const stopLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'stopLanguageServer'); + + 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); + }); }); From ca38b34e861d01547686057ea227b352fcd6fc68 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Date: Mon, 18 Apr 2022 10:19:22 -0700 Subject: [PATCH 25/30] Update src/client/languageServer/watcher.ts Co-authored-by: Kartik Raj --- src/client/languageServer/watcher.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index c83b7e45df5c..bd93181d5742 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -234,11 +234,12 @@ export class LanguageServerWatcher // Watch for settings changes. private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { - const resource = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri; - - if (event.affectsConfiguration('python.languageServer', resource)) { - await this.refreshLanguageServer(resource); - } + const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? [undefined]; + workspacesUris.forEach(async (resource) => { + if (event.affectsConfiguration(`python.${languageServerSetting}`, resource)) { + await this.refreshLanguageServer(resource); + } + }); } // Watch for interpreter changes. From b009b912578f6389cd38da5b18b978b3c2a90cc8 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 18 Apr 2022 11:32:03 -0700 Subject: [PATCH 26/30] Add resource path to "starting ls" message --- package.nls.json | 6 +++--- src/client/common/utils/localize.ts | 9 ++++++--- src/client/languageServer/watcher.ts | 10 +++++----- src/test/languageServer/watcher.unit.test.ts | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) 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/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/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index bd93181d5742..a8fdfd912fba 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -142,7 +142,7 @@ export class LanguageServerWatcher // Start the language server. await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); - logStartup(languageServerType); + logStartup(languageServerType, lsResource); this.languageServerType = languageServerType; this.workspaceInterpreters.set(lsResource.fsPath, interpreter); } else { @@ -271,17 +271,17 @@ export class LanguageServerWatcher } } -function logStartup(languageServerType: LanguageServerType): void { +function logStartup(languageServerType: LanguageServerType, resource: Uri): void { let outputLine; switch (languageServerType) { case LanguageServerType.Jedi: - outputLine = LanguageService.startingJedi(); + outputLine = LanguageService.startingJedi().format(resource.fsPath); break; case LanguageServerType.Node: - outputLine = LanguageService.startingPylance(); + outputLine = LanguageService.startingPylance().format(resource.fsPath); break; case LanguageServerType.None: - outputLine = LanguageService.startingNone(); + outputLine = LanguageService.startingNone().format(resource.fsPath); break; default: throw new Error(`Unknown language server type: ${languageServerType}`); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 780fdcc1d84a..0aedcd141aff 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -253,7 +253,7 @@ suite('Language server watcher', () => { } as IConfigurationService, {} as IExperimentService, ({ - getActiveWorkspaceUri: () => undefined, + getActiveWorkspaceUri: () => ({ folderUri: Uri.parse('workspace') }), } as unknown) as IInterpreterHelper, ({ onDidChange: () => { @@ -288,7 +288,7 @@ suite('Language server watcher', () => { await watcher.startLanguageServer(LanguageServerType.None); - assert.strictEqual(output, LanguageService.startingNone()); + 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 () => { From 9f5c698b2c16ede527b0b6c81a90aba4f920e90e Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 18 Apr 2022 11:37:53 -0700 Subject: [PATCH 27/30] Fix issues with automatically applied change --- src/client/languageServer/watcher.ts | 5 +++-- src/test/languageServer/watcher.unit.test.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index a8fdfd912fba..9b863de74d15 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -234,9 +234,10 @@ export class LanguageServerWatcher // Watch for settings changes. private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { - const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? [undefined]; + const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? []; + workspacesUris.forEach(async (resource) => { - if (event.affectsConfiguration(`python.${languageServerSetting}`, resource)) { + if (event.affectsConfiguration(`python.languageServer`, resource)) { await this.refreshLanguageServer(resource); } }); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 0aedcd141aff..74a6939ec97b 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -376,6 +376,7 @@ suite('Language server watcher', () => { onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { onDidChangeConfigListener = listener; }, + workspaceFolders: [{ uri: Uri.parse('workspace') }], } as unknown) as IWorkspaceService; const getSettingsStub = sandbox.stub(); From 3dbfb3536bf149abb3629bebdfee38ca3096944c Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 18 Apr 2022 11:41:09 -0700 Subject: [PATCH 28/30] Fix issue with get() --- src/client/languageServer/types.ts | 5 ++++- src/client/languageServer/watcher.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index 09f615140ce0..e85a3d0209d3 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -13,7 +13,10 @@ export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); export interface ILanguageServerWatcher { readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; readonly languageServerType: LanguageServerType; - startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; + startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + ): Promise; } export interface ILanguageServerCapabilities extends ILanguageServer { diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 9b863de74d15..7cfae448266d 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -105,7 +105,10 @@ export class LanguageServerWatcher // ILanguageServerWatcher - public async startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise { + 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); @@ -150,19 +153,21 @@ export class LanguageServerWatcher } this.workspaceLanguageServers.set(lsResource.fsPath, languageServerExtensionManager); + + return languageServerExtensionManager; } // ILanguageServerCache public async get(resource?: Resource): Promise { const lsResource = this.getWorkspaceKey(resource); - const languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); + let languageServerExtensionManager = this.workspaceLanguageServers.get(lsResource.fsPath); if (!languageServerExtensionManager) { - this.startLanguageServer(this.languageServerType, resource); + languageServerExtensionManager = await this.startLanguageServer(this.languageServerType, resource); } - return Promise.resolve(languageServerExtensionManager!.get()); + return Promise.resolve(languageServerExtensionManager.get()); } // Private methods From 48927ae34eff42ad4de442747bd2b870c0ab35d2 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 18 Apr 2022 11:44:09 -0700 Subject: [PATCH 29/30] Only display basename --- src/client/languageServer/watcher.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 7cfae448266d..9fc138f80a0e 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -1,6 +1,7 @@ // 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'; @@ -279,15 +280,17 @@ export class LanguageServerWatcher function logStartup(languageServerType: LanguageServerType, resource: Uri): void { let outputLine; + const basename = path.basename(resource.fsPath); + switch (languageServerType) { case LanguageServerType.Jedi: - outputLine = LanguageService.startingJedi().format(resource.fsPath); + outputLine = LanguageService.startingJedi().format(basename); break; case LanguageServerType.Node: - outputLine = LanguageService.startingPylance().format(resource.fsPath); + outputLine = LanguageService.startingPylance().format(basename); break; case LanguageServerType.None: - outputLine = LanguageService.startingNone().format(resource.fsPath); + outputLine = LanguageService.startingNone().format(basename); break; default: throw new Error(`Unknown language server type: ${languageServerType}`); From 6e455e24fd9226dd116ce5e422d572ead08c4c17 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 18 Apr 2022 12:04:30 -0700 Subject: [PATCH 30/30] Amend ILanguageServerExtensionManager comment --- src/client/languageServer/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index e85a3d0209d3..acaf54db3c18 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -26,10 +26,11 @@ export interface ILanguageServerCapabilities extends ILanguageServer { } /** - * Language server extension manager implementations act as a wrapper for anything related to their language server extension. + * `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. * - * They also extend the ILanguageServer interface via ILanguageServerCapabilities to keep providing support to 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;