Skip to content

Commit 8bfdb0f

Browse files
committed
Improve exe based recommendations
1 parent c5134d8 commit 8bfdb0f

10 files changed

Lines changed: 111 additions & 72 deletions

File tree

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export type IExecutableBasedExtensionTip = {
239239
readonly extensionId: string,
240240
readonly extensionName: string,
241241
readonly isExtensionPack: boolean,
242+
readonly exeName: string,
242243
readonly exeFriendlyName: string,
243244
readonly windowsPath?: string,
244245
};

src/vs/platform/extensionManagement/node/extensionTipsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
105105
extensionId,
106106
extensionName,
107107
isExtensionPack,
108+
exeName,
108109
exeFriendlyName: extensionTip.exeFriendlyName,
109110
windowsPath: extensionTip.windowsPath,
110111
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
9494
const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0];
9595
const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName)
9696
: localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName);
97-
this.promptImportantExtensionsInstallNotification([extension], message);
97+
this.promptImportantExtensionsInstallNotification([extension], message, extension, localize('more information', "More Information"));
9898
}
9999
}
100100

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

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ type ExeExtensionRecommendationsClassification = {
2626
export class ExeBasedRecommendations extends ExtensionRecommendations {
2727

2828

29-
private readonly _otherRecommendations: ExtensionRecommendation[] = [];
30-
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
29+
private _otherTips: IExecutableBasedExtensionTip[] = [];
30+
private _importantTips: IExecutableBasedExtensionTip[] = [];
3131

32-
private readonly _importantRecommendations: ExtensionRecommendation[] = [];
33-
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantRecommendations; }
32+
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); }
33+
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); }
3434

3535
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
3636

@@ -58,9 +58,20 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
5858
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
5959
}
6060

61+
getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } {
62+
const important = this._importantTips
63+
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
64+
.map(tip => this.toExtensionRecommendation(tip));
65+
66+
const others = this._otherTips
67+
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
68+
.map(tip => this.toExtensionRecommendation(tip));
69+
70+
return { important, others };
71+
}
72+
6173
protected async doActivate(): Promise<void> {
62-
const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips();
63-
otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip)));
74+
this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips();
6475
await this.fetchImportantExeBasedRecommendations();
6576
}
6677

@@ -74,11 +85,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
7485

7586
private async doFetchImportantExeBasedRecommendations(): Promise<IStringDictionary<IExecutableBasedExtensionTip>> {
7687
const importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip> = {};
77-
const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips();
78-
importantExectuableBasedTips.forEach(tip => {
79-
this._importantRecommendations.push(this.toExtensionRecommendation(tip));
80-
importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip;
81-
});
88+
this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips();
89+
this._importantTips.forEach(tip => importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip);
8290
return importantExeBasedRecommendations;
8391
}
8492

@@ -127,22 +135,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
127135
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
128136
}
129137

