/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IExtensionTipsService, IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import { IStringDictionary } from 'vs/base/common/collections'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { basename } from 'vs/base/common/path'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; export class ExeBasedRecommendations extends ExtensionRecommendations { readonly _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); /* 3s has come out to be the good number to fetch and prompt important exe based recommendations Also fetch important exe based recommendations for reporting telemetry */ timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations()); } protected async doActivate(): Promise { const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips(); otherExectuableBasedTips.forEach(tip => this._recommendations.push(this.toExtensionRecommendation(tip))); } private async fetchAndPromptImportantExeBasedRecommendations(): Promise { const importantExeBasedRecommendations: IStringDictionary = {}; const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips(); importantExectuableBasedTips.forEach(tip => { this._recommendations.push(this.toExtensionRecommendation(tip)); importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; }); const local = await this.extensionManagementService.getInstalled(); const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); /* Log installed and uninstalled exe based recommendations */ for (const extensionId of installed) { const tip = importantExeBasedRecommendations[extensionId]; this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); } for (const extensionId of uninstalled) { const tip = importantExeBasedRecommendations[extensionId]; this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); } this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations); } private promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): void { if (this.hasToIgnoreRecommendationNotifications()) { return; } recommendations = this.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { return; } for (const extensionId of recommendations) { const tip = importantExeBasedRecommendations[extensionId]; const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended as you have {1} installed on your system.", tip.extensionName!, tip.exeFriendlyName || basename(tip.windowsPath!)) : localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName!, tip.exeFriendlyName || basename(tip.windowsPath!)); this.promptImportantExtensionInstallNotification(extensionId, message); } } 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()); recommendationsToSuggest.forEach(id => { if (installedExtensionsIds.has(id.toLowerCase())) { installed.push(id); } else { uninstalled.push(id); } }); return { installed, uninstalled }; } private toExtensionRecommendation(tip: IExecutableBasedExtensionTip): ExtensionRecommendation { return { extensionId: tip.extensionId.toLowerCase(), source: 'executable', reason: { reasonId: ExtensionRecommendationReason.Executable, reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.extensionName) } }; } }