Skip to content

Commit 5cc0211

Browse files
committed
recommend important exe extensions
1 parent 1545b1b commit 5cc0211

3 files changed

Lines changed: 175 additions & 58 deletions

File tree

src/vs/platform/product/common/product.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export interface IProductConfiguration {
7272
readonly recommendationsUrl: string;
7373
};
7474
extensionTips: { [id: string]: string; };
75-
extensionImportantTips: { [id: string]: { name: string; pattern: string; isLocationPattern?: boolean, isExtensionPack?: boolean }; };
76-
readonly exeBasedExtensionTips: { [id: string]: { friendlyName: string, windowsPath?: string, recommendations: readonly string[] }; };
75+
extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; };
76+
readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; };
7777
readonly extensionKeywords: { [extension: string]: readonly string[]; };
7878
readonly extensionAllowedBadgeProviders: readonly string[];
7979
readonly extensionAllowedProposedApi: readonly string[];
@@ -120,6 +120,14 @@ export interface IProductConfiguration {
120120
readonly uiExtensions?: readonly string[];
121121
}
122122

123+
export interface IExeBasedExtensionTip {
124+
friendlyName: string;
125+
windowsPath?: string;
126+
recommendations: readonly string[];
127+
important?: boolean;
128+
exeFriendlyName?: string;
129+
}
130+
123131
export interface ISurveyData {
124132
surveyId: string;
125133
surveyUrl: string;

src/vs/workbench/contrib/extensions/browser/extensionsActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1694,7 +1694,7 @@ export class InstallRecommendedExtensionAction extends Action {
16941694
return this.viewletService.openViewlet(VIEWLET_ID, true)
16951695
.then(viewlet => viewlet as IExtensionsViewlet)
16961696
.then(viewlet => {
1697-
viewlet.search('@recommended ');
1697+
viewlet.search(`@id:${this.extensionId}`);
16981698
viewlet.focus();
16991699
return this.extensionWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation', pageSize: 1 }, CancellationToken.None)
17001700
.then(pager => {

src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts

Lines changed: 164 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { localize } from 'vs/nls';
7-
import { join } from 'vs/base/common/path';
7+
import { join, basename } from 'vs/base/common/path';
88
import { forEach } from 'vs/base/common/collections';
99
import { Disposable } from 'vs/base/common/lifecycle';
1010
import { match } from 'vs/base/common/glob';
@@ -43,6 +43,8 @@ import { CancellationToken } from 'vs/base/common/cancellation';
4343
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
4444
import { extname } from 'vs/base/common/resources';
4545
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
46+
import { IExeBasedExtensionTip } from 'vs/platform/product/common/product';
47+
import { timeout } from 'vs/base/common/async';
4648

4749
const milliSecondsInADay = 1000 * 60 * 60 * 24;
4850
const choiceNever = localize('neverShowAgain', "Don't Show Again");
@@ -71,7 +73,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
7173
_serviceBrand: any;
7274

7375
private _fileBasedRecommendations: { [id: string]: { recommendedTime: number, sources: ExtensionRecommendationSource[] }; } = Object.create(null);
74-
private _exeBasedRecommendations: { [id: string]: string; } = Object.create(null);
76+
private _exeBasedRecommendations: { [id: string]: IExeBasedExtensionTip; } = Object.create(null);
77+
private _importantExeBasedRecommendations: { [id: string]: IExeBasedExtensionTip; } = Object.create(null);
7578
private _availableRecommendations: { [pattern: string]: string[] } = Object.create(null);
7679
private _allWorkspaceRecommendedExtensions: IExtensionRecommendation[] = [];
7780
private _dynamicWorkspaceRecommendations: string[] = [];
@@ -187,7 +190,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
187190

188191
forEach(this._exeBasedRecommendations, entry => output[entry.key.toLowerCase()] = {
189192
reasonId: ExtensionRecommendationReason.Executable,
190-
reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value)
193+
reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value.friendlyName)
191194
});
192195

193196
forEach(this._fileBasedRecommendations, entry => output[entry.key.toLowerCase()] = {
@@ -496,6 +499,100 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
496499

497500
//#endregion
498501

502+
//#region important exe based extension
503+
504+
private async promptForImportantExeBasedExtension(): Promise<boolean> {
505+
506+
const storageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
507+
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
508+
509+
if (config.ignoreRecommendations
510+
|| config.showRecommendationsOnlyOnDemand
511+
|| this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) {
512+
return false;
513+
}
514+
515+
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
516+
let recommendationsToSuggest = Object.keys(this._importantExeBasedRecommendations);
517+
recommendationsToSuggest = this.filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest, installed);
518+
if (recommendationsToSuggest.length === 0) {
519+
return false;
520+
}
521+
522+
const id = recommendationsToSuggest[0];
523+
const tip = this._importantExeBasedRecommendations[id];
524+
const message = localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.friendlyName!, tip.exeFriendlyName || basename(tip.windowsPath!));
525+
526+
this.notificationService.prompt(Severity.Info, message,
527+
[{
528+
label: localize('install', 'Install'),
529+
run: () => {
530+
/* __GDPR__
531+
"exeExtensionRecommendations:popup" : {
532+
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
533+
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
534+
}
535+
*/
536+
this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'install', extensionId: name });
537+
this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run();
538+
}
539+
}, {
540+
label: localize('showRecommendations', "Show Recommendations"),
541+
run: () => {
542+
/* __GDPR__
543+
"exeExtensionRecommendations:popup" : {
544+
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
545+
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
546+
}
547+
*/
548+
this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'show', extensionId: name });
549+
550+
const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
551+
recommendationsAction.run();
552+
recommendationsAction.dispose();
553+
}
554+
}, {
555+
label: choiceNever,
556+
isSecondary: true,
557+
run: () => {
558+
this.addToImportantRecommendationsIgnore(id);
559+
/* __GDPR__
560+
"exeExtensionRecommendations:popup" : {
561+
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
562+
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
563+
}
564+
*/
565+
this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name });
566+
this.notificationService.prompt(
567+
Severity.Info,
568+
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
569+
[{
570+
label: localize('ignoreAll', "Yes, Ignore All"),
571+
run: () => this.setIgnoreRecommendationsConfig(true)
572+
}, {
573+
label: localize('no', "No"),
574+
run: () => this.setIgnoreRecommendationsConfig(false)
575+
}]
576+
);
577+
}
578+
}],
579+
{
580+
sticky: true,
581+
onCancel: () => {
582+
/* __GDPR__
583+
"exeExtensionRecommendations:popup" : {
584+
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
585+
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
586+
}
587+
*/
588+
this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name });
589+
}
590+
}
591+
);
592+
593+
return true;
594+
}
595+
499596
//#region fileBasedRecommendations
500597

