diff --git a/news/1 Enhancements/13122.md b/news/1 Enhancements/13122.md new file mode 100644 index 000000000000..2d36013ce0d7 --- /dev/null +++ b/news/1 Enhancements/13122.md @@ -0,0 +1 @@ +Expose `Pylance` setting in `python.languageServer`. If [Pylance extension](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) is not installed, prompt user to install it. diff --git a/package.json b/package.json index 5292fca5fa28..04cdec81a26c 100644 --- a/package.json +++ b/package.json @@ -2319,6 +2319,7 @@ "type": "string", "enum": [ "Jedi", + "Pylance", "Microsoft", "None" ], diff --git a/package.nls.it.json b/package.nls.it.json index 864c50099f70..c16d6ce74241 100644 --- a/package.nls.it.json +++ b/package.nls.it.json @@ -27,5 +27,5 @@ "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid", "python.snippet.launch.attach.label": "Python: Allega", - "LanguageService.bannerLabelYes": "Sì, prenderò il sondaggio ora" + "ExtensionSurveyBanner.bannerLabelYes": "Sì, prenderò il sondaggio ora" } diff --git a/package.nls.json b/package.nls.json index f4963ea49b25..04201a6c0cc5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -109,14 +109,12 @@ "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.pyramid.label": "Python: Pyramid Application", - "LanguageService.proposePylanceMessage": "Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.", - "LanguageService.tryItNow": "Try it now", - "LanguageService.remindMeLater": "Remind me later", - "LanguageService.bannerLabelYes": "Yes, take survey now", - "LanguageService.bannerLabelNo": "No, thanks", - "LanguageService.lsFailedToStart": "We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", - "LanguageService.lsFailedToDownload": "We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", - "LanguageService.lsFailedToExtract": "We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", + "Pylance.proposePylanceMessage": "Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.", + "Pylance.tryItNow": "Try it now", + "Pylance.remindMeLater": "Remind me later", + "Pylance.installPylanceMessage": "Pylance extension is not installed. Click Yes to open Pylance installation page.", + "Pylance.pylanceNotInstalledMessage": "Pylance extension is not installed.", + "Pylance.pylanceInstalledReloadPromptMessage": "Pylance extension is now installed. Reload window to activate?", "DataScience.unknownMimeTypeFormat": "Mime type {0} is not currently supported.", "DataScience.historyTitle": "Python Interactive", "DataScience.dataExplorerTitle": "Data Viewer", @@ -198,6 +196,8 @@ "OutputChannelNames.pythonTest": "Python Test Log", "OutputChannelNames.jupyter": "Jupyter", "ExtensionSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python extension is working for you?", + "ExtensionSurveyBanner.bannerLabelYes": "Yes, take survey now", + "ExtensionSurveyBanner.bannerLabelNo": "No, thanks", "ExtensionSurveyBanner.maybeLater": "Maybe later", "ExtensionChannels.installingInsidersMessage": "Installing Insiders... ", "ExtensionChannels.installingStableMessage": "Installing Stable... ", @@ -354,11 +354,19 @@ "Testing.configureTests": "Configure Test Framework", "Testing.disableTests": "Disable Tests", "Common.openOutputPanel": "Show output", + "LanguageService.lsFailedToStart": "We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.lsFailedToDownload": "We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.lsFailedToExtract": "We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", "LanguageService.downloadFailedOutputMessage": "Language server download failed", "LanguageService.extractionFailedOutputMessage": "Language server extraction failed", "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.startingJedi": "Starting Jedi Python language engine.", + "LanguageService.startingMicrosoft": "Starting Microsoft Python language server.", + "LanguageService.startingPylance": "Starting Pylance language server.", + "LanguageService.startingNone": "Editor support is inactive since language server is set to None.", + "LanguageService.reloadAfterLanguageServerChange": "Please reload the window switching between language servers.", "AttachProcess.unsupportedOS": "Operating system '{0}' not supported.", "AttachProcess.attachTitle": "Attach to process", "AttachProcess.selectProcessPlaceholder": "Select the process to attach to", @@ -564,5 +572,5 @@ "DataScience.interactiveWindowModeBannerTitle" : "Do you want to open a new Python Interactive window for this file? [More Information](command:workbench.action.openSettings?%5B%22python.dataScience.interactiveWindowMode%22%5D).", "DataScience.interactiveWindowModeBannerSwitchYes" : "Yes", "DataScience.interactiveWindowModeBannerSwitchAlways" : "Always", - "DataScience.interactiveWindowModeBannerSwitchNo" : "No" + "DataScience.interactiveWindowModeBannerSwitchNo" : "No" } diff --git a/package.nls.nl.json b/package.nls.nl.json index 6329f4089f0f..ba4968e78535 100644 --- a/package.nls.nl.json +++ b/package.nls.nl.json @@ -46,8 +46,8 @@ "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.pyramid.label": "Python: Pyramid-applicatie", "python.snippet.launch.attach.label": "Python: aankoppelen", - "LanguageService.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", - "LanguageService.bannerLabelNo": "Nee, bedankt", + "ExtensionSurveyBanner.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", + "ExtensionSurveyBanner.bannerLabelNo": "Nee, bedankt", "LanguageService.lsFailedToStart": "We zijn een probleem tegengekomen bij het starten van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", "LanguageService.lsFailedToDownload": "We zijn een probleem tegengekomen bij het downloaden van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", "LanguageService.lsFailedToExtract": "We zijn een probleem tegengekomen bij het uitpakken van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", diff --git a/package.nls.ru.json b/package.nls.ru.json index 35da3900fe8d..9b6b0a5661da 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -27,6 +27,15 @@ "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", "python.snippet.launch.attach.label": "Python: Подключить отладчик", - "LanguageService.bannerLabelYes": "Да, пройти опрос сейчас", - "LanguageService.bannerLabelNo": "Нет, спасибо" + "ExtensionSurveyBanner.bannerLabelYes": "Да, открыть опрос сейчас", + "ExtensionSurveyBanner.bannerLabelNo": "Нет, спасибо", + "ExtensionSurveyBanner.maybeLater": "Может быть, позже", + "ExtensionSurveyBanner.bannerMessage": "Не могли бы вы потратить пару минут на опрос о языковом сервере Pylance?", + "Pylance.proposePylanceMessage": "Попробуйте новый языковый сервер для Python от Microsoft: Pylance! Установите расширение Pylance.", + "Pylance.tryItNow": "Да, хочу", + "Pylance.remindMeLater": "Напомните позже", + "Pylance.installPylanceMessage": "Расширение Pylance не установлено. Нажмите Да чтобы открыть страницу установки Pylance.", + "Pylance.pylanceNotInstalledMessage": "Расширение Pylance не установлено.", + "Pylance.pylanceInstalledReloadPromptMessage": "Расширение Pylance установлено. Перезагрузить окно для его активации?", + "LanguageService.reloadAfterLanguageServerChange": "Пожалуйста, перезагрузите окно после смены типа языкового сервера." } diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 4b69b11400c4..96eb10983a9a 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -41,8 +41,6 @@ "python.command.python.analysis.clearCache.title": "清除模組分析快取", "python.snippet.launch.module.default": "請輸入-模組-名稱", "python.snippet.launch.attachpid.label": "Python: 使用處理程序 ID 連結", - "LanguageService.bannerLabelYes": "是,現在填寫調查", - "LanguageService.bannerLabelNo": "不了,謝謝", "LanguageService.lsFailedToStart": "啟動語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", "LanguageService.lsFailedToDownload": "下載語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", "LanguageService.lsFailedToExtract": "擷取語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", @@ -59,6 +57,8 @@ "OutputChannelNames.pythonTest": "Python 測試記錄", "OutputChannelNames.jupyter": "Jupyter", "ExtensionSurveyBanner.bannerMessage": "請問您是否可以用兩分鐘的時間,告訴我們 Python 延伸模組在您環境中的運作情況?", + "ExtensionSurveyBanner.bannerLabelYes": "是,現在填寫調查", + "ExtensionSurveyBanner.bannerLabelNo": "不了,謝謝", "ExtensionSurveyBanner.maybeLater": "等一下", "ExtensionChannels.installingInsidersMessage": "正在安裝 Insiders... ", "ExtensionChannels.installingStableMessage": "正在安裝穩定版... ", diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 872c38c81b27..1633cf4b96e0 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -7,24 +7,32 @@ import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported'; import { IDiagnosticsService } from '../application/diagnostics/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IWorkspaceService +} from '../common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { traceError } from '../common/logger'; import { IConfigurationService, IDisposableRegistry, + IExtensions, IOutputChannel, IPersistentStateFactory, IPythonSettings, Resource } from '../common/types'; import { swallowExceptions } from '../common/utils/decorators'; +import { LanguageService } from '../common/utils/localize'; import { noop } from '../common/utils/misc'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { PythonInterpreter } from '../pythonEnvironments/info'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { LanguageServerChangeHandler } from './common/languageServerChangeHandler'; import { Commands } from './languageServer/constants'; import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { @@ -50,8 +58,8 @@ export class LanguageServerExtensionActivationService private activatedServer?: IActivatedServer; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; - private readonly appShell: IApplicationShell; private readonly interpreterService: IInterpreterService; + private readonly languageServerChangeHandler: LanguageServerChangeHandler; private resource!: Resource; constructor( @@ -61,7 +69,7 @@ export class LanguageServerExtensionActivationService this.workspaceService = this.serviceContainer.get(IWorkspaceService); this.interpreterService = this.serviceContainer.get(IInterpreterService); this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.appShell = this.serviceContainer.get(IApplicationShell); + const commandManager = this.serviceContainer.get(ICommandManager); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); @@ -71,6 +79,15 @@ export class LanguageServerExtensionActivationService disposables.push( commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCaches.bind(this)) ); + + this.languageServerChangeHandler = new LanguageServerChangeHandler( + this.getCurrentLanguageServerType(), + this.serviceContainer.get(IExtensions), + this.serviceContainer.get(IApplicationShell), + this.serviceContainer.get(IApplicationEnvironment), + this.serviceContainer.get(ICommandManager) + ); + disposables.push(this.languageServerChangeHandler); } public async activate(resource: Resource): Promise { @@ -126,7 +143,8 @@ export class LanguageServerExtensionActivationService this.activatedServer.server.dispose(); } } - @swallowExceptions('Send telemetry for Language Server current selection') + + @swallowExceptions('Send telemetry for language server current selection') public async sendTelemetryForChosenLanguageServer(languageServer: LanguageServerType): Promise { const state = this.stateFactory.createGlobalPersistentState( 'SWITCH_LS', @@ -167,16 +185,6 @@ export class LanguageServerExtensionActivationService ); } - /** - * Checks if user is using Jedi as intellisense - * @returns `true` if user is using jedi, `false` if user is using language server - */ - public useJedi(): boolean { - const configurationService = this.serviceContainer.get(IConfigurationService); - const lstType = configurationService.getSettings(this.resource).languageServer; - return lstType === LanguageServerType.Jedi; - } - protected async onWorkspaceFoldersChanged() { //If an activated workspace folder was removed, dispose its activator const workspaceKeys = await Promise.all( @@ -197,45 +205,30 @@ export class LanguageServerExtensionActivationService return this.activate(this.resource); } + private getCurrentLanguageServerType(): LanguageServerType { + const configurationService = this.serviceContainer.get(IConfigurationService); + return configurationService.getSettings(this.resource).languageServer; + } + private async createRefCountedServer( resource: Resource, interpreter: PythonInterpreter | undefined, key: string ): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - let serverType = configurationService.getSettings(this.resource).languageServer; - if (!serverType) { - serverType = LanguageServerType.Jedi; - } - - switch (serverType) { - case LanguageServerType.None: - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_NONE, undefined, undefined); - break; - case LanguageServerType.Node: - // No telemetry in development phase. - break; - case LanguageServerType.Microsoft: - if (this.useJedi()) { - serverType = LanguageServerType.Jedi; - break; - } - const lsNotSupportedDiagnosticService = this.serviceContainer.get( - IDiagnosticsService, - LSNotSupportedDiagnosticServiceId - ); - const diagnostic = await lsNotSupportedDiagnosticService.diagnose(undefined); - lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); - if (diagnostic.length) { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { - supported: false - }); - serverType = LanguageServerType.Jedi; - } - break; - default: + let serverType = this.getCurrentLanguageServerType(); + if (serverType === LanguageServerType.Microsoft) { + const lsNotSupportedDiagnosticService = this.serviceContainer.get( + IDiagnosticsService, + LSNotSupportedDiagnosticServiceId + ); + const diagnostic = await lsNotSupportedDiagnosticService.diagnose(undefined); + lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); + if (diagnostic.length) { + sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { + supported: false + }); serverType = LanguageServerType.Jedi; - break; + } } this.sendTelemetryForChosenLanguageServer(serverType).ignoreErrors(); @@ -248,7 +241,7 @@ export class LanguageServerExtensionActivationService if (serverType === LanguageServerType.Jedi) { throw ex; } - await this.logStartup(serverType); + this.output.appendLine(LanguageService.lsFailedToStart()); serverType = LanguageServerType.Jedi; server = this.serviceContainer.get(ILanguageServerActivator, serverType); await server.start(resource, interpreter); @@ -268,16 +261,16 @@ export class LanguageServerExtensionActivationService let outputLine; switch (serverType) { case LanguageServerType.Jedi: - outputLine = 'Starting Jedi Python language engine.'; + outputLine = LanguageService.startingJedi(); break; case LanguageServerType.Microsoft: - outputLine = 'Starting Microsoft Python language server.'; + outputLine = LanguageService.startingMicrosoft(); break; case LanguageServerType.Node: - outputLine = 'Starting Pylance language server.'; + outputLine = LanguageService.startingPylance(); break; case LanguageServerType.None: - outputLine = 'Editor support is inactive since language server is set to None.'; + outputLine = LanguageService.startingNone(); break; default: throw new Error('Unknown langauge server type in activator.'); @@ -285,7 +278,7 @@ export class LanguageServerExtensionActivationService this.output.appendLine(outputLine); } - private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map((workspace) => workspace.uri) : [undefined]; @@ -294,26 +287,12 @@ export class LanguageServerExtensionActivationService ) { return; } - const jedi = this.useJedi(); - if (this.activatedServer) { - if (this.activatedServer.jedi === jedi) { - return; - } - const configurationService = this.serviceContainer.get(IConfigurationService); - const lsType = configurationService.getSettings(this.resource).languageServer; - if (this.activatedServer.key === lsType) { - return; - } - } - - const item = await this.appShell.showInformationMessage( - 'Please reload the window switching between language engines.', - 'Reload' - ); - if (item === 'Reload') { - this.serviceContainer.get(ICommandManager).executeCommand('workbench.action.reloadWindow'); + const lsType = this.getCurrentLanguageServerType(); + if (this.activatedServer?.key !== lsType) { + await this.languageServerChangeHandler.handleLanguageServerChange(lsType); } } + private async getKey(resource: Resource, interpreter?: PythonInterpreter): Promise { const configurationService = this.serviceContainer.get(IConfigurationService); const serverType = configurationService.getSettings(this.resource).languageServer; diff --git a/src/client/activation/common/activatorBase.ts b/src/client/activation/common/activatorBase.ts index a5ad15f521c0..c267d0c0bc59 100644 --- a/src/client/activation/common/activatorBase.ts +++ b/src/client/activation/common/activatorBase.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; import { CancellationToken, CodeLens, @@ -29,14 +28,8 @@ import { IWorkspaceService } from '../../common/application/types'; import { traceDecorators } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, Resource } from '../../common/types'; -import { EXTENSION_ROOT_DIR } from '../../constants'; import { PythonInterpreter } from '../../pythonEnvironments/info'; -import { - ILanguageServerActivator, - ILanguageServerDownloader, - ILanguageServerFolderService, - ILanguageServerManager -} from '../types'; +import { ILanguageServerActivator, ILanguageServerManager } from '../types'; /** * Starts the language server managers per workspaces (currently one for first workspace). @@ -52,8 +45,6 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi protected readonly manager: ILanguageServerManager, private readonly workspace: IWorkspaceService, protected readonly fs: IFileSystem, - protected readonly lsDownloader: ILanguageServerDownloader, - protected readonly languageServerFolderService: ILanguageServerFolderService, protected readonly configurationService: IConfigurationService ) {} @@ -171,23 +162,6 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi return this.handleProvideSignatureHelp(document, position, token, context); } - protected async ensureLanguageServerFileIsAvailable( - resource: Resource, - fileName: string - ): Promise { - const settings = this.configurationService.getSettings(resource); - if (!settings.downloadLanguageServer) { - return; - } - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, fileName); - if (!(await this.fs.fileExists(mscorlib))) { - await this.lsDownloader.downloadLanguageServer(languageServerFolderPath, resource); - } - return languageServerFolderPath; - } - protected getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { const proxy = this.manager.languageProxy; if (proxy) { diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts new file mode 100644 index 000000000000..c154ba83b1fb --- /dev/null +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell, ICommandManager } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IExtensions } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { Common, LanguageService, Pylance } from '../../common/utils/localize'; +import { getPylanceExtensionUri } from '../../languageServices/proposeLanguageServerBanner'; +import { LanguageServerType } from '../types'; + +export async function promptForPylanceInstall( + appShell: IApplicationShell, + appEnv: IApplicationEnvironment +): Promise { + // If not installed, point user to Pylance at the store. + const response = await appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ); + + if (response === Common.bannerLabelYes()) { + appShell.openUrl(getPylanceExtensionUri(appEnv)); + } +} + +// Tracks language server type and issues appropriate reload or install prompts. +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( + private currentLsType: LanguageServerType | undefined, + private readonly extensions: IExtensions, + private readonly appShell: IApplicationShell, + private readonly appEnv: IApplicationEnvironment, + private readonly commands: ICommandManager + ) { + this.pylanceInstalled = this.isPylanceInstalled(); + this.disposables.push( + extensions.onDidChange(async () => { + await this.extensionsChangeHandler(); + }) + ); + } + + public dispose(): void { + while (this.disposables.length) { + this.disposables.pop()?.dispose(); + } + } + + // For tests that need to track Pylance install completion. + get pylanceInstallCompleted(): Promise { + return this.pylanceInstallCompletedDeferred.promise; + } + + public async handleLanguageServerChange(lsType: LanguageServerType | undefined): Promise { + if (this.currentLsType === lsType) { + return; + } + // VS Code has to be reloaded when language server type changes. In case of Pylance + // it also has to be installed manually by the user. We avoid prompting to reload + // if target changes to Pylance when Pylance is not installed since otherwise user + // 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.appEnv); + // At this point Pylance is not yet installed. Skip reload prompt + // since we are going to show it when Pylance becomes available. + } else { + response = await this.appShell.showInformationMessage( + LanguageService.reloadAfterLanguageServerChange(), + Common.reload() + ); + if (response === Common.reload()) { + this.commands.executeCommand('workbench.action.reloadWindow'); + } + } + this.currentLsType = lsType; + } + + private async extensionsChangeHandler(): Promise { + // Track Pylance extension installation state and prompt to reload when it becomes available. + const oldInstallState = this.pylanceInstalled; + this.pylanceInstalled = this.isPylanceInstalled(); + if (oldInstallState === this.pylanceInstalled) { + this.pylanceInstallCompletedDeferred.resolve(); + return; + } + + const response = await this.appShell.showWarningMessage( + Pylance.pylanceInstalledReloadPromptMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ); + + this.pylanceInstallCompletedDeferred.resolve(); + if (response === Common.bannerLabelYes()) { + this.commands.executeCommand('workbench.action.reloadWindow'); + } + } + + private isPylanceInstalled(): boolean { + return !!this.extensions.getExtension(PYLANCE_EXTENSION_ID); + } +} diff --git a/src/client/activation/common/languageServerFolderService.ts b/src/client/activation/common/languageServerFolderService.ts index bcf34b70aa7f..ae501d8b709e 100644 --- a/src/client/activation/common/languageServerFolderService.ts +++ b/src/client/activation/common/languageServerFolderService.ts @@ -53,7 +53,7 @@ export abstract class LanguageServerFolderService implements ILanguageServerFold return `${this.languageServerFolder}.${serverVersion!.version.raw}`; } - @traceDecorators.verbose('Get latest version of Language Server') + @traceDecorators.verbose('Get latest version of language server') public getLatestLanguageServerVersion(resource: Resource): Promise { const minVersion = this.getMinimalLanguageServerVersion(); const lsPackageService = this.serviceContainer.get( diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index d9ca8551b91d..45750a4c9b4d 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -11,7 +11,7 @@ import '../common/extensions'; import { traceDecorators } from '../common/logger'; import { IPlatformService } from '../common/platform/types'; import { IBrowserService, IExperimentsManager, IPersistentStateFactory, IRandom } from '../common/types'; -import { Common, ExtensionSurveyBanner, LanguageService } from '../common/utils/localize'; +import { Common, ExtensionSurveyBanner } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { IExtensionSingleActivationService } from './types'; @@ -78,7 +78,11 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @traceDecorators.error('Failed to display prompt for extension survey') public async showSurvey() { - const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()]; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ 'Yes', 'Maybe later', @@ -91,7 +95,7 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService if (!selection) { return; } - if (selection === LanguageService.bannerLabelYes()) { + if (selection === ExtensionSurveyBanner.bannerLabelYes()) { this.launchSurvey(); // Disable survey for a few weeks await this.persistentState diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index cc4bb9be9678..68becd50ea66 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -5,8 +5,7 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { IWorkspaceService } from '../../common/application/types'; -import { isTestExecution } from '../../common/constants'; -import { traceDecorators } from '../../common/logger'; +import { EXTENSION_ROOT_DIR, isTestExecution } from '../../common/constants'; import { IFileSystem } from '../../common/platform/types'; import { BANNER_NAME_PROPOSE_LS, IConfigurationService, IPythonExtensionBanner, Resource } from '../../common/types'; import { PythonInterpreter } from '../../pythonEnvironments/info'; @@ -27,14 +26,15 @@ export class DotNetLanguageServerActivator extends LanguageServerActivatorBase { @inject(ILanguageServerManager) manager: ILanguageServerManager, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IFileSystem) fs: IFileSystem, - @inject(ILanguageServerDownloader) lsDownloader: ILanguageServerDownloader, - @inject(ILanguageServerFolderService) languageServerFolderService: ILanguageServerFolderService, + @inject(ILanguageServerDownloader) private readonly lsDownloader: ILanguageServerDownloader, + @inject(ILanguageServerFolderService) + private readonly languageServerFolderService: ILanguageServerFolderService, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IPythonExtensionBanner) @named(BANNER_NAME_PROPOSE_LS) private proposePylancePopup: IPythonExtensionBanner ) { - super(manager, workspace, fs, lsDownloader, languageServerFolderService, configurationService); + super(manager, workspace, fs, configurationService); } public async start(resource: Resource, interpreter?: PythonInterpreter): Promise { @@ -44,7 +44,6 @@ export class DotNetLanguageServerActivator extends LanguageServerActivatorBase { return super.start(resource, interpreter); } - @traceDecorators.error('Failed to ensure language server is available') public async ensureLanguageServerIsAvailable(resource: Resource): Promise { const languageServerFolderPath = await this.ensureLanguageServerFileIsAvailable(resource, 'mscorlib.dll'); if (languageServerFolderPath) { @@ -78,4 +77,24 @@ export class DotNetLanguageServerActivator extends LanguageServerActivatorBase { content.runtimeOptions.configProperties['System.Globalization.Invariant'] = true; await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); } + + private async ensureLanguageServerFileIsAvailable( + resource: Resource, + fileName: string + ): Promise { + const settings = this.configurationService.getSettings(resource); + if (settings.downloadLanguageServer === false) { + // Development mode + return; + } + const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); + if (languageServerFolder) { + const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); + const mscorlib = path.join(languageServerFolderPath, fileName); + if (!(await this.fs.fileExists(mscorlib))) { + await this.lsDownloader.downloadLanguageServer(languageServerFolderPath, resource); + } + return languageServerFolderPath; + } + } } diff --git a/src/client/activation/languageServer/languageServerProxy.ts b/src/client/activation/languageServer/languageServerProxy.ts index 1d7c0cf8d33a..bc89929cfa38 100644 --- a/src/client/activation/languageServer/languageServerProxy.ts +++ b/src/client/activation/languageServer/languageServerProxy.ts @@ -33,7 +33,7 @@ export class DotNetLanguageServerProxy implements ILanguageServerProxy { ) { this.startupCompleted = createDeferred(); } - @traceDecorators.verbose('Stopping Language Server') + @traceDecorators.verbose('Stopping language server') public dispose() { if (this.languageClient) { // Do not await on this. @@ -45,7 +45,7 @@ export class DotNetLanguageServerProxy implements ILanguageServerProxy { d.dispose(); } if (this.startupCompleted.completed) { - this.startupCompleted.reject(new Error('Disposed Language Server')); + this.startupCompleted.reject(new Error('Disposed language server')); this.startupCompleted = createDeferred(); } this.disposed = true; @@ -86,7 +86,7 @@ export class DotNetLanguageServerProxy implements ILanguageServerProxy { await this.startupCompleted.promise; } } - @traceDecorators.error('Failed to load Language Server extension') + @traceDecorators.error('Failed to load language server extension') public loadExtension(args?: {}) { if (this.extensionLoadedArgs.has(args || '')) { return; @@ -111,7 +111,7 @@ export class DotNetLanguageServerProxy implements ILanguageServerProxy { } this.startupCompleted.resolve(); } - @swallowExceptions('Activating Unit Tests Manager for Language Server') + @swallowExceptions('Activating Unit Tests Manager for Microsoft Python Language Server') protected async registerTestServices() { if (!this.languageClient) { throw new Error('languageClient not initialized'); diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 9421d4f4099a..6055a6ea35ad 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -58,10 +58,10 @@ export class DotNetLanguageServerManager implements ILanguageServerManager { public get languageProxy() { return this.languageServerProxy; } - @traceDecorators.error('Failed to start Language Server') + @traceDecorators.error('Failed to start language server') public async start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { if (this.languageProxy) { - throw new Error('Language Server already started'); + throw new Error('Language server already started'); } this.registerCommandHandler(); this.resource = resource; @@ -94,8 +94,8 @@ export class DotNetLanguageServerManager implements ILanguageServerManager { protected restartLanguageServerDebounced(): void { this.restartLanguageServer().ignoreErrors(); } - @traceDecorators.error('Failed to restart Language Server') - @traceDecorators.verbose('Restarting Language Server') + @traceDecorators.error('Failed to restart language server') + @traceDecorators.verbose('Restarting language server') protected async restartLanguageServer(): Promise { if (this.languageProxy) { this.languageProxy.dispose(); @@ -109,7 +109,7 @@ export class DotNetLanguageServerManager implements ILanguageServerManager { undefined, DotNetLanguageServerManager.versionTelemetryProps ) - @traceDecorators.verbose('Starting Language Server') + @traceDecorators.verbose('Starting language server') protected async startLanguageServer(): Promise { this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); diff --git a/src/client/activation/node/activator.ts b/src/client/activation/node/activator.ts index 21bcebc07991..fc438bece25b 100644 --- a/src/client/activation/node/activator.ts +++ b/src/client/activation/node/activator.ts @@ -1,19 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + import { inject, injectable } from 'inversify'; import { CancellationToken, CompletionItem, ProviderResult } from 'vscode'; // tslint:disable-next-line: import-name import ProtocolCompletionItem from 'vscode-languageclient/lib/common/protocolCompletionItem'; import { CompletionResolveRequest } from 'vscode-languageclient/node'; -import { IWorkspaceService } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; +import { IConfigurationService, IExtensions, Resource } from '../../common/types'; +import { Pylance } from '../../common/utils/localize'; import { LanguageServerActivatorBase } from '../common/activatorBase'; -import { ILanguageServerDownloader, ILanguageServerFolderService, ILanguageServerManager } from '../types'; +import { promptForPylanceInstall } from '../common/languageServerChangeHandler'; +import { ILanguageServerManager } from '../types'; /** - * Starts the Node.js-based language server managers per workspaces (currently one for first workspace). + * Starts Pylance language server manager. * * @export * @class NodeLanguageServerActivator @@ -26,16 +29,27 @@ export class NodeLanguageServerActivator extends LanguageServerActivatorBase { @inject(ILanguageServerManager) manager: ILanguageServerManager, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IFileSystem) fs: IFileSystem, - @inject(ILanguageServerDownloader) lsDownloader: ILanguageServerDownloader, - @inject(ILanguageServerFolderService) languageServerFolderService: ILanguageServerFolderService, - @inject(IConfigurationService) configurationService: IConfigurationService + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment ) { - super(manager, workspace, fs, lsDownloader, languageServerFolderService, configurationService); + super(manager, workspace, fs, configurationService); } - @traceDecorators.error('Failed to ensure language server is available') public async ensureLanguageServerIsAvailable(resource: Resource): Promise { - await this.ensureLanguageServerFileIsAvailable(resource, 'server.bundle.js'); + const settings = this.configurationService.getSettings(resource); + if (settings.downloadLanguageServer === false) { + // Development mode. + return; + } + if (!this.extensions.getExtension(PYLANCE_EXTENSION_ID)) { + // Pylance is not yet installed. Throw will cause activator to use Jedi + // temporarily. Language server installation tracker will prompt for window + // reload when Pylance becomes available. + await promptForPylanceInstall(this.appShell, this.appEnv); + throw new Error(Pylance.pylanceNotInstalledMessage()); + } } public resolveCompletionItem(item: CompletionItem, token: CancellationToken): ProviderResult { diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index 4d5bfc9429cd..88ec6661db2c 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -51,7 +51,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { }; } - @traceDecorators.verbose('Stopping Language Server') + @traceDecorators.verbose('Stopping language server') public dispose() { if (this.languageClient) { // Do not await on this. @@ -67,7 +67,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { d.dispose(); } if (this.startupCompleted.completed) { - this.startupCompleted.reject(new Error('Disposed Language Server')); + this.startupCompleted.reject(new Error('Disposed language server')); this.startupCompleted = createDeferred(); } this.disposed = true; @@ -155,7 +155,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { this.startupCompleted.resolve(); } - @swallowExceptions('Activating Unit Tests Manager for Language Server') + @swallowExceptions('Activating Unit Tests Manager for Pylance language server') protected async registerTestServices() { if (!this.languageClient) { throw new Error('languageClient not initialized'); diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts index 6f83029a3a64..14d64d9c1d44 100644 --- a/src/client/activation/node/manager.ts +++ b/src/client/activation/node/manager.ts @@ -58,10 +58,10 @@ export class NodeLanguageServerManager implements ILanguageServerManager { return this.languageServerProxy; } - @traceDecorators.error('Failed to start Language Server') + @traceDecorators.error('Failed to start language server') public async start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { if (this.languageProxy) { - throw new Error('Language Server already started'); + throw new Error('Language server already started'); } this.resource = resource; this.interpreter = interpreter; @@ -89,8 +89,8 @@ export class NodeLanguageServerManager implements ILanguageServerManager { this.restartLanguageServer().ignoreErrors(); } - @traceDecorators.error('Failed to restart Language Server') - @traceDecorators.verbose('Restarting Language Server') + @traceDecorators.error('Failed to restart language server') + @traceDecorators.verbose('Restarting language server') protected async restartLanguageServer(): Promise { if (this.languageProxy) { this.languageProxy.dispose(); @@ -105,7 +105,7 @@ export class NodeLanguageServerManager implements ILanguageServerManager { undefined, NodeLanguageServerManager.versionTelemetryProps ) - @traceDecorators.verbose('Starting Language Server') + @traceDecorators.verbose('Starting language server') protected async startLanguageServer(): Promise { this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); diff --git a/src/client/common/featureDeprecationManager.ts b/src/client/common/featureDeprecationManager.ts index 870743ce0723..79a4af0cd566 100644 --- a/src/client/common/featureDeprecationManager.ts +++ b/src/client/common/featureDeprecationManager.ts @@ -30,7 +30,7 @@ const deprecatedFeatures: DeprecatedFeatureInfo[] = [ { doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_FOR_AUTO_COMPLETE_PRELOAD_MODULES', message: - "The setting 'python.autoComplete.preloadModules' is deprecated, please consider using Microsoft Language Server ('python.languageServer' setting).", + "The setting 'python.autoComplete.preloadModules' is deprecated, please consider using Pylance Language Server ('python.languageServer' setting).", moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/1704', setting: { setting: 'autoComplete.preloadModules' } } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b22c61576890..0e8356651e97 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -100,27 +100,56 @@ export namespace AttachProcess { export const refreshList = localize('AttachProcess.refreshList', 'Refresh process list'); } -export namespace LanguageService { +export namespace Pylance { export const proposePylanceMessage = localize( - 'LanguageService.proposePylanceMessage', + 'Pylance.proposePylanceMessage', 'Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.' ); - export const tryItNow = localize('LanguageService.tryItNow', 'Try it now'); - export const remindMeLater = localize('LanguageService.remindMeLater', 'Remind me later'); + export const tryItNow = localize('Pylance.tryItNow', 'Try it now'); + export const remindMeLater = localize('Pylance.remindMeLater', 'Remind me later'); + + export const installPylanceMessage = localize( + 'Pylance.installPylanceMessage', + 'Pylance extension is not installed. Click Yes to open Pylance installation page.' + ); + export const pylanceNotInstalledMessage = localize( + 'Pylance.pylanceNotInstalledMessage', + 'Pylance extension is not installed.' + ); + export const pylanceInstalledReloadPromptMessage = localize( + 'Pylance.pylanceInstalledReloadPromptMessage', + 'Pylance extension is now installed. Reload window to activate?' + ); +} + +export namespace LanguageService { + export const startingJedi = localize('LanguageService.startingJedi', 'Starting Jedi Python language engine.'); + export const startingMicrosoft = localize( + 'LanguageService.startingMicrosoft', + 'Starting Microsoft Python language server.' + ); + export const startingPylance = localize('LanguageService.startingPylance', 'Starting Pylance language server.'); + export const startingNone = localize( + 'LanguageService.startingNone', + 'Editor support is inactive since language server is set to None.' + ); + + export const reloadAfterLanguageServerChange = localize( + 'LanguageService.reloadAfterLanguageServerChange', + 'Please reload the window switching between language servers.' + ); - export const bannerLabelYes = localize('LanguageService.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('LanguageService.bannerLabelNo', 'No, thanks'); export const lsFailedToStart = localize( 'LanguageService.lsFailedToStart', - 'We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.' + 'We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.' ); export const lsFailedToDownload = localize( 'LanguageService.lsFailedToDownload', - 'We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.' + 'We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.' ); export const lsFailedToExtract = localize( 'LanguageService.lsFailedToExtract', - 'We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.' + 'We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.' ); export const downloadFailedOutputMessage = localize( 'LanguageService.downloadFailedOutputMessage', @@ -307,6 +336,8 @@ export namespace ExtensionSurveyBanner { 'ExtensionSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python extension is working for you?' ); + export const bannerLabelYes = localize('ExtensionSurveyBanner.bannerLabelYes', 'Yes, take survey now'); + export const bannerLabelNo = localize('ExtensionSurveyBanner.bannerLabelNo', 'No, thanks'); export const maybeLater = localize('ExtensionSurveyBanner.maybeLater', 'Maybe later'); } diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 71cb79ecc845..c0d5ef24b7d1 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -413,7 +413,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { return this.combineCompletions( await Promise.all([ - // Ensure we wait for a result from Language Server (assumption is LS is faster). + // Ensure we wait for a result from language server (assumption is LS is faster). // Telemetry will prove/disprove this assumption and we'll change this code accordingly. lsCompletions, // Wait for a max of n ms before ignoring results from jupyter (jupyter completion is generally slower). diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts index dfc792c48e47..13acc4527005 100644 --- a/src/client/languageServices/proposeLanguageServerBanner.ts +++ b/src/client/languageServices/proposeLanguageServerBanner.ts @@ -16,7 +16,7 @@ import { IPersistentStateFactory, IPythonExtensionBanner } from '../common/types'; -import { LanguageService } from '../common/utils/localize'; +import { Common, Pylance } from '../common/utils/localize'; export function getPylanceExtensionUri(appEnv: IApplicationEnvironment): string { return `${appEnv.uriScheme}:extension/${PYLANCE_EXTENSION_ID}`; @@ -66,16 +66,16 @@ export class ProposePylanceBanner implements IPythonExtensionBanner { } const response = await this.appShell.showInformationMessage( - LanguageService.proposePylanceMessage(), - LanguageService.tryItNow(), - LanguageService.bannerLabelNo(), - LanguageService.remindMeLater() + Pylance.proposePylanceMessage(), + Pylance.tryItNow(), + Common.bannerLabelNo(), + Pylance.remindMeLater() ); - if (response === LanguageService.tryItNow()) { + if (response === Pylance.tryItNow()) { this.appShell.openUrl(getPylanceExtensionUri(this.appEnv)); await this.disable(); - } else if (response === LanguageService.bannerLabelNo()) { + } else if (response === Common.bannerLabelNo()) { await this.disable(); } else { this.disabledInCurrentSession = true; diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index add675bb2e76..85e719a96796 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -354,7 +354,7 @@ export class JediProxy implements Disposable { // tslint:disable-next-line:max-func-body-length private async spawnProcess() { if (this.languageServerStarted && !this.languageServerStarted.completed) { - this.languageServerStarted.reject(new Error('Language Server not started.')); + this.languageServerStarted.reject(new Error('Language server not started.')); } this.languageServerStarted = createDeferred(); const pythonProcess = await this.serviceContainer diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 42fb49092b33..f0f38e6ac69a 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -76,7 +76,6 @@ export enum EventName { EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION = 'PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION', - PYTHON_LANGUAGE_SERVER_NONE = 'PYTHON_LANGUAGE_SERVER.NONE', PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES = 'PYTHON_LANGUAGE_SERVER.LIST_BLOB_PACKAGES', PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED', PYTHON_LANGUAGE_SERVER_DOWNLOADED = 'PYTHON_LANGUAGE_SERVER.DOWNLOADED', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 9b0071c21894..e0a855190983 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1287,15 +1287,11 @@ export interface IEventNamePropertyMapping { lsVersion?: string; }; /** - * Telemetry event sent when user specified None to the language server. - */ - [EventName.PYTHON_LANGUAGE_SERVER_NONE]: never | undefined; - /** - * Telemetry sent from Language Server (details of telemetry sent can be provided by LS team) + * Telemetry sent from language server (details of telemetry sent can be provided by LS team) */ [EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY]: any; /** - * Telemetry sent when the client makes a request to the Language Server + * Telemetry sent when the client makes a request to the language server */ [EventName.PYTHON_LANGUAGE_SERVER_REQUEST]: any; /** diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index fea8949e050e..6bc984879488 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -21,6 +21,7 @@ import { IConfigurationService, IDisposable, IDisposableRegistry, + IExtensions, IOutputChannel, IPersistentState, IPersistentStateFactory, @@ -65,6 +66,7 @@ suite('Language Server Activation - ActivationService', () => { 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') @@ -125,6 +127,9 @@ suite('Language Server Activation - ActivationService', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IExtensions))) + .returns(() => extensionsMock.object); serviceContainer .setup((s) => s.get( @@ -191,7 +196,7 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => TypeMoq.Mock.ofType().object) .verifiable(TypeMoq.Times.once()); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + pythonSettings.setup((p) => p.languageServer).returns(() => languageServerType); const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService( serviceContainer.object, @@ -672,6 +677,7 @@ suite('Language Server Activation - ActivationService', () => { 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') @@ -720,6 +726,7 @@ suite('Language Server Activation - ActivationService', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); serviceContainer .setup((s) => s.get( @@ -837,6 +844,7 @@ suite('Language Server Activation - ActivationService', () => { 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') @@ -885,6 +893,7 @@ suite('Language Server Activation - ActivationService', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); serviceContainer .setup((s) => s.get( diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 6668914144a6..d49c566df0bf 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -20,7 +20,7 @@ import { IRandom } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; -import { Common, ExtensionSurveyBanner, LanguageService } from '../../client/common/utils/localize'; +import { Common, ExtensionSurveyBanner } from '../../client/common/utils/localize'; import { OSType } from '../../client/common/utils/platform'; import { sleep } from '../core'; @@ -226,7 +226,11 @@ suite('Extension survey prompt - showSurvey()', () => { const packageJson = { version: 'extensionVersion' }; - const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()]; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; const expectedUrl = `https://aka.ms/AA5rjx5?o=Windows&v=vscodeVersion&e=extensionVersion&m=sessionId`; appEnvironment .setup((a) => a.packageJson) @@ -246,7 +250,7 @@ suite('Extension survey prompt - showSurvey()', () => { .verifiable(TypeMoq.Times.once()); appShell .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) - .returns(() => Promise.resolve(LanguageService.bannerLabelYes())) + .returns(() => Promise.resolve(ExtensionSurveyBanner.bannerLabelYes())) .verifiable(TypeMoq.Times.once()); browserService .setup((s) => s.launch(expectedUrl)) @@ -282,7 +286,11 @@ suite('Extension survey prompt - showSurvey()', () => { }); test("Do nothing if 'Maybe later' option is clicked", async () => { - const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()]; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) @@ -321,7 +329,11 @@ suite('Extension survey prompt - showSurvey()', () => { }); test('Do nothing if no option is clicked', async () => { - const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()]; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) @@ -360,7 +372,11 @@ suite('Extension survey prompt - showSurvey()', () => { }); test("Disable prompt if 'Do not show again' option is clicked", async () => { - const prompts = [LanguageService.bannerLabelYes(), ExtensionSurveyBanner.maybeLater(), Common.doNotShowAgain()]; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts index 1b393fa95488..13452eac1356 100644 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ b/src/test/activation/languageServer/activator.unit.test.ts @@ -6,9 +6,7 @@ import * as path from 'path'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; -import { LanguageServerDownloader } from '../../../client/activation/common/downloader'; import { DotNetLanguageServerActivator } from '../../../client/activation/languageServer/activator'; -import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; import { DotNetLanguageServerManager } from '../../../client/activation/languageServer/manager'; import { ILanguageServerDownloader, @@ -16,20 +14,15 @@ import { ILanguageServerManager } from '../../../client/activation/types'; import { 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 { FileSystem } from '../../../client/common/platform/fileSystem'; import { IFileSystem } from '../../../client/common/platform/types'; import { IConfigurationService, IPythonExtensionBanner, IPythonSettings } from '../../../client/common/types'; import { createDeferred } from '../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -import { ProposePylanceBanner } from '../../../client/languageServices/proposeLanguageServerBanner'; import { sleep } from '../../core'; // tslint:disable:max-func-body-length -suite('Language Server - Activator', () => { +suite('Microsoft Language Server - Activator', () => { let activator: DotNetLanguageServerActivator; let workspaceService: IWorkspaceService; let manager: ILanguageServerManager; @@ -41,13 +34,13 @@ suite('Language Server - Activator', () => { let banner: IPythonExtensionBanner; setup(() => { manager = mock(DotNetLanguageServerManager); - workspaceService = mock(WorkspaceService); - fs = mock(FileSystem); - lsDownloader = mock(LanguageServerDownloader); - lsFolderService = mock(DotNetLanguageServerFolderService); - configuration = mock(ConfigurationService); - settings = mock(PythonSettings); - banner = mock(ProposePylanceBanner); + workspaceService = mock(); + fs = mock(); + lsDownloader = mock(); + lsFolderService = mock(); + configuration = mock(); + settings = mock(); + banner = mock(); when(configuration.getSettings(anything())).thenReturn(instance(settings)); activator = new DotNetLanguageServerActivator( instance(manager), @@ -71,10 +64,10 @@ suite('Language Server - Activator', () => { }); test('Manager must be disposed', async () => { activator.dispose(); - verify(manager.dispose()).once(); }); test('Server should be disconnected but be started', async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(false); await activator.start(undefined); verify(manager.start(undefined, undefined)).once(); @@ -151,11 +144,6 @@ suite('Language Server - Activator', () => { verify(workspaceService.workspaceFolders).once(); }); - test('Manager must be disposed', async () => { - activator.dispose(); - - verify(manager.dispose()).once(); - }); test('Download and check if ICU config exists', async () => { const languageServerFolder = 'Some folder name'; const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); diff --git a/src/test/activation/languageServer/downloadChannelRules.unit.test.ts b/src/test/activation/languageServer/downloadChannelRules.unit.test.ts index 656f19c75f70..2b7c09eddb9e 100644 --- a/src/test/activation/languageServer/downloadChannelRules.unit.test.ts +++ b/src/test/activation/languageServer/downloadChannelRules.unit.test.ts @@ -15,10 +15,10 @@ import { import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; -suite('Language Server Download Channel Rules', () => { +suite('Microsoft Language Server Download Channel Rules', () => { [undefined, path.join('a', 'b')].forEach((currentFolderPath) => { const currentFolder = currentFolderPath ? { path: currentFolderPath, version: new SemVer('0.0.0') } : undefined; - const testSuffix = ` (${currentFolderPath ? 'with' : 'without'} an existing Language Server Folder`; + const testSuffix = ` (${currentFolderPath ? 'with' : 'without'} an existing language server Folder`; test(`Daily channel should always download ${testSuffix}`, async () => { const rule = new DownloadDailyChannelRule(); diff --git a/src/test/activation/languageServer/languageServerPackageService.test.ts b/src/test/activation/languageServer/languageServerPackageService.test.ts index e401d524e428..1101876781e2 100644 --- a/src/test/activation/languageServer/languageServerPackageService.test.ts +++ b/src/test/activation/languageServer/languageServerPackageService.test.ts @@ -26,7 +26,7 @@ suite('Language Server Package Service', () => { setup(() => { serviceContainer = typeMoq.Mock.ofType(); }); - test('Ensure new Major versions of Language Server is accounted for (azure blob)', async () => { + test('Ensure new Major versions of language server is accounted for (azure blob)', async () => { const nugetService = new NugetService(); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService); const platformService = new PlatformService(); diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index 003595e47ad4..5d8f3ac37ab8 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -96,7 +96,7 @@ suite('Language Server - Manager', () => { await startLanguageServer(); await expect(manager.start(resource, undefined)).to.eventually.be.rejectedWith( - 'Language Server already started' + 'Language server already started' ); }); test('Changes in analysis options must restart LS', async () => { diff --git a/src/test/activation/node/activator.unit.test.ts b/src/test/activation/node/activator.unit.test.ts new file mode 100644 index 000000000000..3b87588774eb --- /dev/null +++ b/src/test/activation/node/activator.unit.test.ts @@ -0,0 +1,160 @@ +// 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 { + IApplicationEnvironment, + IApplicationShell, + 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 { Common, Pylance } from '../../../client/common/utils/localize'; + +// tslint:disable:max-func-body-length + +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 appEnv: IApplicationEnvironment; + let extensionsChangedEvent: EventEmitter; + + // tslint:disable-next-line: no-any + let pylanceExtension: Extension; + setup(() => { + manager = mock(NodeLanguageServerManager); + workspaceService = mock(WorkspaceService); + fs = mock(FileSystem); + configuration = mock(ConfigurationService); + settings = mock(PythonSettings); + extensions = mock(); + appShell = mock(); + appEnv = mock(); + when(appEnv.uriScheme).thenReturn('scheme'); + + // tslint:disable-next-line: no-any + pylanceExtension = mock>(); + when(configuration.getSettings(anything())).thenReturn(instance(settings)); + when(appEnv.uriScheme).thenReturn('scheme'); + + 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(appEnv) + ); + }); + teardown(() => { + extensionsChangedEvent.dispose(); + }); + + test('Manager must be started without any workspace', async () => { + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + + await activator.start(undefined); + verify(manager.start(undefined, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).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.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelNo())); + + try { + await activator.start(undefined); + // tslint:disable-next-line: no-empty + } catch {} + verify( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).once(); + verify(appShell.openUrl(`scheme:extension/${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.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelYes())); + + try { + await activator.start(undefined); + // tslint:disable-next-line: no-empty + } catch {} + verify(appShell.openUrl(`scheme:extension/${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.hasWorkspaceFolders).thenReturn(true); + 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.hasWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).once(); + }); +}); diff --git a/src/test/activation/node/languageServerChangeHandler.unit.test.ts b/src/test/activation/node/languageServerChangeHandler.unit.test.ts new file mode 100644 index 000000000000..94b7ef180311 --- /dev/null +++ b/src/test/activation/node/languageServerChangeHandler.unit.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anyString, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Extension } from 'vscode'; +import { LanguageServerChangeHandler } from '../../../client/activation/common/languageServerChangeHandler'; +import { LanguageServerType } from '../../../client/activation/types'; +import { IApplicationEnvironment, IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +import { IExtensions } from '../../../client/common/types'; +import { Common, LanguageService, Pylance } from '../../../client/common/utils/localize'; + +suite('Language Server - Change Handler', () => { + let extensions: IExtensions; + let appShell: IApplicationShell; + let appEnv: IApplicationEnvironment; + let commands: ICommandManager; + let extensionsChangedEvent: EventEmitter; + let handler: LanguageServerChangeHandler; + + // tslint:disable-next-line: no-any + let pylanceExtension: Extension; + setup(() => { + extensions = mock(); + appShell = mock(); + appEnv = mock(); + commands = mock(); + + // tslint:disable-next-line: no-any + pylanceExtension = mock>(); + when(appEnv.uriScheme).thenReturn('scheme'); + + extensionsChangedEvent = new EventEmitter(); + when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); + }); + teardown(() => { + extensionsChangedEvent.dispose(); + handler?.dispose(); + }); + + [undefined, LanguageServerType.None, LanguageServerType.Microsoft, LanguageServerType.Node].forEach(async (t) => { + test(`Handler should do nothing if language server is ${t} and did not change`, async () => { + handler = makeHandler(t); + await handler.handleLanguageServerChange(t); + + verify(extensions.getExtension(anyString())).once(); + verify(appShell.openUrl(anyString())).never(); + verify(appShell.showInformationMessage(anyString(), anyString())).never(); + verify(appShell.showWarningMessage(anyString(), anyString())).never(); + verify(commands.executeCommand(anyString())).never(); + }); + }); + + [LanguageServerType.None, LanguageServerType.Microsoft, 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.Microsoft, 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( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(undefined)); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).never(); + verify( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).once(); + }); + + test('Handler should open Pylance store page when language server changes to Pylance, Pylance is not installed and user clicks Yes', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelYes())); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).once(); + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test('Handler should not open Pylance store page when language server changes to Pylance, Pylance is not installed and user clicks No', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelNo())); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).never(); + 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(); + }); + + function makeHandler(initialLSType: LanguageServerType | undefined): LanguageServerChangeHandler { + return new LanguageServerChangeHandler( + initialLSType, + instance(extensions), + instance(appShell), + instance(appEnv), + instance(commands) + ); + } +}); diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts index 53044c724c5c..5a68406b9f32 100644 --- a/src/test/analysisEngineTest.ts +++ b/src/test/analysisEngineTest.ts @@ -13,7 +13,7 @@ process.env.TEST_FILES_SUFFIX = 'ls.test'; function start() { console.log('*'.repeat(100)); - console.log('Start Language Server tests'); + console.log('Start language server tests'); require('../../node_modules/vscode/bin/test'); } start(); diff --git a/src/test/common/utils/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts index 89461b0ae606..8ccd3cf58926 100644 --- a/src/test/common/utils/localize.functional.test.ts +++ b/src/test/common/utils/localize.functional.test.ts @@ -64,7 +64,7 @@ suite('Localization', () => { // Force a config change setLocale('it'); - const val = localize.LanguageService.bannerLabelYes(); + const val = localize.ExtensionSurveyBanner.bannerLabelYes(); assert.equal(val, 'Sì, prenderò il sondaggio ora', 'bannerLabelYes is not being translated'); done(); }); diff --git a/src/test/linters/linterinfo.unit.test.ts b/src/test/linters/linterinfo.unit.test.ts index 4ec1f5f5e03f..a648c1949d46 100644 --- a/src/test/linters/linterinfo.unit.test.ts +++ b/src/test/linters/linterinfo.unit.test.ts @@ -47,7 +47,7 @@ suite('Linter Info - Pylint', () => { expect(linterInfo.isEnabled()).to.be.true; }); - test('Test disabled when using Language Server and Pylint is not configured', async () => { + test('Test disabled when using language server and Pylint is not configured', async () => { const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); when(config.getSettings(anything())).thenReturn({ @@ -63,7 +63,7 @@ suite('Linter Info - Pylint', () => { expect(linterInfo.isEnabled()).to.be.false; }); - test('Should inspect the value of linting.pylintEnabled when using Language Server', async () => { + test('Should inspect the value of linting.pylintEnabled when using language server', async () => { const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); const inspectStub = sinon.stub(); const pythonConfig = { diff --git a/src/test/performance/load.perf.test.ts b/src/test/performance/load.perf.test.ts index 5521cb11566f..05e3ff90627a 100644 --- a/src/test/performance/load.perf.test.ts +++ b/src/test/performance/load.perf.test.ts @@ -77,7 +77,7 @@ suite('Activation Times', () => { console.log(`Dev version loaded in ${devActivationAvgTime}ms`); console.log(`Release version loaded in ${releaseActivationAvgTime}ms`); - console.log(`Language Server loaded in ${languageServerActivationAvgTime}ms`); + console.log(`Language server loaded in ${languageServerActivationAvgTime}ms`); expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan( AllowedIncreaseInActivationDelayInMS, diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index 938630943570..df2208e19481 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -67,7 +67,7 @@ class TestRunner { // Language server. await this.enableLanguageServer(true); const languageServerLogFile = path.join(logFilesPath, `languageServer_loadtimes${i}.txt`); - console.log(`Start Performance Tests: Counter ${i}, for Release version with Language Server`); + console.log(`Start Performance Tests: Counter ${i}, for Release version with language server`); await this.capturePerfTimes(Version.Release, languageServerLogFile); languageServerLogFiles.push(languageServerLogFile); } diff --git a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts index db4911814901..b7e9c56cd7c2 100644 --- a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts +++ b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts @@ -18,7 +18,7 @@ import { IPersistentStateFactory, IPythonSettings } from '../../../client/common/types'; -import { LanguageService } from '../../../client/common/utils/localize'; +import { Common, Pylance } from '../../../client/common/utils/localize'; import { getPylanceExtensionUri, ProposeLSStateKeys, @@ -47,10 +47,10 @@ suite('Propose Pylance Banner', () => { let appEnv: typemoq.IMock; let settings: typemoq.IMock; - const message = LanguageService.proposePylanceMessage(); - const yes = LanguageService.tryItNow(); - const no = LanguageService.bannerLabelNo(); - const later = LanguageService.remindMeLater(); + const message = Pylance.proposePylanceMessage(); + const yes = Pylance.tryItNow(); + const no = Common.bannerLabelNo(); + const later = Pylance.remindMeLater(); setup(() => { config = typemoq.Mock.ofType();