Skip to content

Commit

Permalink
Show prompt asking user to install formatter extension (#20861)
Browse files Browse the repository at this point in the history
For #19653
  • Loading branch information
karthiknadig committed Mar 16, 2023
1 parent b9c4ff7 commit 7ee3f7d
Show file tree
Hide file tree
Showing 11 changed files with 574 additions and 31 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/experiments/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export enum ShowToolsExtensionPrompt {
export enum TerminalEnvVarActivation {
experiment = 'pythonTerminalEnvVarActivation',
}

export enum ShowFormatterExtensionPrompt {
experiment = 'pythonPromptNewFormatterExt',
}
20 changes: 20 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
);
}
12 changes: 8 additions & 4 deletions src/client/common/vscodeApis/extensionsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = unknown>(extensionId: string): Extension<T> | undefined {
return extensions.getExtension(extensionId);
export function getExtension<T = unknown>(extensionId: string): vscode.Extension<T> | 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 {
Expand All @@ -28,3 +28,7 @@ export function isExtensionDisabled(extensionId: string): boolean {
}
return false;
}

export function isInsider(): boolean {
return vscode.env.appName.includes('Insider');
}
50 changes: 26 additions & 24 deletions src/client/common/vscodeApis/workspaceApis.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return workspace.applyEdit(edit);
export function applyEdit(edit: vscode.WorkspaceEdit): Thenable<boolean> {
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<Uri[]> {
return workspace.findFiles(include, exclude, maxResults, token);
token?: vscode.CancellationToken,
): Thenable<vscode.Uri[]> {
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);
}
5 changes: 4 additions & 1 deletion src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -206,13 +207,15 @@ async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
});

// register a dynamic configuration provider for 'python' debug type
context.subscriptions.push(
disposables.push(
debug.registerDebugConfigurationProvider(
DebuggerTypeName,
serviceContainer.get<DynamicPythonDebugConfigurationService>(IDynamicDebugConfigurationService),
DebugConfigurationProviderTriggerKind.Dynamic,
),
);

registerInstallFormatterPrompt(serviceContainer);
}
}

Expand Down
128 changes: 128 additions & 0 deletions src/client/providers/prompts/installFormatterPrompt.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string>('formatting.provider', 'none');
if (!['autopep8', 'black'].includes(formatter)) {
return;
}

const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' });
const defaultFormatter = editorConfig.get<string>('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>(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<boolean>('formatOnSave')) {
await installFormatterPrompt.showInstallFormatterPrompt(e.uri);
}
}),
);
}
38 changes: 38 additions & 0 deletions src/client/providers/prompts/promptUtils.ts
Original file line number Diff line number Diff line change
@@ -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>(IExperimentService);
return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment);
}

export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState<boolean> {
const persistFactory = serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
const promptState = persistFactory.createWorkspacePersistentState<boolean>(key, false);
return promptState;
}

export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise<void> {
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<void> {
await executeCommand('workbench.extensions.installExtension', extensionId, {
installPreReleaseVersion: isInsider(),
});

await updateDefaultFormatter(extensionId, resource);
}
9 changes: 9 additions & 0 deletions src/client/providers/prompts/types.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
}
Loading

0 comments on commit 7ee3f7d

Please sign in to comment.