diff --git a/package.json b/package.json index 150657f638d4..6d5241397978 100644 --- a/package.json +++ b/package.json @@ -906,6 +906,15 @@ "scope": "machine-overridable", "type": "string" }, + "python.pylanceLspNotebooksEnabled": { + "type": "boolean", + "default": false, + "description": "Determines if Pylance's experimental LSP notebooks support is used or not.", + "scope": "machine", + "tags": [ + "experimental" + ] + }, "python.sortImports.args": { "default": [], "description": "Arguments passed in. Each argument is a separate item in the array.", diff --git a/src/client/activation/jedi/languageClientMiddleware.ts b/src/client/activation/jedi/languageClientMiddleware.ts new file mode 100644 index 000000000000..656c47309bb9 --- /dev/null +++ b/src/client/activation/jedi/languageClientMiddleware.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from '../../ioc/types'; +import { LanguageClientMiddleware } from '../languageClientMiddleware'; +import { LanguageServerType } from '../types'; + +export class JediLanguageClientMiddleware extends LanguageClientMiddleware { + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { + super(serviceContainer, LanguageServerType.Jedi, serverVersion); + this.setupHidingMiddleware(serviceContainer); + } +} diff --git a/src/client/activation/jedi/languageServerProxy.ts b/src/client/activation/jedi/languageServerProxy.ts index f8b5aaf9d920..fb60b539161a 100644 --- a/src/client/activation/jedi/languageServerProxy.ts +++ b/src/client/activation/jedi/languageServerProxy.ts @@ -14,7 +14,7 @@ import { IInterpreterPathService, Resource } from '../../common/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { LanguageClientMiddleware } from '../languageClientMiddleware'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; import { ProgressReporting } from '../progress'; import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; import { killPid } from '../../common/process/rawProcessApis'; @@ -57,7 +57,8 @@ export class JediLanguageServerProxy implements ILanguageServerProxy { options: LanguageClientOptions, ): Promise { this.lsVersion = - (options.middleware ? (options.middleware).serverVersion : undefined) ?? '0.19.3'; + (options.middleware ? (options.middleware).serverVersion : undefined) ?? + '0.19.3'; this.languageClient = await this.factory.createLanguageClient(resource, interpreter, options); this.registerHandlers(); diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts index 29f1106c5f8c..fa3fbc8b7505 100644 --- a/src/client/activation/jedi/manager.ts +++ b/src/client/activation/jedi/manager.ts @@ -14,13 +14,8 @@ import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { Commands } from '../commands'; -import { LanguageClientMiddleware } from '../languageClientMiddleware'; -import { - ILanguageServerAnalysisOptions, - ILanguageServerManager, - ILanguageServerProxy, - LanguageServerType, -} from '../types'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging'; export class JediLanguageServerManager implements ILanguageServerManager { @@ -28,7 +23,7 @@ export class JediLanguageServerManager implements ILanguageServerManager { private interpreter: PythonEnvironment | undefined; - private middleware: LanguageClientMiddleware | undefined; + private middleware: JediLanguageClientMiddleware | undefined; private disposables: IDisposable[] = []; @@ -132,7 +127,7 @@ export class JediLanguageServerManager implements ILanguageServerManager { @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { const options = await this.analysisOptions.getAnalysisOptions(); - this.middleware = new LanguageClientMiddleware(this.serviceContainer, LanguageServerType.Jedi, this.lsVersion); + this.middleware = new JediLanguageClientMiddleware(this.serviceContainer, this.lsVersion); options.middleware = this.middleware; // Make sure the middleware is connected if we restart and we we're already connected. diff --git a/src/client/activation/languageClientMiddleware.ts b/src/client/activation/languageClientMiddleware.ts index 7474c020217b..110d7461c615 100644 --- a/src/client/activation/languageClientMiddleware.ts +++ b/src/client/activation/languageClientMiddleware.ts @@ -14,11 +14,16 @@ import { createHidingMiddleware } from '@vscode/jupyter-lsp-middleware'; export class LanguageClientMiddleware extends LanguageClientMiddlewareBase { public constructor(serviceContainer: IServiceContainer, serverType: LanguageServerType, serverVersion?: string) { super(serviceContainer, serverType, sendTelemetryEvent, serverVersion); + } - if (serverType === LanguageServerType.None) { - return; - } - + /** + * Creates the HidingMiddleware if needed and sets up code to do so if needed after + * Jupyter is installed. + * + * This method should be called from the constructor of derived classes. It is separated + * from the constructor to allow derived classes to initialize before it is called. + */ + protected setupHidingMiddleware(serviceContainer: IServiceContainer) { const jupyterDependencyManager = serviceContainer.get( IJupyterExtensionDependencyManager, ); @@ -26,19 +31,28 @@ export class LanguageClientMiddleware extends LanguageClientMiddlewareBase { const extensions = serviceContainer.get(IExtensions); // Enable notebook support if jupyter support is installed - if (jupyterDependencyManager && jupyterDependencyManager.isJupyterExtensionInstalled) { + if (this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { this.notebookAddon = createHidingMiddleware(); } + disposables.push( - extensions?.onDidChange(() => { - if (jupyterDependencyManager) { - if (this.notebookAddon && !jupyterDependencyManager.isJupyterExtensionInstalled) { - this.notebookAddon = undefined; - } else if (!this.notebookAddon && jupyterDependencyManager.isJupyterExtensionInstalled) { - this.notebookAddon = createHidingMiddleware(); - } - } + extensions?.onDidChange(async () => { + await this.onExtensionChange(jupyterDependencyManager); }), ); } + + protected shouldCreateHidingMiddleware(jupyterDependencyManager: IJupyterExtensionDependencyManager): boolean { + return jupyterDependencyManager && jupyterDependencyManager.isJupyterExtensionInstalled; + } + + protected async onExtensionChange(jupyterDependencyManager: IJupyterExtensionDependencyManager): Promise { + if (jupyterDependencyManager) { + if (this.notebookAddon && !this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { + this.notebookAddon = undefined; + } else if (!this.notebookAddon && this.shouldCreateHidingMiddleware(jupyterDependencyManager)) { + this.notebookAddon = createHidingMiddleware(); + } + } + } } diff --git a/src/client/activation/languageClientMiddlewareBase.ts b/src/client/activation/languageClientMiddlewareBase.ts index 8a2981b6d9dd..2e240231fa64 100644 --- a/src/client/activation/languageClientMiddlewareBase.ts +++ b/src/client/activation/languageClientMiddlewareBase.ts @@ -86,7 +86,9 @@ export class LanguageClientMiddlewareBase implements Middleware { const settingDict: LSPObject & { pythonPath: string; _envPYTHONPATH: string } = settings[ i ] as LSPObject & { pythonPath: string; _envPYTHONPATH: string }; - settingDict.pythonPath = configService.getSettings(uri).pythonPath; + + settingDict.pythonPath = + (await this.getPythonPathOverride(uri)) ?? configService.getSettings(uri).pythonPath; const env = await envService.getEnvironmentVariables(uri); const envPYTHONPATH = env.PYTHONPATH; @@ -100,6 +102,11 @@ export class LanguageClientMiddlewareBase implements Middleware { }, }; + // eslint-disable-next-line class-methods-use-this + protected async getPythonPathOverride(_uri: Uri | undefined): Promise { + return undefined; + } + private get connected(): Promise { return this.connectedPromise.promise; } diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 7518666aa9d0..49573315e1ef 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -6,10 +6,15 @@ import { IWorkspaceService } from '../../common/application/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; +import { LspNotebooksExperiment } from './lspNotebooksExperiment'; export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor(lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService) { + constructor( + lsOutputChannel: ILanguageServerOutputChannel, + workspace: IWorkspaceService, + private readonly lspNotebooksExperiment: LspNotebooksExperiment, + ) { super(lsOutputChannel, workspace); } @@ -18,6 +23,7 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt return ({ experimentationSupport: true, trustedWorkspaceSupport: true, + lspNotebooksSupport: this.lspNotebooksExperiment.isInNotebooksExperiment(), } as unknown) as LanguageClientOptions; } } diff --git a/src/client/activation/node/languageClientMiddleware.ts b/src/client/activation/node/languageClientMiddleware.ts new file mode 100644 index 000000000000..5c124ef461ba --- /dev/null +++ b/src/client/activation/node/languageClientMiddleware.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IJupyterExtensionDependencyManager } from '../../common/application/types'; +import { IServiceContainer } from '../../ioc/types'; +import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; +import { traceLog } from '../../logging'; +import { LanguageClientMiddleware } from '../languageClientMiddleware'; + +import { LanguageServerType } from '../types'; + +import { LspNotebooksExperiment } from './lspNotebooksExperiment'; + +export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { + private readonly lspNotebooksExperiment: LspNotebooksExperiment; + + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { + super(serviceContainer, LanguageServerType.Node, serverVersion); + + this.lspNotebooksExperiment = serviceContainer.get(LspNotebooksExperiment); + this.setupHidingMiddleware(serviceContainer); + } + + protected shouldCreateHidingMiddleware(jupyterDependencyManager: IJupyterExtensionDependencyManager): boolean { + return ( + super.shouldCreateHidingMiddleware(jupyterDependencyManager) && + !this.lspNotebooksExperiment.isInNotebooksExperiment() + ); + } + + protected async onExtensionChange(jupyterDependencyManager: IJupyterExtensionDependencyManager): Promise { + if (jupyterDependencyManager && jupyterDependencyManager.isJupyterExtensionInstalled) { + await this.lspNotebooksExperiment.onJupyterInstalled(); + } + + super.onExtensionChange(jupyterDependencyManager); + } + + protected async getPythonPathOverride(uri: Uri | undefined): Promise { + if (!uri || !this.lspNotebooksExperiment.isInNotebooksExperiment()) { + return undefined; + } + + const jupyterExtensionIntegration = this.serviceContainer?.get( + JupyterExtensionIntegration, + ); + const jupyterPythonPathFunction = jupyterExtensionIntegration?.getJupyterPythonPathFunction(); + if (!jupyterPythonPathFunction) { + return undefined; + } + + const result = await jupyterPythonPathFunction(uri); + + if (result) { + traceLog(`Jupyter provided interpreter path override: ${result}`); + } + + return result; + } +} diff --git a/src/client/activation/node/lspNotebooksExperiment.ts b/src/client/activation/node/lspNotebooksExperiment.ts new file mode 100644 index 000000000000..a94536cad1d8 --- /dev/null +++ b/src/client/activation/node/lspNotebooksExperiment.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import * as semver from 'semver'; +import { Disposable, extensions } from 'vscode'; +import { IConfigurationService } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IExtensionSingleActivationService, LanguageServerType } from '../types'; +import { traceLog, traceVerbose } from '../../logging'; +import { IJupyterExtensionDependencyManager } from '../../common/application/types'; +import { ILanguageServerWatcher } from '../../languageServer/types'; +import { IServiceContainer } from '../../ioc/types'; +import { sleep } from '../../common/utils/async'; +import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; + +@injectable() +export class LspNotebooksExperiment implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + private pylanceExtensionChangeHandler: Disposable | undefined; + + private isJupyterInstalled = false; + + private isInExperiment: boolean | undefined; + + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IJupyterExtensionDependencyManager) jupyterDependencyManager: IJupyterExtensionDependencyManager, + ) { + if (!LspNotebooksExperiment.isPylanceInstalled()) { + this.pylanceExtensionChangeHandler = extensions.onDidChange(this.pylanceExtensionsChangeHandler.bind(this)); + } + + this.isJupyterInstalled = jupyterDependencyManager.isJupyterExtensionInstalled; + } + + public async activate(): Promise { + this.updateExperimentSupport(); + } + + public async onJupyterInstalled(): Promise { + if (this.isJupyterInstalled) { + return; + } + + if (LspNotebooksExperiment.jupyterSupportsNotebooksExperiment()) { + await this.waitForJupyterToRegisterPythonPathFunction(); + this.updateExperimentSupport(); + } + + this.isJupyterInstalled = true; + } + + public isInNotebooksExperiment(): boolean { + return this.isInExperiment ?? false; + } + + private updateExperimentSupport(): void { + const wasInExperiment = this.isInExperiment; + const isInTreatmentGroup = this.configurationService.getSettings().pylanceLspNotebooksEnabled; + const languageServerType = this.configurationService.getSettings().languageServer; + + this.isInExperiment = false; + if (languageServerType !== LanguageServerType.Node) { + traceLog(`LSP Notebooks experiment is disabled -- not using Pylance`); + } else if (!isInTreatmentGroup) { + traceLog(`LSP Notebooks experiment is disabled -- not in treatment group`); + } else if (!LspNotebooksExperiment.isJupyterInstalled()) { + traceLog(`LSP Notebooks experiment is disabled -- Jupyter disabled or not installed`); + } else if (!LspNotebooksExperiment.jupyterSupportsNotebooksExperiment()) { + traceLog(`LSP Notebooks experiment is disabled -- Jupyter does not support experiment`); + } else if (!LspNotebooksExperiment.isPylanceInstalled()) { + traceLog(`LSP Notebooks experiment is disabled -- Pylance disabled or not installed`); + } else if (!LspNotebooksExperiment.pylanceSupportsNotebooksExperiment()) { + traceLog(`LSP Notebooks experiment is disabled -- Pylance does not support experiment`); + } else { + this.isInExperiment = true; + traceLog(`LSP Notebooks experiment is enabled`); + } + + if (this.isInExperiment) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_LSP_NOTEBOOKS); + } + + // Our "in experiment" status can only change from false to true. That's possible if Pylance + // or Jupyter is installed after Python is activated. A true to false transition would require + // either Pylance or Jupyter to be uninstalled or downgraded after Python activated, and that + // would require VS Code to be reloaded before the new extension version could be used. + if (wasInExperiment === false && this.isInExperiment === true) { + const watcher = this.serviceContainer.get(ILanguageServerWatcher); + if (watcher) { + watcher.restartLanguageServers(); + } + } + } + + private static jupyterSupportsNotebooksExperiment(): boolean { + const jupyterVersion = extensions.getExtension(JUPYTER_EXTENSION_ID)?.packageJSON.version; + return ( + jupyterVersion && + (semver.gt(jupyterVersion, '2022.5.1001391015') || semver.eq(jupyterVersion, '2022.4.100')) + ); + } + + private static pylanceSupportsNotebooksExperiment(): boolean { + const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version; + return ( + pylanceVersion && + (semver.gte(pylanceVersion, '2022.5.3-pre.1') || semver.prerelease(pylanceVersion)?.includes('dev')) + ); + } + + private async waitForJupyterToRegisterPythonPathFunction(): Promise { + const jupyterExtensionIntegration = this.serviceContainer.get( + JupyterExtensionIntegration, + ); + + let success = false; + for (let tryCount = 0; tryCount < 20; tryCount += 1) { + const jupyterPythonPathFunction = jupyterExtensionIntegration.getJupyterPythonPathFunction(); + if (jupyterPythonPathFunction) { + traceVerbose(`Jupyter called registerJupyterPythonPathFunction`); + success = true; + break; + } + + await sleep(500); + } + + if (!success) { + traceVerbose(`Timed out waiting for Jupyter to call registerJupyterPythonPathFunction`); + } + } + + private static isPylanceInstalled(): boolean { + return !!extensions.getExtension(PYLANCE_EXTENSION_ID); + } + + private static isJupyterInstalled(): boolean { + return !!extensions.getExtension(JUPYTER_EXTENSION_ID); + } + + private async pylanceExtensionsChangeHandler(): Promise { + if (LspNotebooksExperiment.isPylanceInstalled() && this.pylanceExtensionChangeHandler) { + this.pylanceExtensionChangeHandler.dispose(); + this.pylanceExtensionChangeHandler = undefined; + + this.updateExperimentSupport(); + } + } +} diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index e2a67f6fecdb..63c866748ce6 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -10,13 +10,8 @@ import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { Commands } from '../commands'; -import { LanguageClientMiddleware } from '../languageClientMiddleware'; -import { - ILanguageServerAnalysisOptions, - ILanguageServerManager, - ILanguageServerProxy, - LanguageServerType, -} from '../types'; +import { NodeLanguageClientMiddleware } from './languageClientMiddleware'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types'; import { traceDecoratorError, traceDecoratorVerbose } from '../../logging'; import { PYLANCE_EXTENSION_ID } from '../../common/constants'; @@ -25,7 +20,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { private interpreter: PythonEnvironment | undefined; - private middleware: LanguageClientMiddleware | undefined; + private middleware: NodeLanguageClientMiddleware | undefined; private disposables: IDisposable[] = []; @@ -122,7 +117,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { @traceDecoratorVerbose('Starting language server') protected async startLanguageServer(): Promise { const options = await this.analysisOptions.getAnalysisOptions(); - this.middleware = new LanguageClientMiddleware(this.serviceContainer, LanguageServerType.Node, this.lsVersion); + this.middleware = new NodeLanguageClientMiddleware(this.serviceContainer, this.lsVersion); options.middleware = this.middleware; // Make sure the middleware is connected if we restart and we we're already connected. diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 53042df2c4de..d7512b533cad 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -16,6 +16,7 @@ import { LoadLanguageServerExtension } from './common/loadLanguageServerExtensio import { PartialModeStatusItem } from './partialModeStatus'; import { ILanguageServerWatcher } from '../languageServer/types'; import { LanguageServerWatcher } from '../languageServer/watcher'; +import { LspNotebooksExperiment } from './node/lspNotebooksExperiment'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); @@ -36,4 +37,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); serviceManager.addBinding(ILanguageServerWatcher, ILanguageServerCache); + serviceManager.addSingleton(LspNotebooksExperiment, LspNotebooksExperiment); + serviceManager.addBinding(LspNotebooksExperiment, IExtensionSingleActivationService); } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 2c095797b1c0..eeeed9e6550b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -116,6 +116,8 @@ export class PythonSettings implements IPythonSettings { public globalModuleInstallation = false; + public pylanceLspNotebooksEnabled = false; + public experiments!: IExperiments; public languageServer: LanguageServerType = LanguageServerType.Node; @@ -294,6 +296,7 @@ export class PythonSettings implements IPythonSettings { this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; + this.pylanceLspNotebooksEnabled = pythonSettings.get('pylanceLspNotebooksEnabled') === true; const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; if (this.sortImports) { diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 84ce8e753ca5..d83420548955 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -190,6 +190,7 @@ export interface IPythonSettings { readonly envFile: string; readonly disableInstallationChecks: boolean; readonly globalModuleInstallation: boolean; + readonly pylanceLspNotebooksEnabled: boolean; readonly onDidChange: Event; readonly experiments: IExperiments; readonly languageServer: LanguageServerType; diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 0d41e98505c0..63e3194891b8 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -157,6 +157,8 @@ type PythonApiForJupyterExtension = { resource: Resource, interpreter?: PythonEnvironment, ): Promise; + + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; }; type JupyterExtensionApi = { @@ -182,6 +184,8 @@ type JupyterExtensionApi = { export class JupyterExtensionIntegration { private jupyterExtension: Extension | undefined; + private jupyterPythonPathFunction: ((uri: Uri) => Promise) | undefined; + constructor( @inject(IExtensions) private readonly extensions: IExtensions, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @@ -275,6 +279,8 @@ export class JupyterExtensionIntegration { getCondaVersion: () => this.condaService.getCondaVersion(), getEnvironmentActivationShellCommands: (resource: Resource, interpreter?: PythonEnvironment) => this.envActivation.getEnvironmentActivationShellCommands(resource, interpreter), + registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => + this.registerJupyterPythonPathFunction(func), }); return undefined; } @@ -320,4 +326,12 @@ export class JupyterExtensionIntegration { } return undefined; } + + private registerJupyterPythonPathFunction(func: (uri: Uri) => Promise) { + this.jupyterPythonPathFunction = func; + } + + public getJupyterPythonPathFunction(): ((uri: Uri) => Promise) | undefined { + return this.jupyterPythonPathFunction; + } } diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index 6c1791a72414..faa1bb75c4bc 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -5,6 +5,7 @@ import { promptForPylanceInstall } from '../activation/common/languageServerChan import { NodeLanguageServerAnalysisOptions } from '../activation/node/analysisOptions'; import { NodeLanguageClientFactory } from '../activation/node/languageClientFactory'; import { NodeLanguageServerProxy } from '../activation/node/languageServerProxy'; +import { LspNotebooksExperiment } from '../activation/node/lspNotebooksExperiment'; import { NodeLanguageServerManager } from '../activation/node/manager'; import { ILanguageServerOutputChannel } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; @@ -50,10 +51,15 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities fileSystem: IFileSystem, private readonly extensions: IExtensions, readonly applicationShell: IApplicationShell, + lspNotebooksExperiment: LspNotebooksExperiment, ) { super(); - this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); + this.analysisOptions = new NodeLanguageServerAnalysisOptions( + outputChannel, + workspaceService, + lspNotebooksExperiment, + ); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); this.serverProxy = new NodeLanguageServerProxy( this.clientFactory, diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts index f0376bf81cbf..6fbeca379a86 100644 --- a/src/client/languageServer/types.ts +++ b/src/client/languageServer/types.ts @@ -14,6 +14,7 @@ export interface ILanguageServerWatcher { readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; readonly languageServerType: LanguageServerType; startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; + restartLanguageServers(): Promise; } export interface ILanguageServerCapabilities extends ILanguageServer { diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index b3f01a97a672..3188c077cdd6 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -34,6 +34,7 @@ import { JediLSExtensionManager } from './jediLSExtensionManager'; import { NoneLSExtensionManager } from './noneLSExtensionManager'; import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; +import { LspNotebooksExperiment } from '../activation/node/lspNotebooksExperiment'; const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @@ -74,6 +75,7 @@ export class LanguageServerWatcher @inject(IFileSystem) private readonly fileSystem: IFileSystem, @inject(IExtensions) private readonly extensions: IExtensions, @inject(IApplicationShell) readonly applicationShell: IApplicationShell, + @inject(LspNotebooksExperiment) private readonly lspNotebooksExperiment: LspNotebooksExperiment, @inject(IDisposableRegistry) readonly disposables: IDisposableRegistry, ) { this.workspaceInterpreters = new Map(); @@ -184,6 +186,14 @@ export class LanguageServerWatcher return languageServerExtensionManager; } + public async restartLanguageServers(): Promise { + this.workspaceLanguageServers.forEach(async (_, resourceString) => { + const resource = Uri.parse(resourceString); + this.stopLanguageServer(resource); + await this.startLanguageServer(this.languageServerType, resource); + }); + } + // ILanguageServerCache public async get(resource?: Resource): Promise { @@ -239,6 +249,7 @@ export class LanguageServerWatcher this.fileSystem, this.extensions, this.applicationShell, + this.lspNotebooksExperiment, ); break; case LanguageServerType.None: diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 81305350b958..67117fe0c167 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -61,6 +61,7 @@ export enum EventName { UNITTEST_DISABLED = 'UNITTEST.DISABLED', PYTHON_EXPERIMENTS_INIT_PERFORMANCE = 'PYTHON_EXPERIMENTS_INIT_PERFORMANCE', + PYTHON_EXPERIMENTS_LSP_NOTEBOOKS = 'PYTHON_EXPERIMENTS_LSP_NOTEBOOKS', PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS = 'PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS', EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index c2daec3dab5c..70127c684721 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1409,6 +1409,15 @@ export interface IEventNamePropertyMapping { "create_new_file_command" : { "owner": "luabud" } */ [EventName.CREATE_NEW_FILE_COMMAND]: unknown; + /** + * Telemetry event sent when the installed versions of Python, Jupyter, and Pylance are all capable + * of supporting the LSP notebooks experiment. This does not indicate that the experiment is enabled. + */ + + /* __GDPR__ + "python_experiments_lsp_notebooks" : { "owner": "luabud" } + */ + [EventName.PYTHON_EXPERIMENTS_LSP_NOTEBOOKS]: unknown; /** * Telemetry event sent once on session start with details on which experiments are opted into and opted out from. */ diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index 3e14130c650e..0518fac170e9 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -6,6 +6,7 @@ import { WorkspaceFolder } from 'vscode'; import { DocumentFilter } from 'vscode-languageclient/node'; import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions'; +import { LspNotebooksExperiment } from '../../../client/activation/node/lspNotebooksExperiment'; import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; @@ -31,6 +32,7 @@ suite('Pylance Language Server - Analysis Options', () => { let outputChannel: IOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; + let lspNotebooksExperiment: typemoq.IMock; setup(() => { outputChannel = typemoq.Mock.ofType().object; @@ -38,7 +40,9 @@ suite('Pylance Language Server - Analysis Options', () => { workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); lsOutputChannel = typemoq.Mock.ofType(); lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); - analysisOptions = new TestClass(lsOutputChannel.object, workspace.object); + lspNotebooksExperiment = typemoq.Mock.ofType(); + lspNotebooksExperiment.setup((l) => l.isInNotebooksExperiment()).returns(() => false); + analysisOptions = new TestClass(lsOutputChannel.object, workspace.object, lspNotebooksExperiment.object); }); test('Workspace folder is undefined', () => { diff --git a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts index 418aa7812795..1a51c93d4783 100644 --- a/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts +++ b/src/test/languageServer/pylanceLSExtensionManager.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import { LspNotebooksExperiment } from '../../client/activation/node/lspNotebooksExperiment'; import { ILanguageServerOutputChannel } from '../../client/activation/types'; import { IWorkspaceService, ICommandManager, IApplicationShell } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; @@ -37,6 +38,7 @@ suite('Language Server - Pylance LS extension manager', () => { {} as IFileSystem, {} as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, ); }); @@ -66,6 +68,7 @@ suite('Language Server - Pylance LS extension manager', () => { getExtension: () => ({}), } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, ); const result = manager.canStartLanguageServer(); @@ -93,6 +96,7 @@ suite('Language Server - Pylance LS extension manager', () => { getExtension: () => undefined, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, ); const result = manager.canStartLanguageServer(); diff --git a/src/test/languageServer/watcher.unit.test.ts b/src/test/languageServer/watcher.unit.test.ts index 6eac31dd2d83..18620a57fc6c 100644 --- a/src/test/languageServer/watcher.unit.test.ts +++ b/src/test/languageServer/watcher.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { ConfigurationChangeEvent, Disposable, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { JediLanguageServerManager } from '../../client/activation/jedi/manager'; +import { LspNotebooksExperiment } from '../../client/activation/node/lspNotebooksExperiment'; import { NodeLanguageServerManager } from '../../client/activation/node/manager'; import { ILanguageServerOutputChannel, LanguageServerType } from '../../client/activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; @@ -75,6 +76,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); }); @@ -124,6 +126,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, disposables, ); @@ -171,6 +174,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, disposables, ); @@ -245,6 +249,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -319,6 +324,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -397,6 +403,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -466,6 +473,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -528,6 +536,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -591,6 +600,7 @@ suite('Language server watcher', () => { ({ showWarningMessage: () => Promise.resolve(undefined), } as unknown) as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -648,6 +658,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -736,6 +747,7 @@ suite('Language server watcher', () => { ({ showWarningMessage: () => Promise.resolve(undefined), } as unknown) as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -814,6 +826,7 @@ suite('Language server watcher', () => { ({ showWarningMessage: () => Promise.resolve(undefined), } as unknown) as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -901,6 +914,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -980,6 +994,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -1062,6 +1077,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], ); @@ -1144,6 +1160,7 @@ suite('Language server watcher', () => { }, } as unknown) as IExtensions, {} as IApplicationShell, + {} as LspNotebooksExperiment, [] as Disposable[], );