Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,4 @@
]
}
]
}
}
1 change: 1 addition & 0 deletions news/1 Enhancements/3349.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow users to request the 'Install missing Linter' prompt to not show again for pylint.
52 changes: 50 additions & 2 deletions src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IPlatformService } from '../platform/types';
import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types';
import { ITerminalServiceFactory } from '../terminal/types';
import { IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product, ProductType } from '../types';
import {
IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel,
IPersistentStateFactory, ModuleNamePurpose, Product, ProductType
} from '../types';
import { ProductNames } from './productNames';
import { IInstallationChannelManager, IProductPathService, IProductService } from './types';

Expand Down Expand Up @@ -88,6 +91,7 @@ export abstract class BaseInstaller {
.catch(() => false);
}
}

protected abstract promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse>;
protected getExecutableNameFromSettings(product: Product, resource?: Uri): string {
const productType = this.productService.getProductType(product);
Expand Down Expand Up @@ -168,12 +172,21 @@ export class FormatterInstaller extends BaseInstaller {

export class LinterInstaller extends BaseInstaller {
protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse> {
const isPylint = product === Product.pylint;

const productName = ProductNames.get(product)!;
const install = 'Install';
const disableAllLinting = 'Disable linting';
const disableThisLinter = `Disable ${productName}`;
const disableInstallPrompt = 'Do not show again';
const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`;

if (isPylint && this.getStoredResponse(disableLinterInstallPromptKey) === true) {
return InstallerResponse.Ignore;
}

const options = isPylint ? [disableThisLinter, disableAllLinting, disableInstallPrompt] : [disableThisLinter, disableAllLinting];

const options = [disableThisLinter, disableAllLinting];
let message = `Linter ${productName} is not installed.`;
if (this.isExecutableAModule(product, resource)) {
options.splice(0, 0, install);
Expand All @@ -185,7 +198,11 @@ export class LinterInstaller extends BaseInstaller {
const response = await this.appShell.showErrorMessage(message, ...options);
if (response === install) {
return this.install(product, resource);
} else if (response === disableInstallPrompt) {
await this.setStoredResponse(disableLinterInstallPromptKey, true);
return InstallerResponse.Ignore;
}

const lm = this.serviceContainer.get<ILinterManager>(ILinterManager);
if (response === disableAllLinting) {
await lm.enableLintingAsync(false);
Expand All @@ -196,6 +213,37 @@ export class LinterInstaller extends BaseInstaller {
}
return InstallerResponse.Ignore;
}

/**
* For installers that want to avoid prompting the user over and over, they can make use of a
* persisted true/false value representing user responses to 'stop showing this prompt'. This method
* gets the persisted value given the installer-defined key.
*
* @param key Key to use to get a persisted response value, each installer must define this for themselves.
* @returns Boolean: The current state of the stored response key given.
*/
private getStoredResponse(key: string): boolean {
const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined);
return state.value;
}

/**
* For installers that want to avoid prompting the user over and over, they can make use of a
* persisted true/false value representing user responses to 'stop showing this prompt'. This
* method will set that persisted value given the installer-defined key.
*
* @param key Key to use to get a persisted response value, each installer must define this for themselves.
* @param value Boolean value to store for the user - if they choose to not be prompted again for instance.
* @returns Boolean: The current state of the stored response key given.
*/
private async setStoredResponse(key: string, value: boolean): Promise<void> {
const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined);
if (state && state.value !== value) {
await state.updateValue(value);
}
}
}

export class TestFrameworkInstaller extends BaseInstaller {
Expand Down
14 changes: 12 additions & 2 deletions src/test/common/installer/installer.invalidPath.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import '../../../client/common/extensions';
import { ProductInstaller } from '../../../client/common/installer/productInstaller';
import { ProductService } from '../../../client/common/installer/productService';
import { IProductPathService, IProductService } from '../../../client/common/installer/types';
import { Product } from '../../../client/common/types';
import { IPersistentState, IPersistentStateFactory, Product } from '../../../client/common/types';
import { getNamesAndValues } from '../../../client/common/utils/enum';
import { IServiceContainer } from '../../../client/ioc/types';

Expand All @@ -30,6 +30,8 @@ suite('Module Installer - Invalid Paths', () => {
let app: TypeMoq.IMock<IApplicationShell>;
let workspaceService: TypeMoq.IMock<IWorkspaceService>;
let productPathService: TypeMoq.IMock<IProductPathService>;
let persistentState: TypeMoq.IMock<IPersistentStateFactory>;

setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
const outputChannel = TypeMoq.Mock.ofType<OutputChannel>();
Expand All @@ -43,6 +45,9 @@ suite('Module Installer - Invalid Paths', () => {
productPathService = TypeMoq.Mock.ofType<IProductPathService>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())).returns(() => productPathService.object);

persistentState = TypeMoq.Mock.ofType<IPersistentStateFactory>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())).returns(() => persistentState.object);

installer = new ProductInstaller(serviceContainer.object, outputChannel.object);
});

Expand Down Expand Up @@ -74,7 +79,12 @@ suite('Module Installer - Invalid Paths', () => {
})
.returns(() => Promise.resolve(undefined))
.verifiable(TypeMoq.Times.exactly(1));

const persistValue = TypeMoq.Mock.ofType<IPersistentState<boolean>>();
persistValue.setup(pv => pv.value).returns(() => false);
persistValue.setup(pv => pv.updateValue(TypeMoq.It.isValue(true)));
persistentState.setup(ps =>
ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined))
).returns(() => persistValue.object);
await installer.promptToInstall(product.value, resource);
productPathService.verifyAll();
});
Expand Down
Loading