diff --git a/package-lock.json b/package-lock.json index ef50646d7457..20e07c98ec63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,7 +129,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.75.0" + "vscode": "^1.76.0" } }, "node_modules/@azure/abort-controller": { diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 18d035cde6c9..277baffd19a4 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -62,7 +62,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['workbench.action.quickOpen']: [string]; ['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined]; ['workbench.extensions.installExtension']: [ - Uri | 'ms-python.python', + Uri | string, ( | { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index b575a116096c..5884aafd122d 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -10,3 +10,7 @@ export enum ShowToolsExtensionPrompt { export enum TerminalEnvVarActivation { experiment = 'pythonTerminalEnvVarActivation', } + +export enum ShowFormatterExtensionPrompt { + experiment = 'pythonPromptNewFormatterExt', +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 8673fe7cc8cd..06f7e34e0742 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -473,4 +473,24 @@ export namespace ToolsExtensions { export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); export const installISortExtension = l10n.t('Install isort extension'); + + export const selectBlackFormatterPrompt = l10n.t( + 'You have Black formatter extension installed, would you like to use that as the default formatter?', + ); + + export const selectAutopep8FormatterPrompt = l10n.t( + 'You have Autopep8 formatter extension installed, would you like to use that as the default formatter?', + ); + + export const selectMultipleFormattersPrompt = l10n.t( + 'You have multiple formatters installed, would you like to select one as the default formatter?', + ); + + export const installBlackFormatterPrompt = l10n.t( + 'You triggered formatting with Black, would you like to install one of our new formatter extensions? This will also set it as the default formatter for Python.', + ); + + export const installAutopep8FormatterPrompt = l10n.t( + 'You triggered formatting with Autopep8, would you like to install one of our new formatter extension? This will also set it as the default formatter for Python.', + ); } diff --git a/src/client/common/vscodeApis/extensionsApi.ts b/src/client/common/vscodeApis/extensionsApi.ts index 27e0657f0687..ece424847a16 100644 --- a/src/client/common/vscodeApis/extensionsApi.ts +++ b/src/client/common/vscodeApis/extensionsApi.ts @@ -3,15 +3,15 @@ import * as path from 'path'; import * as fs from 'fs-extra'; -import { Extension, extensions } from 'vscode'; +import * as vscode from 'vscode'; import { PVSC_EXTENSION_ID } from '../constants'; -export function getExtension(extensionId: string): Extension | undefined { - return extensions.getExtension(extensionId); +export function getExtension(extensionId: string): vscode.Extension | undefined { + return vscode.extensions.getExtension(extensionId); } export function isExtensionEnabled(extensionId: string): boolean { - return extensions.getExtension(extensionId) !== undefined; + return vscode.extensions.getExtension(extensionId) !== undefined; } export function isExtensionDisabled(extensionId: string): boolean { @@ -28,3 +28,7 @@ export function isExtensionDisabled(extensionId: string): boolean { } return false; } + +export function isInsider(): boolean { + return vscode.env.appName.includes('Insider'); +} diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index fda05e2477af..5db6752f7f9c 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -1,43 +1,45 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { - CancellationToken, - ConfigurationScope, - GlobPattern, - Uri, - workspace, - WorkspaceConfiguration, - WorkspaceEdit, - WorkspaceFolder, -} from 'vscode'; +import * as vscode from 'vscode'; import { Resource } from '../types'; -export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined { - return workspace.workspaceFolders; +export function getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { + return vscode.workspace.workspaceFolders; } -export function getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { - return uri ? workspace.getWorkspaceFolder(uri) : undefined; +export function getWorkspaceFolder(uri: Resource): vscode.WorkspaceFolder | undefined { + return uri ? vscode.workspace.getWorkspaceFolder(uri) : undefined; } export function getWorkspaceFolderPaths(): string[] { - return workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; + return vscode.workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; } -export function getConfiguration(section?: string, scope?: ConfigurationScope | null): WorkspaceConfiguration { - return workspace.getConfiguration(section, scope); +export function getConfiguration( + section?: string, + scope?: vscode.ConfigurationScope | null, +): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(section, scope); } -export function applyEdit(edit: WorkspaceEdit): Thenable { - return workspace.applyEdit(edit); +export function applyEdit(edit: vscode.WorkspaceEdit): Thenable { + return vscode.workspace.applyEdit(edit); } export function findFiles( - include: GlobPattern, - exclude?: GlobPattern | null, + include: vscode.GlobPattern, + exclude?: vscode.GlobPattern | null, maxResults?: number, - token?: CancellationToken, -): Thenable { - return workspace.findFiles(include, exclude, maxResults, token); + token?: vscode.CancellationToken, +): Thenable { + return vscode.workspace.findFiles(include, exclude, maxResults, token); +} + +export function onDidSaveTextDocument( + listener: (e: vscode.TextDocument) => unknown, + thisArgs?: unknown, + disposables?: vscode.Disposable[], +): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables); } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 1f2e3e94cefe..af11da753242 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -62,6 +62,7 @@ import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; +import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -206,13 +207,15 @@ async function activateLegacy(ext: ExtensionState): Promise { }); // register a dynamic configuration provider for 'python' debug type - context.subscriptions.push( + disposables.push( debug.registerDebugConfigurationProvider( DebuggerTypeName, serviceContainer.get(IDynamicDebugConfigurationService), DebugConfigurationProviderTriggerKind.Dynamic, ), ); + + registerInstallFormatterPrompt(serviceContainer); } } diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts new file mode 100644 index 000000000000..db23e130d1fd --- /dev/null +++ b/src/client/providers/prompts/installFormatterPrompt.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { Common, ToolsExtensions } from '../../common/utils/localize'; +import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { getConfiguration, onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IServiceContainer } from '../../ioc/types'; +import { + doNotShowPromptState, + inFormatterExtensionExperiment, + installFormatterExtension, + updateDefaultFormatter, +} from './promptUtils'; +import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './types'; + +const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; + +export class InstallFormatterPrompt implements IInstallFormatterPrompt { + private shownThisSession = false; + + constructor(private readonly serviceContainer: IServiceContainer) {} + + public async showInstallFormatterPrompt(resource?: Uri): Promise { + if (!inFormatterExtensionExperiment(this.serviceContainer)) { + return; + } + + const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); + if (this.shownThisSession || promptState.value) { + return; + } + + const config = getConfiguration('python', resource); + const formatter = config.get('formatting.provider', 'none'); + if (!['autopep8', 'black'].includes(formatter)) { + return; + } + + const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); + const defaultFormatter = editorConfig.get('defaultFormatter', ''); + if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { + return; + } + + const black = isExtensionEnabled(BLACK_EXTENSION); + const autopep8 = isExtensionEnabled(AUTOPEP8_EXTENSION); + + let selection: string | undefined; + + if (black || autopep8) { + this.shownThisSession = true; + if (black && autopep8) { + selection = await showInformationMessage( + ToolsExtensions.selectMultipleFormattersPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ); + } else if (black) { + selection = await showInformationMessage( + ToolsExtensions.selectBlackFormatterPrompt, + Common.bannerLabelYes, + Common.doNotShowAgain, + ); + if (selection === Common.bannerLabelYes) { + selection = 'Black'; + } + } else if (autopep8) { + selection = await showInformationMessage( + ToolsExtensions.selectAutopep8FormatterPrompt, + Common.bannerLabelYes, + Common.doNotShowAgain, + ); + if (selection === Common.bannerLabelYes) { + selection = 'Autopep8'; + } + } + } else if (formatter === 'black' && !black) { + this.shownThisSession = true; + selection = await showInformationMessage( + ToolsExtensions.installBlackFormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ); + } else if (formatter === 'autopep8' && !autopep8) { + this.shownThisSession = true; + selection = await showInformationMessage( + ToolsExtensions.installAutopep8FormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ); + } + + if (selection === 'Black') { + if (black) { + await updateDefaultFormatter(BLACK_EXTENSION, resource); + } else { + await installFormatterExtension(BLACK_EXTENSION, resource); + } + } else if (selection === 'Autopep8') { + if (autopep8) { + await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); + } else { + await installFormatterExtension(AUTOPEP8_EXTENSION, resource); + } + } else if (selection === Common.doNotShowAgain) { + await promptState.updateValue(true); + } + } +} + +export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { + const disposables = serviceContainer.get(IDisposableRegistry); + const installFormatterPrompt = new InstallFormatterPrompt(serviceContainer); + disposables.push( + onDidSaveTextDocument(async (e) => { + const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); + if (e.languageId === 'python' && editorConfig.get('formatOnSave')) { + await installFormatterPrompt.showInstallFormatterPrompt(e.uri); + } + }), + ); +} diff --git a/src/client/providers/prompts/promptUtils.ts b/src/client/providers/prompts/promptUtils.ts new file mode 100644 index 000000000000..05b1b28f061a --- /dev/null +++ b/src/client/providers/prompts/promptUtils.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, Uri } from 'vscode'; +import { ShowFormatterExtensionPrompt } from '../../common/experiments/groups'; +import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { isInsider } from '../../common/vscodeApis/extensionsApi'; +import { getConfiguration, getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { IServiceContainer } from '../../ioc/types'; + +export function inFormatterExtensionExperiment(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment); +} + +export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState { + const persistFactory = serviceContainer.get(IPersistentStateFactory); + const promptState = persistFactory.createWorkspacePersistentState(key, false); + return promptState; +} + +export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise { + const scope = getWorkspaceFolder(resource) ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; + + const config = getConfiguration('python', resource); + const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); + await editorConfig.update('defaultFormatter', extensionId, scope, true); + await config.update('formatting.provider', 'none', scope); +} + +export async function installFormatterExtension(extensionId: string, resource?: Uri): Promise { + await executeCommand('workbench.extensions.installExtension', extensionId, { + installPreReleaseVersion: isInsider(), + }); + + await updateDefaultFormatter(extensionId, resource); +} diff --git a/src/client/providers/prompts/types.ts b/src/client/providers/prompts/types.ts new file mode 100644 index 000000000000..47fead687cf5 --- /dev/null +++ b/src/client/providers/prompts/types.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const BLACK_EXTENSION = 'ms-python.black-formatter'; +export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; + +export interface IInstallFormatterPrompt { + showInstallFormatterPrompt(): Promise; +} diff --git a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts b/src/test/providers/prompt/installFormatterPrompt.unit.test.ts new file mode 100644 index 000000000000..fbd3a72d8cef --- /dev/null +++ b/src/test/providers/prompt/installFormatterPrompt.unit.test.ts @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { IPersistentState } from '../../../client/common/types'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import * as extensionsApi from '../../../client/common/vscodeApis/extensionsApi'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { InstallFormatterPrompt } from '../../../client/providers/prompts/installFormatterPrompt'; +import * as promptUtils from '../../../client/providers/prompts/promptUtils'; +import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from '../../../client/providers/prompts/types'; +import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; + +suite('Formatter Extension prompt tests', () => { + let inFormatterExtensionExperimentStub: sinon.SinonStub; + let doNotShowPromptStateStub: sinon.SinonStub; + let prompt: IInstallFormatterPrompt; + let serviceContainer: TypeMoq.IMock; + let persistState: TypeMoq.IMock>; + let getConfigurationStub: sinon.SinonStub; + let isExtensionEnabledStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock; + let editorConfig: TypeMoq.IMock; + let showInformationMessageStub: sinon.SinonStub; + let installFormatterExtensionStub: sinon.SinonStub; + let updateDefaultFormatterStub: sinon.SinonStub; + + setup(() => { + inFormatterExtensionExperimentStub = sinon.stub(promptUtils, 'inFormatterExtensionExperiment'); + inFormatterExtensionExperimentStub.returns(true); + + doNotShowPromptStateStub = sinon.stub(promptUtils, 'doNotShowPromptState'); + persistState = TypeMoq.Mock.ofType>(); + doNotShowPromptStateStub.returns(persistState.object); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + pythonConfig = TypeMoq.Mock.ofType(); + editorConfig = TypeMoq.Mock.ofType(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); + isExtensionEnabledStub = sinon.stub(extensionsApi, 'isExtensionEnabled'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + installFormatterExtensionStub = sinon.stub(promptUtils, 'installFormatterExtension'); + updateDefaultFormatterStub = sinon.stub(promptUtils, 'updateDefaultFormatter'); + + serviceContainer = TypeMoq.Mock.ofType(); + + prompt = new InstallFormatterPrompt(serviceContainer.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Not in experiment', async () => { + inFormatterExtensionExperimentStub.returns(false); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(doNotShowPromptStateStub.notCalled); + }); + + test('Do not show was set', async () => { + persistState.setup((p) => p.value).returns(() => true); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(getConfigurationStub.notCalled); + }); + + test('Formatting provider is set to none', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'none'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(isExtensionEnabledStub.notCalled); + }); + + test('Formatting provider is set to yapf', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'yapf'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(isExtensionEnabledStub.notCalled); + }); + + test('Formatting provider is set to autopep8, and autopep8 extension is set as default formatter', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => AUTOPEP8_EXTENSION); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(isExtensionEnabledStub.notCalled); + }); + + test('Formatting provider is set to black, and black extension is set as default formatter', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => BLACK_EXTENSION); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue(isExtensionEnabledStub.notCalled); + }); + + test('Prompt: user selects do not show', async () => { + persistState.setup((p) => p.value).returns(() => false); + persistState + .setup((p) => p.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns(undefined); + + showInformationMessageStub.resolves(Common.doNotShowAgain); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.installAutopep8FormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + persistState.verifyAll(); + }); + + test('Prompt (autopep8): user selects Autopep8', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns(undefined); + + showInformationMessageStub.resolves('Autopep8'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.installAutopep8FormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), + 'installFormatterExtension should be called', + ); + }); + + test('Prompt (autopep8): user selects Black', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns(undefined); + + showInformationMessageStub.resolves('Black'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.installAutopep8FormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), + 'installFormatterExtension should be called', + ); + }); + + test('Prompt (black): user selects Autopep8', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns(undefined); + + showInformationMessageStub.resolves('Autopep8'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.installBlackFormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), + 'installFormatterExtension should be called', + ); + }); + + test('Prompt (black): user selects Black', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns(undefined); + + showInformationMessageStub.resolves('Black'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.installBlackFormatterPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), + 'installFormatterExtension should be called', + ); + }); + + test('Prompt: Black and Autopep8 installed user selects Black as default', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns({}); + + showInformationMessageStub.resolves('Black'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.selectMultipleFormattersPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), + 'updateDefaultFormatter should be called', + ); + }); + + test('Prompt: Black and Autopep8 installed user selects Autopep8 as default', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.returns({}); + + showInformationMessageStub.resolves('Autopep8'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.selectMultipleFormattersPrompt, + 'Black', + 'Autopep8', + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), + 'updateDefaultFormatter should be called', + ); + }); + + test('Prompt: Black installed user selects Black as default', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.callsFake((extensionId) => { + if (extensionId === BLACK_EXTENSION) { + return {}; + } + return undefined; + }); + + showInformationMessageStub.resolves('Black'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.selectBlackFormatterPrompt, + Common.bannerLabelYes, + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), + 'updateDefaultFormatter should be called', + ); + }); + + test('Prompt: Autopep8 installed user selects Autopep8 as default', async () => { + persistState.setup((p) => p.value).returns(() => false); + pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); + editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); + isExtensionEnabledStub.callsFake((extensionId) => { + if (extensionId === AUTOPEP8_EXTENSION) { + return {}; + } + return undefined; + }); + + showInformationMessageStub.resolves('Autopep8'); + + await prompt.showInstallFormatterPrompt(); + assert.isTrue( + showInformationMessageStub.calledWith( + ToolsExtensions.selectAutopep8FormatterPrompt, + Common.bannerLabelYes, + Common.doNotShowAgain, + ), + 'showInformationMessage should be called', + ); + assert.isTrue( + updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), + 'updateDefaultFormatter should be called', + ); + }); +});