501598
getFileBasedRecommendations(): IExtensionRecommendation[] {
@@ -646,21 +743,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
646743
}
647744

648745
private async promptRecommendedExtensionForFileType(recommendationsToSuggest: string[], installed: ILocalExtension[]): Promise<boolean> {
649-
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]'));
650-
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
651-
recommendationsToSuggest = recommendationsToSuggest.filter(id => {
652-
if (importantRecommendationsIgnoreList.indexOf(id) !== -1) {
653-
return false;
654-
}
655-
if (!this.isExtensionAllowedToBeRecommended(id)) {
656-
return false;
657-
}
658-
if (installedExtensionsIds.has(id.toLowerCase())) {
659-
return false;
660-
}
661-
return true;
662-
});
663746

747+
recommendationsToSuggest = this.filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest, installed);
664748
if (recommendationsToSuggest.length === 0) {
665749
return false;
666750
}
@@ -674,18 +758,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
674758
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", name);
675759
if (entry.isExtensionPack) {
676760
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", name);
677-
} else if (entry.isLocationPattern) {
678-
message = localize('reallyRecommendedLocationPattern', "The '{0}' extension is recommended for files at this location.", name);
679761
}
680762

681-
const setIgnoreRecommendationsConfig = (configVal: boolean) => {
682-
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER);
683-
if (configVal) {
684-
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
685-
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
686-
}
687-
};
688-
689763
this.notificationService.prompt(Severity.Info, message,
690764
[{
691765
label: localize('install', 'Install'),
@@ -718,12 +792,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
718792
label: choiceNever,
719793
isSecondary: true,
720794
run: () => {
721-
importantRecommendationsIgnoreList.push(id);
722-
this.storageService.store(
723-
'extensionsAssistant/importantRecommendationsIgnore',
724-
JSON.stringify(importantRecommendationsIgnoreList),
725-
StorageScope.GLOBAL
726-
);
795+
this.addToImportantRecommendationsIgnore(id);
727796
/* __GDPR__
728797
"extensionRecommendations:popup" : {
729798
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
@@ -736,10 +805,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
736805
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
737806
[{
738807
label: localize('ignoreAll', "Yes, Ignore All"),
739-
run: () => setIgnoreRecommendationsConfig(true)
808+
run: () => this.setIgnoreRecommendationsConfig(true)
740809
}, {
741810
label: localize('no', "No"),
742-
run: () => setIgnoreRecommendationsConfig(false)
811+
run: () => this.setIgnoreRecommendationsConfig(false)
743812
}]
744813
);
745814
}
@@ -831,6 +900,42 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
831900
);
832901
}
833902

903+
private filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest: string[], installed: ILocalExtension[]): string[] {
904+
905+
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]'));
906+
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
907+
return recommendationsToSuggest.filter(id => {
908+
if (importantRecommendationsIgnoreList.indexOf(id) !== -1) {
909+
return false;
910+
}
911+
if (!this.isExtensionAllowedToBeRecommended(id)) {
912+
return false;
913+
}
914+
if (installedExtensionsIds.has(id.toLowerCase())) {
915+
return false;
916+
}
917+
return true;
918+
});
919+
}
920+
921+
private addToImportantRecommendationsIgnore(id: string) {
922+
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]'));
923+
importantRecommendationsIgnoreList.push(id);
924+
this.storageService.store(
925+
'extensionsAssistant/importantRecommendationsIgnore',
926+
JSON.stringify(importantRecommendationsIgnoreList),
927+
StorageScope.GLOBAL
928+
);
929+
}
930+
931+
private setIgnoreRecommendationsConfig(configVal: boolean) {
932+
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER);
933+
if (configVal) {
934+
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
935+
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
936+
}
937+
}
938+
834939
//#endregion
835940

836941
//#region otherRecommendations
@@ -857,18 +962,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
857962
}
858963

859964
private fetchProactiveRecommendations(calledDuringStartup?: boolean): Promise<void> {
860-
let fetchPromise = Promise.resolve(undefined);
965+
let fetchPromise = Promise.resolve<any>(undefined);
861966
if (!this.proactiveRecommendationsFetched) {
862967
this.proactiveRecommendationsFetched = true;
863968

864-
// Executable based recommendations carry out a lot of file stats, so run them after 10 secs
865-
// So that the startup is not affected
969+
// Executable based recommendations carry out a lot of file stats, delay the resolution so that the startup is not affected
970+
// 10 sec for regular extensions
971+
// 3 secs for important
866972

867-
fetchPromise = new Promise((c, e) => {
868-
setTimeout(() => {
869-
Promise.all([this.fetchExecutableRecommendations(), this.fetchDynamicWorkspaceRecommendations()]).then(() => c(undefined));
870-
}, calledDuringStartup ? 10000 : 0);
871-
});
973+
const importantExeBasedRecommendations = timeout(calledDuringStartup ? 3000 : 0).then(_ => this.fetchExecutableRecommendations(true));
974+
importantExeBasedRecommendations.then(_ => this.promptForImportantExeBasedExtension());
975+
976+
fetchPromise = timeout(calledDuringStartup ? 10000 : 0).then(_ => Promise.all([this.fetchDynamicWorkspaceRecommendations(), this.fetchExecutableRecommendations(false), importantExeBasedRecommendations]));
872977

873978
}
874979
return fetchPromise;
@@ -877,20 +982,22 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
877982
/**
878983
* If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations
879984
*/
880-
private fetchExecutableRecommendations(): Promise<void> {
985+
private fetchExecutableRecommendations(important: boolean): Promise<void> {
881986
const homeDir = os.homedir();
882987
let foundExecutables: Set<string> = new Set<string>();
883988

884-
let findExecutable = (exeName: string, path: string) => {
989+
let findExecutable = (exeName: string, tip: IExeBasedExtensionTip, path: string) => {
885990
return pfs.fileExists(path).then(exists => {
886991
if (exists && !foundExecutables.has(exeName)) {
887992
foundExecutables.add(exeName);
888-
(product.exeBasedExtensionTips[exeName]['recommendations'] || [])
889-
.forEach(extensionId => {
890-
if (product.exeBasedExtensionTips[exeName]['friendlyName']) {
891-
this._exeBasedRecommendations[extensionId.toLowerCase()] = product.exeBasedExtensionTips[exeName]['friendlyName'];
993+
(tip['recommendations'] || []).forEach(extensionId => {
994+
if (tip.friendlyName) {
995+
if (important) {
996+
this._importantExeBasedRecommendations[extensionId.toLowerCase()] = tip;
892997
}
893-
});
998+
this._exeBasedRecommendations[extensionId.toLowerCase()] = tip;
999+
}
1000+
});
8941001
}
8951002
});
8961003
};
@@ -901,8 +1008,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
9011008
if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) {
9021009
return;
9031010
}
904-
905-
let exeName = entry.key;
1011+
if (important !== !!entry.value.important) {
1012+
return;
1013+
}
1014+
const exeName = entry.key;
9061015
if (process.platform === 'win32') {
9071016
let windowsPath = entry.value['windowsPath'];
9081017
if (!windowsPath || typeof windowsPath !== 'string') {
@@ -913,10 +1022,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
9131022
.replace('%ProgramFiles%', process.env['ProgramFiles']!)
9141023
.replace('%APPDATA%', process.env['APPDATA']!)
9151024
.replace('%WINDIR%', process.env['WINDIR']!);
916-
promises.push(findExecutable(exeName, windowsPath));
1025+
promises.push(findExecutable(exeName, entry.value, windowsPath));
9171026
} else {
918-
promises.push(findExecutable(exeName, join('/usr/local/bin', exeName)));
919-
promises.push(findExecutable(exeName, join(homeDir, exeName)));
1027+
promises.push(findExecutable(exeName, entry.value, join('/usr/local/bin', exeName)));
1028+
promises.push(findExecutable(exeName, entry.value, join(homeDir, exeName)));
9201029
}
9211030
});
9221031

0 commit comments

Comments
 (0)