diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts index abc70e210e1f..db8081172338 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { IDisposable, IDisposableRegistry, Resource } from '../../common/types'; +import { asyncFilter } from '../../common/utils/arrayUtils'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -21,8 +22,8 @@ export abstract class BaseDiagnostic implements IDiagnostic { public readonly severity: DiagnosticSeverity, public readonly scope: DiagnosticScope, public readonly resource: Resource, - public readonly invokeHandler: 'always' | 'default' = 'default', public readonly shouldShowPrompt = true, + public readonly invokeHandler: 'always' | 'default' = 'default', ) {} } @@ -48,7 +49,10 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi if (diagnostics.length === 0) { return; } - const diagnosticsToHandle = diagnostics.filter((item) => { + const diagnosticsToHandle = await asyncFilter(diagnostics, async (item) => { + if (!(await this.canHandle(item))) { + return false; + } if (item.invokeHandler && item.invokeHandler === 'always') { return true; } diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts index d6757d5a844e..440ff16856d3 100644 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -39,7 +39,6 @@ export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, - 'always', shouldShowPrompt, ); } @@ -131,9 +130,6 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { } private async handleDiagnostic(diagnostic: IDiagnostic): Promise { - if (!this.canHandle(diagnostic)) { - return; - } if (!diagnostic.shouldShowPrompt) { await this.fixLaunchJson(diagnostic.code); return; diff --git a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts index add725eac9c0..f08c09956838 100644 --- a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts @@ -38,7 +38,15 @@ class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { | DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, resource: Resource, ) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, 'always'); + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + undefined, + 'always', + ); } } diff --git a/src/client/application/diagnostics/checks/powerShellActivation.ts b/src/client/application/diagnostics/checks/powerShellActivation.ts index f4d445ec042a..4ffdf21a9173 100644 --- a/src/client/application/diagnostics/checks/powerShellActivation.ts +++ b/src/client/application/diagnostics/checks/powerShellActivation.ts @@ -34,6 +34,7 @@ export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic { DiagnosticSeverity.Warning, DiagnosticScope.Global, resource, + undefined, 'always', ); } diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 89d63c230d92..8befefe47bb5 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -6,11 +6,10 @@ import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import '../../../common/extensions'; import * as nls from 'vscode-nls'; +import * as path from 'path'; import { IDisposableRegistry, Resource } from '../../../common/types'; import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; import { DiagnosticCodes } from '../constants'; @@ -23,28 +22,44 @@ import { IDiagnosticMessageOnCloseHandler, } from '../types'; import { Common } from '../../../common/utils/localize'; +import { Commands } from '../../../common/constants'; +import { IWorkspaceService } from '../../../common/application/types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; const localize: nls.LocalizeFunc = nls.loadMessageBundle(); const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: localize( 'DiagnosticCodes.NoPythonInterpretersDiagnostic', - 'Python is not installed. Please download and install Python before using the extension.', + 'No Python interpreter is selected. Please select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', ), - [DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic]: localize( + [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: localize( 'DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic', - 'No Python interpreter is selected. You need to select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', + 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging.', ), }; export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { constructor( - code: - | DiagnosticCodes.NoPythonInterpretersDiagnostic - | DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + code: DiagnosticCodes.NoPythonInterpretersDiagnostic | DiagnosticCodes.InvalidPythonInterpreterDiagnostic, resource: Resource, + workspaceService: IWorkspaceService, + scope = DiagnosticScope.WorkspaceFolder, ) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); + let formatArg = ''; + if ( + workspaceService.workspaceFile && + workspaceService.workspaceFolders && + workspaceService.workspaceFolders?.length > 1 + ) { + // Specify folder name in case of multiroot scenarios + const folder = workspaceService.getWorkspaceFolder(resource); + if (folder) { + formatArg = ` ${localize('Common.forWorkspace', 'for workspace')} ${path.basename(folder.uri.fsPath)}`; + } + } + super(code, messages[code].format(formatArg), DiagnosticSeverity.Error, scope, resource, undefined, 'always'); } } @@ -57,10 +72,7 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { super( - [ - DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - ], + [DiagnosticCodes.NoPythonInterpretersDiagnostic, DiagnosticCodes.InvalidPythonInterpreterDiagnostic], serviceContainer, disposableRegistry, false, @@ -68,26 +80,43 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { } public async diagnose(resource: Resource): Promise { + const workspaceService = this.serviceContainer.get(IWorkspaceService); const interpreterService = this.serviceContainer.get(IInterpreterService); const hasInterpreters = await interpreterService.hasInterpreters(); if (!hasInterpreters) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, resource)]; + return [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + resource, + workspaceService, + DiagnosticScope.Global, + ), + ]; } const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { return [ new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, resource, + workspaceService, ), ]; } - return []; } + public async validateInterpreterPathInSettings(resource: Resource): Promise { + const diagnostics = await this.diagnose(resource); + if (!diagnostics.length) { + return true; + } + this.handle(diagnostics).ignoreErrors(); + return false; + } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { return; @@ -110,33 +139,15 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService { private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - switch (diagnostic.code) { - case DiagnosticCodes.NoPythonInterpretersDiagnostic: { - return [ - { - prompt: Common.download, - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://www.python.org/downloads', - }), - }, - ]; - } - case DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic: { - return [ - { - prompt: Common.selectPythonInterpreter, - command: commandFactory.createCommand(diagnostic, { - type: 'executeVSCCommand', - options: 'python.setInterpreter', - }), - }, - ]; - } - default: { - throw new Error("Invalid diagnostic for 'InvalidPythonInterpreterService'"); - } - } + return [ + { + prompt: Common.selectPythonInterpreter, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.Set_Interpreter, + }), + }, + ]; } } diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index 850b7dab3855..9fdd6ff13723 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -11,7 +11,7 @@ export enum DiagnosticCodes { InvalidPythonPathInDebuggerSettingsDiagnostic = 'InvalidPythonPathInDebuggerSettingsDiagnostic', InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic', EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic', - NoCurrentlySelectedPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic', PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts index 343ba0f02cd3..ced9930c81ab 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -53,6 +53,12 @@ export interface IDiagnosticCommand { export type IDiagnosticMessageOnCloseHandler = (response?: string) => void; +export const IInvalidPythonPathInSettings = Symbol('IInvalidPythonPathInSettings'); + +export interface IInvalidPythonPathInSettings extends IDiagnosticsService { + validateInterpreterPathInSettings(resource: Resource): Promise; +} + export const IInvalidPythonPathInDebuggerService = Symbol('IInvalidPythonPathInDebuggerService'); export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 8fcf78de34df..bbf3f95eaa78 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -18,14 +18,18 @@ import { MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { + DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, IDiagnosticsService, } from '../../../../client/application/diagnostics/types'; import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; import { IPlatformService } from '../../../../client/common/platform/types'; import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { Common } from '../../../../client/common/utils/localize'; import { noop } from '../../../../client/common/utils/misc'; import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; @@ -38,11 +42,17 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { let settings: typemoq.IMock; let interpreterService: typemoq.IMock; let platformService: typemoq.IMock; + let workspaceService: typemoq.IMock; let helper: typemoq.IMock; const pythonPath = 'My Python Path in Settings'; let serviceContainer: typemoq.IMock; function createContainer() { serviceContainer = typemoq.Mock.ofType(); + workspaceService = typemoq.Mock.ofType(); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); messageHandler = typemoq.Mock.ofType>(); serviceContainer .setup((s) => @@ -94,7 +104,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { for (const code of [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, ]) { const diagnostic = typemoq.Mock.ofType(); diagnostic @@ -107,17 +117,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { diagnostic.verifyAll(); } }); - test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); test('Should return diagnostics if there are no interpreters after double-checking', async () => { interpreterService .setup((i) => i.hasInterpreters()) @@ -130,7 +130,14 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal( - [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, undefined)], + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined, + workspaceService.object, + DiagnosticScope.Global, + ), + ], 'not the same', ); }); @@ -150,8 +157,9 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(diagnostics).to.be.deep.equal( [ new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, undefined, + workspaceService.object, ), ], 'not the same', @@ -176,40 +184,11 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { settings.verifyAll(); interpreterService.verifyAll(); }); - test('Handling no interpreters diagnostic should return download link', async () => { + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.NoPythonInterpretersDiagnostic, undefined, - ); - const cmd = ({} as any) as IDiagnosticCommand; - let messagePrompt: MessageCommandPrompt | undefined; - messageHandler - .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ type: 'launch' }), - ), - ) - .returns(() => cmd) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - messageHandler.verifyAll(); - commandFactory.verifyAll(); - expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); - expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Download', command: cmd }]); - expect(messagePrompt!.onClose).to.not.be.equal(undefined, 'onClose handler should be set.'); - }); - test('Handling no currently selected interpreter diagnostic should show select interpreter message', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - undefined, + workspaceService.object, ); const cmd = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; @@ -224,6 +203,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { typemoq.It.isAny(), typemoq.It.isObjectWith>({ type: 'executeVSCCommand', + options: Commands.Set_Interpreter, }), ), ) @@ -235,15 +215,20 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { messageHandler.verifyAll(); commandFactory.verifyAll(); expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); - expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ - { prompt: 'Select Python Interpreter', command: cmd }, + { + prompt: Common.selectPythonInterpreter, + command: cmd, + }, ]); + expect(messagePrompt!.onClose).to.not.be.equal(undefined, 'onClose handler should be set.'); }); - test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { + + test('Handling no currently selected interpreter diagnostic should show select interpreter message', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, undefined, + workspaceService.object, ); const cmd = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; @@ -269,10 +254,10 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { messageHandler.verifyAll(); commandFactory.verifyAll(); expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); - expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ - { prompt: 'Select Python Interpreter', command: cmd }, + { prompt: Common.selectPythonInterpreter, command: cmd }, ]); + expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); }); test('Handling an empty diagnostic should not show a message nor return a command', async () => { const diagnostics: IDiagnostic[] = []; @@ -302,8 +287,9 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, undefined, + workspaceService.object, ); const cmd = ({} as any) as IDiagnosticCommand; const diagnosticServiceMock = (typemoq.Mock.ofInstance(diagnosticService) as any) as typemoq.IMock<