130-
if (tips.length === 1) {
131-
const tip = tips[0];
132-
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!))
133-
: localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!));
134-
this.promptImportantExtensionsInstallNotification(extensionIds, message);
135-
}
136-
137-
else if (tips.length === 2) {
138-
const message = localize('two extensions recommended', "The '{0}' and '{1}' extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!));
139-
this.promptImportantExtensionsInstallNotification(extensionIds, message);
140-
}
141-
142-
else if (tips.length > 2) {
143-
const message = localize('more than two extensions recommended', "The '{0}', '{1}' and other extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!));
144-
this.promptImportantExtensionsInstallNotification(extensionIds, message);
145-
}
138+
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install recommendations for it?", tips[0].exeFriendlyName);
139+
this.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
146140
}
147141
}
148142

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

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
88
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
99
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1010
import { localize } from 'vs/nls';
11-
import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
11+
import { InstallRecommendedExtensionsAction, SearchExtensionsAction, OpenExtensionEditorAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
1212
import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
1313
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
1414
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
@@ -57,38 +57,33 @@ export abstract class ExtensionRecommendations extends Disposable {
5757
return this._activationPromise;
5858
}
5959

60-
private runAction(action: IAction) {
60+
private async runAction(action: IAction): Promise<void> {
6161
try {
62-
action.run();
62+
await action.run();
6363
} finally {
6464
action.dispose();
6565
}
6666
}
6767

68-
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void {
68+
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, showRecommendationsLabel?: string): void {
6969
this.notificationService.prompt(Severity.Info, message,
7070
[{
71-
label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"),
71+
label: localize('install', 'Install'),
7272
run: async () => {
7373
for (const extensionId of extensionIds) {
7474
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId });
7575
}
76-
if (extensionIds.length === 1) {
77-
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0]));
78-
} else {
79-
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations'));
80-
}
76+
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, searchValue, 'install-recommendations'));
8177
}
8278
}, {
83-
label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"),
84-
run: () => {
79+
label: showRecommendationsLabel || localize('show recommendations', "Show Recommendations"),
80+
run: async () => {
8581
for (const extensionId of extensionIds) {
8682
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId });
8783
}
84+
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
8885
if (extensionIds.length === 1) {
89-
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0]));
90-
} else {
91-
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL));
86+
this.runAction(this.instantiationService.createInstance(OpenExtensionEditorAction, extensionIds[0]));
9287
}
9388
}
9489
}, {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
191191
return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations);
192192
}
193193

194+
async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
195+
await this.exeBasedRecommendations.activate();
196+
const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)
197+
: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };
198+
return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) };
199+
}
200+
194201
getFileBasedRecommendations(): IExtensionRecommendation[] {
195202
return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations);
196203
}

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

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,7 @@ export class InstallRecommendedExtensionsAction extends Action {
18421842
id: string,
18431843
label: string,
18441844
recommendations: string[],
1845+
private readonly searchValue: string,
18451846
private readonly source: string,
18461847
@IViewletService private readonly viewletService: IViewletService,
18471848
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -1854,22 +1855,17 @@ export class InstallRecommendedExtensionsAction extends Action {
18541855
this.recommendations = recommendations;
18551856
}
18561857

1857-
run(): Promise<any> {
1858-
return this.viewletService.openViewlet(VIEWLET_ID, true)
1859-
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
1860-
.then(viewlet => {
1861-
viewlet.search('@recommended ');
1862-
viewlet.focus();
1863-
const names = this.recommendations;
1864-
return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => {
1865-
let installPromises: Promise<any>[] = [];
1866-
let model = new PagedModel(pager);
1867-
for (let i = 0; i < pager.total; i++) {
1868-
installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e)));
1869-
}
1870-
return Promise.all(installPromises);
1871-
});
1872-
});
1858+
async run(): Promise<any> {
1859+
await new SearchExtensionsAction(this.searchValue, this.viewletService).run();
1860+
const names = this.recommendations;
1861+
const pager = await this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None);
1862+
const installPromises: Promise<any>[] = [];
1863+
const model = new PagedModel(pager);
1864+
for (let i = 0; i < pager.total; i++) {
1865+
installPromises.push(model.resolve(i, CancellationToken.None)
1866+
.then(e => this.installExtension(e)));
1867+
}
1868+
return Promise.all(installPromises);
18731869
}
18741870

18751871
private async installExtension(extension: IExtension): Promise<void> {
@@ -1885,6 +1881,7 @@ export class InstallRecommendedExtensionsAction extends Action {
18851881
return;
18861882
}
18871883
}
1884+
this.extensionWorkbenchService.open(extension, { pinned: true });
18881885
await this.extensionWorkbenchService.install(extension);
18891886
} catch (err) {
18901887
console.error(err);
@@ -1904,7 +1901,7 @@ export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommen
19041901
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
19051902
@IProductService productService: IProductService,
19061903
) {
1907-
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations',
1904+
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, '@recommended ', 'install-all-workspace-recommendations',
19081905
viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService);
19091906
}
19101907
}
@@ -1943,6 +1940,24 @@ export class ShowRecommendedExtensionAction extends Action {
19431940
}
19441941
}
19451942

