提交 442e5e20 编写于 作者: J Jackson Kearl 提交者: Ramya Rao

Add ability to ignore recommendations both globally and at a per-workspace level (#51941)

* WIP

* Initail filtering and pulling filter list logic.

* Imporive typing and naming

* Remove the significant duplication

* Bug fixes.

* Fix bug where arrow functions dont maintain `this` in point free style

* WIP on extension ignoring UI.

* UI for global ignores.

* Add "unwantedRecommendations" to extensions.json template and intelisense

* Notify of workspace non-recommended extensions

* Wording of extensions.json template

* More UI for ignore button.

* Use seprate notification channel of recommendation changes.

* Reload search when recommended extensions changes

* Tests for ExtensionTipsService

* Test extensions workbench service

* Naming and add default vaule to workspace settings

* Initial revisions

* Global ignore need not call workspace refresh

* Fix build issues

* Skip refreshing workspace twice

* WIP

* WIP

* Reduce file accesses.
Remove bug causing all open editors to show exrtension as ignored

* Fix some of the build issues.

* Hackish thing that fixes the test.

* Rename id to extensionId in RecommendationChangeNotification

* updateRecommendedness ??

* Not needed

* Remove point free style thing

* Simplify

* naming

* remove extrenous getWrokspaceRecommendations call

* Casing

* Gracefull handle missing 'extensions' field in multiroot project config

* Refresh recommendation views on recommendation change

* more case sensitivity stuff

* naming/refactroing

* Simplify return types

* Wording

* Add telemetry

* guard telemetry service
上级 e4fe04ad
......@@ -376,6 +376,21 @@ export interface IExtensionEnablementService {
setEnablement(extension: ILocalExtension, state: EnablementState): TPromise<boolean>;
}
export interface IIgnoredRecommendations {
global: string[];
workspace: string[];
}
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
}
export type RecommendationChangeNotification = {
extensionId: string,
isRecommended: boolean
};
export const IExtensionTipsService = createDecorator<IExtensionTipsService>('extensionTipsService');
export interface IExtensionTipsService {
......@@ -387,6 +402,9 @@ export interface IExtensionTipsService {
getKeymapRecommendations(): string[];
getKeywordsForExtension(extension: string): string[];
getRecommendationsForExtension(extension: string): string[];
getAllIgnoredRecommendations(): IIgnoredRecommendations;
ignoreExtensionRecommendation(extensionId: string): void;
onRecommendationChange: Event<RecommendationChangeNotification>;
}
export enum ExtensionRecommendationReason {
......
......@@ -30,7 +30,7 @@ export interface IProductConfiguration {
};
extensionTips: { [id: string]: string; };
extensionImportantTips: { [id: string]: { name: string; pattern: string; }; };
exeBasedExtensionTips: { [id: string]: any; };
exeBasedExtensionTips: { [id: string]: { friendlyName: string, windowsPath?: string, recommendations: string[] }; };
extensionKeywords: { [extension: string]: string[]; };
extensionAllowedBadgeProviders: string[];
extensionAllowedProposedApi: string[];
......
......@@ -13,10 +13,20 @@ export const ExtensionsConfigurationSchema: IJSONSchema = {
allowComments: true,
type: 'object',
title: localize('app.extensions.json.title', "Extensions"),
additionalProperties: false,
properties: {
recommendations: {
type: 'array',
description: localize('app.extensions.json.recommendations', "List of extensions recommendations. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."),
description: localize('app.extensions.json.recommendations', "List of extensions which should be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."),
items: {
type: 'string',
pattern: EXTENSION_IDENTIFIER_PATTERN,
errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.")
},
},
unwantedRecommendations: {
type: 'array',
description: localize('app.extensions.json.unwantedRecommendations', "List of extensions that will be skipped from the recommendations that VS Code makes for the users of this workspace. These are extensions that you may consider to be irrelevant, redundant, or otherwise unwanted. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."),
items: {
type: 'string',
pattern: EXTENSION_IDENTIFIER_PATTERN,
......@@ -31,6 +41,12 @@ export const ExtensionsConfigurationInitialContent: string = [
'\t// See http://go.microsoft.com/fwlink/?LinkId=827846',
'\t// for the documentation about the extensions.json format',
'\t"recommendations": [',
'\t\t// List of extensions which should be recommended for users of this workspace.',
'\t\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp',
'\t\t',
'\t],',
'\t"unwantedRecommendations": [',
'\t\t// List of extensions that will be skipped from the recommendations that VS Code makes for the users of this workspace. These are extensions that you may consider to be irrelevant, redundant, or otherwise unwanted.',
'\t\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp',
'\t\t',
'\t]',
......
......@@ -31,7 +31,7 @@ import { Renderer, DataSource, Controller } from 'vs/workbench/parts/extensions/
import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets';
import { EditorOptions } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction, MultiServerInstallAction, MultiServerUpdateAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction, MultiServerInstallAction, MultiServerUpdateAction, IgnoreExtensionRecommendationAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { WebviewElement } from 'vs/workbench/parts/webview/electron-browser/webviewElement';
import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
......@@ -164,6 +164,8 @@ export class ExtensionEditor extends BaseEditor {
private navbar: NavBar;
private content: HTMLElement;
private recommendation: HTMLElement;
private recommendationText: any;
private ignoreActionbar: ActionBar;
private header: HTMLElement;
private extensionReadme: Cache<string>;
......@@ -256,15 +258,24 @@ export class ExtensionEditor extends BaseEditor {
return null;
}
});
this.disposables.push(this.extensionActionBar);
this.recommendation = append(details, $('.recommendation'));
this.recommendationText = append(this.recommendation, $('.recommendation-text'));
this.ignoreActionbar = new ActionBar(this.recommendation, { animated: false });
this.disposables.push(this.extensionActionBar);
this.disposables.push(this.ignoreActionbar);
chain(this.extensionActionBar.onDidRun)
.map(({ error }) => error)
.filter(error => !!error)
.on(this.onError, this, this.disposables);
chain(this.ignoreActionbar.onDidRun)
.map(({ error }) => error)
.filter(error => !!error)
.on(this.onError, this, this.disposables);
const body = append(root, $('.body'));
this.navbar = new NavBar(body);
......@@ -295,24 +306,25 @@ export class ExtensionEditor extends BaseEditor {
this.publisher.textContent = extension.publisherDisplayName;
this.description.textContent = extension.description;
removeClass(this.header, 'recommendation-ignored');
const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason();
let recommendationsData = {};
if (extRecommendations[extension.id.toLowerCase()]) {
addClass(this.header, 'recommended');
this.recommendation.textContent = extRecommendations[extension.id.toLowerCase()].reasonText;
this.recommendationText.textContent = extRecommendations[extension.id.toLowerCase()].reasonText;
recommendationsData = { recommendationReason: extRecommendations[extension.id.toLowerCase()].reasonId };
} else {
removeClass(this.header, 'recommended');
this.recommendation.textContent = '';
this.recommendationText.textContent = '';
}
/* __GDPR__
"extensionGallery:openExtension" : {
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
"extensionGallery:openExtension" : {
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
this.telemetryService.publicLog('extensionGallery:openExtension', assign(extension.telemetryData, recommendationsData));
......@@ -376,6 +388,21 @@ export class ExtensionEditor extends BaseEditor {
this.extensionActionBar.push([disabledStatusAction, reloadAction, updateAction, enableAction, disableAction, installAction, maliciousStatusAction], { icon: true, label: true });
this.transientDisposables.push(enableAction, updateAction, reloadAction, disableAction, installAction, maliciousStatusAction, disabledStatusAction);
const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction);
ignoreAction.extension = extension;
this.extensionTipsService.onRecommendationChange(change => {
if (change.extensionId.toLowerCase() === extension.id.toLowerCase() && change.isRecommended === false) {
addClass(this.header, 'recommendation-ignored');
removeClass(this.header, 'recommended');
this.recommendationText.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
}
});
this.ignoreActionbar.clear();
this.ignoreActionbar.push([ignoreAction], { icon: true, label: true });
this.transientDisposables.push(ignoreAction);
this.content.innerHTML = ''; // Clear content before setting navbar actions.
this.navbar.clear();
......
......@@ -1488,6 +1488,36 @@ export class InstallRecommendedExtensionAction extends Action {
}
}
export class IgnoreExtensionRecommendationAction extends Action {
static readonly ID = 'extensions.ignore';
private static readonly Class = 'extension-action ignore octicon octicon-x';
private disposables: IDisposable[] = [];
extension: IExtension;
constructor(
@IExtensionTipsService private extensionsTipsService: IExtensionTipsService,
) {
super(IgnoreExtensionRecommendationAction.ID);
this.class = IgnoreExtensionRecommendationAction.Class;
this.tooltip = localize('ignoreExtensionRecommendation', "Do not recommend this extension again");
this.enabled = true;
}
public run(): TPromise<any> {
this.extensionsTipsService.ignoreExtensionRecommendation(this.extension.id);
return TPromise.as(null);
}
dispose(): void {
super.dispose();
this.disposables = dispose(this.disposables);
}
}
export class ShowRecommendedKeymapExtensionsAction extends Action {
......
......@@ -167,15 +167,12 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
data.icon.style.visibility = 'inherit';
}
data.root.setAttribute('aria-label', extension.displayName);
removeClass(data.root, 'recommended');
const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.id.toLowerCase()]) {
data.root.setAttribute('aria-label', extension.displayName + '. ' + extRecommendations[extension.id.toLowerCase()].reasonText);
addClass(data.root, 'recommended');
data.root.title = extRecommendations[extension.id.toLowerCase()].reasonText;
}
this.updateRecommendationStatus(extension, data);
data.extensionDisposables.push(this.extensionTipsService.onRecommendationChange(change => {
if (change.extensionId.toLowerCase() === extension.id.toLowerCase() && change.isRecommended === false) {
this.updateRecommendationStatus(extension, data);
}
}));
data.name.textContent = extension.displayName;
data.author.textContent = extension.publisherDisplayName;
......@@ -190,6 +187,20 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
});
}
private updateRecommendationStatus(extension: IExtension, data: ITemplateData) {
const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason();
if (!extRecommendations[extension.id.toLowerCase()]) {
data.root.setAttribute('aria-label', extension.displayName);
data.root.title = '';
removeClass(data.root, 'recommended');
} else {
data.root.setAttribute('aria-label', extension.displayName + '. ' + extRecommendations[extension.id]);
data.root.title = extRecommendations[extension.id.toLowerCase()].reasonText;
addClass(data.root, 'recommended');
}
}
disposeTemplate(data: ITemplateData): void {
data.disposables = dispose(data.disposables);
}
......
......@@ -32,7 +32,7 @@ import {
} from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { LocalExtensionType, IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput';
import { ExtensionsListView, InstalledExtensionsView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, GroupByServerExtensionsView } from './extensionsViews';
import { ExtensionsListView, InstalledExtensionsView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, GroupByServerExtensionsView, DefaultRecommendedExtensionsView } from './extensionsViews';
import { OpenGlobalSettingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
......@@ -182,7 +182,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
id: 'extensions.recommendedList',
name: localize('recommendedExtensions', "Recommended"),
container: VIEW_CONTAINER,
ctor: RecommendedExtensionsView,
ctor: DefaultRecommendedExtensionsView,
when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')),
weight: 70,
order: 2,
......
......@@ -55,7 +55,7 @@ export class ExtensionsListView extends ViewletPanel {
@IExtensionService private extensionService: IExtensionService,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IEditorService private editorService: IEditorService,
@IExtensionTipsService private tipsService: IExtensionTipsService,
@IExtensionTipsService protected tipsService: IExtensionTipsService,
@IModeService private modeService: IModeService,
@ITelemetryService private telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService
......@@ -447,10 +447,10 @@ export class ExtensionsListView extends ViewletPanel {
.then(recommendations => {
const names = recommendations.filter(name => name.toLowerCase().indexOf(value) > -1);
/* __GDPR__
"extensionWorkspaceRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
"extensionWorkspaceRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length });
if (!names.length) {
......@@ -645,15 +645,48 @@ export class BuiltInBasicsExtensionsView extends ExtensionsListView {
}
}
export class DefaultRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this.disposables.push(this.tipsService.onRecommendationChange(() => {
this.show('');
}));
}
async show(query: string): TPromise<IPagedModel<IExtension>> {
return super.show('@recommended:all');
}
}
export class RecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this.disposables.push(this.tipsService.onRecommendationChange(() => {
this.show('');
}));
}
async show(query: string): TPromise<IPagedModel<IExtension>> {
return super.show(!query.trim() ? '@recommended:all' : '@recommended');
return super.show('@recommended');
}
}
export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this.disposables.push(this.tipsService.onRecommendationChange(() => {
this.show('');
}));
}
renderHeader(container: HTMLElement): void {
super.renderHeader(container);
......
......@@ -71,4 +71,29 @@
.hc-black .extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage,
.vs-dark .extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage {
background: url('manage-inverse.svg') center center no-repeat;
}
.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container {
justify-content: flex-start;
}
.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .action-item .action-label.extension-action.ignore {
height: 13px;
width: 8px;
border: none;
outline-offset: 0;
margin-left: 6px;
padding-left: 0;
margin-top: 3px;
background-color: transparent;
color: hsl(0, 66%, 50%);
}
.hc-black .extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .action-item .action-label.extension-action.ignore,
.vs-dark .extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .action-item .action-label.extension-action.ignore {
color: hsla(0, 66%, 77%, 1);
}
.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .action-item .action-label.extension-action.ignore:hover {
filter: brightness(.8);
}
\ No newline at end of file
......@@ -20,7 +20,9 @@
font-size: 14px;
}
.extension-editor > .header.recommended {
.extension-editor > .header.recommended,
.extension-editor > .header.recommendation-ignored
{
height: 140px;
}
......@@ -128,16 +130,22 @@
padding: 1px 6px;
}
.extension-editor > .header.recommended > .details > .recommendation {
display: none;
}
.extension-editor > .header.recommended > .details > .recommendation {
display: block;
.extension-editor > .header.recommended > .details > .recommendation,
.extension-editor > .header.recommendation-ignored > .details > .recommendation {
display: flex;
margin-top: 2px;
font-size: 13px;
}
.extension-editor > .header.recommendation-ignored > .details > .recommendation > .recommendation-text {
font-style: italic;
}
.extension-editor > .header.recommendation-ignored > .details > .recommendation > .monaco-action-bar {
display: none;
}
.extension-editor > .body {
height: calc(100% - 168px);
overflow: hidden;
......
......@@ -5,6 +5,7 @@
'use strict';
import * as sinon from 'sinon';
import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs';
......@@ -36,7 +37,7 @@ import { IPager } from 'vs/base/common/paging';
import { assign } from 'vs/base/common/objects';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IExtensionsWorkbenchService, ConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService';
......@@ -243,27 +244,30 @@ suite('ExtensionsTipsService Test', () => {
});
});
teardown((done) => {
teardown(done => {
(<ExtensionTipsService>testObject).dispose();
(<ExtensionsWorkbenchService>extensionsWorkbenchService).dispose();
if (parentResource) {
extfs.del(parentResource, os.tmpdir(), () => { }, done);
} else {
done();
}
});
function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[]): TPromise<void> {
function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): TPromise<void> {
const id = uuid.generateUuid();
parentResource = path.join(os.tmpdir(), 'vsctests', id);
return setUpFolder(folderName, parentResource, recommendedExtensions);
return setUpFolder(folderName, parentResource, recommendedExtensions, ignoredRecommendations);
}
function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[]): TPromise<void> {
function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): TPromise<void> {
const folderDir = path.join(parentDir, folderName);
const workspaceSettingsDir = path.join(folderDir, '.vscode');
return mkdirp(workspaceSettingsDir, 493).then(() => {
const configPath = path.join(workspaceSettingsDir, 'extensions.json');
fs.writeFileSync(configPath, JSON.stringify({
'recommendations': recommendedExtensions
'recommendations': recommendedExtensions,
'unwantedRecommendations': ignoredRecommendations,
}, null, '\t'));
const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir }));
......@@ -276,7 +280,7 @@ suite('ExtensionsTipsService Test', () => {
function testNoPromptForValidRecommendations(recommendations: string[]) {
return setUpFolderWorkspace('myFolder', recommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.promptWorkspaceRecommendationsPromise.then(() => {
return testObject.loadRecommendationsPromise.then(() => {
assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length);
assert.ok(!prompted);
});
......@@ -286,7 +290,7 @@ suite('ExtensionsTipsService Test', () => {
function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) {
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
assert.equal(!testObject.promptWorkspaceRecommendationsPromise, true);
assert.equal(!testObject.loadRecommendationsPromise, true);
assert.ok(!prompted);
return testObject.getWorkspaceRecommendations().then(() => {
......@@ -313,7 +317,7 @@ suite('ExtensionsTipsService Test', () => {
test('ExtensionTipsService: Prompt for valid workspace recommendations', () => {
return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.promptWorkspaceRecommendationsPromise.then(() => {
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
......@@ -345,7 +349,7 @@ suite('ExtensionsTipsService Test', () => {
testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true });
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.promptWorkspaceRecommendationsPromise.then(() => {
return testObject.loadRecommendationsPromise.then(() => {
assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, 0);
assert.ok(!prompted);
});
......@@ -357,17 +361,152 @@ suite('ExtensionsTipsService Test', () => {
return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions);
});
test('ExtensionTipsService: No Recommendations of globally ignored recommendations', () => {
const storageGetterStub = (a, _, c) => {
const storedRecommendations = '["ms-vscode.csharp", "ms-python.python", "eg2.tslint"]';
const ignoredRecommendations = '["ms-vscode.csharp", "mockpublisher2.mockextension2"]'; // ignore a stored recommendation and a workspace recommendation.
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
if (a === 'extensionsAssistant/ignored_recommendations') { return ignoredRecommendations; }
return c;
};
instantiationService.stub(IStorageService, {
get: storageGetterStub,
getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
});
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been globally ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
assert.ok(recommendations['mockpublisher1.mockextension1']); // workspace recommendation
assert.ok(!recommendations['mockpublisher2.mockextension2']); // workspace recommendation that has been globally ignored
});
});
});
test('ExtensionTipsService: No Recommendations of workspace ignored recommendations', () => {
const ignoredRecommendations = ['ms-vscode.csharp', 'mockpublisher2.mockextension2']; // ignore a stored recommendation and a workspace recommendation.
const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]';
instantiationService.stub(IStorageService, {
get: (a, b, c) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c,
getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
});
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been workspace ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
assert.ok(recommendations['mockpublisher1.mockextension1']); // workspace recommendation
assert.ok(!recommendations['mockpublisher2.mockextension2']); // workspace recommendation that has been workspace ignored
});
});
});
test('ExtensionTipsService: Able to retrieve collection of all ignored recommendations', () => {
const storageGetterStub = (a, _, c) => {
const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]';
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; }
return c;
};
const workspaceIgnoredRecommendations = ['ms-vscode.csharp']; // ignore a stored recommendation and a workspace recommendation.
instantiationService.stub(IStorageService, {
get: storageGetterStub,
getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
});
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getAllIgnoredRecommendations();
assert.deepStrictEqual(recommendations,
{
global: ['mockpublisher2.mockextension2'],
workspace: ['ms-vscode.csharp']
});
});
});
});
test('ExtensionTipsService: Able to dynamically ignore global recommendations', () => {
const storageGetterStub = (a, _, c) => {
const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]';
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; }
return c;
};
instantiationService.stub(IStorageService, {
get: storageGetterStub,
store: () => { },
getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
});
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getAllIgnoredRecommendations();
assert.deepStrictEqual(recommendations,
{
global: ['mockpublisher2.mockextension2'],
workspace: []
});
return testObject.ignoreExtensionRecommendation('mockpublisher1.mockextension1');
}).then(() => {
const recommendations = testObject.getAllIgnoredRecommendations();
assert.deepStrictEqual(recommendations,
{
global: ['mockpublisher2.mockextension2', 'mockpublisher1.mockextension1'],
workspace: []
});
});
});
});
test('test global extensions are modified and recommendation change event is fired when an extension is ignored', () => {
const storageSetterTarget = sinon.spy();
const changeHandlerTarget = sinon.spy();
const ignoredExtensionId = 'Some.Extension';
instantiationService.stub(IStorageService, {
get: (a, b, c) => a === 'extensionsAssistant/ignored_recommendations' ? '["ms-vscode.vscode"]' : c,
store: (...args) => {
storageSetterTarget(...args);
}
});
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
testObject.onRecommendationChange(changeHandlerTarget);
testObject.ignoreExtensionRecommendation(ignoredExtensionId);
assert.ok(changeHandlerTarget.calledOnce);
assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: 'Some.Extension', isRecommended: false }));
assert.ok(storageSetterTarget.calledWithExactly('extensionsAssistant/ignored_recommendations', `["ms-vscode.vscode","${ignoredExtensionId.toLowerCase()}"]`, StorageScope.GLOBAL));
});
});
test('ExtensionTipsService: Get file based recommendations from storage (old format)', () => {
const storedRecommendations = '["ms-vscode.csharp", "ms-python.python", "eg2.tslint"]';
instantiationService.stub(IStorageService, { get: (a, b, c) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c });
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
});
});
});
......@@ -380,12 +519,14 @@ suite('ExtensionsTipsService Test', () => {
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionTipsService);
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
assert.ok(recommendations.indexOf('lukehoban.Go') === -1); //stored recommendation that is older than a week
return testObject.loadRecommendationsPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
assert.ok(recommendations.indexOf('lukehoban.Go') === -1); //stored recommendation that is older than a week
});
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册