Improve exe based recommendations

This commit is contained in:
Sandeep Somavarapu
2020-08-26 01:18:50 +02:00
parent c5134d83f7
commit 8bfdb0f87b
10 changed files with 111 additions and 72 deletions
@@ -239,6 +239,7 @@ export type IExecutableBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly exeName: string,
readonly exeFriendlyName: string,
readonly windowsPath?: string,
};
@@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
extensionId,
extensionName,
isExtensionPack,
exeName,
exeFriendlyName: extensionTip.exeFriendlyName,
windowsPath: extensionTip.windowsPath,
});
@@ -94,7 +94,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0];
const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName)
: localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName);
this.promptImportantExtensionsInstallNotification([extension], message);
this.promptImportantExtensionsInstallNotification([extension], message, extension, localize('more information', "More Information"));
}
}
@@ -26,11 +26,11 @@ type ExeExtensionRecommendationsClassification = {
export class ExeBasedRecommendations extends ExtensionRecommendations {
private readonly _otherRecommendations: ExtensionRecommendation[] = [];
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
private _otherTips: IExecutableBasedExtensionTip[] = [];
private _importantTips: IExecutableBasedExtensionTip[] = [];
private readonly _importantRecommendations: ExtensionRecommendation[] = [];
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantRecommendations; }
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); }
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); }
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
@@ -58,9 +58,20 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
}
getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } {
const important = this._importantTips
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
.map(tip => this.toExtensionRecommendation(tip));
const others = this._otherTips
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
.map(tip => this.toExtensionRecommendation(tip));
return { important, others };
}
protected async doActivate(): Promise<void> {
const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips();
otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip)));
this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips();
await this.fetchImportantExeBasedRecommendations();
}
@@ -74,11 +85,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
private async doFetchImportantExeBasedRecommendations(): Promise<IStringDictionary<IExecutableBasedExtensionTip>> {
const importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip> = {};
const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips();
importantExectuableBasedTips.forEach(tip => {
this._importantRecommendations.push(this.toExtensionRecommendation(tip));
importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip;
});
this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips();
this._importantTips.forEach(tip => importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip);
return importantExeBasedRecommendations;
}
@@ -127,22 +135,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}
if (tips.length === 1) {
const tip = tips[0];
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.promptImportantExtensionsInstallNotification(extensionIds, message);
}
else if (tips.length === 2) {
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!));
this.promptImportantExtensionsInstallNotification(extensionIds, message);
}
else if (tips.length > 2) {
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!));
this.promptImportantExtensionsInstallNotification(extensionIds, message);
}
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install recommendations for it?", tips[0].exeFriendlyName);
this.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
}
}
@@ -8,7 +8,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { InstallRecommendedExtensionsAction, SearchExtensionsAction, OpenExtensionEditorAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
@@ -57,38 +57,33 @@ export abstract class ExtensionRecommendations extends Disposable {
return this._activationPromise;
}
private runAction(action: IAction) {
private async runAction(action: IAction): Promise<void> {
try {
action.run();
await action.run();
} finally {
action.dispose();
}
}
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void {
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, showRecommendationsLabel?: string): void {
this.notificationService.prompt(Severity.Info, message,
[{
label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"),
label: localize('install', 'Install'),
run: async () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId });
}
if (extensionIds.length === 1) {
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0]));
} else {
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations'));
}
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, searchValue, 'install-recommendations'));
}
}, {
label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"),
run: () => {
label: showRecommendationsLabel || localize('show recommendations', "Show Recommendations"),
run: async () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId });
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
if (extensionIds.length === 1) {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0]));
} else {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL));
this.runAction(this.instantiationService.createInstance(OpenExtensionEditorAction, extensionIds[0]));
}
}
}, {
@@ -191,6 +191,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations);
}
async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
await this.exeBasedRecommendations.activate();
const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)
: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };
return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) };
}
getFileBasedRecommendations(): IExtensionRecommendation[] {
return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations);
}
@@ -1842,6 +1842,7 @@ export class InstallRecommendedExtensionsAction extends Action {
id: string,
label: string,
recommendations: string[],
private readonly searchValue: string,
private readonly source: string,
@IViewletService private readonly viewletService: IViewletService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -1854,22 +1855,17 @@ export class InstallRecommendedExtensionsAction extends Action {
this.recommendations = recommendations;
}
run(): Promise<any> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@recommended ');
viewlet.focus();
const names = this.recommendations;
return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => {
let installPromises: Promise<any>[] = [];
let model = new PagedModel(pager);
for (let i = 0; i < pager.total; i++) {
installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e)));
}
return Promise.all(installPromises);
});
});
async run(): Promise<any> {
await new SearchExtensionsAction(this.searchValue, this.viewletService).run();
const names = this.recommendations;
const pager = await this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None);
const installPromises: Promise<any>[] = [];
const model = new PagedModel(pager);
for (let i = 0; i < pager.total; i++) {
installPromises.push(model.resolve(i, CancellationToken.None)
.then(e => this.installExtension(e)));
}
return Promise.all(installPromises);
}
private async installExtension(extension: IExtension): Promise<void> {
@@ -1885,6 +1881,7 @@ export class InstallRecommendedExtensionsAction extends Action {
return;
}
}
this.extensionWorkbenchService.open(extension, { pinned: true });
await this.extensionWorkbenchService.install(extension);
} catch (err) {
console.error(err);
@@ -1904,7 +1901,7 @@ export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommen
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IProductService productService: IProductService,
) {
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations',
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, '@recommended ', 'install-all-workspace-recommendations',
viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService);
}
}
@@ -1943,6 +1940,24 @@ export class ShowRecommendedExtensionAction extends Action {
}
}
export class OpenExtensionEditorAction extends Action {
constructor(
private readonly extensionId: string,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
) {
super('extensions.openExtension', localize('open extension', "Open Extension"), undefined, true);
}
async run(): Promise<any> {
const pager = await this.extensionWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation', pageSize: 1 }, CancellationToken.None);
if (pager && pager.firstPage && pager.firstPage.length) {
const extension = pager.firstPage[0];
return this.extensionWorkbenchService.open(extension);
}
}
}
export class InstallRecommendedExtensionAction extends Action {
static readonly ID = 'workbench.extensions.action.installRecommendedExtension';
@@ -2087,12 +2102,23 @@ export class SearchCategoryAction extends Action {
}
run(): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search(`@category:"${this.category.toLowerCase()}"`);
viewlet.focus();
});
return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run();
}
}
export class SearchExtensionsAction extends Action {
constructor(
private readonly searchValue: string,
@IViewletService private readonly viewletService: IViewletService
) {
super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true);
}
async run(): Promise<void> {
const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer;
viewPaneContainer.search(this.searchValue);
viewPaneContainer.focus();
}
}
@@ -460,6 +460,8 @@ export class ExtensionsListView extends ViewPane {
return this.getWorkspaceRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
return this.getKeymapRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
return this.getExeRecommendationsModel(query, options, token);
} else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) {
return this.getAllRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
@@ -732,6 +734,19 @@ export class ExtensionsListView extends ViewPane {
.then(result => this.getPagedModel(result));
}
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
const { important } = await this.tipsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
const names: string[] = important.map(({ extensionId }) => extensionId);
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations-exe';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(result => this.getPagedModel(result));
}
// Sorts the firstPage of the pager in the same order as given array of extension ids
private sortFirstPage(pager: IPager<IExtension>, ids: string[]) {
ids = ids.map(x => x.toLowerCase());
@@ -864,6 +879,10 @@ export class ExtensionsListView extends ViewPane {
return /@recommended:workspace/i.test(query);
}
static isExeRecommendedExtensionsQuery(query: string): boolean {
return /@exe:.+/i.test(query);
}
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
return /@recommended:keymaps/i.test(query);
}
@@ -226,13 +226,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
if (!entry) {
return false;
}
const extensionName = entry.name;
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName);
if (entry.isExtensionPack) {
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName);
}
this.promptImportantExtensionsInstallNotification([extensionId], message);
this.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install recommendations for this file?"), extensionId);
return true;
}
@@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService {
getAllRecommendationsWithReason(): IStringDictionary<IExtensionRecommendationReson>;
getFileBasedRecommendations(): IExtensionRecommendation[];
getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>;
getImportantRecommendations(): Promise<IExtensionRecommendation[]>;
getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]>;
getOtherRecommendations(): Promise<IExtensionRecommendation[]>;