1943+
export class OpenExtensionEditorAction extends Action {
1944+
1945+
constructor(
1946+
private readonly extensionId: string,
1947+
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
1948+
) {
1949+
super('extensions.openExtension', localize('open extension', "Open Extension"), undefined, true);
1950+
}
1951+
1952+
async run(): Promise<any> {
1953+
const pager = await this.extensionWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation', pageSize: 1 }, CancellationToken.None);
1954+
if (pager && pager.firstPage && pager.firstPage.length) {
1955+
const extension = pager.firstPage[0];
1956+
return this.extensionWorkbenchService.open(extension);
1957+
}
1958+
}
1959+
}
1960+
19461961
export class InstallRecommendedExtensionAction extends Action {
19471962

19481963
static readonly ID = 'workbench.extensions.action.installRecommendedExtension';
@@ -2087,12 +2102,23 @@ export class SearchCategoryAction extends Action {
20872102
}
20882103

20892104
run(): Promise<void> {
2090-
return this.viewletService.openViewlet(VIEWLET_ID, true)
2091-
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
2092-
.then(viewlet => {
2093-
viewlet.search(`@category:"${this.category.toLowerCase()}"`);
2094-
viewlet.focus();
2095-
});
2105+
return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run();
2106+
}
2107+
}
2108+
2109+
export class SearchExtensionsAction extends Action {
2110+
2111+
constructor(
2112+
private readonly searchValue: string,
2113+
@IViewletService private readonly viewletService: IViewletService
2114+
) {
2115+
super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true);
2116+
}
2117+
2118+
async run(): Promise<void> {
2119+
const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer;
2120+
viewPaneContainer.search(this.searchValue);
2121+
viewPaneContainer.focus();
20962122
}
20972123
}
20982124

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ export class ExtensionsListView extends ViewPane {
460460
return this.getWorkspaceRecommendationsModel(query, options, token);
461461
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
462462
return this.getKeymapRecommendationsModel(query, options, token);
463+
} else if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
464+
return this.getExeRecommendationsModel(query, options, token);
463465
} else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) {
464466
return this.getAllRecommendationsModel(query, options, token);
465467
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
@@ -732,6 +734,19 @@ export class ExtensionsListView extends ViewPane {
732734
.then(result => this.getPagedModel(result));
733735
}
734736

737+
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
738+
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
739+
const { important } = await this.tipsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
740+
const names: string[] = important.map(({ extensionId }) => extensionId);
741+
742+
if (!names.length) {
743+
return Promise.resolve(new PagedModel([]));
744+
}
745+
options.source = 'recommendations-exe';
746+
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
747+
.then(result => this.getPagedModel(result));
748+
}
749+
735750
// Sorts the firstPage of the pager in the same order as given array of extension ids
736751
private sortFirstPage(pager: IPager<IExtension>, ids: string[]) {
737752
ids = ids.map(x => x.toLowerCase());
@@ -864,6 +879,10 @@ export class ExtensionsListView extends ViewPane {
864879
return /@recommended:workspace/i.test(query);
865880
}
866881

882+
static isExeRecommendedExtensionsQuery(query: string): boolean {
883+
return /@exe:.+/i.test(query);
884+
}
885+
867886
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
868887
return /@recommended:keymaps/i.test(query);
869888
}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
226226
if (!entry) {
227227
return false;
228228
}
229-
const extensionName = entry.name;
230-
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName);
231-
if (entry.isExtensionPack) {
232-
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName);
233-
}
234229

235-
this.promptImportantExtensionsInstallNotification([extensionId], message);
230+
this.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install recommendations for this file?"), extensionId);
236231
return true;
237232
}
238233

src/vs/workbench/services/extensionManagement/common/extensionManagement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService {
128128

129129
getAllRecommendationsWithReason(): IStringDictionary<IExtensionRecommendationReson>;
130130
getFileBasedRecommendations(): IExtensionRecommendation[];
131+
getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>;
131132
getImportantRecommendations(): Promise<IExtensionRecommendation[]>;
132133
getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]>;
133134
getOtherRecommendations(): Promise<IExtensionRecommendation[]>;

0 commit comments

Comments
 (0)