diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 34811829b09..a572f0a2860 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -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, }; diff --git a/src/vs/platform/extensionManagement/node/extensionTipsService.ts b/src/vs/platform/extensionManagement/node/extensionTipsService.ts index 5780216da37..3dd401c785b 100644 --- a/src/vs/platform/extensionManagement/node/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/node/extensionTipsService.ts @@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { extensionId, extensionName, isExtensionPack, + exeName, exeFriendlyName: extensionTip.exeFriendlyName, windowsPath: extensionTip.windowsPath, }); diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 8215282b35b..61ab9d55b35 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -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")); } } diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 60a16d6fc88..875f7833943 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -26,11 +26,11 @@ type ExeExtensionRecommendationsClassification = { export class ExeBasedRecommendations extends ExtensionRecommendations { - private readonly _otherRecommendations: ExtensionRecommendation[] = []; - get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } + private _otherTips: IExecutableBasedExtensionTip[] = []; + private _importantTips: IExecutableBasedExtensionTip[] = []; - private readonly _importantRecommendations: ExtensionRecommendation[] = []; - get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + get otherRecommendations(): ReadonlyArray { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); } + get importantRecommendations(): ReadonlyArray { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); } get recommendations(): ReadonlyArray { 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 { - 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> { const importantExeBasedRecommendations: IStringDictionary = {}; - 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('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}"`); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index 4347c796632..985a4811904 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -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 { 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])); } } }, { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 559f5346458..d844e46c511 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -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); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 66df3029505..8639396bc57 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -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 { - 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[] = []; - 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 { + 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[] = []; + 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 { @@ -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 { + 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 { - 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 { + const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(this.searchValue); + viewPaneContainer.focus(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8e34bdaf743..d7f579aad69 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -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> { + 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, 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); } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index fcc7c701ba1..2e95bd74871 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -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; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 34647803e9b..12c949d67d2 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService { getAllRecommendationsWithReason(): IStringDictionary; getFileBasedRecommendations(): IExtensionRecommendation[]; + getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; getImportantRecommendations(): Promise; getConfigBasedRecommendations(): Promise; getOtherRecommendations(): Promise;