diff --git a/src/client/activation/node/languageClientFactory.ts b/src/client/activation/node/languageClientFactory.ts index ef2ac872c190..9543f265468f 100644 --- a/src/client/activation/node/languageClientFactory.ts +++ b/src/client/activation/node/languageClientFactory.ts @@ -11,7 +11,7 @@ import { PythonEnvironment } from '../../pythonEnvironments/info'; import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; import { ILanguageClientFactory } from '../types'; -const languageClientName = 'Pylance'; +export const PYLANCE_NAME = 'Pylance'; export class NodeLanguageClientFactory implements ILanguageClientFactory { constructor(private readonly fs: IFileSystem, private readonly extensions: IExtensions) {} @@ -50,6 +50,6 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory { }, }; - return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); + return new LanguageClient(PYTHON_LANGUAGE, PYLANCE_NAME, serverOptions, clientOptions); } } diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index dea261514702..8f1b66c562e2 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -20,6 +20,7 @@ import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; import { IWorkspaceService } from '../../common/application/types'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { PylanceApi } from './pylanceApi'; // eslint-disable-next-line @typescript-eslint/no-namespace namespace InExperiment { @@ -56,6 +57,8 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { private lsVersion: string | undefined; + private pylanceApi: PylanceApi | undefined; + constructor( private readonly factory: ILanguageClientFactory, private readonly experimentService: IExperimentService, @@ -89,9 +92,16 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { interpreter: PythonEnvironment | undefined, options: LanguageClientOptions, ): Promise { - const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + const extension = await this.getPylanceExtension(); this.lsVersion = extension?.packageJSON.version || '0'; + const api = extension?.exports as PylanceApi | undefined; + if (api && api.client && api.client.isEnabled()) { + this.pylanceApi = api; + await api.client.start(); + return; + } + this.cancellationStrategy = new FileBasedCancellationStrategy(); options.connectionOptions = { cancellationStrategy: this.cancellationStrategy }; @@ -111,6 +121,12 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { @traceDecoratorVerbose('Disposing language server') public async stop(): Promise { + if (this.pylanceApi) { + const api = this.pylanceApi; + this.pylanceApi = undefined; + await api.client!.stop(); + } + while (this.disposables.length > 0) { const d = this.disposables.shift()!; d.dispose(); @@ -203,4 +219,17 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { })), ); } + + private async getPylanceExtension() { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (!extension) { + return undefined; + } + + if (!extension.isActive) { + await extension.activate(); + } + + return extension; + } } diff --git a/src/client/activation/node/pylanceApi.ts b/src/client/activation/node/pylanceApi.ts new file mode 100644 index 000000000000..507fad0b4f83 --- /dev/null +++ b/src/client/activation/node/pylanceApi.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface PylanceApi { + client?: { + isEnabled(): boolean; + start(): Promise; + stop(): Promise; + }; +} diff --git a/src/client/api.ts b/src/client/api.ts index 78663feab712..8d618d6d053c 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -5,8 +5,11 @@ import { noop } from 'lodash'; import { Uri, Event } from 'vscode'; +import { BaseLanguageClient } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { IExtensionApi } from './apiTypes'; -import { isTestExecution } from './common/constants'; +import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; import { IEnvironmentVariablesProvider } from './common/variables/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; @@ -33,6 +36,9 @@ export function buildApi( pylance: { getPythonPathVar: (resource?: Uri) => Promise; readonly onDidEnvironmentVariablesChange: Event; + createClient(...args: any[]): BaseLanguageClient; + start(client: BaseLanguageClient): Promise; + stop(client: BaseLanguageClient): Promise; }; } = { // 'ready' will propagate the exception, but we must log it here first. @@ -83,6 +89,10 @@ export function buildApi( return envs.PYTHONPATH; }, onDidEnvironmentVariablesChange: envService.onDidEnvironmentVariablesChange, + createClient: (...args: any[]): BaseLanguageClient => + new LanguageClient(PYTHON_LANGUAGE, PYLANCE_NAME, args[0], args[1]), + start: (client: BaseLanguageClient): Promise => client.start(), + stop: (client: BaseLanguageClient): Promise => client.stop(), }, }; diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index c9ecf8ac1212..783df7a5fddb 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -10,12 +10,14 @@ import { LanguageServerType } from '../activation/types'; import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; import { EventName } from '../telemetry/constants'; import { createStatusItem } from './intellisenseStatus'; +import { PylanceApi } from '../activation/node/pylanceApi'; interface BrowserConfig { distUrl: string; // URL to Pylance's dist folder. } let languageClient: LanguageClient | undefined; +let pylanceApi: PylanceApi | undefined; export async function activate(context: vscode.ExtensionContext): Promise { const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); @@ -34,17 +36,35 @@ export async function activate(context: vscode.ExtensionContext): Promise } export async function deactivate(): Promise { - const client = languageClient; - languageClient = undefined; + if (pylanceApi) { + const api = pylanceApi; + pylanceApi = undefined; + await api.client!.stop(); + } + + if (languageClient) { + const client = languageClient; + languageClient = undefined; - await client?.stop(); - await client?.dispose(); + await client.stop(); + await client.dispose(); + } } async function runPylance( context: vscode.ExtensionContext, pylanceExtension: vscode.Extension, ): Promise { + context.subscriptions.push(createStatusItem()); + + pylanceExtension = await getActivatedExtension(pylanceExtension); + const api = pylanceExtension.exports as PylanceApi; + if (api.client && api.client.isEnabled()) { + pylanceApi = api; + await api.client.start(); + return; + } + const { extensionUri, packageJSON } = pylanceExtension; const distUrl = vscode.Uri.joinPath(extensionUri, 'dist'); @@ -111,8 +131,6 @@ async function runPylance( ); await client.start(); - - context.subscriptions.push(createStatusItem()); } catch (e) { console.log(e); } @@ -196,3 +214,11 @@ function sendTelemetryEventBrowser( reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); } } + +async function getActivatedExtension(extension: vscode.Extension): Promise> { + if (!extension.isActive) { + await extension.activate(); + } + + return extension; +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index c71c33b7ad28..cd34bbb78847 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -262,11 +262,11 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang return lsManager; } - private async refreshLanguageServer(resource?: Resource): Promise { + private async refreshLanguageServer(resource?: Resource, forced?: boolean): Promise { const lsResource = this.getWorkspaceUri(resource); const languageServerType = this.configurationService.getSettings(lsResource).languageServer; - if (languageServerType !== this.languageServerType) { + if (languageServerType !== this.languageServerType || forced) { await this.stopLanguageServer(resource); await this.startLanguageServer(languageServerType, lsResource); } @@ -283,6 +283,8 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang workspacesUris.forEach(async (resource) => { if (event.affectsConfiguration(`python.languageServer`, resource)) { await this.refreshLanguageServer(resource); + } else if (event.affectsConfiguration(`python.analysis.pylanceLspClientEnabled`, resource)) { + await this.refreshLanguageServer(resource, /* forced */ true); } }); }