Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Sep 27, 2020
1 parent b03cf79 commit 772aa07
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { forEach } from 'vs/base/common/collections';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { IRequestService } from 'vs/platform/request/common/request';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
import { timeout } from 'vs/base/common/async';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';

type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
Expand All @@ -32,6 +33,8 @@ type IExeBasedExtensionTips = {
readonly recommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[];
};

const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';

export class ExtensionTipsService extends BaseExtensionTipsService {

_serviceBrand: any;
Expand All @@ -43,6 +46,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
Expand Down Expand Up @@ -108,13 +112,14 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
}

const recommendationsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
const promptedExecutableTips = this.getPromptedExecutableTips();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (tip && (!promptedExecutableTips[tip.exeName] || !promptedExecutableTips[tip.exeName].includes(tip.extensionId))) {
let tips = recommendationsByExe.get(tip.exeName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
recommendationsByExe.set(tip.exeName, tips);
}
tips.push(tip);
}
Expand All @@ -123,10 +128,25 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
for (const [, tips] of recommendationsByExe) {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`)
.then(result => {
if (result) {
this.addToRecommendedExecutables(tips[0].exeName, extensionIds);
}
});
}
}

private getPromptedExecutableTips(): IStringDictionary<string[]> {
return JSON.parse(this.storageService.get(promptedExecutableTipsStorageKey, StorageScope.GLOBAL, '{}'));
}

private addToRecommendedExecutables(exeName: string, extensions: string[]) {
const promptedExecutableTips = this.getPromptedExecutableTips();
promptedExecutableTips[exeName] = extensions;
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL);
}

private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class ExtensionRecommendationNotificationServiceChannel implements IServe

call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case 'promptImportantExtensionsInstallNotification': this.service.promptImportantExtensionsInstallNotification(args[0], args[1], args[2]); return Promise.resolve();
case 'promptImportantExtensionsInstallNotification': return this.service.promptImportantExtensionsInstallNotification(args[0], args[1], args[2]);
}

throw new Error(`Call not found: ${command}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,58 +96,64 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}

this.notificationService.prompt(Severity.Info, message,
[{
label: localize('install', "Install"),
run: async () => {
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
await Promise.all(extensions.map(async extension => {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
}, {
label: localize('show recommendations', "Show Recommendations"),
run: async () => {
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
return new Promise<boolean>((c, e) => {
let cancelled: boolean = false;
const handle = this.notificationService.prompt(Severity.Info, message,
[{
label: localize('install', "Install"),
run: async () => {
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
await Promise.all(extensions.map(async extension => {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
for (const extension of extensions) {
this.addToImportantRecommendationsIgnore(extension.identifier.id);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id });
}, {
label: localize('show recommendations', "Show Recommendations"),
run: async () => {
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
this.notificationService.prompt(
Severity.Info,
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
[{
label: localize('ignoreAll', "Yes, Ignore All"),
run: () => this.setIgnoreRecommendationsConfig(true)
}, {
label: localize('no', "No"),
run: () => this.setIgnoreRecommendationsConfig(false)
}]
);
}
}],
{
sticky: true,
onCancel: () => {
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id });
}, {
label: choiceNever,
isSecondary: true,
run: () => {
for (const extension of extensions) {
this.addToImportantRecommendationsIgnore(extension.identifier.id);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id });
}
this.notificationService.prompt(
Severity.Info,
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
[{
label: localize('ignoreAll', "Yes, Ignore All"),
run: () => this.setIgnoreRecommendationsConfig(true)
}, {
label: localize('no', "No"),
run: () => this.setIgnoreRecommendationsConfig(false)
}]
);
}
}],
{
sticky: true,
onCancel: () => {
cancelled = true;
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id });
}
}
}
}
);

return true;
);
const disposable = handle.onDidClose(() => {
disposable.dispose();
c(!cancelled);
});
});
}

async promptWorkspaceRecommendations(recommendations: string[]): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type FileExtensionSuggestionClassification = {
fileExtension: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};

const promptedRecommendationsStorageKey = 'fileBasedRecommendations/promptedRecommendations';
const recommendationsStorageKey = 'extensionsAssistant/recommendations';
const searchMarketplace = localize('searchMarketplace', "Search Marketplace");
const milliSecondsInADay = 1000 * 60 * 60 * 24;
Expand Down Expand Up @@ -212,7 +213,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {

const installed = await this.extensionsWorkbenchService.queryLocal();
if (importantRecommendations.length &&
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) {
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), language, importantRecommendations, installed)) {
return;
}

Expand All @@ -229,7 +230,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
this.promptRecommendedExtensionForFileExtension(fileExtension, installed);
}

private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {
private async promptRecommendedExtensionForFileType(name: string, language: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {

recommendations = this.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
Expand All @@ -247,10 +248,30 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
return false;
}

this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`);
const promptedRecommendations = this.getPromptedRecommendations();
if (promptedRecommendations[language] && promptedRecommendations[language].includes(extensionId)) {
return false;
}

this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`)
.then(result => {
if (result) {
this.addToPromptedRecommendations(language, [extensionId]);
}
});
return true;
}

private getPromptedRecommendations(): IStringDictionary<string[]> {
return JSON.parse(this.storageService.get(promptedRecommendationsStorageKey, StorageScope.GLOBAL, '{}'));
}

private addToPromptedRecommendations(exeName: string, extensions: string[]) {
const promptedRecommendations = this.getPromptedRecommendations();
promptedRecommendations[exeName] = extensions;
this.storageService.store(promptedRecommendationsStorageKey, JSON.stringify(promptedRecommendations), StorageScope.GLOBAL);
}

private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: IExtension[]): Promise<void> {
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) {
Expand Down

0 comments on commit 772aa07

Please sign in to comment.